feat: manage persistent uploads and coordinated backups
This commit is contained in:
44
scripts/maintenance.ts
Normal file
44
scripts/maintenance.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createInterface } from 'node:readline/promises';
|
||||
import { Writable } from 'node:stream';
|
||||
import { dataDirectory } from '../src/server/db';
|
||||
import { initialize } from '../src/server/initialize';
|
||||
import { provisionOwner } from '../src/server/owner';
|
||||
import { importLegacy } from '../src/server/legacy-import';
|
||||
import { backup, restore } from '../src/server/backup';
|
||||
import { withContentLock } from '../src/server/write-lock';
|
||||
|
||||
async function credentials() {
|
||||
if (process.env.OWNER_EMAIL && process.env.OWNER_PASSWORD) return { email: process.env.OWNER_EMAIL, password: process.env.OWNER_PASSWORD };
|
||||
if (!process.stdin.isTTY) throw new Error('Use an interactive terminal, or supply OWNER_EMAIL and OWNER_PASSWORD through the environment');
|
||||
let muted = false;
|
||||
const output = new Writable({ write(chunk, _encoding, callback) { if (!muted) process.stdout.write(chunk); callback(); } });
|
||||
const prompt = createInterface({ input: process.stdin, output, terminal: true });
|
||||
try {
|
||||
const email = (await prompt.question('Owner email: ')).trim();
|
||||
process.stdout.write('Password (12–128 characters, hidden): '); muted = true;
|
||||
const password = await prompt.question(''); muted = false; process.stdout.write('\n');
|
||||
process.stdout.write('Confirm password (hidden): '); muted = true;
|
||||
const confirmation = await prompt.question(''); muted = false; process.stdout.write('\n');
|
||||
if (password !== confirmation) throw new Error('Passwords do not match');
|
||||
return { email, password };
|
||||
} finally { prompt.close(); }
|
||||
}
|
||||
async function main() {
|
||||
const [command, argument, target] = process.argv.slice(2);
|
||||
if (command === 'restore') {
|
||||
if (!argument || !target) throw new Error('Usage: restore BACKUP_DIRECTORY NEW_DATA_DIRECTORY');
|
||||
restore(argument, target); console.log('Restored into the new directory. Stop the app before switching its data volume.'); return;
|
||||
}
|
||||
if (!['init', 'create-owner', 'reset-password', 'import-legacy', 'backup'].includes(command)) throw new Error('Commands: init, create-owner, reset-password, import-legacy, backup DIRECTORY, restore BACKUP_DIRECTORY NEW_DATA_DIRECTORY');
|
||||
const connection = initialize();
|
||||
try {
|
||||
if (command === 'create-owner' || command === 'reset-password') {
|
||||
const { email, password } = await credentials();
|
||||
await withContentLock(() => provisionOwner(connection, email, password, command === 'reset-password'));
|
||||
console.log(command === 'create-owner' ? 'Owner created.' : 'Password updated. All sessions revoked.');
|
||||
} else if (command === 'import-legacy') { await withContentLock(() => importLegacy(connection)); console.log('Existing portfolio imported.'); }
|
||||
else if (command === 'backup') { if (!argument) throw new Error('Supply a new backup directory'); await backup(connection, dataDirectory(), argument); console.log('Backup complete.'); }
|
||||
else console.log('Database initialized.');
|
||||
} finally { connection.sqlite.close(); }
|
||||
}
|
||||
main().catch(error => { console.error(error instanceof Error ? error.message : 'Maintenance failed'); process.exitCode = 1; });
|
||||
Reference in New Issue
Block a user