Initial import of borg-restore
This commit is contained in:
+64
@@ -0,0 +1,64 @@
|
||||
# borg-restore 2.x - state snapshot
|
||||
|
||||
## Current state
|
||||
- Working dir: `borg-restore_2x/borg-restore`
|
||||
- Latest version in `plugin.conf`: `2.0.7`
|
||||
- Latest package: `borg-restore_2x/2.0.7/borg-restore.tar.gz`
|
||||
|
||||
## Packaging layout (required)
|
||||
- Source (editable): `borg-restore_2x/borg-restore/`
|
||||
- Packages: `borg-restore_2x/<version>/borg-restore.tar.gz`
|
||||
|
||||
## Key behavior (current)
|
||||
- Restore types:
|
||||
- `web` (Pliki www)
|
||||
- `mail` (Zawartosc skrzynki pocztowej)
|
||||
- Restore modes:
|
||||
- `location` (przywracanie do lokalizacji)
|
||||
- `overwrite` (nadpisywanie)
|
||||
- `update` (scalenie; UI label: "Polaczenie danych kopii zapasowej z obecnymi danymi")
|
||||
- For mail merge:
|
||||
- rsync excludes Dovecot indexes and can optionally delete them after merge.
|
||||
- Target path picker (location mode) is limited to `/home/DA_USER`.
|
||||
- Restore path restriction (if enabled) is applied only to restore path and overwrite/update.
|
||||
- Warning modal uses pink background; overlay uses transparent dark scrim.
|
||||
|
||||
## Config (plugin-settings.conf)
|
||||
- `BB_PATH=/opt/bb`
|
||||
- `BORG_PATH=/usr/bin/borg`
|
||||
- `BORG_VARIABLES_FILE=/opt/bb/bbvars.sh`
|
||||
- `WEB_RESTORE_LOCATION=/home/DA_USER/domains`
|
||||
- `MAIL_RESTORE_LOCATION=/home/DA_USER/imap`
|
||||
- `DEFAULT_RESTORE_DESTINATION=/home/DA_USER/bb`
|
||||
- `BLOCK_RESTORE_OUTSIDE_LOCATION=TRUE`
|
||||
- `PREVENT_RESTORE_MAIN_DOMAINS_DIR=TRUE`
|
||||
- `PREVENT_RESTORE_MAIN_IMAP_DIR=TRUE`
|
||||
- `CLEAR_DOVECOT_INDEXES_AFTER_MERGE=TRUE`
|
||||
- `RESTORE_PATH_MODE=full`
|
||||
|
||||
## Notable logic changes
|
||||
- Mail merge uses rsync with excludes for:
|
||||
- `dovecot.index*`, `dovecot-uidlist`, `dovecot-uidvalidity`,
|
||||
`dovecot.list.index*`, `dovecot.list.log`
|
||||
- Optional cleanup of these indexes after merge if config flag is TRUE.
|
||||
- Logs: lines containing "Permanently added" and "known host" are filtered out of user-facing logs.
|
||||
- New config flags to allow/deny restoring whole `/home/DA_USER/domains` and `/home/DA_USER/imap`.
|
||||
- Default target path for location mode uses `DEFAULT_RESTORE_DESTINATION`.
|
||||
- Picker: creating a new dir jumps into it.
|
||||
|
||||
## CSS/UI
|
||||
- Overlay: `rgba(15, 23, 42, 0.35)`
|
||||
- Warning modal: class `br-modal-warning` with background `#f3c8c8`
|
||||
- Picker modal: white background (default `.br-modal`)
|
||||
|
||||
## Recent files touched
|
||||
- `borg-restore_2x/borg-restore/user/index.html`
|
||||
- `borg-restore_2x/borg-restore/scripts/restore.sh`
|
||||
- `borg-restore_2x/borg-restore/plugin-settings.conf`
|
||||
- `borg-restore_2x/borg-restore/user/enhanced.css`
|
||||
- `borg-restore_2x/borg-restore/user/evolution.css`
|
||||
|
||||
## Next step template (for new chat)
|
||||
- "Pracujemy w `borg-restore_2x/borg-restore/`. Aktualna wersja w plugin.conf: 2.0.7."
|
||||
- "Ostatnia paczka: `borg-restore_2x/2.0.7/borg-restore.tar.gz`."
|
||||
- "Cel: <opis zmiany>."
|
||||
Executable
+787
@@ -0,0 +1,787 @@
|
||||
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/borg-restore/php.ini
|
||||
<?php
|
||||
|
||||
function h(string $value): string
|
||||
{
|
||||
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function parse_sh_value(string $value): string
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
if ($value[0] === "'" && substr($value, -1) === "'") {
|
||||
return substr($value, 1, -1);
|
||||
}
|
||||
if ($value[0] === '"' && substr($value, -1) === '"') {
|
||||
return stripcslashes(substr($value, 1, -1));
|
||||
}
|
||||
$value = preg_replace('/\s+#.*$/', '', $value);
|
||||
return trim((string)$value);
|
||||
}
|
||||
|
||||
function parse_bool(string $value, bool $default = false): bool
|
||||
{
|
||||
$normalized = strtolower(trim($value));
|
||||
if ($normalized === '') {
|
||||
return $default;
|
||||
}
|
||||
if (in_array($normalized, ['1', 'true', 'yes', 'y', 'on'], true)) {
|
||||
return true;
|
||||
}
|
||||
if (in_array($normalized, ['0', 'false', 'no', 'n', 'off'], true)) {
|
||||
return false;
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
function read_params(): array
|
||||
{
|
||||
$params = [];
|
||||
if (!empty($_GET)) {
|
||||
$params = array_merge($params, $_GET);
|
||||
}
|
||||
if (!empty($_POST)) {
|
||||
$params = array_merge($params, $_POST);
|
||||
}
|
||||
|
||||
$get_raw = getenv('GET') ?: getenv('get') ?: '';
|
||||
if ($get_raw !== '') {
|
||||
parse_str($get_raw, $parsed);
|
||||
$params = array_merge($parsed, $params);
|
||||
}
|
||||
|
||||
$qs = $_SERVER['QUERY_STRING'] ?? '';
|
||||
if ($qs !== '') {
|
||||
parse_str($qs, $parsed);
|
||||
$params = array_merge($parsed, $params);
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
function load_settings(string $path): array
|
||||
{
|
||||
$settings = [
|
||||
'BB_PATH' => '/opt/bb',
|
||||
'BORG_VARIABLES_FILE' => '/opt/bb/bbvars.sh',
|
||||
'ADMIN_MODE_ENABLE' => 'TRUE',
|
||||
'RESULTS_PER_PAGE' => '10',
|
||||
'DEBUG_MODE' => 'FALSE',
|
||||
];
|
||||
if (!is_readable($path)) {
|
||||
return $settings;
|
||||
}
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES);
|
||||
if ($lines === false) {
|
||||
return $settings;
|
||||
}
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^(BB_PATH|BORG_VARIABLES_FILE|ADMIN_MODE_ENABLE|RESULTS_PER_PAGE|DEBUG_MODE)=(.*)$/', $line, $m)) {
|
||||
$settings[$m[1]] = parse_sh_value($m[2]);
|
||||
}
|
||||
}
|
||||
return $settings;
|
||||
}
|
||||
|
||||
function load_borg_vars(string $path): array
|
||||
{
|
||||
$vars = [
|
||||
'BORG_PASSPHRASE' => null,
|
||||
'REPO' => null,
|
||||
];
|
||||
if (!is_readable($path)) {
|
||||
return $vars;
|
||||
}
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES);
|
||||
if ($lines === false) {
|
||||
return $vars;
|
||||
}
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^(?:export\\s+)?(BORG_PASSPHRASE|REPO)=(.*)$/', $line, $m)) {
|
||||
$vars[$m[1]] = parse_sh_value($m[2]);
|
||||
}
|
||||
}
|
||||
return $vars;
|
||||
}
|
||||
|
||||
function load_user_skin(string $user): string
|
||||
{
|
||||
$path = '/usr/local/directadmin/data/users/' . $user . '/user.conf';
|
||||
if (!is_readable($path)) {
|
||||
return 'enhanced';
|
||||
}
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES);
|
||||
if ($lines === false) {
|
||||
return 'enhanced';
|
||||
}
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^skin=(.+)$/', $line, $m)) {
|
||||
$skin = strtolower(trim($m[1]));
|
||||
return $skin !== '' ? $skin : 'enhanced';
|
||||
}
|
||||
}
|
||||
return 'enhanced';
|
||||
}
|
||||
|
||||
function load_job_vars(string $path): array
|
||||
{
|
||||
$vars = [
|
||||
'DA_USER' => '',
|
||||
'REPONAME' => '',
|
||||
'USERRESTOREPATH' => '',
|
||||
'TARGETPATH' => '',
|
||||
'RESTOREMODE' => '',
|
||||
'RESTORETYPE' => '',
|
||||
'START_TS' => '',
|
||||
];
|
||||
if (!is_readable($path)) {
|
||||
return $vars;
|
||||
}
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES);
|
||||
if ($lines === false) {
|
||||
return $vars;
|
||||
}
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^(DA_USER|REPONAME|USERRESTOREPATH|TARGETPATH|RESTOREMODE|RESTORETYPE|START_TS|USERLANG)=(.*)$/', $line, $m)) {
|
||||
$vars[$m[1]] = parse_sh_value($m[2]);
|
||||
}
|
||||
}
|
||||
return $vars;
|
||||
}
|
||||
|
||||
function job_id_from_file(string $file): string
|
||||
{
|
||||
$base = basename($file);
|
||||
if (substr($base, -3) === '.ok') {
|
||||
$base = substr($base, 0, -3);
|
||||
} elseif (substr($base, -5) === '.fail') {
|
||||
$base = substr($base, 0, -5);
|
||||
} elseif (substr($base, -7) === '.cancel') {
|
||||
$base = substr($base, 0, -7);
|
||||
}
|
||||
if (substr($base, -4) === '.env') {
|
||||
$base = substr($base, 0, -4);
|
||||
}
|
||||
return $base;
|
||||
}
|
||||
|
||||
function find_job_file(string $job_base, string $file, string $job_id): string
|
||||
{
|
||||
$candidates = [
|
||||
$job_base . '/processing/' . $file,
|
||||
$job_base . '/pending/' . $file,
|
||||
$job_base . '/done/' . $file,
|
||||
];
|
||||
if ($job_id !== '') {
|
||||
$candidates[] = $job_base . '/processing/' . $job_id . '.env';
|
||||
$candidates[] = $job_base . '/pending/' . $job_id . '.env';
|
||||
$candidates[] = $job_base . '/done/' . $job_id . '.ok';
|
||||
$candidates[] = $job_base . '/done/' . $job_id . '.fail';
|
||||
$candidates[] = $job_base . '/done/' . $job_id . '.cancel';
|
||||
$candidates[] = $job_base . '/done/' . $job_id . '.env.ok';
|
||||
$candidates[] = $job_base . '/done/' . $job_id . '.env.fail';
|
||||
$candidates[] = $job_base . '/done/' . $job_id . '.env.cancel';
|
||||
}
|
||||
foreach ($candidates as $path) {
|
||||
if (is_file($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function read_log_tail(string $path, int $max_bytes = 200000): string
|
||||
{
|
||||
if (!is_readable($path)) {
|
||||
return '';
|
||||
}
|
||||
$size = filesize($path);
|
||||
if ($size === false || $size <= $max_bytes) {
|
||||
return (string)file_get_contents($path);
|
||||
}
|
||||
$fp = fopen($path, 'rb');
|
||||
if ($fp === false) {
|
||||
return '';
|
||||
}
|
||||
fseek($fp, -$max_bytes, SEEK_END);
|
||||
$data = fread($fp, $max_bytes);
|
||||
fclose($fp);
|
||||
return (string)$data;
|
||||
}
|
||||
|
||||
function normalize_log(string $text): string
|
||||
{
|
||||
$text = str_replace("\r", "\n", $text);
|
||||
$lines = preg_split('/\n/', $text);
|
||||
if (!is_array($lines)) {
|
||||
return $text;
|
||||
}
|
||||
$filtered = [];
|
||||
foreach ($lines as $line) {
|
||||
$trimmed = trim($line);
|
||||
if ($trimmed !== '' && stripos($trimmed, 'Permanently added') !== false && stripos($trimmed, 'known host') !== false) {
|
||||
continue;
|
||||
}
|
||||
$filtered[] = $line;
|
||||
}
|
||||
return implode("\n", $filtered);
|
||||
}
|
||||
|
||||
function last_non_empty_line(string $text): string
|
||||
{
|
||||
$lines = preg_split('/\r\n|\n|\r/', $text);
|
||||
if (!is_array($lines)) {
|
||||
return '';
|
||||
}
|
||||
for ($i = count($lines) - 1; $i >= 0; $i--) {
|
||||
$line = trim((string)$lines[$i]);
|
||||
if ($line !== '') {
|
||||
return $line;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function is_calculating_line(string $line): bool
|
||||
{
|
||||
return (bool)preg_match('/calculat/i', $line);
|
||||
}
|
||||
|
||||
function extract_progress(string $line): ?int
|
||||
{
|
||||
if (preg_match('/\b(\d{1,3})%\b/', $line, $m)) {
|
||||
$p = (int)$m[1];
|
||||
if ($p < 0) {
|
||||
$p = 0;
|
||||
} elseif ($p > 100) {
|
||||
$p = 100;
|
||||
}
|
||||
return $p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function get_job_status(string $job_id, string $job_base): string
|
||||
{
|
||||
if ($job_id === '') {
|
||||
return 'unknown';
|
||||
}
|
||||
if (is_file($job_base . '/processing/' . $job_id . '.env')) {
|
||||
return 'processing';
|
||||
}
|
||||
if (is_file($job_base . '/pending/' . $job_id . '.env')) {
|
||||
return 'pending';
|
||||
}
|
||||
if (is_file($job_base . '/done/' . $job_id . '.ok') || is_file($job_base . '/done/' . $job_id . '.env.ok')) {
|
||||
return 'done';
|
||||
}
|
||||
if (is_file($job_base . '/done/' . $job_id . '.fail') || is_file($job_base . '/done/' . $job_id . '.env.fail')) {
|
||||
return 'failed';
|
||||
}
|
||||
if (is_file($job_base . '/done/' . $job_id . '.cancel') || is_file($job_base . '/done/' . $job_id . '.env.cancel')) {
|
||||
return 'canceled';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function display_status(string $base_status, string $last_line): string
|
||||
{
|
||||
if ($base_status === 'processing' && $last_line !== '' && is_calculating_line($last_line)) {
|
||||
return 'calculating';
|
||||
}
|
||||
return $base_status;
|
||||
}
|
||||
|
||||
function normalize_admin_status(string $status): string
|
||||
{
|
||||
if (in_array($status, ['pending', 'processing', 'calculating'], true)) {
|
||||
return 'processing';
|
||||
}
|
||||
if ($status === 'done') {
|
||||
return 'done';
|
||||
}
|
||||
if (in_array($status, ['failed', 'canceled'], true)) {
|
||||
return 'failed';
|
||||
}
|
||||
return 'failed';
|
||||
}
|
||||
|
||||
function admin_status_label(string $status): string
|
||||
{
|
||||
if ($status === 'done') {
|
||||
return 'Zakończone';
|
||||
}
|
||||
if ($status === 'processing') {
|
||||
return 'W trakcie';
|
||||
}
|
||||
return 'Błąd';
|
||||
}
|
||||
|
||||
function admin_status_sort(string $status): int
|
||||
{
|
||||
if ($status === 'processing') {
|
||||
return 1;
|
||||
}
|
||||
if ($status === 'done') {
|
||||
return 2;
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
|
||||
function restore_mode_label(string $mode): string
|
||||
{
|
||||
if ($mode === 'overwrite') {
|
||||
return 'Nadpisz aktualne dane';
|
||||
}
|
||||
if ($mode === 'update') {
|
||||
return 'Scalenie';
|
||||
}
|
||||
if ($mode === 'location') {
|
||||
return 'Przywracanie do lokalizacji';
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
function restore_type_label(string $type): string
|
||||
{
|
||||
if ($type === 'mail') {
|
||||
return 'Zawartość skrzynki pocztowej';
|
||||
}
|
||||
if ($type === 'web') {
|
||||
return 'Pliki www';
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
function display_restore_path(string $value): string
|
||||
{
|
||||
$trimmed = trim($value);
|
||||
if ($trimmed === '') {
|
||||
return '-';
|
||||
}
|
||||
if ($trimmed[0] !== '/') {
|
||||
$trimmed = '/' . $trimmed;
|
||||
}
|
||||
return $trimmed;
|
||||
}
|
||||
|
||||
function log_missing_include_explanation(string $log_text, string $restore_path_label = ''): string
|
||||
{
|
||||
if (stripos($log_text, 'W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki') !== false) {
|
||||
return '';
|
||||
}
|
||||
if (stripos($log_text, 'The specified restore path was not found in the backup') !== false) {
|
||||
return '';
|
||||
}
|
||||
if (preg_match("/Include pattern '.*' never matched\\./i", $log_text)) {
|
||||
$suffix = '';
|
||||
if ($restore_path_label !== '' && $restore_path_label !== '-') {
|
||||
$suffix = ' (' . $restore_path_label . ')';
|
||||
}
|
||||
return 'W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki' . $suffix . '. Prosimy spróbować przywrócić dane z innego archiwum, w którym te pliki mogły się znajdować.';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function collect_jobs(string $job_base, string $log_dir): array
|
||||
{
|
||||
$rows = [];
|
||||
$sources = [
|
||||
['dir' => $job_base . '/pending', 'pattern' => '*.env', 'status' => 'pending'],
|
||||
['dir' => $job_base . '/processing', 'pattern' => '*.env', 'status' => 'processing'],
|
||||
['dir' => $job_base . '/done', 'pattern' => '*.ok', 'status' => 'done'],
|
||||
['dir' => $job_base . '/done', 'pattern' => '*.env.ok', 'status' => 'done'],
|
||||
['dir' => $job_base . '/done', 'pattern' => '*.fail', 'status' => 'failed'],
|
||||
['dir' => $job_base . '/done', 'pattern' => '*.env.fail', 'status' => 'failed'],
|
||||
['dir' => $job_base . '/done', 'pattern' => '*.cancel', 'status' => 'canceled'],
|
||||
['dir' => $job_base . '/done', 'pattern' => '*.env.cancel', 'status' => 'canceled'],
|
||||
];
|
||||
|
||||
foreach ($sources as $source) {
|
||||
foreach (glob($source['dir'] . '/' . $source['pattern']) ?: [] as $path) {
|
||||
$file = basename($path);
|
||||
if (isset($rows[$file])) {
|
||||
continue;
|
||||
}
|
||||
$vars = load_job_vars($path);
|
||||
$job_id = job_id_from_file($file);
|
||||
if ($job_id === '') {
|
||||
continue;
|
||||
}
|
||||
$log_path = $log_dir . '/' . $job_id . '.log';
|
||||
$base_status = $source['status'];
|
||||
if ($base_status === 'processing' && is_readable($log_path)) {
|
||||
$tail = normalize_log(read_log_tail($log_path, 20000));
|
||||
$last = last_non_empty_line($tail);
|
||||
$base_status = display_status($base_status, $last);
|
||||
}
|
||||
$status = normalize_admin_status($base_status);
|
||||
$start_ts = is_numeric($vars['START_TS'] ?? '') ? (int)$vars['START_TS'] : (int)(@filemtime($path) ?: 0);
|
||||
if ($start_ts <= 0) {
|
||||
$start_ts = time();
|
||||
}
|
||||
|
||||
$rows[$file] = [
|
||||
'file' => $file,
|
||||
'job_id' => $job_id,
|
||||
'user' => (string)($vars['DA_USER'] ?? ''),
|
||||
'start_ts' => $start_ts,
|
||||
'start_text' => date('Y-m-d H:i:s', $start_ts),
|
||||
'type' => (string)($vars['RESTORETYPE'] ?? ''),
|
||||
'type_label' => restore_type_label((string)($vars['RESTORETYPE'] ?? '')),
|
||||
'mode' => (string)($vars['RESTOREMODE'] ?? ''),
|
||||
'mode_label' => restore_mode_label((string)($vars['RESTOREMODE'] ?? '')),
|
||||
'status' => $status,
|
||||
'status_label' => admin_status_label($status),
|
||||
'status_sort' => admin_status_sort($status),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($rows);
|
||||
}
|
||||
|
||||
function compare_rows(array $a, array $b, string $sort, string $dir): int
|
||||
{
|
||||
$cmp = 0;
|
||||
switch ($sort) {
|
||||
case 'user':
|
||||
$cmp = strcasecmp((string)$a['user'], (string)$b['user']);
|
||||
break;
|
||||
case 'type':
|
||||
$cmp = strcasecmp((string)$a['type_label'], (string)$b['type_label']);
|
||||
break;
|
||||
case 'mode':
|
||||
$cmp = strcasecmp((string)$a['mode_label'], (string)$b['mode_label']);
|
||||
break;
|
||||
case 'status':
|
||||
$cmp = ((int)$a['status_sort']) <=> ((int)$b['status_sort']);
|
||||
break;
|
||||
case 'start':
|
||||
default:
|
||||
$cmp = ((int)$a['start_ts']) <=> ((int)$b['start_ts']);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($cmp === 0) {
|
||||
$cmp = ((int)$a['start_ts']) <=> ((int)$b['start_ts']);
|
||||
}
|
||||
if ($cmp === 0) {
|
||||
$cmp = strcmp((string)$a['job_id'], (string)$b['job_id']);
|
||||
}
|
||||
|
||||
return $dir === 'asc' ? $cmp : -$cmp;
|
||||
}
|
||||
|
||||
function build_admin_url(array $base, array $override = []): string
|
||||
{
|
||||
$query = array_merge($base, $override);
|
||||
foreach ($query as $key => $value) {
|
||||
if ($value === null || $value === '') {
|
||||
unset($query[$key]);
|
||||
}
|
||||
}
|
||||
$qs = http_build_query($query);
|
||||
return '/CMD_PLUGINS_ADMIN/borg-restore/index.html' . ($qs !== '' ? ('?' . $qs) : '');
|
||||
}
|
||||
|
||||
$params = read_params();
|
||||
$action = (string)($params['action'] ?? '');
|
||||
|
||||
$username = getenv('USERNAME');
|
||||
if ($username === false || $username === '') {
|
||||
$username = getenv('LOGNAME') ?: getenv('USER') ?: 'admin';
|
||||
}
|
||||
|
||||
$plugin_dir = realpath(__DIR__ . '/..') ?: '/usr/local/directadmin/plugins/borg-restore';
|
||||
$settings_path = $plugin_dir . '/plugin-settings.conf';
|
||||
$settings = load_settings($settings_path);
|
||||
|
||||
$admin_mode_enabled = parse_bool((string)($settings['ADMIN_MODE_ENABLE'] ?? 'TRUE'), true);
|
||||
$debug_mode = parse_bool((string)($settings['DEBUG_MODE'] ?? 'FALSE'), false);
|
||||
$bb_path = trim((string)($settings['BB_PATH'] ?? '/opt/bb'));
|
||||
$borg_vars_file = trim((string)($settings['BORG_VARIABLES_FILE'] ?? '/opt/bb/bbvars.sh'));
|
||||
$bbvars_path = $borg_vars_file !== '' ? $borg_vars_file : $bb_path;
|
||||
if ($bbvars_path !== '' && substr($bbvars_path, -3) !== '.sh') {
|
||||
$bbvars_path = rtrim($bbvars_path, '/') . '/bbvars.sh';
|
||||
}
|
||||
$bbvars = load_borg_vars($bbvars_path);
|
||||
$admin_missing_passphrase = $debug_mode && is_readable($bbvars_path) && trim((string)($bbvars['BORG_PASSPHRASE'] ?? '')) === '';
|
||||
$results_per_page = (int)trim((string)($settings['RESULTS_PER_PAGE'] ?? '10'));
|
||||
if ($results_per_page <= 0) {
|
||||
$results_per_page = 10;
|
||||
}
|
||||
if ($results_per_page > 200) {
|
||||
$results_per_page = 200;
|
||||
}
|
||||
|
||||
$skin = load_user_skin($username);
|
||||
$css_file = $skin === 'evolution' ? 'evolution.css' : 'enhanced.css';
|
||||
$inline_css = '';
|
||||
$css_path = $plugin_dir . '/user/' . $css_file;
|
||||
if (is_readable($css_path)) {
|
||||
$inline_css = (string)file_get_contents($css_path);
|
||||
}
|
||||
|
||||
$job_base = '/usr/local/directadmin/plugins/borg-restore/data/jobs';
|
||||
$log_dir = '/usr/local/directadmin/plugins/borg-restore/data/logs';
|
||||
|
||||
$allowed_sorts = ['user', 'start', 'type', 'mode', 'status'];
|
||||
$sort = strtolower((string)($params['sort'] ?? 'start'));
|
||||
if (!in_array($sort, $allowed_sorts, true)) {
|
||||
$sort = 'start';
|
||||
}
|
||||
$dir = strtolower((string)($params['dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc';
|
||||
$page = (int)($params['page'] ?? 1);
|
||||
if ($page < 1) {
|
||||
$page = 1;
|
||||
}
|
||||
|
||||
$base_link_params = ['sort' => $sort, 'dir' => $dir, 'page' => $page];
|
||||
$jobs = [];
|
||||
$total = 0;
|
||||
$total_pages = 1;
|
||||
$page_start = 0;
|
||||
$page_end = 0;
|
||||
$page_jobs = [];
|
||||
|
||||
if ($admin_mode_enabled) {
|
||||
$jobs = collect_jobs($job_base, $log_dir);
|
||||
usort($jobs, static function (array $a, array $b) use ($sort, $dir): int {
|
||||
return compare_rows($a, $b, $sort, $dir);
|
||||
});
|
||||
|
||||
$total = count($jobs);
|
||||
$total_pages = max(1, (int)ceil($total / $results_per_page));
|
||||
if ($page > $total_pages) {
|
||||
$page = $total_pages;
|
||||
$base_link_params['page'] = $page;
|
||||
}
|
||||
$offset = ($page - 1) * $results_per_page;
|
||||
$page_jobs = array_slice($jobs, $offset, $results_per_page);
|
||||
if ($total > 0) {
|
||||
$page_start = $offset + 1;
|
||||
$page_end = min($offset + count($page_jobs), $total);
|
||||
}
|
||||
}
|
||||
|
||||
$view_log = null;
|
||||
if ($admin_mode_enabled && $action === 'view_log') {
|
||||
$file = basename((string)($params['job'] ?? ''));
|
||||
if ($file !== '' && preg_match('/^[-A-Za-z0-9_.]+$/', $file)) {
|
||||
$job_id = job_id_from_file($file);
|
||||
$job_file_path = find_job_file($job_base, $file, $job_id);
|
||||
if ($job_file_path !== '') {
|
||||
$vars = load_job_vars($job_file_path);
|
||||
$log_path = $log_dir . '/' . $job_id . '.log';
|
||||
$log_message = is_readable($log_path) ? normalize_log((string)file_get_contents($log_path)) : 'Log nie istnieje.';
|
||||
$restore_path_label = display_restore_path((string)($vars['USERRESTOREPATH'] ?? ''));
|
||||
$explanation = log_missing_include_explanation($log_message, $restore_path_label);
|
||||
if ($explanation !== '') {
|
||||
$log_message = rtrim($log_message) . "\n\n" . $explanation;
|
||||
}
|
||||
|
||||
$base_status = get_job_status($job_id, $job_base);
|
||||
$last_line = last_non_empty_line($log_message);
|
||||
$progress = extract_progress($last_line);
|
||||
$display = display_status($base_status, $last_line);
|
||||
$status = normalize_admin_status($display);
|
||||
if ($status === 'done') {
|
||||
$progress_text = '100%';
|
||||
} elseif ($status === 'failed') {
|
||||
$progress_text = '0%';
|
||||
} else {
|
||||
$progress_text = $progress !== null ? ($progress . '%') : ($last_line !== '' ? $last_line : '-');
|
||||
}
|
||||
|
||||
$start_ts = is_numeric($vars['START_TS'] ?? '') ? (int)$vars['START_TS'] : (int)(@filemtime($job_file_path) ?: 0);
|
||||
if ($start_ts <= 0) {
|
||||
$start_ts = time();
|
||||
}
|
||||
|
||||
$view_log = [
|
||||
'job' => $file,
|
||||
'user' => (string)($vars['DA_USER'] ?? '-'),
|
||||
'start' => date('Y-m-d H:i:s', $start_ts),
|
||||
'type' => restore_type_label((string)($vars['RESTORETYPE'] ?? '')),
|
||||
'mode' => restore_mode_label((string)($vars['RESTOREMODE'] ?? '')),
|
||||
'path' => $restore_path_label,
|
||||
'target' => display_restore_path((string)($vars['TARGETPATH'] ?? '')),
|
||||
'status' => admin_status_label($status),
|
||||
'progress' => $progress_text,
|
||||
'content' => $log_message,
|
||||
];
|
||||
} else {
|
||||
$view_log = [
|
||||
'error' => 'Nie znaleziono wskazanego zadania.',
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$view_log = [
|
||||
'error' => 'Nieprawidłowy identyfikator zadania.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$sort_labels = [
|
||||
'user' => 'Nazwa użytkownika',
|
||||
'start' => 'Data rozpoczęcia',
|
||||
'type' => 'Rodzaj przywracanych danych',
|
||||
'mode' => 'Tryb przywracania',
|
||||
'status' => 'Status',
|
||||
];
|
||||
|
||||
$sort_arrow = static function (string $field) use ($sort, $dir): string {
|
||||
if ($field !== $sort) {
|
||||
return '';
|
||||
}
|
||||
return $dir === 'asc' ? ' ▲' : ' ▼';
|
||||
};
|
||||
|
||||
$sort_link = static function (string $field) use ($sort, $dir, $base_link_params): string {
|
||||
$next_dir = ($sort === $field && $dir === 'asc') ? 'desc' : 'asc';
|
||||
return build_admin_url($base_link_params, ['sort' => $field, 'dir' => $next_dir, 'page' => 1]);
|
||||
};
|
||||
|
||||
$view_back_url = build_admin_url($base_link_params, ['action' => null, 'job' => null]);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>BorgBackup - Przywracanie plików (Admin)</title>
|
||||
<?php if ($inline_css !== '') { ?>
|
||||
<style><?php echo $inline_css; ?></style>
|
||||
<?php } ?>
|
||||
<style>
|
||||
.br-admin-subtitle { margin: 0 0 8px; color: var(--br-muted); }
|
||||
.br-sort-link { color: inherit; text-decoration: none; }
|
||||
.br-sort-link:hover { text-decoration: underline; }
|
||||
.br-col-log { width: 130px; text-align: center; }
|
||||
.br-pagination {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.br-pagination .br-page-meta { color: var(--br-muted); font-size: 13px; }
|
||||
.br-pagination .br-page-actions { display: flex; gap: 8px; }
|
||||
.br-log-content { max-height: 58vh; overflow: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="br-container">
|
||||
<div class="br-header">
|
||||
<div class="br-title">BorgBackup - Przywracanie plików (Admin)</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$admin_mode_enabled) { ?>
|
||||
<div class="br-card">
|
||||
<div class="br-alert br-alert-warn">Poziom administratora został wyłączony</div>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
|
||||
<?php if ($view_log !== null) { ?>
|
||||
<div class="br-card">
|
||||
<div class="br-actions" style="justify-content:flex-start; margin-bottom: 10px;">
|
||||
<a class="br-btn br-btn-ghost" href="<?php echo h($view_back_url); ?>">Powrót do zestawienia</a>
|
||||
</div>
|
||||
<?php if (!empty($view_log['error'])) { ?>
|
||||
<div class="br-alert br-alert-error"><?php echo h((string)$view_log['error']); ?></div>
|
||||
<?php } else { ?>
|
||||
<h3 class="m-0" style="margin-bottom:10px;">Log zadania</h3>
|
||||
<div class="br-log-meta">
|
||||
<div><strong>Nazwa użytkownika:</strong> <?php echo h((string)$view_log['user']); ?></div>
|
||||
<div><strong>Data rozpoczęcia:</strong> <?php echo h((string)$view_log['start']); ?></div>
|
||||
<div><strong>Rodzaj przywracanych danych:</strong> <?php echo h((string)$view_log['type']); ?></div>
|
||||
<div><strong>Tryb przywracania:</strong> <?php echo h((string)$view_log['mode']); ?></div>
|
||||
<div><strong>Ścieżka do przywrócenia:</strong> <?php echo h((string)$view_log['path']); ?></div>
|
||||
<div><strong>Lokalizacja docelowa:</strong> <?php echo h((string)$view_log['target']); ?></div>
|
||||
</div>
|
||||
<div class="br-status-line">Status: <strong><?php echo h((string)$view_log['status']); ?></strong></div>
|
||||
<div class="br-status-line">Postęp: <strong><?php echo h((string)$view_log['progress']); ?></strong></div>
|
||||
<pre class="br-log-content" id="log-content"><?php echo h((string)$view_log['content']); ?></pre>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="br-card">
|
||||
<h3 style="margin-bottom:6px;">Zestawienie operacji przywracania</h3>
|
||||
<p class="br-admin-subtitle">Wyniki na stronę: <?php echo h((string)$results_per_page); ?></p>
|
||||
<?php if ($admin_missing_passphrase) { ?>
|
||||
<div class="br-alert br-alert-warn">
|
||||
Brak zmiennej <strong>BORG_PASSPHRASE</strong> w pliku <?php echo h($bbvars_path); ?>.
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<?php if (empty($jobs)) { ?>
|
||||
<p class="br-muted">Brak operacji do wyświetlenia.</p>
|
||||
<?php } else { ?>
|
||||
<table class="br-table" border="1" cellpadding="6" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<?php foreach ($sort_labels as $field => $label) { ?>
|
||||
<th>
|
||||
<a class="br-sort-link" href="<?php echo h($sort_link($field)); ?>">
|
||||
<?php echo h($label . $sort_arrow($field)); ?>
|
||||
</a>
|
||||
</th>
|
||||
<?php } ?>
|
||||
<th class="br-col-log">Logi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($page_jobs as $job) { ?>
|
||||
<tr>
|
||||
<td><?php echo h((string)$job['user']); ?></td>
|
||||
<td><?php echo h((string)$job['start_text']); ?></td>
|
||||
<td><?php echo h((string)$job['type_label']); ?></td>
|
||||
<td><?php echo h((string)$job['mode_label']); ?></td>
|
||||
<td><?php echo h((string)$job['status_label']); ?></td>
|
||||
<td class="br-col-log">
|
||||
<a class="br-btn br-btn-ghost br-btn-small" href="<?php echo h(build_admin_url($base_link_params, ['action' => 'view_log', 'job' => (string)$job['file']])); ?>">Pokaż logi</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="br-pagination">
|
||||
<div class="br-page-meta">
|
||||
<?php echo h('Strona ' . $page . ' z ' . $total_pages . ' (wyniki ' . $page_start . '-' . $page_end . ' z ' . $total . ')'); ?>
|
||||
</div>
|
||||
<div class="br-page-actions">
|
||||
<?php if ($page > 1) { ?>
|
||||
<a class="br-btn br-btn-ghost br-btn-small" href="<?php echo h(build_admin_url($base_link_params, ['page' => $page - 1])); ?>">Poprzednia</a>
|
||||
<?php } ?>
|
||||
<?php if ($page < $total_pages) { ?>
|
||||
<a class="br-btn br-btn-ghost br-btn-small" href="<?php echo h(build_admin_url($base_link_params, ['page' => $page + 1])); ?>">Następna</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
<a href="/CMD_PLUGINS_ADMIN/borg-restore/index.html" target="_top"><img src="/CMD_PLUGINS_ADMIN/borg-restore/images/admin_icon.svg">BorgBackup - Przywracanie plików (Admin)<br></a>
|
||||
@@ -0,0 +1 @@
|
||||
<a href="/CMD_PLUGINS_ADMIN/borg-restore/index.html" target="_top">BorgBackup - Przywracanie plików (Admin)</a>
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
|
||||
user="${USERNAME:-${LOGNAME:-${USER:-}}}"
|
||||
lang="${LANGUAGE:-}"
|
||||
|
||||
normalize_lang() {
|
||||
val="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
|
||||
case "$val" in
|
||||
pl*|polish*) echo "pl" ;;
|
||||
en*|english*) echo "en" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
lang="$(normalize_lang "$lang")"
|
||||
if [ -z "$lang" ] && [ -n "$user" ] && [ -r "/usr/local/directadmin/data/users/$user/user.conf" ]; then
|
||||
cfg_lang="$(grep -i -m1 -E '^(lang|language|locale)=' "/usr/local/directadmin/data/users/$user/user.conf" | cut -d= -f2-)"
|
||||
lang="$(normalize_lang "$cfg_lang")"
|
||||
fi
|
||||
|
||||
if [ "$lang" = "pl" ]; then
|
||||
label="BorgBackup - Przywracanie plików"
|
||||
else
|
||||
label="BorgBackup - File Restore"
|
||||
fi
|
||||
|
||||
printf '[{"name":"%s","icon":"","entries":[{"name":"%s","icon":"","href":"/CMD_PLUGINS/borg-restore","newTab":false,"updates":0}]}]\n' "$label" "$label"
|
||||
@@ -0,0 +1 @@
|
||||
<a href="/CMD_PLUGINS/borg-restore">BorgBackup - Przywracanie plików</a>
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
PLUGIN_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SCRIPT="$PLUGIN_DIR/scripts/install.sh"
|
||||
|
||||
if [ ! -x "$SCRIPT" ]; then
|
||||
chmod 755 "$SCRIPT" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
exec "$SCRIPT"
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
return [
|
||||
'BorgBackup - Przywracanie Plików' => 'BorgBackup - File Restore',
|
||||
'Lista backupow (borg list)' => 'Backup list (borg list)',
|
||||
'Status zadań' => 'Job status',
|
||||
'Nazwa' => 'Name',
|
||||
'Data' => 'Date',
|
||||
'Hash' => 'Hash',
|
||||
'PID' => 'PID',
|
||||
'Status' => 'Status',
|
||||
'Backup' => 'Backup',
|
||||
'Ścieżka do przywrócenia:' => 'Restore path:',
|
||||
'Rodzaj przywracanych danych' => 'Type of data to restore',
|
||||
'Rodzaj przywracanych danych:' => 'Type of data to restore:',
|
||||
'Odzyskiwanie plików stron www' => 'Restore website files',
|
||||
'Odzyskiwanie zawartości skrzynki pocztowej' => 'Restore mailbox contents',
|
||||
'Pliki www' => 'Website files',
|
||||
'Zawartość skrzynki pocztowej' => 'Mailbox contents',
|
||||
'Tryb przywracania' => 'Restore mode',
|
||||
'Tryb przywracania:' => 'Restore mode:',
|
||||
'Tryb przywracania danych:' => 'Restore data mode:',
|
||||
'Przywracanie do lokalizacji' => 'Restore to location',
|
||||
'Połączenie danych kopii zapasowej z obecnymi danymi' => 'Merge backup data with existing data',
|
||||
'Scalenie' => 'Merge',
|
||||
'Nadpisz aktualne dane' => 'Overwrite existing data',
|
||||
'Przykłady użycia:' => 'Usage examples:',
|
||||
'Uwaga: znaki / z początku i końca linii zostaną usunięte.' => 'Note: leading and trailing / characters will be removed.',
|
||||
'Lokalizacja docelowa:' => 'Target location:',
|
||||
'Domyślna lokalizacja docelowa:' => 'Default target location:',
|
||||
'Przywróć' => 'Restore',
|
||||
'Brak wpisów do wyświetlenia.' => 'No entries to display.',
|
||||
'Odśwież' => 'Refresh',
|
||||
'Log zadania' => 'Job log',
|
||||
'Kopiuj do schowka' => 'Copy to clipboard',
|
||||
'Zamknij' => 'Close',
|
||||
'Podgląd na żywo (odświeżanie co 3s)' => 'Live view (refresh every 3s)',
|
||||
'Data rozpoczęcia:' => 'Start time:',
|
||||
'Status:' => 'Status:',
|
||||
'Postęp:' => 'Progress:',
|
||||
'Zapisz log' => 'Download log',
|
||||
'Wybierz lokalizację' => 'Select location',
|
||||
'Wybierz katalog' => 'Select directory',
|
||||
'Nowy katalog' => 'New directory',
|
||||
'Utwórz' => 'Create',
|
||||
'Anuluj' => 'Cancel',
|
||||
'Wybierz' => 'Select',
|
||||
'Zaznacz wszystkie' => 'Select all',
|
||||
'Usuń zaznaczone zdarzenia' => 'Delete selected entries',
|
||||
'Czy na pewno usunąć zaznaczone zdarzenia?' => 'Are you sure you want to delete the selected entries?',
|
||||
'Anulować zadanie?' => 'Cancel the job?',
|
||||
'Usunąć zadanie?' => 'Delete the job?',
|
||||
'Użytkownik' => 'User',
|
||||
'Ścieżka' => 'Path',
|
||||
'Cel' => 'Target',
|
||||
'Czas' => 'Time',
|
||||
'Akcje' => 'Actions',
|
||||
'Postęp' => 'Progress',
|
||||
'Logi' => 'Logs',
|
||||
'Usuń' => 'Delete',
|
||||
'Potwierdź nadpisanie danych' => 'Confirm data overwrite',
|
||||
'Potwierdź scalenie danych' => 'Confirm data merge',
|
||||
'Wybrano tryb nadpisywania danych. Istniejący katalog zostanie zastąpiony.' => 'Overwrite mode selected. The existing directory will be replaced.',
|
||||
'Wybrano tryb scalenia danych. Istniejący katalog zostanie połączony z danymi z backupu.' => 'Merge mode selected. The existing directory will be merged with backup data.',
|
||||
'DANE W KATALOGU ' => 'DATA IN DIRECTORY ',
|
||||
'ZOSTANĄ UTRACONE.' => 'WILL BE LOST.',
|
||||
'Tak, kontynuuj' => 'Yes, continue',
|
||||
'Uwaga:' => 'Warning:',
|
||||
'Brak zadań do wyświetlenia.' => 'No jobs to display.',
|
||||
'Brak zadań do wyczyszczenia.' => 'No jobs to clear.',
|
||||
'Zadania zostały wyczyszczone.' => 'Jobs have been cleared.',
|
||||
'Zadanie zostało anulowane.' => 'Job has been canceled.',
|
||||
'Zadanie zostało usunięte.' => 'Job has been deleted.',
|
||||
'Zadanie nie jest w trakcie.' => 'Job is not in progress.',
|
||||
'Wybrane zadania zostały usunięte.' => 'Selected jobs have been deleted.',
|
||||
'Oczekuje' => 'Pending',
|
||||
'W trakcie' => 'In progress',
|
||||
'Przygotowanie' => 'Preparing',
|
||||
'Zakończone' => 'Completed',
|
||||
'Nie udało się anulować zadania.' => 'Failed to cancel job.',
|
||||
'Nie znaleziono procesu do anulowania.' => 'No process found to cancel.',
|
||||
'Brak obsługi posix_kill na serwerze.' => 'posix_kill is not available on the server.',
|
||||
'Nie znaleziono zadania.' => 'Job not found.',
|
||||
'Nieprawidłowe zadanie.' => 'Invalid job.',
|
||||
'Nieprawidłowy identyfikator logu.' => 'Invalid log identifier.',
|
||||
'Nieprawidłowy identyfikator logu.\n' => "Invalid log identifier.\n",
|
||||
'Nieprawidłowy token CSRF.' => 'Invalid CSRF token.',
|
||||
'Nieprawidłowa nazwa katalogu.' => 'Invalid directory name.',
|
||||
'Katalog już istnieje.' => 'Directory already exists.',
|
||||
'Podaj nazwę katalogu.' => 'Provide a directory name.',
|
||||
'Nie udało się utworzyć katalogu.' => 'Failed to create directory.',
|
||||
'Nie udało się utworzyć katalogu zadań.' => 'Failed to create jobs directory.',
|
||||
'Nie udało się zapisać pliku zadania.' => 'Failed to write job file.',
|
||||
'Nie udało się wczytać katalogów.' => 'Failed to load directories.',
|
||||
'Nie udało się wysłać żądania.' => 'Failed to send request.',
|
||||
'Nie udało się przetworzyć odpowiedzi.' => 'Failed to process response.',
|
||||
'Nie udało się pobrać logu.' => 'Failed to download log.',
|
||||
'Nie udało się uruchomić borg.' => 'Failed to run borg.',
|
||||
'Nie podano ścieżki do przywrócenia.' => 'Restore path not provided.',
|
||||
'Nie podano lokalizacji docelowej.' => 'Target location not provided.',
|
||||
'Nie wybrano backupu.' => 'No backup selected.',
|
||||
'Nie wybrano żadnych zadań.' => 'No jobs selected.',
|
||||
'Brak katalogów' => 'No directories',
|
||||
'Brak wpisów do wyświetlenia.' => 'No entries to display.',
|
||||
'Brak dostępu' => 'Access denied',
|
||||
'Brak dostępu do zadania.' => 'No access to the job.',
|
||||
"Brak dostępu do logu.\n" => "No access to the log.\n",
|
||||
'Log nie istnieje.' => 'Log does not exist.',
|
||||
"Log nie istnieje.\n" => "Log does not exist.\n",
|
||||
'Kod bledu: ' => 'Error code: ',
|
||||
'Błąd borg list: ' => 'borg list error: ',
|
||||
'Błąd' => 'Error',
|
||||
'Anulowane' => 'Canceled',
|
||||
'Nieznany' => 'Unknown',
|
||||
'BB_PATH jest puste w plugin-settings.conf.' => 'BB_PATH is empty in plugin-settings.conf.',
|
||||
'BORG_PATH jest puste w plugin-settings.conf.' => 'BORG_PATH is empty in plugin-settings.conf.',
|
||||
'BORG_PATH nie jest plikiem wykonywalnym: ' => 'BORG_PATH is not executable: ',
|
||||
'BORG_SSH_KEY nie jest plikiem: ' => 'BORG_SSH_KEY is not a file: ',
|
||||
'BORG_VARIABLES_FILE jest puste w plugin-settings.conf.' => 'BORG_VARIABLES_FILE is empty in plugin-settings.conf.',
|
||||
'WEB_RESTORE_LOCATION jest puste w plugin-settings.conf.' => 'WEB_RESTORE_LOCATION is empty in plugin-settings.conf.',
|
||||
'MAIL_RESTORE_LOCATION jest puste w plugin-settings.conf.' => 'MAIL_RESTORE_LOCATION is empty in plugin-settings.conf.',
|
||||
'DEFAULT_RESTORE_DESTINATION jest puste w plugin-settings.conf.' => 'DEFAULT_RESTORE_DESTINATION is empty in plugin-settings.conf.',
|
||||
'Brak pliku plugin-settings.conf.' => 'Missing plugin-settings.conf file.',
|
||||
'Brak pliku bbvars.sh: ' => 'Missing bbvars.sh file: ',
|
||||
'Brak zmiennej REPO w ' => 'Missing REPO variable in ',
|
||||
'nie ustawiono BB_PATH' => 'BB_PATH not set',
|
||||
'BLOCK_RESTORE_OUTSIDE_LOCATION jest wyłączone - ścieżka do przywrócenia nie jest ograniczona.' => 'BLOCK_RESTORE_OUTSIDE_LOCATION is disabled - restore path is not restricted.',
|
||||
'Ścieżka nie może zawierać "..".' => 'Path cannot contain "..".',
|
||||
'Lokalizacja docelowa nie może zawierać "..".' => 'Target location cannot contain "..".',
|
||||
'Lokalizacja docelowa musi zaczynać się od /.' => 'Target location must start with /.',
|
||||
'Lokalizacja docelowa musi być w katalogu: ' => 'Target location must be inside: ',
|
||||
'Ścieżka musi być w katalogu: ' => 'Path must be inside: ',
|
||||
'Lokalizacja docelowa istnieje, ale nie jest katalogiem.' => 'Target location exists but is not a directory.',
|
||||
'Lokalizacja docelowa nie jest pusta.' => 'Target location is not empty.',
|
||||
'Nie można odczytać katalogu docelowego.' => 'Cannot read target directory.',
|
||||
'Nie można utworzyć katalogu poza dozwoloną ścieżką.' => 'Cannot create directory outside the allowed path.',
|
||||
'Nie można utworzyć katalogu bezpośrednio w ' => 'Cannot create directory directly in ',
|
||||
' w katalogu nadrzędnym ścieżki do przywrócenia.' => ' in the parent directory of the restore path.',
|
||||
'Nie można przywrócić całego ' => 'Cannot restore the entire ',
|
||||
' w trybie nadpisywania danych. Podaj podkatalog w ramach ' => ' in overwrite mode. Provide a subdirectory under ',
|
||||
' lub wybierz tryb przywracania do lokalizacji.' => ' or choose restore-to-location mode.',
|
||||
' w trybie nadpisywania lub scalania danych. Podaj podkatalog w ramach ' => ' in overwrite or merge mode. Provide a subdirectory under ',
|
||||
'Backup zostanie utworzony jako *_przed_przywroceniem_' => 'Backup will be created as *_before_restore_',
|
||||
'Backup zostanie utworzony jako: ' => 'Backup will be created as: ',
|
||||
'Nieprawidłowe uprawnienia plików konfiguracyjnych lub zadań. Przywracanie zablokowane.' => 'Invalid permissions for configuration or job files. Restore blocked.',
|
||||
'Problem z konfiguracją środowiska backupowego - skontaktuj się z administratorem w celu uzyskania pomocy' => 'Backup environment configuration problem — contact the administrator for help',
|
||||
'W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki' => 'The specified restore path was not found in the backup',
|
||||
'Prosimy spróbować przywrócić dane z innego archiwum, w którym te pliki mogły się znajdować.' => 'Please try restoring from another archive that may contain those files.',
|
||||
'Wyjaśnienie: wskazana ścieżka do przywrócenia' => 'Explanation: the specified restore path',
|
||||
'nie została odnaleziona w kopii zapasowej.' => 'was not found in the backup.',
|
||||
'----- logi Borg -----' => '----- Borg logs -----',
|
||||
'Przywracana ścieżka:' => 'Restored path:',
|
||||
'Zadanie przywracania danych dodane do kolejki, zostaniesz powiadomiony przez system wiadomości po zakończeniu tego procesu.' => 'Restore job queued; you will be notified by the message system when it completes.',
|
||||
'Plik zadania' => 'Job file',
|
||||
'⟵ W górę' => '⟵ Up',
|
||||
'Metoda niedozwolona.' => 'Method Not Allowed.',
|
||||
'Method Not Allowed.' => 'Method Not Allowed.',
|
||||
'ma nieprawidłowe uprawnienia: ' => 'has invalid permissions: ',
|
||||
'ma nieprawidłowego właściciela (' => 'has invalid owner (',
|
||||
'ma nieprawidłowego właściciela: ' => 'has invalid owner: ',
|
||||
'ma pustą ścieżkę.' => 'has an empty path.',
|
||||
'nie istnieje: ' => 'does not exist: ',
|
||||
'nie jest zwykłym plikiem: ' => 'is not a regular file: ',
|
||||
'nie może być zapisywalny dla grupy/innych: ' => 'cannot be group/other writable: ',
|
||||
];
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
$en = include __DIR__ . '/en.php';
|
||||
$pl = [];
|
||||
foreach ($en as $key => $_value) {
|
||||
$pl[$key] = $key;
|
||||
}
|
||||
return $pl;
|
||||
@@ -0,0 +1,47 @@
|
||||
# Ustawienia pluginu borg-restore.
|
||||
# BB_PATH - katalog z plikiem bbvars.sh (zawiera BORG_PASSPHRASE, REPO i opcjonalnie BORG_SSH_KEY).
|
||||
BB_PATH=/opt/bb
|
||||
# BORG_PATH - pełna ścieżka do binarki borg.
|
||||
BORG_PATH=/usr/bin/borg
|
||||
# BORG_VARIABLES_FILE - ścieżka do pliku z REPO/BORG_PASSPHRASE/BORG_SSH_KEY.
|
||||
BORG_VARIABLES_FILE=/opt/bb/bbvars.sh
|
||||
# ADMIN_MODE_ENABLE - TRUE włącza panel administratora z raportem przywraceń.
|
||||
ADMIN_MODE_ENABLE=TRUE
|
||||
# RESULTS_PER_PAGE - liczba rekordów na stronę w tabeli administratora.
|
||||
RESULTS_PER_PAGE=10
|
||||
# WEB_RESTORE_LOCATION - domyślna ścieżka bazowa dla przywracania stron www.
|
||||
WEB_RESTORE_LOCATION=/home/DA_USER/domains
|
||||
# MAIL_RESTORE_LOCATION - domyślna ścieżka bazowa dla przywracania poczty.
|
||||
MAIL_RESTORE_LOCATION=/home/DA_USER/imap
|
||||
# ENABLE_WEB_RESTORE - TRUE pozwala przywracać dane stron www.
|
||||
ENABLE_WEB_RESTORE=TRUE
|
||||
# ENABLE_MAILBOX_RESTORE - TRUE pozwala przywracać dane skrzynek pocztowych.
|
||||
ENABLE_MAILBOX_RESTORE=TRUE
|
||||
# DEFAULT_RESTORE_DESTINATION - domyślna lokalizacja docelowa dla trybu przywracania do lokalizacji.
|
||||
DEFAULT_RESTORE_DESTINATION=/home/DA_USER/bb
|
||||
# BLOCK_RESTORE_OUTSIDE_LOCATION - TRUE blokuje przywracanie poza WEB/MAIL_RESTORE_LOCATION.
|
||||
BLOCK_RESTORE_OUTSIDE_LOCATION=TRUE
|
||||
# PREVENT_RESTORE_MAIN_DOMAINS_DIR - TRUE blokuje przywrócenie całego katalogu WEB_RESTORE_LOCATION.
|
||||
PREVENT_RESTORE_MAIN_DOMAINS_DIR=TRUE
|
||||
# PREVENT_RESTORE_MAIN_IMAP_DIR - TRUE blokuje przywrócenie całego katalogu MAIL_RESTORE_LOCATION.
|
||||
PREVENT_RESTORE_MAIN_IMAP_DIR=TRUE
|
||||
# PREVENT_CREATE_DIRS_IN_MAIN_DOMAIN - TRUE blokuje tworzenie katalogów bezpośrednio w /home/DA_USER/domains.
|
||||
PREVENT_CREATE_DIRS_IN_MAIN_DOMAIN=TRUE
|
||||
# PREVENT_CREATE_DIRS_IN_MAIN_IMAP - TRUE blokuje tworzenie katalogów bezpośrednio w /home/DA_USER/imap.
|
||||
PREVENT_CREATE_DIRS_IN_MAIN_IMAP=TRUE
|
||||
# HASH_VISIBILITY - FALSE ukrywa kolumnę Hash w tabeli archiwów.
|
||||
HASH_VISIBILITY=FALSE
|
||||
# ENABLE_LOGS_OVERLAY - TRUE pokazuje logi jako overlay.
|
||||
ENABLE_LOGS_OVERLAY=TRUE
|
||||
# SELECTABLE_DOMAIN_RESTORE_DESTINATION - TRUE pozwala wybrać lokalizację docelową (www).
|
||||
SELECTABLE_DOMAIN_RESTORE_DESTINATION=TRUE
|
||||
# SELECTABLE_MAIL_RESTORE_DESTINATION - TRUE pozwala wybrać lokalizację docelową (poczta).
|
||||
SELECTABLE_MAIL_RESTORE_DESTINATION=TRUE
|
||||
# CLEAR_DOVECOT_INDEXES_AFTER_MERGE - TRUE usuwa indeksy Dovecota po scaleniu poczty.
|
||||
CLEAR_DOVECOT_INDEXES_AFTER_MERGE=TRUE
|
||||
# RESTORE_PATH_MODE - full (pełna ścieżka) lub dir (tylko ostatni katalog).
|
||||
RESTORE_PATH_MODE=dir
|
||||
# ADD_DATE_TO_RESTORED_DIR - TRUE dodaje datę do ostatniego katalogu w trybie przywrócenia do lokalizacji.
|
||||
ADD_DATE_TO_RESTORED_DIR=FALSE
|
||||
# DEBUG_MODE - TRUE pokazuje szczegółowe błędy i pełny output systemowy.
|
||||
DEBUG_MODE=FALSE
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
active=yes
|
||||
admin_run_as=root
|
||||
admin_widgets=
|
||||
author=HITME.PL
|
||||
id=borg-restore
|
||||
installed=yes
|
||||
menu_admin=
|
||||
menu_reseller=
|
||||
menu_user=
|
||||
name=Borg-restore
|
||||
reseller_run_as=
|
||||
reseller_widgets=
|
||||
timeout=
|
||||
type=user
|
||||
update_url=
|
||||
user_run_as=root
|
||||
user_widgets=
|
||||
version=2.3.3
|
||||
version_url=
|
||||
Executable
+129
@@ -0,0 +1,129 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
CONFIG_FILE="${PLUGIN_DIR}/plugin-settings.conf"
|
||||
DATA_DIR="${PLUGIN_DIR}/data"
|
||||
JOB_DIR="${DATA_DIR}/jobs"
|
||||
LOG_DIR="${DATA_DIR}/logs"
|
||||
PID_DIR="${DATA_DIR}/pids"
|
||||
CANCEL_DIR="${DATA_DIR}/cancel"
|
||||
TMP_DIR="${DATA_DIR}/tmp"
|
||||
CSRF_DIR="${DATA_DIR}/csrf"
|
||||
|
||||
warn() {
|
||||
printf '[fix_permissions] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
strip_quotes() {
|
||||
local v="$1"
|
||||
v="${v#"${v%%[![:space:]]*}"}"
|
||||
v="${v%"${v##*[![:space:]]}"}"
|
||||
if [[ "$v" == \"*\" && "$v" == *\" ]]; then
|
||||
v="${v:1:${#v}-2}"
|
||||
elif [[ "$v" == \'*\' && "$v" == *\' ]]; then
|
||||
v="${v:1:${#v}-2}"
|
||||
fi
|
||||
printf '%s' "$v"
|
||||
}
|
||||
|
||||
get_cfg() {
|
||||
local key="$1"
|
||||
local value=""
|
||||
if [ -r "$CONFIG_FILE" ]; then
|
||||
while IFS= read -r line; do
|
||||
line="${line%%#*}"
|
||||
line="$(strip_quotes "$line")"
|
||||
if [[ "$line" == "$key="* ]]; then
|
||||
value="${line#*=}"
|
||||
fi
|
||||
done < "$CONFIG_FILE"
|
||||
fi
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
owner_name="$(stat -c %U "$PLUGIN_DIR" 2>/dev/null || true)"
|
||||
if [ "$owner_name" != "root" ] && [ "$owner_name" != "diradmin" ]; then
|
||||
owner_name="root"
|
||||
fi
|
||||
owner_group="$owner_name"
|
||||
|
||||
fix_dir() {
|
||||
local path="$1"
|
||||
if [ -z "$path" ]; then
|
||||
return
|
||||
fi
|
||||
mkdir -p "$path"
|
||||
chown "$owner_name":"$owner_group" "$path" || true
|
||||
chmod 700 "$path" || true
|
||||
}
|
||||
|
||||
fix_file() {
|
||||
local path="$1"
|
||||
local label="$2"
|
||||
if [ -z "$path" ]; then
|
||||
return
|
||||
fi
|
||||
if [ ! -e "$path" ]; then
|
||||
warn "${label} missing: ${path}"
|
||||
return
|
||||
fi
|
||||
if [ -L "$path" ] || [ ! -f "$path" ]; then
|
||||
warn "${label} not a regular file: ${path}"
|
||||
return
|
||||
fi
|
||||
chown "$owner_name":"$owner_group" "$path" || true
|
||||
chmod 600 "$path" || true
|
||||
}
|
||||
|
||||
BB_PATH="$(get_cfg "BB_PATH")"
|
||||
BORG_VARIABLES_FILE="$(get_cfg "BORG_VARIABLES_FILE")"
|
||||
|
||||
if [ -z "$BB_PATH" ]; then
|
||||
BB_PATH="/opt/bb"
|
||||
fi
|
||||
|
||||
bbvars_path=""
|
||||
if [ -n "$BORG_VARIABLES_FILE" ]; then
|
||||
bbvars_path="${BORG_VARIABLES_FILE}"
|
||||
if [[ "$bbvars_path" == *DA_USER* ]]; then
|
||||
if [ -n "${DA_USER:-}" ]; then
|
||||
bbvars_path="${bbvars_path//DA_USER/${DA_USER}}"
|
||||
elif [ -f "/opt/bb/bbvars.sh" ]; then
|
||||
bbvars_path="/opt/bb/bbvars.sh"
|
||||
else
|
||||
bbvars_path=""
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [[ "$BB_PATH" == *.sh ]]; then
|
||||
bbvars_path="$BB_PATH"
|
||||
else
|
||||
bbvars_path="${BB_PATH%/}/bbvars.sh"
|
||||
fi
|
||||
fi
|
||||
|
||||
fix_dir "$DATA_DIR"
|
||||
fix_dir "$JOB_DIR/pending"
|
||||
fix_dir "$JOB_DIR/processing"
|
||||
fix_dir "$JOB_DIR/done"
|
||||
fix_dir "$LOG_DIR"
|
||||
fix_dir "$PID_DIR"
|
||||
fix_dir "$CANCEL_DIR"
|
||||
fix_dir "$TMP_DIR"
|
||||
fix_dir "$CSRF_DIR"
|
||||
|
||||
fix_file "$CONFIG_FILE" "plugin-settings.conf"
|
||||
if [ -n "$bbvars_path" ]; then
|
||||
fix_file "$bbvars_path" "bbvars.sh"
|
||||
else
|
||||
warn "bbvars.sh path not resolved; skipped"
|
||||
fi
|
||||
|
||||
for f in "$JOB_DIR"/pending/*.env "$JOB_DIR"/processing/*.env "$JOB_DIR"/done/*.{ok,fail,cancel,env.ok,env.fail,env.cancel}; do
|
||||
if [ -f "$f" ]; then
|
||||
fix_file "$f" "job file"
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
# Ensure expected permissions and ownership so DirectAdmin can update plugin.conf.
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
chown -R diradmin:diradmin "$PLUGIN_DIR" || true
|
||||
else
|
||||
echo "borg-restore: warning: not running as root, cannot chown to diradmin" >&2
|
||||
fi
|
||||
|
||||
chmod 755 "$PLUGIN_DIR" "$PLUGIN_DIR/hooks" "$PLUGIN_DIR/user" "$PLUGIN_DIR/scripts" "$PLUGIN_DIR/images" || true
|
||||
chmod 755 "$PLUGIN_DIR/user/index.html" "$PLUGIN_DIR/scripts/install.sh" "$PLUGIN_DIR/scripts/uninstall.sh" "$PLUGIN_DIR/scripts/restore.sh" "$PLUGIN_DIR/scripts/worker.sh" || true
|
||||
chmod 644 "$PLUGIN_DIR/plugin.conf" "$PLUGIN_DIR/hooks/user_txt.html" "$PLUGIN_DIR/plugin-settings.conf" "$PLUGIN_DIR/user/enhanced.css" "$PLUGIN_DIR/user/evolution.css" "$PLUGIN_DIR/images/user_icon.svg" || true
|
||||
|
||||
DATA_DIR="$PLUGIN_DIR/data"
|
||||
mkdir -p "$DATA_DIR/jobs/pending" "$DATA_DIR/jobs/processing" "$DATA_DIR/jobs/done" "$DATA_DIR/logs" "$DATA_DIR/pids" "$DATA_DIR/cancel"
|
||||
chmod 700 "$DATA_DIR" "$DATA_DIR/jobs" "$DATA_DIR/jobs/pending" "$DATA_DIR/jobs/processing" "$DATA_DIR/jobs/done" "$DATA_DIR/logs" "$DATA_DIR/pids" "$DATA_DIR/cancel" || true
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
chown -R diradmin:diradmin "$DATA_DIR" || true
|
||||
fi
|
||||
|
||||
CRON_FILE="/etc/cron.d/borg-restore"
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
echo "* * * * * root /usr/local/directadmin/plugins/borg-restore/scripts/worker.sh >/dev/null 2>&1" > "$CRON_FILE"
|
||||
chmod 644 "$CRON_FILE" || true
|
||||
else
|
||||
echo "borg-restore: warning: not running as root, cannot install cron job" >&2
|
||||
fi
|
||||
|
||||
echo "borg-restore: install complete"
|
||||
Executable
+495
@@ -0,0 +1,495 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
ERROR_MSG=""
|
||||
SUCCESS_MSG=""
|
||||
RESTORE_OK=0
|
||||
DA_USER=""
|
||||
|
||||
JOB_FILE="${1:-}"
|
||||
if [ -z "$JOB_FILE" ] || [ ! -f "$JOB_FILE" ]; then
|
||||
echo "borg-restore: missing job file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JOB_BASE="$(basename "$JOB_FILE")"
|
||||
JOB_ID="${JOB_BASE%%.*}"
|
||||
|
||||
PLUGIN_DIR="/usr/local/directadmin/plugins/borg-restore"
|
||||
LOG_DIR="${PLUGIN_DIR}/data/logs"
|
||||
LOG_FILE="${LOG_DIR}/${JOB_ID}.log"
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
fail() {
|
||||
local msg="$1"
|
||||
ERROR_MSG="$msg"
|
||||
echo "$msg" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
format_restore_path() {
|
||||
local path="${1:-}"
|
||||
path="$(echo "$path" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
if [ -z "$path" ]; then
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
if [[ "$path" != /* ]]; then
|
||||
path="/$path"
|
||||
fi
|
||||
echo "$path"
|
||||
}
|
||||
|
||||
append_missing_include_explanation() {
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
return
|
||||
fi
|
||||
if ! grep -qi "Include pattern '.*' never matched" "$LOG_FILE"; then
|
||||
return
|
||||
fi
|
||||
if grep -Fq "W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki" "$LOG_FILE" || grep -Fq "The specified restore path was not found in the backup" "$LOG_FILE"; then
|
||||
return
|
||||
fi
|
||||
local label
|
||||
label="$(format_restore_path "${USERRESTOREPATH:-}")"
|
||||
local msg
|
||||
if [ "${USERLANG:-}" = "en" ]; then
|
||||
msg="The specified restore path"
|
||||
else
|
||||
msg="W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki"
|
||||
fi
|
||||
if [ -n "$label" ]; then
|
||||
msg+=" (${label})"
|
||||
fi
|
||||
if [ "${USERLANG:-}" = "en" ]; then
|
||||
msg+=" was not found in the backup. Please try restoring from another archive that may contain those files."
|
||||
else
|
||||
msg+=". Prosimy spróbować przywrócić dane z innego archiwum, w którym te pliki mogły się znajdować."
|
||||
fi
|
||||
printf "\n%s\n" "$msg" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
has_missing_include() {
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
return 1
|
||||
fi
|
||||
grep -qi "Include pattern '.*' never matched" "$LOG_FILE"
|
||||
}
|
||||
|
||||
filter_log_content() {
|
||||
awk '{
|
||||
line=tolower($0);
|
||||
if (line ~ /permanently added/) next;
|
||||
if (line ~ /known host/) next;
|
||||
print;
|
||||
}'
|
||||
}
|
||||
|
||||
secure_source_check() {
|
||||
local path="$1"
|
||||
local label="$2"
|
||||
local check_writable="${3:-1}"
|
||||
if [ -z "$path" ] || [ ! -e "$path" ]; then
|
||||
fail "Missing ${label}: ${path}"
|
||||
exit 1
|
||||
fi
|
||||
if [ -L "$path" ] || [ ! -f "$path" ]; then
|
||||
fail "Insecure ${label}: not a regular file (${path})"
|
||||
exit 1
|
||||
fi
|
||||
local owner_uid owner_name mode
|
||||
owner_uid="$(stat -c %u "$path" 2>/dev/null || true)"
|
||||
owner_name="$(stat -c %U "$path" 2>/dev/null || true)"
|
||||
mode="$(stat -c %a "$path" 2>/dev/null || true)"
|
||||
if [ -z "$owner_uid" ] || [ -z "$mode" ]; then
|
||||
fail "Unable to stat ${label}: ${path}"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$owner_uid" != "0" ] && [ "$owner_uid" != "500" ] && [ "$owner_name" != "root" ] && [ "$owner_name" != "diradmin" ]; then
|
||||
fail "Insecure ${label} owner (${owner_name}:${owner_uid}): ${path}"
|
||||
exit 1
|
||||
fi
|
||||
mode=$((10#$mode))
|
||||
if [ "$check_writable" = "1" ] && (( (mode & 022) != 0 )); then
|
||||
fail "Insecure ${label} permissions (group/other writable): ${path}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
urlencode() {
|
||||
local input="$1"
|
||||
local hex out i
|
||||
hex="$(printf '%s' "$input" | od -An -tx1 | tr -d ' \n')"
|
||||
out=""
|
||||
for ((i=0; i<${#hex}; i+=2)); do
|
||||
out+="%${hex:i:2}"
|
||||
done
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
notify_user() {
|
||||
local code="$1"
|
||||
if [ -z "${DA_USER:-}" ]; then
|
||||
return
|
||||
fi
|
||||
local subject message
|
||||
if [ "$code" -eq 0 ] && [ "$RESTORE_OK" -eq 1 ]; then
|
||||
if [ "${USERLANG:-}" = "en" ]; then
|
||||
subject="BorgBackup restore completed successfully"
|
||||
message="Restore of ${USERRESTOREPATH:-} from ${REPONAME:-} completed."
|
||||
else
|
||||
subject="Przywracanie plików BorgBackup zakończone pomyślnie"
|
||||
message="${SUCCESS_MSG:-Przywracanie ${USERRESTOREPATH:-} z ${REPONAME:-} zakończone.}"
|
||||
fi
|
||||
else
|
||||
if [ "${USERLANG:-}" = "en" ]; then
|
||||
subject="BorgBackup restore error"
|
||||
message="Restore failed for ${USERRESTOREPATH:-} from ${REPONAME:-}."
|
||||
else
|
||||
subject="Błąd przywracania plików BorgBackup"
|
||||
if [ -n "$ERROR_MSG" ]; then
|
||||
message="Błąd przywracania: ${ERROR_MSG}"
|
||||
else
|
||||
message="Błąd przywracania ${USERRESTOREPATH:-} z ${REPONAME:-}."
|
||||
fi
|
||||
fi
|
||||
if [ -n "${LOG_FILE:-}" ] && [ -f "$LOG_FILE" ]; then
|
||||
local log_content
|
||||
log_content="$(cat "$LOG_FILE" | filter_log_content)"
|
||||
message="${message}\n\nLog:\n${log_content}"
|
||||
fi
|
||||
fi
|
||||
local task_queue="/usr/local/directadmin/data/task.queue"
|
||||
local users="select1%3D$(urlencode "$DA_USER")"
|
||||
local line="action=notify&value=users&subject=$(urlencode "$subject")&message=$(urlencode "$message")&users=${users}"
|
||||
printf "%s\n" "$line" >> "$task_queue"
|
||||
}
|
||||
|
||||
on_exit() {
|
||||
local code="$1"
|
||||
notify_user "$code"
|
||||
}
|
||||
trap 'on_exit $?' EXIT
|
||||
|
||||
secure_source_check "$JOB_FILE" "job file" 0
|
||||
# shellcheck disable=SC1090
|
||||
source "$JOB_FILE"
|
||||
|
||||
DA_USER="${DA_USER:-}"
|
||||
if [ -z "${DA_USER:-}" ]; then
|
||||
echo "Missing DA_USER in job file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CONFIG_FILE="${PLUGIN_DIR}/plugin-settings.conf"
|
||||
|
||||
set_owner() {
|
||||
local path="$1"
|
||||
if [ -z "$path" ] || [ ! -e "$path" ]; then
|
||||
return
|
||||
fi
|
||||
if [ "$RESTORETYPE" = "mail" ]; then
|
||||
chown -R "${DA_USER}:mail" "$path"
|
||||
else
|
||||
chown -R "${DA_USER}:${DA_USER}" "$path"
|
||||
fi
|
||||
}
|
||||
|
||||
is_true() {
|
||||
local v="${1:-}"
|
||||
v="$(printf '%s' "$v" | tr '[:upper:]' '[:lower:]')"
|
||||
case "$v" in
|
||||
1|true|yes|on) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
fail "Missing ${CONFIG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
secure_source_check "$CONFIG_FILE" "plugin-settings.conf"
|
||||
# shellcheck disable=SC1090
|
||||
source "$CONFIG_FILE"
|
||||
|
||||
BB_PATH="${BB_PATH:-/opt/bb}"
|
||||
BORG_PATH="${BORG_PATH:-/usr/bin/borg}"
|
||||
BORG_VARIABLES_FILE="${BORG_VARIABLES_FILE:-}"
|
||||
WEB_RESTORE_LOCATION="${WEB_RESTORE_LOCATION:-/home/DA_USER/domains}"
|
||||
MAIL_RESTORE_LOCATION="${MAIL_RESTORE_LOCATION:-/home/DA_USER/imap}"
|
||||
BLOCK_RESTORE_OUTSIDE_LOCATION="${BLOCK_RESTORE_OUTSIDE_LOCATION:-TRUE}"
|
||||
CLEAR_DOVECOT_INDEXES_AFTER_MERGE="${CLEAR_DOVECOT_INDEXES_AFTER_MERGE:-TRUE}"
|
||||
RESTORE_PATH_MODE="${RESTORE_PATH_MODE:-dir}"
|
||||
|
||||
if [ -z "$BB_PATH" ]; then
|
||||
fail "BB_PATH is empty in plugin-settings.conf"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$BORG_PATH" ]; then
|
||||
fail "BORG_PATH is empty in plugin-settings.conf"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BBVARS_PATH="${BORG_VARIABLES_FILE:-}"
|
||||
BBVARS_PATH="${BBVARS_PATH//DA_USER/${DA_USER}}"
|
||||
if [ -z "$BBVARS_PATH" ]; then
|
||||
BBVARS_PATH="$BB_PATH"
|
||||
if [[ "$BBVARS_PATH" != *.sh ]]; then
|
||||
BBVARS_PATH="${BBVARS_PATH%/}/bbvars.sh"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f "$BBVARS_PATH" ]; then
|
||||
fail "Missing ${BBVARS_PATH}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
secure_source_check "$BBVARS_PATH" "bbvars.sh" 0
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "$BBVARS_PATH"
|
||||
set +a
|
||||
|
||||
if [ -z "${REPO:-}" ]; then
|
||||
fail "Missing REPO in ${BBVARS_PATH}"
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "${BORG_SSH_KEY:-}" ]; then
|
||||
if [ ! -f "${BORG_SSH_KEY}" ]; then
|
||||
fail "BORG_SSH_KEY is not a file: ${BORG_SSH_KEY}"
|
||||
exit 1
|
||||
fi
|
||||
export BORG_RSH="ssh -i \"${BORG_SSH_KEY}\""
|
||||
fi
|
||||
|
||||
if [ -z "${REPONAME:-}" ] || [ -z "${USERRESTOREPATH:-}" ]; then
|
||||
fail "Missing REPONAME or USERRESTOREPATH in job file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RESTOREMODE="${RESTOREMODE:-location}"
|
||||
RESTORETYPE="${RESTORETYPE:-web}"
|
||||
if [ "$RESTORETYPE" != "mail" ]; then
|
||||
RESTORETYPE="web"
|
||||
fi
|
||||
if [ "$RESTORETYPE" = "mail" ]; then
|
||||
if [ "$RESTOREMODE" != "location" ] && [ "$RESTOREMODE" != "update" ]; then
|
||||
RESTOREMODE="update"
|
||||
fi
|
||||
else
|
||||
if [ "$RESTOREMODE" != "location" ] && [ "$RESTOREMODE" != "overwrite" ]; then
|
||||
RESTOREMODE="location"
|
||||
fi
|
||||
fi
|
||||
|
||||
RESTOREPATHMODE="${RESTOREPATHMODE:-$RESTORE_PATH_MODE}"
|
||||
RESTOREPATHMODE="$(echo "$RESTOREPATHMODE" | tr '[:upper:]' '[:lower:]')"
|
||||
RESTOREPATHMODE_FLAG="full"
|
||||
if [ "$RESTOREPATHMODE" = "dir" ]; then
|
||||
RESTOREPATHMODE_FLAG="dir"
|
||||
fi
|
||||
|
||||
TARGETPATH="${TARGETPATH:-}"
|
||||
RESTORE_FULL="/${USERRESTOREPATH#/}"
|
||||
WEB_BASE="${WEB_RESTORE_LOCATION//DA_USER/$DA_USER}"
|
||||
MAIL_BASE="${MAIL_RESTORE_LOCATION//DA_USER/$DA_USER}"
|
||||
BASE_LOCATION="$WEB_BASE"
|
||||
if [ "$RESTORETYPE" = "mail" ]; then
|
||||
BASE_LOCATION="$MAIL_BASE"
|
||||
fi
|
||||
if [ -z "$BASE_LOCATION" ]; then
|
||||
fail "Base restore location is empty"
|
||||
exit 1
|
||||
fi
|
||||
TARGET_DIR="$BASE_LOCATION"
|
||||
if [ -z "$TARGETPATH" ]; then
|
||||
TARGETPATH="$TARGET_DIR"
|
||||
fi
|
||||
|
||||
BLOCK_FLAG="$(echo "$BLOCK_RESTORE_OUTSIDE_LOCATION" | tr '[:upper:]' '[:lower:]')"
|
||||
if [ "$BLOCK_FLAG" = "1" ] || [ "$BLOCK_FLAG" = "true" ] || [ "$BLOCK_FLAG" = "yes" ] || [ "$BLOCK_FLAG" = "on" ]; then
|
||||
if [ -n "$BASE_LOCATION" ]; then
|
||||
BASE_LOCATION="${BASE_LOCATION%/}"
|
||||
if [[ "$RESTORE_FULL" != "$BASE_LOCATION" && "$RESTORE_FULL" != "$BASE_LOCATION/"* ]]; then
|
||||
fail "Restore path outside base location: ${RESTORE_FULL}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$USERRESTOREPATH" == *".."* ]]; then
|
||||
fail "Invalid restore path (contains ..)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$TARGETPATH" == *".."* ]]; then
|
||||
fail "Invalid target path (contains ..)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$RESTOREMODE" = "overwrite" ] || [ "$RESTOREMODE" = "update" ]; then
|
||||
TMP_BASE="/home/${DA_USER}/bb.tmp"
|
||||
RESTORE_BASE="${RESTORE_FULL##*/}"
|
||||
RESTORE_PARENT="${RESTORE_FULL%/*}"
|
||||
|
||||
if [ -z "$RESTORE_BASE" ] || [ -z "$RESTORE_PARENT" ]; then
|
||||
fail "Nieprawidłowa ścieżka do przywrócenia."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$TMP_BASE"
|
||||
chown -R "${DA_USER}:${DA_USER}" "$TMP_BASE"
|
||||
|
||||
rm -rf "${TMP_BASE}/${USERRESTOREPATH#/}" "${TMP_BASE}/${RESTORE_BASE}" 2>/dev/null || true
|
||||
|
||||
cd "$TMP_BASE"
|
||||
set +e
|
||||
"$BORG_PATH" extract --progress "${REPO}::${REPONAME}" -- "${USERRESTOREPATH}" >> "$LOG_FILE" 2>&1
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
append_missing_include_explanation
|
||||
|
||||
if [ "$EXIT_CODE" -ne 0 ]; then
|
||||
if ! has_missing_include; then
|
||||
fail "Borg extract zakończył się błędem. Sprawdź logi w tabeli zdarzeń na stronie pluginu."
|
||||
fi
|
||||
else
|
||||
EXTRACTED_PATH="${TMP_BASE}/${USERRESTOREPATH#/}"
|
||||
TMP_ITEM="${TMP_BASE}/${RESTORE_BASE}"
|
||||
if [ ! -e "$EXTRACTED_PATH" ]; then
|
||||
fail "Nie znaleziono przywróconego katalogu: ${EXTRACTED_PATH}"
|
||||
EXIT_CODE=1
|
||||
else
|
||||
if [ -e "$TMP_ITEM" ]; then
|
||||
rm -rf "$TMP_ITEM" 2>/dev/null || true
|
||||
fi
|
||||
mv "$EXTRACTED_PATH" "$TMP_ITEM"
|
||||
|
||||
if [ "$RESTOREMODE" = "update" ]; then
|
||||
if [ -e "$RESTORE_FULL" ]; then
|
||||
PRE_SUFFIX="$(date +%d-%m-%Y_%H%M%S)"
|
||||
PRE_NAME="${RESTORE_BASE}_przed_przywroceniem_${PRE_SUFFIX}"
|
||||
PRE_PATH="${RESTORE_PARENT}/${PRE_NAME}"
|
||||
mkdir -p "$PRE_PATH"
|
||||
set +e
|
||||
rsync -a "$RESTORE_FULL"/ "$PRE_PATH"/ >> "$LOG_FILE" 2>&1
|
||||
RSYNC_CODE=$?
|
||||
set -e
|
||||
if [ "$RSYNC_CODE" -ne 0 ]; then
|
||||
fail "Backup katalogu przed aktualizacją zakończył się błędem."
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
else
|
||||
mkdir -p "$RESTORE_FULL"
|
||||
fi
|
||||
if [ "$EXIT_CODE" -eq 0 ]; then
|
||||
RSYNC_EXCLUDES=()
|
||||
if [ "$RESTORETYPE" = "mail" ]; then
|
||||
RSYNC_EXCLUDES+=(--exclude 'dovecot.index*')
|
||||
RSYNC_EXCLUDES+=(--exclude 'dovecot-uidlist')
|
||||
RSYNC_EXCLUDES+=(--exclude 'dovecot-uidvalidity')
|
||||
RSYNC_EXCLUDES+=(--exclude 'dovecot.list.index*')
|
||||
RSYNC_EXCLUDES+=(--exclude 'dovecot.list.log')
|
||||
fi
|
||||
set +e
|
||||
rsync -a "${RSYNC_EXCLUDES[@]}" "$TMP_ITEM"/ "$RESTORE_FULL"/ >> "$LOG_FILE" 2>&1
|
||||
RSYNC_CODE=$?
|
||||
set -e
|
||||
if [ "$RSYNC_CODE" -ne 0 ]; then
|
||||
fail "Aktualizacja danych przez rsync zakończyła się błędem."
|
||||
EXIT_CODE=1
|
||||
else
|
||||
if [ "$RESTORETYPE" = "mail" ] && is_true "$CLEAR_DOVECOT_INDEXES_AFTER_MERGE"; then
|
||||
find "$RESTORE_FULL" -type f \( -name 'dovecot.index*' -o -name 'dovecot-uidlist' -o -name 'dovecot-uidvalidity' -o -name 'dovecot.list.index*' -o -name 'dovecot.list.log' \) -delete >> "$LOG_FILE" 2>&1 || true
|
||||
fi
|
||||
set_owner "$RESTORE_FULL"
|
||||
rm -rf "$TMP_BASE"
|
||||
RESTORE_OK=1
|
||||
SUCCESS_MSG="Przywracanie ${USERRESTOREPATH} z ${REPONAME} zakończone. Pliki w ${RESTORE_FULL}."
|
||||
echo "$SUCCESS_MSG" >> "$LOG_FILE"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [ -e "$RESTORE_FULL" ]; then
|
||||
PRE_SUFFIX="$(date +%d-%m-%Y_%H%M%S)"
|
||||
PRE_NAME="${RESTORE_BASE}_przed_przywroceniem_${PRE_SUFFIX}"
|
||||
PRE_PATH="${RESTORE_PARENT}/${PRE_NAME}"
|
||||
mv "$RESTORE_FULL" "$PRE_PATH"
|
||||
fi
|
||||
|
||||
mkdir -p "$RESTORE_PARENT"
|
||||
mv "$TMP_ITEM" "$RESTORE_FULL"
|
||||
set_owner "$RESTORE_FULL"
|
||||
rm -rf "$TMP_BASE"
|
||||
RESTORE_OK=1
|
||||
SUCCESS_MSG="Przywracanie ${USERRESTOREPATH} z ${REPONAME} zakończone. Pliki w ${RESTORE_FULL}."
|
||||
echo "$SUCCESS_MSG" >> "$LOG_FILE"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [ ! -d "$TARGETPATH" ]; then
|
||||
mkdir -p "$TARGETPATH"
|
||||
fi
|
||||
if [ -d "$TARGETPATH" ] && [ "$(ls -A "$TARGETPATH" 2>/dev/null)" != "" ]; then
|
||||
fail "Target directory is not empty: ${TARGETPATH}"
|
||||
exit 1
|
||||
fi
|
||||
set_owner "$TARGETPATH"
|
||||
|
||||
TARGET_DIR="$TARGETPATH"
|
||||
|
||||
if [ "$RESTOREPATHMODE_FLAG" = "dir" ]; then
|
||||
IFS='/' read -r -a RESTORE_PARTS <<< "${USERRESTOREPATH#/}"
|
||||
PARTS_COUNT="${#RESTORE_PARTS[@]}"
|
||||
STRIP_COUNT=0
|
||||
if [ "$PARTS_COUNT" -gt 0 ]; then
|
||||
STRIP_COUNT=$((PARTS_COUNT - 1))
|
||||
fi
|
||||
|
||||
cd "$TARGET_DIR"
|
||||
set +e
|
||||
"$BORG_PATH" extract --progress --strip-components "$STRIP_COUNT" "${REPO}::${REPONAME}" -- "${USERRESTOREPATH}" >> "$LOG_FILE" 2>&1
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
append_missing_include_explanation
|
||||
|
||||
if [ "$EXIT_CODE" -ne 0 ]; then
|
||||
if ! has_missing_include; then
|
||||
fail "Borg extract zakończył się błędem. Sprawdź logi w tabeli zdarzeń na stronie pluginu."
|
||||
fi
|
||||
else
|
||||
BASE_NAME="$(basename "${USERRESTOREPATH#/}")"
|
||||
DEST_PATH="${TARGET_DIR%/}/${BASE_NAME}"
|
||||
if [ -e "$DEST_PATH" ]; then
|
||||
set_owner "$DEST_PATH"
|
||||
fi
|
||||
RESTORE_OK=1
|
||||
SUCCESS_MSG="Przywracanie ${USERRESTOREPATH} z ${REPONAME} zakończone. Pliki w ${DEST_PATH}."
|
||||
echo "$SUCCESS_MSG" >> "$LOG_FILE"
|
||||
fi
|
||||
else
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
set +e
|
||||
"$BORG_PATH" extract --progress "${REPO}::${REPONAME}" -- "${USERRESTOREPATH}" >> "$LOG_FILE" 2>&1
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
append_missing_include_explanation
|
||||
|
||||
set_owner "$TARGET_DIR"
|
||||
|
||||
if [ "$EXIT_CODE" -eq 0 ]; then
|
||||
RESTORE_OK=1
|
||||
SUCCESS_MSG="Przywracanie ${USERRESTOREPATH} z ${REPONAME} zakończone. Pliki w ${TARGET_DIR}."
|
||||
echo "$SUCCESS_MSG" >> "$LOG_FILE"
|
||||
else
|
||||
if ! has_missing_include; then
|
||||
fail "Borg extract zakończył się błędem. Sprawdź logi w tabeli zdarzeń na stronie pluginu."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
exit "$EXIT_CODE"
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
# Safety checks to avoid deleting the wrong path.
|
||||
if [ "$(basename "$PLUGIN_DIR")" != "borg-restore" ]; then
|
||||
echo "borg-restore: safety check failed (basename is not borg-restore): $PLUGIN_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$PLUGIN_DIR/plugin.conf" ]; then
|
||||
echo "borg-restore: safety check failed (plugin.conf missing): $PLUGIN_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CRON_FILE="/etc/cron.d/borg-restore"
|
||||
if [ -f "$CRON_FILE" ]; then
|
||||
rm -f "$CRON_FILE" || true
|
||||
fi
|
||||
|
||||
echo "borg-restore: uninstall complete"
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
|
||||
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DATA_DIR="$PLUGIN_DIR/data"
|
||||
JOB_DIR="$DATA_DIR/jobs/pending"
|
||||
PROCESSING_DIR="$DATA_DIR/jobs/processing"
|
||||
DONE_DIR="$DATA_DIR/jobs/done"
|
||||
LOCK_DIR="$DATA_DIR/lock"
|
||||
PID_DIR="$DATA_DIR/pids"
|
||||
CANCEL_DIR="$DATA_DIR/cancel"
|
||||
|
||||
mkdir -p "$JOB_DIR" "$PROCESSING_DIR" "$DONE_DIR" "$PID_DIR" "$CANCEL_DIR"
|
||||
|
||||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
rmdir "$LOCK_DIR" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
JOB_FILE=$(ls -1 "$JOB_DIR"/*.env 2>/dev/null | head -n 1)
|
||||
if [ -z "${JOB_FILE:-}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
BASE_NAME="$(basename "$JOB_FILE")"
|
||||
RUN_FILE="$PROCESSING_DIR/$BASE_NAME"
|
||||
chmod 600 "$JOB_FILE" 2>/dev/null || true
|
||||
mv "$JOB_FILE" "$RUN_FILE"
|
||||
chmod 600 "$RUN_FILE" 2>/dev/null || true
|
||||
JOB_ID="${BASE_NAME%.env}"
|
||||
PID_FILE="$PID_DIR/${JOB_ID}.pid"
|
||||
|
||||
set +e
|
||||
"$PLUGIN_DIR/scripts/restore.sh" "$RUN_FILE" &
|
||||
RESTORE_PID=$!
|
||||
echo "PID=${RESTORE_PID}" >> "$RUN_FILE"
|
||||
chmod 600 "$RUN_FILE" 2>/dev/null || true
|
||||
echo "${RESTORE_PID}" > "$PID_FILE"
|
||||
wait "$RESTORE_PID"
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
rm -f "$PID_FILE"
|
||||
|
||||
CANCEL_FLAG="${CANCEL_DIR}/${JOB_ID}.flag"
|
||||
if [ -f "$CANCEL_FLAG" ]; then
|
||||
rm -f "$CANCEL_FLAG"
|
||||
mv "$RUN_FILE" "$DONE_DIR/${BASE_NAME}.cancel"
|
||||
elif [ "$EXIT_CODE" -eq 0 ]; then
|
||||
mv "$RUN_FILE" "$DONE_DIR/${BASE_NAME}.ok"
|
||||
else
|
||||
mv "$RUN_FILE" "$DONE_DIR/${BASE_NAME}.fail"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
PLUGIN_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SCRIPT="$PLUGIN_DIR/scripts/uninstall.sh"
|
||||
|
||||
if [ ! -x "$SCRIPT" ]; then
|
||||
chmod 755 "$SCRIPT" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
exec "$SCRIPT"
|
||||
Executable
+3239
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,450 @@
|
||||
:root {
|
||||
--br-bg: #f4f6f8;
|
||||
--br-card: #ffffff;
|
||||
--br-text: #1f2a37;
|
||||
--br-muted: #6b7280;
|
||||
--br-border: #d9dee5;
|
||||
--br-accent: #0ea5a4;
|
||||
--br-accent-dark: #0f766e;
|
||||
--br-danger: #dc2626;
|
||||
--br-warn: #f59e0b;
|
||||
--br-shadow: 0 8px 24px rgba(31, 41, 55, 0.12);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--br-bg);
|
||||
color: var(--br-text);
|
||||
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
.br-container {
|
||||
max-width: 1100px;
|
||||
margin: 24px auto;
|
||||
padding: 16px;
|
||||
}
|
||||
.br-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.br-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
.br-card {
|
||||
background: var(--br-card);
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 10px;
|
||||
padding: 24px;
|
||||
box-shadow: var(--br-shadow);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.br-muted { color: var(--br-muted); }
|
||||
.br-alert {
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
margin: 8px 0;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.br-alert-error { background: #fff1f2; border-color: #fecdd3; color: #9f1239; }
|
||||
.br-alert-warn { background: #fffbeb; border-color: #fde68a; color: #92400e; }
|
||||
.br-alert-success { background: #ecfdf3; border-color: #bbf7d0; color: #166534; }
|
||||
.br-error-debug { margin: 8px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.br-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
.br-modal {
|
||||
width: min(560px, 92vw);
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 2px solid var(--br-border);
|
||||
box-shadow: var(--br-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 90vh;
|
||||
}
|
||||
.br-modal-log {
|
||||
width: 90vw;
|
||||
background: #ffffff;
|
||||
}
|
||||
.br-modal-warning {
|
||||
background: #f3c8c8;
|
||||
}
|
||||
.br-modal-warning .br-btn-ghost {
|
||||
background: #ffffff;
|
||||
}
|
||||
.br-modal-warning .br-btn-ghost:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.br-modal-danger {
|
||||
border-color: var(--br-danger);
|
||||
}
|
||||
.br-modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.br-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.br-modal-title {
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
}
|
||||
.br-modal-body {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.br-modal-scroll {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.br-log-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.br-log-header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.br-log-meta {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin: 10px 0 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
font-size: 13px;
|
||||
}
|
||||
.br-log-actions {
|
||||
margin: 8px 0 10px;
|
||||
}
|
||||
.br-picker-path {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.br-picker-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 10px;
|
||||
max-height: 45vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.br-picker-create {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.br-picker-create input {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.br-picker-list li {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--br-border);
|
||||
}
|
||||
.br-picker-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.br-picker-list li:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.br-picker-list li.active {
|
||||
background: #e0f2fe;
|
||||
}
|
||||
.br-btn-small {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.br-actions-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.br-actions-row form {
|
||||
margin: 0;
|
||||
}
|
||||
.br-icon-btn {
|
||||
border: 1px solid var(--br-border);
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
.br-icon-btn:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.br-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.br-table th {
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--br-muted);
|
||||
background: #f3f4f6;
|
||||
padding: 10px;
|
||||
}
|
||||
.br-table td {
|
||||
padding: 10px;
|
||||
border-top: 1px solid var(--br-border);
|
||||
vertical-align: top;
|
||||
}
|
||||
.br-table tr:nth-child(even) td { background: #f9fafb; }
|
||||
.br-col-select { width: 50px; text-align: center; }
|
||||
.br-col-status { width: 120px; }
|
||||
.br-col-progress { width: 90px; }
|
||||
.br-col-mode { width: 180px; }
|
||||
.br-col-user { width: 140px; }
|
||||
.br-col-backup { width: 220px; overflow-wrap: anywhere; }
|
||||
.br-col-path { width: 260px; overflow-wrap: anywhere; }
|
||||
.br-col-target { width: 220px; overflow-wrap: anywhere; }
|
||||
.br-col-time { width: 150px; }
|
||||
.br-col-actions { width: 120px; }
|
||||
.br-table input[type="checkbox"] {
|
||||
accent-color: var(--br-accent);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.br-actions { display: flex; gap: 8px; align-items: center; margin: 10px 0; }
|
||||
.br-btn {
|
||||
background: var(--br-accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.br-btn:hover { background: var(--br-accent-dark); }
|
||||
.br-btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--br-text);
|
||||
border: 1px solid var(--br-border);
|
||||
}
|
||||
.br-btn-ghost:hover { background: #eef2f7; }
|
||||
.br-btn-danger {
|
||||
background: var(--br-danger);
|
||||
}
|
||||
.br-btn-danger:hover { background: #b91c1c; }
|
||||
.br-link-danger { color: var(--br-danger); text-decoration: none; }
|
||||
.br-link-danger:hover { text-decoration: underline; }
|
||||
.br-inline-form { display: inline; }
|
||||
.br-link-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
.br-link-button:hover { text-decoration: underline; }
|
||||
.br-link-button.br-link-danger { color: var(--br-danger); }
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
max-width: 680px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
pre {
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
a { color: var(--br-accent-dark); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.br-toast {
|
||||
position: fixed;
|
||||
bottom: 18px;
|
||||
right: 18px;
|
||||
background: #16a34a;
|
||||
color: #fff;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 12px 28px rgba(17, 24, 39, 0.25);
|
||||
z-index: 9999;
|
||||
font-weight: 600;
|
||||
}
|
||||
.br-toast-stack {
|
||||
position: fixed;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 9999;
|
||||
}
|
||||
.br-toast-stack .br-toast {
|
||||
position: static;
|
||||
top: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
.br-toast-error {
|
||||
background: #dc2626;
|
||||
top: 18px;
|
||||
bottom: auto;
|
||||
}
|
||||
.br-toast-success {
|
||||
background: #16a34a;
|
||||
}
|
||||
.br-status-line {
|
||||
margin: 6px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.br-progress {
|
||||
height: 8px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
margin: 6px 0 12px;
|
||||
}
|
||||
.br-progress-bar {
|
||||
height: 100%;
|
||||
background: var(--br-accent);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.br-restore-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin: 12px 0 10px;
|
||||
}
|
||||
.br-restore-box {
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 10px;
|
||||
background: #f2f4f7;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.br-restore-box-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.br-restore-box-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.br-radio-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.br-radio-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.br-radio-row input[type="radio"] {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.br-target-panel {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #d1d5db;
|
||||
}
|
||||
.br-target-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.br-target-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.br-target-input-wrap input[type="text"] {
|
||||
max-width: none;
|
||||
flex: 1;
|
||||
}
|
||||
.br-usage-box {
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 10px;
|
||||
background: #f3f4f6;
|
||||
padding: 12px 14px;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
.br-usage-box-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.br-restore-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.br-italic {
|
||||
font-style:italic;
|
||||
}
|
||||
|
||||
.small { font-size:85%; }
|
||||
.m-0 {margin:0;}
|
||||
.my-5 {margin: 12px 0px;}
|
||||
.br-border { border: 1px solid #eaeaea;}
|
||||
.br-bg-light { background-color: #f9f9f9;}
|
||||
.br-pad-x { padding-left: 1.25rem; padding-right: 1.25rem; }
|
||||
.br-strong { font-weight: bold;}
|
||||
.br-d-inline-block { display: inline-block; }
|
||||
.br-d-block { display: block; }
|
||||
.br-d-flex { display: flex; }
|
||||
.br-g-4 { gap: 4px; }
|
||||
.br-flex-col { flex-direction: column;}
|
||||
.br-btn { border-radius: 16px; text-transform: uppercase; font-weight: normal;}
|
||||
.br-icon-btn { padding: 9px; }
|
||||
.border-top { border-top: 1px solid #eaeaea; padding-top: 12px; }
|
||||
.border-bottom { border-bottom: 1px solid #eaeaea; padding-bottom: 12px; }
|
||||
.align-self-center { align-self: center; }
|
||||
#job-status {
|
||||
background: transparent !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
#job-status > img {
|
||||
display: none !important;
|
||||
}
|
||||
.br-jobs-empty {
|
||||
margin: 0;
|
||||
color: var(--br-muted);
|
||||
background: transparent !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
:root {
|
||||
--br-bg: #f5f8ff;
|
||||
--br-card: #ffffff;
|
||||
--br-text: #111827;
|
||||
--br-muted: #64748b;
|
||||
--br-border: #e2e8f0;
|
||||
--br-accent: #2563eb;
|
||||
--br-accent-dark: #1d4ed8;
|
||||
--br-danger: #dc2626;
|
||||
--br-warn: #f59e0b;
|
||||
--br-shadow: 0 10px 28px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--br-bg);
|
||||
color: var(--br-text);
|
||||
font-family: "IBM Plex Sans", "Segoe UI", Arial, sans-serif;
|
||||
}
|
||||
.br-container {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
margin: 16px auto;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
.br-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.br-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
.br-card {
|
||||
background: var(--br-card);
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: var(--br-shadow);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.br-muted { color: var(--br-muted); }
|
||||
.br-alert {
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
margin: 8px 0;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.br-alert-error { background: #fff1f2; border-color: #fecdd3; color: #9f1239; }
|
||||
.br-alert-warn { background: #fffbeb; border-color: #fde68a; color: #92400e; }
|
||||
.br-alert-success { background: #ecfdf3; border-color: #bbf7d0; color: #166534; }
|
||||
.br-error-debug { margin: 8px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.br-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
.br-modal {
|
||||
width: min(560px, 92vw);
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 2px solid var(--br-border);
|
||||
box-shadow: var(--br-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 90vh;
|
||||
}
|
||||
.br-modal-log {
|
||||
width: 90vw;
|
||||
background: #ffffff;
|
||||
}
|
||||
.br-modal-warning {
|
||||
background: #f3c8c8;
|
||||
}
|
||||
.br-modal-warning .br-btn-ghost {
|
||||
background: #ffffff;
|
||||
}
|
||||
.br-modal-warning .br-btn-ghost:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.br-modal-danger {
|
||||
border-color: var(--br-danger);
|
||||
}
|
||||
.br-modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.br-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.br-modal-title {
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
}
|
||||
.br-modal-body {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.br-modal-scroll {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.br-log-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.br-log-header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.br-log-meta {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin: 10px 0 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
font-size: 13px;
|
||||
}
|
||||
.br-log-actions {
|
||||
margin: 8px 0 10px;
|
||||
}
|
||||
.br-picker-path {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.br-picker-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 10px;
|
||||
max-height: 45vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.br-picker-create {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.br-picker-create input {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.br-picker-list li {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--br-border);
|
||||
}
|
||||
.br-picker-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.br-picker-list li:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.br-picker-list li.active {
|
||||
background: #d1fae5;
|
||||
}
|
||||
.br-btn-small {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.br-actions-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.br-actions-row form {
|
||||
margin: 0;
|
||||
}
|
||||
.br-icon-btn {
|
||||
border: 1px solid var(--br-border);
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
.br-icon-btn:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.br-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.br-table th {
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--br-muted);
|
||||
background: #f1f5f9;
|
||||
padding: 10px;
|
||||
}
|
||||
.br-table td {
|
||||
padding: 10px;
|
||||
border-top: 1px solid var(--br-border);
|
||||
vertical-align: top;
|
||||
}
|
||||
.br-table tr:nth-child(even) td { background: #f8fafc; }
|
||||
.br-col-select { width: 50px; text-align: center; }
|
||||
.br-col-status { width: 120px; }
|
||||
.br-col-progress { width: 90px; }
|
||||
.br-col-mode { width: 180px; }
|
||||
.br-col-user { width: 140px; }
|
||||
.br-col-backup { width: 220px; overflow-wrap: anywhere; }
|
||||
.br-col-path { width: 260px; overflow-wrap: anywhere; }
|
||||
.br-col-target { width: 220px; overflow-wrap: anywhere; }
|
||||
.br-col-time { width: 150px; }
|
||||
.br-col-actions { width: 120px; }
|
||||
.br-table input[type="checkbox"] {
|
||||
accent-color: var(--br-accent);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.br-actions { display: flex; gap: 8px; align-items: center; margin: 10px 0; }
|
||||
.br-btn {
|
||||
background: var(--br-accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.br-btn:hover { background: var(--br-accent-dark); }
|
||||
.br-btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--br-text);
|
||||
border: 1px solid var(--br-border);
|
||||
}
|
||||
.br-btn-ghost:hover { background: #e2e8f0; }
|
||||
.br-btn-danger {
|
||||
background: var(--br-danger);
|
||||
}
|
||||
.br-btn-danger:hover { background: #b91c1c; }
|
||||
.br-link-danger { color: var(--br-danger); text-decoration: none; }
|
||||
.br-link-danger:hover { text-decoration: underline; }
|
||||
.br-inline-form { display: inline; }
|
||||
.br-link-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
.br-link-button:hover { text-decoration: underline; }
|
||||
.br-link-button.br-link-danger { color: var(--br-danger); }
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
max-width: 680px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
pre {
|
||||
background: #0b1020;
|
||||
color: #e2e8f0;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
a { color: var(--br-accent-dark); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.br-toast {
|
||||
position: fixed;
|
||||
bottom: 18px;
|
||||
right: 18px;
|
||||
background: #16a34a;
|
||||
color: #fff;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 28px rgba(16, 24, 40, 0.25);
|
||||
z-index: 9999;
|
||||
font-weight: 600;
|
||||
}
|
||||
.br-toast-stack {
|
||||
position: fixed;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 9999;
|
||||
}
|
||||
.br-toast-stack .br-toast {
|
||||
position: static;
|
||||
top: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
.br-toast-error {
|
||||
background: #dc2626;
|
||||
top: 18px;
|
||||
bottom: auto;
|
||||
}
|
||||
.br-toast-success {
|
||||
background: #16a34a;
|
||||
}
|
||||
.br-status-line {
|
||||
margin: 6px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.br-progress {
|
||||
height: 8px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
margin: 6px 0 12px;
|
||||
}
|
||||
.br-progress-bar {
|
||||
height: 100%;
|
||||
background: var(--br-accent);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.br-restore-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin: 12px 0 10px;
|
||||
}
|
||||
.br-restore-box {
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 10px;
|
||||
background: #f1f5f9;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.br-restore-box-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.br-restore-box-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.br-radio-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.br-radio-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.br-radio-row input[type="radio"] {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.br-target-panel {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #cbd5e1;
|
||||
}
|
||||
.br-target-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.br-target-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.br-target-input-wrap input[type="text"] {
|
||||
max-width: none;
|
||||
flex: 1;
|
||||
}
|
||||
.br-usage-box {
|
||||
border: 1px solid var(--br-border);
|
||||
border-radius: 10px;
|
||||
background: #f1f5f9;
|
||||
padding: 12px 14px;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
.br-usage-box-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.br-restore-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.br-italic {
|
||||
font-style:italic;
|
||||
}
|
||||
|
||||
.small { font-size:85%; }
|
||||
.m-0 {margin:0;}
|
||||
.my-5 {margin: 12px 0px;}
|
||||
.br-border { border: 1px solid #eaeaea;}
|
||||
.br-bg-light { background-color: #f9f9f9;}
|
||||
.br-pad-x { padding-left: 1.25rem; padding-right: 1.25rem; }
|
||||
.br-strong { font-weight: bold;}
|
||||
.br-d-inline-block { display: inline-block; }
|
||||
.br-d-block { display: block; }
|
||||
.br-d-flex { display: flex; }
|
||||
.br-g-4 { gap: 4px; }
|
||||
.br-flex-col { flex-direction: column;}
|
||||
.br-btn { border-radius: 16px; text-transform: uppercase; font-weight: normal;}
|
||||
.br-icon-btn { padding: 8px; }
|
||||
.border-top { border-top: 1px solid #eaeaea; padding-top: 12px; }
|
||||
.border-bottom { border-bottom: 1px solid #eaeaea; padding-bottom: 12px; }
|
||||
.align-self-center { align-self: center; }
|
||||
#job-status {
|
||||
background: transparent !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
#job-status > img {
|
||||
display: none !important;
|
||||
}
|
||||
.br-jobs-empty {
|
||||
margin: 0;
|
||||
color: var(--br-muted);
|
||||
background: transparent !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
Executable
+3385
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user