Backup: restore from a backup file, and a personal data export
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a20858a6e1
commit
3e3505ba33
2 files changed
+79
-4
No files matched your search
@@ -0,0 +1,27 @@
|
||||
// Backup restore and personal export: a good backup is swapped in (the old data kept aside), unsafe archives are refused, and an export holds only your own data without sign-in tokens.
|
||||
const assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path'), http = require('http'), { execFileSync } = require('child_process');
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-backup-')); process.env.DATA_DIR = dir; process.env.NO_RESTART_AFTER_RESTORE = '1';
|
||||
const users = require('../users'), backup = require('../backup');
|
||||
const a = users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), b = users.create({ name: 'Bo', role: 'user', handle: 'ZP' });
|
||||
fs.writeFileSync(path.join(users.dirOf(a.id), 'tasks.json'), '{"mine":1}'); fs.writeFileSync(path.join(users.dirOf(a.id), 'mail.json'), '{"token":"secret"}'); fs.writeFileSync(path.join(users.dirOf(b.id), 'tasks.json'), '{"bo":1}');
|
||||
const hub = http.createServer((req, res) => users.runAs(a, () => backup.handle(req, res, 'Ann'))).listen(0, async () => {
|
||||
const base = 'http://127.0.0.1:' + hub.address().port;
|
||||
const mk = (name, fn) => { const w = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-bk-src-')); fn(w); const f = path.join(os.tmpdir(), name); execFileSync('tar', ['-czf', f, '-C', w, '.']); return fs.readFileSync(f); };
|
||||
try {
|
||||
// export: only my data, no tokens
|
||||
const ex = Buffer.from(await (await fetch(base + '/api/export')).arrayBuffer()), ef = path.join(os.tmpdir(), 'nehub-ex.tar.gz'); fs.writeFileSync(ef, ex);
|
||||
const list = execFileSync('tar', ['-tzf', ef]).toString(); assert.match(list, /me\/tasks\.json/); assert.ok(!/mail\.json/.test(list), 'sign-in tokens are left out'); assert.ok(!list.includes(b.id), 'nobody else\'s data');
|
||||
// a good backup is swapped in; the old data is kept
|
||||
const good = mk('good.tar.gz', w => { fs.writeFileSync(path.join(w, 'secret.json'), '{"restored":true}'); fs.mkdirSync(path.join(w, 'users')); });
|
||||
let r = await fetch(base + '/api/backup/restore', { method: 'POST', body: good }); let j = await r.json(); assert.equal(r.status, 200, JSON.stringify(j)); assert.ok(j.restarting);
|
||||
assert.equal(JSON.parse(fs.readFileSync(path.join(dir, 'secret.json'), 'utf8')).restored, true); assert.ok(fs.existsSync(path.join(dir, j.kept, 'users')), 'the old accounts are kept aside');
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.restore')) && !fs.existsSync(path.join(dir, '.restore.tar.gz')), 'the staging files are cleaned up');
|
||||
// unsafe or foreign archives are refused and change nothing
|
||||
const evil = mk('evil.tar.gz', w => { fs.writeFileSync(path.join(w, 'secret.json'), '{"evil":true}'); fs.symlinkSync('/etc/passwd', path.join(w, 'users')); });
|
||||
r = await fetch(base + '/api/backup/restore', { method: 'POST', body: evil }); assert.equal(r.status, 400); assert.match((await r.json()).error, /links/); assert.equal(JSON.parse(fs.readFileSync(path.join(dir, 'secret.json'), 'utf8')).restored, true, 'nothing changed');
|
||||
const foreign = mk('foreign.tar.gz', w => fs.writeFileSync(path.join(w, 'readme.txt'), 'hi')); r = await fetch(base + '/api/backup/restore', { method: 'POST', body: foreign }); assert.equal(r.status, 400); assert.match((await r.json()).error, /NE Hub backup/);
|
||||
r = await fetch(base + '/api/backup/restore', { method: 'POST', body: Buffer.from('not a tar') }); assert.equal(r.status, 400);
|
||||
console.log('backup test ok');
|
||||
} catch (e) { console.error(e); process.exitCode = 1; }
|
||||
hub.close();
|
||||
});
|
||||
Reference in new issue
Block a user