Compare commits
2 Commits
9eb5f7f73a
...
3098324627
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3098324627 | ||
|
|
3ad44a62f2 |
@ -16,6 +16,7 @@ LP.features.register({
|
|||||||
scripts: [
|
scripts: [
|
||||||
'/components/backup/js/backup-schema.js',
|
'/components/backup/js/backup-schema.js',
|
||||||
'/components/backup/js/backup-page.js',
|
'/components/backup/js/backup-page.js',
|
||||||
|
'/components/backup/js/backup-cron-schedule.js',
|
||||||
'/core/lib/backup-app-card.js',
|
'/core/lib/backup-app-card.js',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,122 @@
|
|||||||
|
// backup-cron-schedule.js — standalone cron-next utility (BackupPage prototype).
|
||||||
|
// Parses a 5-field crontab + computes the next fire time for the dashboard's
|
||||||
|
// "next run" hint. Extracted verbatim from backup-page.js; loaded AFTER it.
|
||||||
|
Object.assign(BackupPage.prototype, {
|
||||||
|
// formatRelative's future-tense sibling — "in 6h", "tomorrow at 5am", etc.
|
||||||
|
// Used by the backup-status header to summarise the next scheduled run.
|
||||||
|
formatRelativeFuture(when) {
|
||||||
|
if (!when) return '';
|
||||||
|
const t = when.getTime();
|
||||||
|
const diff = t - Date.now();
|
||||||
|
if (diff <= 0) return 'imminent';
|
||||||
|
const s = Math.floor(diff / 1000);
|
||||||
|
if (s < 60) return 'in less than a minute';
|
||||||
|
const m = Math.floor(s / 60);
|
||||||
|
if (m < 60) return `in ${m}m`;
|
||||||
|
const h = Math.floor(m / 60);
|
||||||
|
if (h < 24) return `in ${h}h`;
|
||||||
|
const sameDay = (a, b) => a.toDateString() === b.toDateString();
|
||||||
|
const tomorrow = new Date(Date.now() + 86400000);
|
||||||
|
if (sameDay(when, tomorrow)) return 'tomorrow';
|
||||||
|
const days = Math.floor(h / 24);
|
||||||
|
return `in ${days}d`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// "05:00" or "Mon 05:00" depending on whether it's later today or not.
|
||||||
|
formatScheduleClock(when) {
|
||||||
|
if (!when) return '';
|
||||||
|
const sameDay = (new Date()).toDateString() === when.toDateString();
|
||||||
|
const t = when.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
if (sameDay) return `at ${t}`;
|
||||||
|
const day = when.toLocaleDateString([], { weekday: 'short' });
|
||||||
|
return `${day} ${t}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tiny cron-next utility — given a 5-field crontab expression
|
||||||
|
// (minute hour dom month dow) returns a Date for the next fire after
|
||||||
|
// now, or null if the expression is unparseable / never fires within
|
||||||
|
// the lookahead window. Supports the common syntax: *, N, lists
|
||||||
|
// (N,M,O), ranges (N-M), and steps (* /N or N-M/S). Doesn't try to
|
||||||
|
// be a full cron implementation — just enough for the
|
||||||
|
// CFG_BACKUP_CRONTAB_APP value the WebUI shows in the header.
|
||||||
|
nextCronFireTime(expr) {
|
||||||
|
const fields = String(expr || '').trim().split(/\s+/);
|
||||||
|
if (fields.length !== 5) return null;
|
||||||
|
const ranges = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];
|
||||||
|
let sets;
|
||||||
|
try {
|
||||||
|
sets = fields.map((f, i) => this._cronFieldSet(f, ranges[i][0], ranges[i][1]));
|
||||||
|
} catch (_) { return null; }
|
||||||
|
if (sets.some(s => !s.length)) return null;
|
||||||
|
|
||||||
|
const [minSet, hourSet, domSet, monSet, dowSet] = sets;
|
||||||
|
const minOk = new Set(minSet);
|
||||||
|
const hourOk = new Set(hourSet);
|
||||||
|
const domOk = new Set(domSet);
|
||||||
|
const monOk = new Set(monSet);
|
||||||
|
const dowOk = new Set(dowSet);
|
||||||
|
const domStar = fields[2] === '*';
|
||||||
|
const dowStar = fields[4] === '*';
|
||||||
|
|
||||||
|
// Start one minute from now (rounded down to the minute) and walk
|
||||||
|
// forward checking each candidate. Cap at ~366 days so a totally
|
||||||
|
// unmatchable expression doesn't loop forever.
|
||||||
|
const start = new Date();
|
||||||
|
start.setSeconds(0, 0);
|
||||||
|
start.setMinutes(start.getMinutes() + 1);
|
||||||
|
const limit = start.getTime() + 366 * 86400 * 1000;
|
||||||
|
const cur = new Date(start);
|
||||||
|
while (cur.getTime() < limit) {
|
||||||
|
const m = cur.getMinutes();
|
||||||
|
const h = cur.getHours();
|
||||||
|
const dom = cur.getDate();
|
||||||
|
const mon = cur.getMonth() + 1;
|
||||||
|
const dow = cur.getDay(); // 0 = Sun
|
||||||
|
// POSIX cron: if both DOM and DOW are restricted, fire when
|
||||||
|
// EITHER matches. If only one is restricted, that one must
|
||||||
|
// match. If both are *, day passes.
|
||||||
|
let dayMatch;
|
||||||
|
if (domStar && dowStar) dayMatch = true;
|
||||||
|
else if (domStar) dayMatch = dowOk.has(dow);
|
||||||
|
else if (dowStar) dayMatch = domOk.has(dom);
|
||||||
|
else dayMatch = domOk.has(dom) || dowOk.has(dow);
|
||||||
|
if (dayMatch && monOk.has(mon) && hourOk.has(h) && minOk.has(m)) {
|
||||||
|
return new Date(cur);
|
||||||
|
}
|
||||||
|
cur.setMinutes(cur.getMinutes() + 1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Expand one cron field into a sorted list of valid numeric values.
|
||||||
|
// Throws on bad syntax so nextCronFireTime can drop back to null.
|
||||||
|
_cronFieldSet(field, lo, hi) {
|
||||||
|
const out = new Set();
|
||||||
|
for (const part of String(field).split(',')) {
|
||||||
|
// step (every-Nth): "value/step" — value is "*", a single
|
||||||
|
// number, or a range "a-b".
|
||||||
|
let stepBase = part, step = 1;
|
||||||
|
const slash = part.indexOf('/');
|
||||||
|
if (slash !== -1) {
|
||||||
|
stepBase = part.slice(0, slash);
|
||||||
|
step = parseInt(part.slice(slash + 1), 10);
|
||||||
|
if (!Number.isFinite(step) || step < 1) throw new Error('bad step');
|
||||||
|
}
|
||||||
|
let from = lo, to = hi;
|
||||||
|
if (stepBase === '*' || stepBase === '') {
|
||||||
|
// range stays lo..hi
|
||||||
|
} else if (stepBase.includes('-')) {
|
||||||
|
const [a, b] = stepBase.split('-').map(n => parseInt(n, 10));
|
||||||
|
if (!Number.isFinite(a) || !Number.isFinite(b)) throw new Error('bad range');
|
||||||
|
from = a; to = b;
|
||||||
|
} else {
|
||||||
|
const n = parseInt(stepBase, 10);
|
||||||
|
if (!Number.isFinite(n)) throw new Error('bad value');
|
||||||
|
from = n; to = n;
|
||||||
|
}
|
||||||
|
if (from < lo || to > hi || from > to) throw new Error('out of range');
|
||||||
|
for (let v = from; v <= to; v += step) out.add(v);
|
||||||
|
}
|
||||||
|
return [...out].sort((a, b) => a - b);
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -2348,123 +2348,6 @@ class BackupPage {
|
|||||||
return new Date(iso).toLocaleDateString();
|
return new Date(iso).toLocaleDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatRelative's future-tense sibling — "in 6h", "tomorrow at 5am", etc.
|
|
||||||
// Used by the backup-status header to summarise the next scheduled run.
|
|
||||||
formatRelativeFuture(when) {
|
|
||||||
if (!when) return '';
|
|
||||||
const t = when.getTime();
|
|
||||||
const diff = t - Date.now();
|
|
||||||
if (diff <= 0) return 'imminent';
|
|
||||||
const s = Math.floor(diff / 1000);
|
|
||||||
if (s < 60) return 'in less than a minute';
|
|
||||||
const m = Math.floor(s / 60);
|
|
||||||
if (m < 60) return `in ${m}m`;
|
|
||||||
const h = Math.floor(m / 60);
|
|
||||||
if (h < 24) return `in ${h}h`;
|
|
||||||
const sameDay = (a, b) => a.toDateString() === b.toDateString();
|
|
||||||
const tomorrow = new Date(Date.now() + 86400000);
|
|
||||||
if (sameDay(when, tomorrow)) return 'tomorrow';
|
|
||||||
const days = Math.floor(h / 24);
|
|
||||||
return `in ${days}d`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// "05:00" or "Mon 05:00" depending on whether it's later today or not.
|
|
||||||
formatScheduleClock(when) {
|
|
||||||
if (!when) return '';
|
|
||||||
const sameDay = (new Date()).toDateString() === when.toDateString();
|
|
||||||
const t = when.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
||||||
if (sameDay) return `at ${t}`;
|
|
||||||
const day = when.toLocaleDateString([], { weekday: 'short' });
|
|
||||||
return `${day} ${t}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tiny cron-next utility — given a 5-field crontab expression
|
|
||||||
// (minute hour dom month dow) returns a Date for the next fire after
|
|
||||||
// now, or null if the expression is unparseable / never fires within
|
|
||||||
// the lookahead window. Supports the common syntax: *, N, lists
|
|
||||||
// (N,M,O), ranges (N-M), and steps (* /N or N-M/S). Doesn't try to
|
|
||||||
// be a full cron implementation — just enough for the
|
|
||||||
// CFG_BACKUP_CRONTAB_APP value the WebUI shows in the header.
|
|
||||||
nextCronFireTime(expr) {
|
|
||||||
const fields = String(expr || '').trim().split(/\s+/);
|
|
||||||
if (fields.length !== 5) return null;
|
|
||||||
const ranges = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];
|
|
||||||
let sets;
|
|
||||||
try {
|
|
||||||
sets = fields.map((f, i) => this._cronFieldSet(f, ranges[i][0], ranges[i][1]));
|
|
||||||
} catch (_) { return null; }
|
|
||||||
if (sets.some(s => !s.length)) return null;
|
|
||||||
|
|
||||||
const [minSet, hourSet, domSet, monSet, dowSet] = sets;
|
|
||||||
const minOk = new Set(minSet);
|
|
||||||
const hourOk = new Set(hourSet);
|
|
||||||
const domOk = new Set(domSet);
|
|
||||||
const monOk = new Set(monSet);
|
|
||||||
const dowOk = new Set(dowSet);
|
|
||||||
const domStar = fields[2] === '*';
|
|
||||||
const dowStar = fields[4] === '*';
|
|
||||||
|
|
||||||
// Start one minute from now (rounded down to the minute) and walk
|
|
||||||
// forward checking each candidate. Cap at ~366 days so a totally
|
|
||||||
// unmatchable expression doesn't loop forever.
|
|
||||||
const start = new Date();
|
|
||||||
start.setSeconds(0, 0);
|
|
||||||
start.setMinutes(start.getMinutes() + 1);
|
|
||||||
const limit = start.getTime() + 366 * 86400 * 1000;
|
|
||||||
const cur = new Date(start);
|
|
||||||
while (cur.getTime() < limit) {
|
|
||||||
const m = cur.getMinutes();
|
|
||||||
const h = cur.getHours();
|
|
||||||
const dom = cur.getDate();
|
|
||||||
const mon = cur.getMonth() + 1;
|
|
||||||
const dow = cur.getDay(); // 0 = Sun
|
|
||||||
// POSIX cron: if both DOM and DOW are restricted, fire when
|
|
||||||
// EITHER matches. If only one is restricted, that one must
|
|
||||||
// match. If both are *, day passes.
|
|
||||||
let dayMatch;
|
|
||||||
if (domStar && dowStar) dayMatch = true;
|
|
||||||
else if (domStar) dayMatch = dowOk.has(dow);
|
|
||||||
else if (dowStar) dayMatch = domOk.has(dom);
|
|
||||||
else dayMatch = domOk.has(dom) || dowOk.has(dow);
|
|
||||||
if (dayMatch && monOk.has(mon) && hourOk.has(h) && minOk.has(m)) {
|
|
||||||
return new Date(cur);
|
|
||||||
}
|
|
||||||
cur.setMinutes(cur.getMinutes() + 1);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expand one cron field into a sorted list of valid numeric values.
|
|
||||||
// Throws on bad syntax so nextCronFireTime can drop back to null.
|
|
||||||
_cronFieldSet(field, lo, hi) {
|
|
||||||
const out = new Set();
|
|
||||||
for (const part of String(field).split(',')) {
|
|
||||||
// step (every-Nth): "value/step" — value is "*", a single
|
|
||||||
// number, or a range "a-b".
|
|
||||||
let stepBase = part, step = 1;
|
|
||||||
const slash = part.indexOf('/');
|
|
||||||
if (slash !== -1) {
|
|
||||||
stepBase = part.slice(0, slash);
|
|
||||||
step = parseInt(part.slice(slash + 1), 10);
|
|
||||||
if (!Number.isFinite(step) || step < 1) throw new Error('bad step');
|
|
||||||
}
|
|
||||||
let from = lo, to = hi;
|
|
||||||
if (stepBase === '*' || stepBase === '') {
|
|
||||||
// range stays lo..hi
|
|
||||||
} else if (stepBase.includes('-')) {
|
|
||||||
const [a, b] = stepBase.split('-').map(n => parseInt(n, 10));
|
|
||||||
if (!Number.isFinite(a) || !Number.isFinite(b)) throw new Error('bad range');
|
|
||||||
from = a; to = b;
|
|
||||||
} else {
|
|
||||||
const n = parseInt(stepBase, 10);
|
|
||||||
if (!Number.isFinite(n)) throw new Error('bad value');
|
|
||||||
from = n; to = n;
|
|
||||||
}
|
|
||||||
if (from < lo || to > hi || from > to) throw new Error('out of range');
|
|
||||||
for (let v = from; v <= to; v += step) out.add(v);
|
|
||||||
}
|
|
||||||
return [...out].sort((a, b) => a - b);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.BackupPage = BackupPage;
|
window.BackupPage = BackupPage;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user