Support for doing both auto backup on backend creation and force export

This commit is contained in:
arabcoders
2025-04-03 00:31:17 +03:00
parent 7b61660290
commit abd60715c2
5 changed files with 147 additions and 67 deletions

View File

@@ -269,34 +269,30 @@
</p>
</div>
</div>
<div class="field" v-if="backends.length > 0">
<div class="field">
<hr>
<label class="label has-text-danger" for="force_export">
Export current local database state to this backend?
<label class="label has-text-danger" for="backup_data">
Create backup for this backend data?
</label>
<div class="control">
<input id="force_export" type="checkbox" class="switch is-success"
v-model="forceExport">
<label for="force_export">Yes</label>
<input id="backup_data" type="checkbox" class="switch is-success" v-model="backup_data">
<label for="backup_data">Yes</label>
<p class="help">
<span class="icon has-text-danger"><i class="fas fa-exclamation-triangle"></i></span>
If this is a new backend, you need to get it in sync with your current database,
enabling this option will initiate a one time force export the current local database state to the
backend. This will override the backend state to be inline with the local database state.
This will run a one time backup for the backend data.
</p>
</div>
</div>
<div class="field" v-else>
<div class="field" v-if="backends.length > 0">
<hr>
<label class="label has-text-danger" for="run_import">
Export current local database state to this backend?
<label class="label has-text-danger" for="force_export">
Force Export local data to this backend?
</label>
<div class="control">
<input id="run_import" type="checkbox" class="switch is-success" v-model="runImport">
<label for="run_import">Yes</label>
<p class="help">
<span class="icon has-text-danger"><i class="fas fa-info-circle"></i></span>
Do you want to run a one time import for this backend after adding this backend?
<input id="force_export" type="checkbox" class="switch is-success" v-model="force_export">
<label for="force_export">Yes</label>
<p class="help has-text-danger">
<span class="icon"><i class="fas fa-info-circle"></i></span>
THIS OPTION WILL OVERRIDE THE BACKEND DATA with locally stored data.
</p>
</div>
</div>
@@ -335,7 +331,7 @@ import request from '~/utils/request'
import {awaitElement, explode, notification, ucFirst} from '~/utils/index'
import {useStorage} from "@vueuse/core";
const emit = defineEmits(['addBackend', 'forceExport', 'runImport'])
const emit = defineEmits(['addBackend', 'backupData', 'forceExport'])
const props = defineProps({
backends: {
@@ -377,8 +373,8 @@ const uuidLoading = ref(false)
const serversLoading = ref(false)
const exposeToken = ref(false)
const error = ref()
const forceExport = ref(false)
const runImport = ref(false)
const backup_data = ref(true)
const force_export = ref(false)
const isLimited = ref(false)
const accessTokenResponse = ref({})
@@ -674,16 +670,15 @@ const addBackend = async () => {
notification('success', 'Information', `Backend ${backend.value.name} added successfully.`)
let event
if (true === Boolean(forceExport?.value ?? false)) {
event = 'forceExport'
} else if (true === Boolean(runImport?.value ?? false)) {
event = 'runImport'
} else {
event = 'addBackend'
if (true === Boolean(backup_data?.value ?? false)) {
emit('backupData', backend)
}
emit(event, backend)
if (true === Boolean(force_export?.value ?? false)) {
emit('forceExport', backend)
}
emit('addBackend')
return true
}

View File

@@ -35,8 +35,9 @@
</div>
<div class="column is-12" v-if="toggleForm">
<BackendAdd @forceExport="e => handleEvents('forceExport', e)" :backends="backends"
@runImport="e => handleEvents('runImport', e)" @addBackend="e => handleEvents('addBackend', e)"/>
<BackendAdd @backupData="e => handleEvents('backupData', e)" :backends="backends"
@forceExport="e => handleEvents('forceExport', e)"
@addBackend="e => handleEvents('addBackend', e)"/>
</div>
<template v-else>
<div class="column is-12" v-if="backends.length<1">
@@ -188,7 +189,7 @@ import 'assets/css/bulma-switch.css'
import moment from 'moment'
import request from '~/utils/request'
import BackendAdd from '~/components/BackendAdd'
import {ag, copyText, makeConsoleCommand, notification, r, TOOLTIP_DATE_FORMAT} from '~/utils/index'
import {ag, copyText, makeConsoleCommand, notification, queue_event, r, TOOLTIP_DATE_FORMAT} from '~/utils/index'
import {useStorage} from '@vueuse/core'
import Message from '~/components/Message'
@@ -206,31 +207,31 @@ const usefulCommands = {
export_now: {
id: 1,
title: "Run normal export.",
command: 'state:export -v -s {name}',
command: 'state:export -v -u {user} -s {name}',
state_key: 'export.enabled',
},
import_now: {
id: 2,
title: "Run normal import.",
command: 'state:import -v -s {name}',
command: 'state:import -v -u {user} -s {name}',
state_key: 'import.enabled'
},
force_export: {
id: 3,
title: "Force export local play state to this backend.",
command: 'state:export -fi -v -s {name}',
command: 'state:export -fi -v -u {user} -s {name}',
state_key: 'export.enabled',
},
backup_now: {
id: 4,
title: "Backup this backend play state.",
command: "state:backup -v -s {name} --file '{date}.manual_{name}.json'",
command: "state:backup -v -u {user} -s {name} --file '{date}.manual_{name}.json'",
state_key: 'import.enabled',
},
metadata_only: {
id: 5,
title: "Import this backend metadata.",
command: "state:import -v --metadata-only -s {name}",
command: "state:import -v --metadata-only -u {user} -s {name}",
state_key: 'import.enabled',
},
}
@@ -247,7 +248,7 @@ const forwardCommand = async backend => {
date: moment().format('YYYYMMDD'),
}
await navigateTo(makeConsoleCommand(r(usefulCommands[index].command, {...backend, ...util})));
await navigateTo(makeConsoleCommand(r(usefulCommands[index].command, {...backend, ...util, user: api_user.value})));
}
const loadContent = async () => {
@@ -285,13 +286,48 @@ const updateValue = async (backend, key, newValue) => {
const handleEvents = async (event, backend) => {
switch (event) {
case 'forceExport':
notification('warning', 'Warning', `We are going to sync '${backend.value.name}' play state to match the current local database.`, 10000)
await navigateTo(makeConsoleCommand(`state:export -fi -v -s ${backend.value.name}`, true))
case 'backupData':
try {
const backup_status = await queue_event('run_console', {
command: 'state:backup',
args: [
'-v',
'--user',
api_user.value,
'--select-backend',
backend.value.name,
'--file',
'{user}.{backend}.{date}.initial_backup.json',
]
})
console.log(backup_status);
notification('info', 'Info', `We are going to initiate a backup for '${backend.value.name}' in little bit.`, 5000)
} catch (e) {
notification('error', 'Error', `Failed to queue backup request. ${e.message}`)
}
break
case 'runImport':
notification('info', 'Info', `We are going to import '${backend.value.name}' play state to the local database.`, 10000)
await navigateTo(makeConsoleCommand(`state:import -v -s ${backend.value.name}`, true))
case 'forceExport':
try {
const export_status = await queue_event('run_console', {
command: 'state:export',
args: [
'-fi',
'-v',
'--user',
api_user.value,
'--dry-run',
'--select-backend',
backend.value.name,
]
}, 180)
console.log(export_status);
notification('info', 'Info', `Soon we are going to force export the local data to '${backend.value.name}'.`, 5000)
} catch (e) {
notification('error', 'Error', `Failed to queue force export request. ${e.message}`)
}
break
case 'addBackend':
toggleForm.value = false
@@ -299,4 +335,5 @@ const handleEvents = async (event, backend) => {
break
}
}
</script>

View File

@@ -253,7 +253,7 @@ const makeGUIDLink = (type, source, guid, data) => {
const link = ag(guid_links, `${type}.${source}`, null)
return null == link ? '' : r(link, { _guid: guid, ...toRaw(data) })
return null == link ? '' : r(link, {_guid: guid, ...toRaw(data)})
}
/**
@@ -339,7 +339,7 @@ const makeSearchLink = (type, query) => {
* @param detail
* @returns {boolean}
*/
const dEvent = (eventName, detail = {}) => window.dispatchEvent(new CustomEvent(eventName, { detail }))
const dEvent = (eventName, detail = {}) => window.dispatchEvent(new CustomEvent(eventName, {detail}))
/**
* Make name
@@ -358,7 +358,7 @@ const makeName = (item, asMovie = false) => {
const type = ag(item, 'type', 'movie');
if (['show', 'movie'].includes(type) || asMovie) {
return r('{title} ({year})', { title, year })
return r('{title} ({year})', {title, year})
}
return r('{title} ({year}) - {season}x{episode}', {
@@ -470,7 +470,7 @@ const parse_api_response = async r => {
try {
return await r.json()
} catch (e) {
return { error: { code: r.status, message: r.statusText } }
return {error: {code: r.status, message: r.statusText}}
}
}
@@ -493,6 +493,39 @@ const goto_history_item = async item => {
await navigateTo(`/history/${item.item_id}`)
}
/**
* Queue event.
*
* @param {string} event The event name.
* @param {object} event_data The event data.
* @param {int} delay delay running the event in XXX seconds.
* @param {object} opts additional options.
*
* @returns {Promise<number>} The status code of the response.
*/
const queue_event = async (event, event_data = {}, delay = 0, opts = {}) => {
let reqData = {event}
if (event_data) {
reqData.event_data = event_data
}
delay = parseInt(delay)
if (0 !== delay) {
reqData.DELAY_BY = delay
}
if (opts) {
reqData = {...reqData, ...opts}
}
const resp = await request(`/system/events`, {
method: 'POST', body: JSON.stringify(reqData)
})
return resp.status
}
export {
r,
ag_set,
@@ -515,5 +548,6 @@ export {
explode,
basename,
parse_api_response,
goto_history_item
goto_history_item,
queue_event
}

View File

@@ -8,18 +8,18 @@ use App\API\Backends\Index;
use App\Command;
use App\Libs\Attributes\Route\Cli;
use App\Libs\Config;
use App\Libs\Mappers\ImportInterface as iImport;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Entity\StateEntity;
use App\Libs\Extends\ConsoleOutput;
use App\Libs\Extends\Date;
use App\Libs\UserContext;
use App\Libs\Mappers\ImportInterface as iImport;
use App\Libs\Options;
use App\Libs\UserContext;
use Cron\CronExpression;
use LimitIterator;
use Psr\Log\LoggerInterface as iLogger;
use RuntimeException;
use SplFileObject;
use Psr\Log\LoggerInterface as iLogger;
use Symfony\Component\Console\Input\InputInterface as iInput;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface as iOutput;
@@ -57,7 +57,13 @@ final class ReportCommand extends Command
{
$this->setName(self::ROUTE)
->setDescription('Show basic information for diagnostics.')
->addOption('limit', 'l', InputOption::VALUE_OPTIONAL, 'Show last X number of log lines.', self::DEFAULT_LIMIT)
->addOption(
'limit',
'l',
InputOption::VALUE_OPTIONAL,
'Show last X number of log lines.',
self::DEFAULT_LIMIT
)
->addOption(
'include-db-sample',
's',
@@ -153,7 +159,7 @@ final class ReportCommand extends Command
if (count($usersContext) > 1) {
$output->writeln(
r('Users? {users}' . PHP_EOL, [
'users' => count($usersContext) >= 1 ? implode(', ', array_keys($usersContext)) : 'None',
'users' => implode(', ', array_keys($usersContext)),
])
);
}
@@ -392,12 +398,12 @@ final class ReportCommand extends Command
private function handleLog(iOutput $output, string $type, string|int $date, int|string $limit): void
{
$logFile = Config::get('tmpDir') . '/logs/' . r(
'{type}.{date}.log',
[
'{type}.{date}.log',
[
'type' => $type,
'date' => $date
]
);
);
$output->writeln(r('[ <value>{logFile}</value> ]' . PHP_EOL, ['logFile' => $logFile]));

View File

@@ -69,10 +69,13 @@ final class TasksCommand extends Command
*/
protected function configure(): void
{
$tasksName = implode(', ', array_map(
fn ($val) => '<comment>' . strtoupper($val) . '</comment>',
array_keys(Config::get('tasks.list', []))
));
$tasksName = implode(
', ',
array_map(
fn($val) => '<comment>' . strtoupper($val) . '</comment>',
array_keys(Config::get('tasks.list', []))
)
);
$this->setName(self::ROUTE)
->addOption('run', null, InputOption::VALUE_NONE, 'Run scheduled tasks.')
@@ -194,7 +197,8 @@ final class TasksCommand extends Command
$eventName = $event->getEvent()->event;
switch ($eventName) {
case self::NAME: {
case self::NAME:
{
if (null === ($name = ag($event->getData(), 'name'))) {
$event->addLog(r('No task name was specified.'));
return $event;
@@ -202,11 +206,14 @@ final class TasksCommand extends Command
$task = self::getTasks($name);
if (empty($task)) {
$event->addLog(r("Invalid task '{name}'. There are no task with that name registered.", ['name' => $name]));
$event->addLog(
r("Invalid task '{name}'. There are no task with that name registered.", ['name' => $name])
);
return $event;
}
}
case self::CNAME: {
case self::CNAME:
{
if (null === ag($event->getData(), 'command')) {
$event->addLog(r('No command name was specified.'));
return $event;
@@ -223,9 +230,9 @@ final class TasksCommand extends Command
$input->setOption('save-log', true);
$input->setOption('live', false);
$this->clear = fn () => $event->clearLogs();
$this->clear = fn() => $event->clearLogs();
$this->save = fn () => $this->eventsRepo->save($event->getEvent());
$this->save = fn() => $this->eventsRepo->save($event->getEvent());
$this->writer = function ($msg) use (&$event) {
static $lastSave = null;
@@ -260,8 +267,9 @@ final class TasksCommand extends Command
'name' => $eventName,
'code' => $exitCode,
]));
}
} else {
if (self::NAME === $eventName && !empty($task)) {
$event->addLog(r("Task: Run '{command}'.", ['command' => ag($task, 'command')]));
$exitCode = $this->runTask($task, $input, Container::get(iOutput::class));
$event->addLog(r("Task: End '{command}' (Exit Code: {code})", [