Upload via GUI (7543 Dateien)
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const http = require('node:http');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const archiver = require('archiver');
|
||||
|
||||
require('../src/git/gitBinary.js').useBundledGit();
|
||||
const { registerFeatureIpc } = require('../src/features/featuresIpc.js');
|
||||
|
||||
const git = (cwd, ...a) => spawnSync('git', ['-c', 'user.name=t', '-c', 'user.email=t@t', ...a], { cwd, encoding: 'utf8' });
|
||||
const tmp = (p) => fs.mkdtempSync(path.join(os.tmpdir(), p));
|
||||
|
||||
function makeZip(file, entries) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const out = fs.createWriteStream(file);
|
||||
const zip = archiver('zip');
|
||||
out.on('close', resolve);
|
||||
zip.on('error', reject);
|
||||
zip.pipe(out);
|
||||
for (const [name, content] of entries) zip.append(content, { name });
|
||||
zip.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
// Backup-Ordner wie von GiteaRepoBackup angelegt: Spiegel + ZIP-Stände
|
||||
async function makeBackupDir() {
|
||||
const work = tmp('gm-src-');
|
||||
git(work, 'init', '-q', '-b', 'main');
|
||||
fs.writeFileSync(path.join(work, 'a.txt'), 'hallo');
|
||||
git(work, 'add', '.'); git(work, 'commit', '-qm', 'eins');
|
||||
git(work, 'branch', 'feature'); git(work, 'tag', 'v1.0');
|
||||
const dir = tmp('gm-bk-');
|
||||
fs.mkdirSync(path.join(dir, 'M_Viper'));
|
||||
git(dir, 'clone', '-q', '--mirror', work, path.join(dir, 'M_Viper', 'deskhub.git'));
|
||||
const zipDir = path.join(dir, 'M_Viper', 'deskhub-archiv');
|
||||
fs.mkdirSync(zipDir);
|
||||
await makeZip(path.join(zipDir, 'deskhub-2026-10-01_10-00.zip'), [['deskhub/a.txt', 'alt'], ['deskhub/src/b.kt', 'fun b() {}']]);
|
||||
await makeZip(path.join(zipDir, 'deskhub-2026-10-20_10-00.zip'), [['deskhub/a.txt', 'neu'], ['deskhub/src/b.kt', 'fun b() {}']]);
|
||||
fs.utimesSync(path.join(zipDir, 'deskhub-2026-10-01_10-00.zip'), new Date('2026-10-01'), new Date('2026-10-01'));
|
||||
fs.writeFileSync(path.join(dir, 'backup-protokoll.json'), '{}');
|
||||
return { dir, work };
|
||||
}
|
||||
|
||||
// Gitea-Nachbau: neue Repos zeigen auf echte lokale Bare-Repos, damit der Push wirklich läuft
|
||||
function fakeGitea() {
|
||||
const created = [];
|
||||
const patches = [];
|
||||
const repoRoot = tmp('gm-remote-');
|
||||
const server = http.createServer((req, res) => {
|
||||
const u = new URL(req.url, 'http://x');
|
||||
const send = (code, obj) => { res.statusCode = code; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); };
|
||||
let body = '';
|
||||
req.on('data', c => body += c);
|
||||
req.on('end', () => {
|
||||
if (u.pathname === '/api/v1/user') return send(200, { login: 'M_Viper' });
|
||||
if (u.pathname === '/api/v1/user/repos' && req.method === 'POST') {
|
||||
const b = JSON.parse(body);
|
||||
if (b.name === 'deskhub') return send(409, { message: 'repository already exists' });
|
||||
const bare = path.join(repoRoot, `${b.name}.git`);
|
||||
spawnSync('git', ['init', '-q', '--bare', '-b', 'main', bare]);
|
||||
created.push(b);
|
||||
return send(201, { name: b.name, full_name: `M_Viper/${b.name}`, owner: { login: 'M_Viper' }, clone_url: bare, html_url: `http://gitea/M_Viper/${b.name}`, default_branch: 'main' });
|
||||
}
|
||||
if (u.pathname.startsWith('/api/v1/repos/M_Viper/') && req.method === 'PATCH') { patches.push(JSON.parse(body)); return send(200, {}); }
|
||||
send(404, { message: 'not found' });
|
||||
});
|
||||
});
|
||||
return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, created, patches, repoRoot, url: `http://127.0.0.1:${server.address().port}` })));
|
||||
}
|
||||
|
||||
function setup(url) {
|
||||
const handlers = {};
|
||||
const userData = tmp('gm-ud-');
|
||||
registerFeatureIpc({
|
||||
ipcMain: { handle: (name, fn) => { handlers[name] = fn; } },
|
||||
app: { getPath: () => userData },
|
||||
readCredentials: () => ({ giteaURL: url, giteaToken: 'geheim123' }),
|
||||
buildItemsFromPaths: async () => ({ items: [], skipped: [], ignored: [] }),
|
||||
clearGitTransportBlock: () => false
|
||||
});
|
||||
return (name, data) => handlers[name]({ sender: { send() {} } }, data);
|
||||
}
|
||||
|
||||
test('Sicherungen werden gefunden: Spiegel und ZIP-Stände, neuester zuerst', async () => {
|
||||
const { dir } = await makeBackupDir();
|
||||
const call = setup('http://127.0.0.1:1');
|
||||
const r = await call('repo-backup-list', { dir });
|
||||
assert.strictEqual(r.ok, true, r.error);
|
||||
assert.strictEqual(r.backups.length, 1);
|
||||
const b = r.backups[0];
|
||||
assert.deepStrictEqual([b.owner, b.repo], ['M_Viper', 'deskhub']);
|
||||
assert.ok(b.mirror);
|
||||
assert.deepStrictEqual(b.zips.map(z => z.file), ['deskhub-2026-10-20_10-00.zip', 'deskhub-2026-10-01_10-00.zip']);
|
||||
});
|
||||
|
||||
test('Lokal: Spiegel wird zur Arbeitskopie mit allen Branches, ZIP ohne Oberordner entpackt', async () => {
|
||||
const { dir } = await makeBackupDir();
|
||||
const call = setup('http://127.0.0.1:1');
|
||||
const { backups: [b] } = await call('repo-backup-list', { dir });
|
||||
|
||||
const dest1 = tmp('gm-dst-');
|
||||
const m = await call('repo-restore-local', { kind: 'mirror', source: b.mirror.path, destParent: dest1, repo: 'deskhub' });
|
||||
assert.strictEqual(m.ok, true, m.error);
|
||||
assert.strictEqual(fs.readFileSync(path.join(dest1, 'deskhub', 'a.txt'), 'utf8'), 'hallo');
|
||||
assert.match(git(path.join(dest1, 'deskhub'), 'branch', '-r').stdout, /feature/);
|
||||
|
||||
// Zweiter Versuch in denselben Ordner darf nichts überschreiben
|
||||
const again = await call('repo-restore-local', { kind: 'mirror', source: b.mirror.path, destParent: dest1, repo: 'deskhub' });
|
||||
assert.strictEqual(again.ok, false);
|
||||
assert.match(again.error, /existiert bereits/);
|
||||
|
||||
const dest2 = tmp('gm-dst-');
|
||||
const z = await call('repo-restore-local', { kind: 'zip', source: b.zips[0].path, destParent: dest2, repo: 'deskhub' });
|
||||
assert.strictEqual(z.ok, true, z.error);
|
||||
assert.strictEqual(z.files, 2);
|
||||
assert.strictEqual(fs.readFileSync(path.join(dest2, 'deskhub', 'a.txt'), 'utf8'), 'neu');
|
||||
assert.ok(fs.existsSync(path.join(dest2, 'deskhub', 'src', 'b.kt')));
|
||||
});
|
||||
|
||||
// ZIP von Hand bauen: archiver entschärft "../" schon beim Erzeugen, ein echter Angreifer nicht.
|
||||
function rawZip(file, entries) {
|
||||
const zlib = require('node:zlib');
|
||||
const locals = [];
|
||||
const centrals = [];
|
||||
let offset = 0;
|
||||
for (const [name, content] of entries) {
|
||||
const n = Buffer.from(name);
|
||||
const data = Buffer.from(content);
|
||||
const crc = zlib.crc32(data);
|
||||
const lh = Buffer.alloc(30);
|
||||
lh.writeUInt32LE(0x04034b50, 0); lh.writeUInt16LE(20, 4); lh.writeUInt32LE(crc, 14);
|
||||
lh.writeUInt32LE(data.length, 18); lh.writeUInt32LE(data.length, 22); lh.writeUInt16LE(n.length, 26);
|
||||
const ch = Buffer.alloc(46);
|
||||
ch.writeUInt32LE(0x02014b50, 0); ch.writeUInt16LE(20, 4); ch.writeUInt16LE(20, 6); ch.writeUInt32LE(crc, 16);
|
||||
ch.writeUInt32LE(data.length, 20); ch.writeUInt32LE(data.length, 24); ch.writeUInt16LE(n.length, 28); ch.writeUInt32LE(offset, 42);
|
||||
locals.push(lh, n, data);
|
||||
centrals.push(ch, n);
|
||||
offset += lh.length + n.length + data.length;
|
||||
}
|
||||
const cd = Buffer.concat(centrals);
|
||||
const end = Buffer.alloc(22);
|
||||
end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(entries.length, 8); end.writeUInt16LE(entries.length, 10);
|
||||
end.writeUInt32LE(cd.length, 12); end.writeUInt32LE(offset, 16);
|
||||
fs.writeFileSync(file, Buffer.concat([...locals, cd, end]));
|
||||
}
|
||||
|
||||
test('Archive mit Pfaden außerhalb des Ziels werden abgewiesen', async () => {
|
||||
const d = tmp('gm-evil-');
|
||||
const zip = path.join(d, 'evil.zip');
|
||||
rawZip(zip, [['deskhub/ok.txt', 'ok'], ['deskhub/../../boese.txt', 'x']]);
|
||||
const unzipper = require('unzipper');
|
||||
const names = (await unzipper.Open.file(zip)).files.map(f => f.path);
|
||||
assert.ok(names.includes('deskhub/../../boese.txt'), 'Testarchiv muss den gefährlichen Pfad wirklich enthalten');
|
||||
const call = setup('http://127.0.0.1:1');
|
||||
const destParent = tmp('gm-dst-');
|
||||
const r = await call('repo-restore-local', { kind: 'zip', source: zip, destParent, repo: 'x' });
|
||||
assert.strictEqual(r.ok, false);
|
||||
assert.match(r.error, /Unsicherer Pfad/);
|
||||
assert.ok(!fs.existsSync(path.join(destParent, 'x', 'ok.txt')), 'auch harmlose Dateien dürfen dann nicht entpackt werden');
|
||||
assert.ok(!fs.existsSync(path.join(destParent, 'boese.txt')));
|
||||
});
|
||||
|
||||
test('Gitea: Spiegel und ZIP als neues Repo, Namenskonflikt und ZIP-Überschreiben werden abgefangen', async () => {
|
||||
const { dir } = await makeBackupDir();
|
||||
const g = await fakeGitea();
|
||||
try {
|
||||
const call = setup(g.url);
|
||||
const { backups: [b] } = await call('repo-backup-list', { dir });
|
||||
|
||||
const m = await call('repo-restore-gitea', { kind: 'mirror', source: b.mirror.path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'deskhub-wieder', isPrivate: true });
|
||||
assert.strictEqual(m.ok, true, m.error);
|
||||
const bare = path.join(g.repoRoot, 'deskhub-wieder.git');
|
||||
const refs = git(bare, 'for-each-ref', '--format=%(refname)').stdout;
|
||||
assert.match(refs, /refs\/heads\/main/);
|
||||
assert.match(refs, /refs\/heads\/feature/);
|
||||
assert.match(refs, /refs\/tags\/v1\.0/);
|
||||
assert.strictEqual(g.created[0].private, true);
|
||||
|
||||
const z = await call('repo-restore-gitea', { kind: 'zip', source: b.zips[0].path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'deskhub-zip' });
|
||||
assert.strictEqual(z.ok, true, z.error);
|
||||
assert.strictEqual(z.method, 'Git');
|
||||
const files = git(path.join(g.repoRoot, 'deskhub-zip.git'), 'ls-tree', '-r', '--name-only', 'main').stdout.trim().split('\n');
|
||||
assert.deepStrictEqual(files.sort(), ['a.txt', 'src/b.kt']);
|
||||
|
||||
const conflict = await call('repo-restore-gitea', { kind: 'mirror', source: b.mirror.path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'deskhub' });
|
||||
assert.strictEqual(conflict.ok, false);
|
||||
assert.match(conflict.error, /existiert bereits/);
|
||||
|
||||
const zipOverwrite = await call('repo-restore-gitea', { kind: 'zip', source: b.zips[0].path, owner: 'M_Viper', repo: 'deskhub', mode: 'overwrite' });
|
||||
assert.strictEqual(zipOverwrite.ok, false);
|
||||
assert.match(zipOverwrite.error, /nur mit einem Git-Spiegel/);
|
||||
|
||||
const badName = await call('repo-restore-gitea', { kind: 'zip', source: b.zips[0].path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'mit leerzeichen' });
|
||||
assert.strictEqual(badName.ok, false);
|
||||
} finally {
|
||||
g.server.close();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user