Compare commits
2 Commits
86fc41fe77
...
66a48ea8b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66a48ea8b8 | ||
|
|
eeb1baf563 |
@ -14,6 +14,7 @@ LP.features.register({
|
||||
routes: ['/backup', '/backup*'],
|
||||
// Controllers the feature needs; lazy-loaded on first mount (idempotent).
|
||||
scripts: [
|
||||
'/js/components/backup/backup-schema.js',
|
||||
'/js/components/backup/backup-page.js',
|
||||
'/js/components/backup/backup-app-card.js',
|
||||
],
|
||||
|
||||
@ -2,91 +2,8 @@
|
||||
// Reads JSON snapshots written by scripts/webui/data/generators/backup/* and
|
||||
// dispatches actions back into the task system (which calls bash CLI).
|
||||
|
||||
// Retention presets — pick the persona that matches you. Each maps to the
|
||||
// five underlying restic --keep-* values. "Custom" reveals the raw fields.
|
||||
const BACKUP_RETENTION_PRESETS = {
|
||||
'inherit-global': { last: '', daily: '', weekly: '', monthly: '', yearly: '' },
|
||||
'self-hosting': { last: '', daily: '30', weekly: '', monthly: '', yearly: '' },
|
||||
'personal': { last: '', daily: '30', weekly: '', monthly: '6', yearly: '' },
|
||||
'enterprise': { last: '', daily: '30', weekly: '', monthly: '12', yearly: '5' }
|
||||
};
|
||||
|
||||
const BACKUP_RETENTION_PRESET_META = {
|
||||
'inherit-global': { label: 'Inherit global retention', hint: 'Use whatever the Configuration tab specifies. Pick something else here only when this location needs a different policy.' },
|
||||
'self-hosting': { label: 'Self-hosting', hint: '30 days of daily backups. Plenty for a homelab — covers accidental deletes and app screw-ups.' },
|
||||
'personal': { label: 'Personal', hint: '30 days of daily backups plus 6 monthly backups. Good for personal data where "what did this look like last summer" matters.' },
|
||||
'enterprise': { label: 'Enterprise', hint: '30 daily + 12 monthly + 5 yearly. Compliance-style retention with multi-year history.' },
|
||||
'custom': { label: 'Custom…', hint: 'Define each retention tier yourself.' }
|
||||
};
|
||||
|
||||
// Per-location field metadata. Configs.json doesn't carry titles for
|
||||
// CFG_BACKUP_LOC_N_* (locations are dynamic), so we provide them inline.
|
||||
// ConfigShared.generateField uses TITLE + key-based widget heuristics; the
|
||||
// regexes in config-options.js / config-shared.js already cover _TYPE,
|
||||
// _KEEP_*, _SECRET_KEY, _ACCOUNT_KEY for the right widgets.
|
||||
const BACKUP_LOC_FIELD_DEFS = {
|
||||
NAME: { title: 'Friendly name', description: 'Shown in lists and on the dashboard.' },
|
||||
ENABLED: { title: 'Enabled', description: 'Push backups to this location.' },
|
||||
ENGINE: { title: 'Engine', description: 'Backup engine used at this location.' },
|
||||
TYPE: { title: 'Type', description: 'Backend the engine uses to talk to this location.' },
|
||||
PATH_MODE: { title: 'Path Mode', description: 'Automatic uses the Default Backup Location from the Backup Engine config (one subfolder per location). Pick Custom to use a specific path (e.g. an attached drive or a NAS mount).' },
|
||||
PATH: { title: 'Custom path', description: 'Filesystem path on this server. Used only when Path is set to Custom.' },
|
||||
URI: { title: 'Repository URI (override)', description: 'Custom restic URI — leave blank to build from the fields below.' },
|
||||
SSH_USER: { title: 'SSH user', description: '' },
|
||||
SSH_HOST: { title: 'SSH host', description: '' },
|
||||
SSH_PORT: { title: 'SSH port', description: '' },
|
||||
SSH_PATH: { title: 'SSH remote path', description: 'Path on the remote host where the repo lives.' },
|
||||
SSH_AUTH: { title: 'SSH authentication', description: 'Key auth uses ~/.ssh/id_rsa on this host. Password mode pipes via sshpass — restic + borg only; kopia requires keys.' },
|
||||
SSH_PASS: { title: 'SSH password', description: 'Used only when SSH authentication is set to Password.' },
|
||||
S3_ACCESS_KEY: { title: 'S3 access key', description: '' },
|
||||
S3_SECRET_KEY: { title: 'S3 secret', description: '' },
|
||||
B2_ACCOUNT_ID: { title: 'B2 account ID', description: '' },
|
||||
B2_ACCOUNT_KEY: { title: 'B2 account key', description: '' },
|
||||
APPEND_ONLY: { title: 'Append-only', description: 'Ransomware-safe — refuse forget/prune for this location even if LibrePortal itself is compromised. Trades off automatic retention cleanup.' },
|
||||
CUSTOM_RETENTION: { title: 'Use custom retention', description: 'Otherwise this location inherits the global retention.' },
|
||||
KEEP_LAST: { title: 'Keep last', description: 'Backups to always retain.' },
|
||||
KEEP_DAILY: { title: 'Keep daily', description: 'One backup per day for this many days.' },
|
||||
KEEP_WEEKLY: { title: 'Keep weekly', description: 'One backup per week for this many weeks.' },
|
||||
KEEP_MONTHLY: { title: 'Keep monthly', description: 'One backup per month for this many months.' },
|
||||
KEEP_YEARLY: { title: 'Keep yearly', description: 'One backup per year for this many years.' }
|
||||
};
|
||||
|
||||
// Fallback for the per-type field schema. The live source is the generator-
|
||||
// emitted data/backup/generated/schema.json (loaded into this.locSchema and
|
||||
// read via locFieldsForType); this map is only used if that fetch fails.
|
||||
// Type leads each list (it shapes the rest of the form); ENGINE stays in the
|
||||
// list but locFieldGroups folds it into the Advanced tab.
|
||||
const BACKUP_LOC_FIELDS_BY_TYPE = {
|
||||
local: ['TYPE', 'NAME', 'ENGINE', 'PATH_MODE', 'PATH', 'APPEND_ONLY'],
|
||||
sftp: ['TYPE', 'NAME', 'ENGINE', 'SSH_USER', 'SSH_HOST', 'SSH_PORT', 'SSH_PATH', 'SSH_AUTH', 'SSH_PASS', 'URI', 'APPEND_ONLY'],
|
||||
rest: ['TYPE', 'NAME', 'ENGINE', 'URI', 'APPEND_ONLY'],
|
||||
s3: ['TYPE', 'NAME', 'ENGINE', 'URI', 'S3_ACCESS_KEY', 'S3_SECRET_KEY', 'APPEND_ONLY'],
|
||||
b2: ['TYPE', 'NAME', 'ENGINE', 'URI', 'B2_ACCOUNT_ID', 'B2_ACCOUNT_KEY', 'APPEND_ONLY'],
|
||||
gs: ['TYPE', 'NAME', 'ENGINE', 'URI', 'APPEND_ONLY'],
|
||||
azure: ['TYPE', 'NAME', 'ENGINE', 'URI', 'APPEND_ONLY'],
|
||||
rclone: ['TYPE', 'NAME', 'ENGINE', 'URI', 'APPEND_ONLY']
|
||||
};
|
||||
|
||||
// Suffixes that live in the editor's "Advanced" tab. configs.json can flag
|
||||
// more via a **ADVANCED** comment marker; this set keeps the known overrides
|
||||
// advanced even on legacy location.configs that predate the marker. Engine is
|
||||
// here too — the system picks a sensible default, so most users never touch it.
|
||||
const LOC_ADVANCED_SUFFIXES = new Set(['ENGINE', 'URI', 'SSH_PORT', 'APPEND_ONLY']);
|
||||
|
||||
function backupRetentionDetectPreset(values, includeInherit = false) {
|
||||
const norm = (v) => (v == null ? '' : String(v).trim());
|
||||
for (const [key, p] of Object.entries(BACKUP_RETENTION_PRESETS)) {
|
||||
if (key === 'inherit-global' && !includeInherit) continue;
|
||||
if (norm(values.last) === norm(p.last) &&
|
||||
norm(values.daily) === norm(p.daily) &&
|
||||
norm(values.weekly) === norm(p.weekly) &&
|
||||
norm(values.monthly) === norm(p.monthly) &&
|
||||
norm(values.yearly) === norm(p.yearly)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return 'custom';
|
||||
}
|
||||
// Module-level schema/retention data moved to backup-schema.js (loaded first).
|
||||
|
||||
class BackupPage {
|
||||
constructor() {
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
// Backup schema + retention data — the module-level constants and the
|
||||
// retention-preset detector extracted verbatim from backup-page.js.
|
||||
// Loaded before backup-page.js (feature scripts array, sequential) so the
|
||||
// BackupPage methods that reference these globals at runtime resolve them.
|
||||
|
||||
// Retention presets — pick the persona that matches you. Each maps to the
|
||||
// five underlying restic --keep-* values. "Custom" reveals the raw fields.
|
||||
const BACKUP_RETENTION_PRESETS = {
|
||||
'inherit-global': { last: '', daily: '', weekly: '', monthly: '', yearly: '' },
|
||||
'self-hosting': { last: '', daily: '30', weekly: '', monthly: '', yearly: '' },
|
||||
'personal': { last: '', daily: '30', weekly: '', monthly: '6', yearly: '' },
|
||||
'enterprise': { last: '', daily: '30', weekly: '', monthly: '12', yearly: '5' }
|
||||
};
|
||||
|
||||
const BACKUP_RETENTION_PRESET_META = {
|
||||
'inherit-global': { label: 'Inherit global retention', hint: 'Use whatever the Configuration tab specifies. Pick something else here only when this location needs a different policy.' },
|
||||
'self-hosting': { label: 'Self-hosting', hint: '30 days of daily backups. Plenty for a homelab — covers accidental deletes and app screw-ups.' },
|
||||
'personal': { label: 'Personal', hint: '30 days of daily backups plus 6 monthly backups. Good for personal data where "what did this look like last summer" matters.' },
|
||||
'enterprise': { label: 'Enterprise', hint: '30 daily + 12 monthly + 5 yearly. Compliance-style retention with multi-year history.' },
|
||||
'custom': { label: 'Custom…', hint: 'Define each retention tier yourself.' }
|
||||
};
|
||||
|
||||
// Per-location field metadata. Configs.json doesn't carry titles for
|
||||
// CFG_BACKUP_LOC_N_* (locations are dynamic), so we provide them inline.
|
||||
// ConfigShared.generateField uses TITLE + key-based widget heuristics; the
|
||||
// regexes in config-options.js / config-shared.js already cover _TYPE,
|
||||
// _KEEP_*, _SECRET_KEY, _ACCOUNT_KEY for the right widgets.
|
||||
const BACKUP_LOC_FIELD_DEFS = {
|
||||
NAME: { title: 'Friendly name', description: 'Shown in lists and on the dashboard.' },
|
||||
ENABLED: { title: 'Enabled', description: 'Push backups to this location.' },
|
||||
ENGINE: { title: 'Engine', description: 'Backup engine used at this location.' },
|
||||
TYPE: { title: 'Type', description: 'Backend the engine uses to talk to this location.' },
|
||||
PATH_MODE: { title: 'Path Mode', description: 'Automatic uses the Default Backup Location from the Backup Engine config (one subfolder per location). Pick Custom to use a specific path (e.g. an attached drive or a NAS mount).' },
|
||||
PATH: { title: 'Custom path', description: 'Filesystem path on this server. Used only when Path is set to Custom.' },
|
||||
URI: { title: 'Repository URI (override)', description: 'Custom restic URI — leave blank to build from the fields below.' },
|
||||
SSH_USER: { title: 'SSH user', description: '' },
|
||||
SSH_HOST: { title: 'SSH host', description: '' },
|
||||
SSH_PORT: { title: 'SSH port', description: '' },
|
||||
SSH_PATH: { title: 'SSH remote path', description: 'Path on the remote host where the repo lives.' },
|
||||
SSH_AUTH: { title: 'SSH authentication', description: 'Key auth uses ~/.ssh/id_rsa on this host. Password mode pipes via sshpass — restic + borg only; kopia requires keys.' },
|
||||
SSH_PASS: { title: 'SSH password', description: 'Used only when SSH authentication is set to Password.' },
|
||||
S3_ACCESS_KEY: { title: 'S3 access key', description: '' },
|
||||
S3_SECRET_KEY: { title: 'S3 secret', description: '' },
|
||||
B2_ACCOUNT_ID: { title: 'B2 account ID', description: '' },
|
||||
B2_ACCOUNT_KEY: { title: 'B2 account key', description: '' },
|
||||
APPEND_ONLY: { title: 'Append-only', description: 'Ransomware-safe — refuse forget/prune for this location even if LibrePortal itself is compromised. Trades off automatic retention cleanup.' },
|
||||
CUSTOM_RETENTION: { title: 'Use custom retention', description: 'Otherwise this location inherits the global retention.' },
|
||||
KEEP_LAST: { title: 'Keep last', description: 'Backups to always retain.' },
|
||||
KEEP_DAILY: { title: 'Keep daily', description: 'One backup per day for this many days.' },
|
||||
KEEP_WEEKLY: { title: 'Keep weekly', description: 'One backup per week for this many weeks.' },
|
||||
KEEP_MONTHLY: { title: 'Keep monthly', description: 'One backup per month for this many months.' },
|
||||
KEEP_YEARLY: { title: 'Keep yearly', description: 'One backup per year for this many years.' }
|
||||
};
|
||||
|
||||
// Fallback for the per-type field schema. The live source is the generator-
|
||||
// emitted data/backup/generated/schema.json (loaded into this.locSchema and
|
||||
// read via locFieldsForType); this map is only used if that fetch fails.
|
||||
// Type leads each list (it shapes the rest of the form); ENGINE stays in the
|
||||
// list but locFieldGroups folds it into the Advanced tab.
|
||||
const BACKUP_LOC_FIELDS_BY_TYPE = {
|
||||
local: ['TYPE', 'NAME', 'ENGINE', 'PATH_MODE', 'PATH', 'APPEND_ONLY'],
|
||||
sftp: ['TYPE', 'NAME', 'ENGINE', 'SSH_USER', 'SSH_HOST', 'SSH_PORT', 'SSH_PATH', 'SSH_AUTH', 'SSH_PASS', 'URI', 'APPEND_ONLY'],
|
||||
rest: ['TYPE', 'NAME', 'ENGINE', 'URI', 'APPEND_ONLY'],
|
||||
s3: ['TYPE', 'NAME', 'ENGINE', 'URI', 'S3_ACCESS_KEY', 'S3_SECRET_KEY', 'APPEND_ONLY'],
|
||||
b2: ['TYPE', 'NAME', 'ENGINE', 'URI', 'B2_ACCOUNT_ID', 'B2_ACCOUNT_KEY', 'APPEND_ONLY'],
|
||||
gs: ['TYPE', 'NAME', 'ENGINE', 'URI', 'APPEND_ONLY'],
|
||||
azure: ['TYPE', 'NAME', 'ENGINE', 'URI', 'APPEND_ONLY'],
|
||||
rclone: ['TYPE', 'NAME', 'ENGINE', 'URI', 'APPEND_ONLY']
|
||||
};
|
||||
|
||||
// Suffixes that live in the editor's "Advanced" tab. configs.json can flag
|
||||
// more via a **ADVANCED** comment marker; this set keeps the known overrides
|
||||
// advanced even on legacy location.configs that predate the marker. Engine is
|
||||
// here too — the system picks a sensible default, so most users never touch it.
|
||||
const LOC_ADVANCED_SUFFIXES = new Set(['ENGINE', 'URI', 'SSH_PORT', 'APPEND_ONLY']);
|
||||
|
||||
function backupRetentionDetectPreset(values, includeInherit = false) {
|
||||
const norm = (v) => (v == null ? '' : String(v).trim());
|
||||
for (const [key, p] of Object.entries(BACKUP_RETENTION_PRESETS)) {
|
||||
if (key === 'inherit-global' && !includeInherit) continue;
|
||||
if (norm(values.last) === norm(p.last) &&
|
||||
norm(values.daily) === norm(p.daily) &&
|
||||
norm(values.weekly) === norm(p.weekly) &&
|
||||
norm(values.monthly) === norm(p.monthly) &&
|
||||
norm(values.yearly) === norm(p.yearly)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return 'custom';
|
||||
}
|
||||
@ -23,9 +23,14 @@
|
||||
this._unsubs = []; // backs ctx.sub() — SSE/bus/timer handles
|
||||
}
|
||||
|
||||
// Lazy-load the feature's controller scripts (idempotent across re-mounts).
|
||||
loadScripts(list) {
|
||||
return Promise.all((list || []).map(src => this.shell.loadScript(src)));
|
||||
// Lazy-load the feature's controller scripts in array order (idempotent
|
||||
// across re-mounts). Sequential, not parallel, so a feature can list a
|
||||
// base file before files that augment it (e.g. a class definition before
|
||||
// prototype-extension modules) — order in the array is honoured.
|
||||
async loadScripts(list) {
|
||||
for (const src of (list || [])) {
|
||||
await this.shell.loadScript(src);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch an HTML fragment (same path the legacy handlers used).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user