mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-30 18:46:00 +00:00
fix(backups): prevent recursion in path that is backed up
This commit is contained in:
@@ -184,9 +184,21 @@ export async function createBackup(): Promise<BackupInfo> {
|
||||
// Exclude the place-photo and trek-memory caches: both are re-derivable
|
||||
// (re-fetched on demand, keyed on stable ids) and would otherwise dominate
|
||||
// backup size. Restores self-heal — the cache dirs are recreated at startup.
|
||||
//
|
||||
// Also exclude backups/ and restore-*/: these live under data/, not uploads/,
|
||||
// but when an install maps data and uploads to the SAME directory (a
|
||||
// misconfiguration, but a catastrophic one) the glob would otherwise sweep
|
||||
// every prior backup zip into the new archive — each run embedding all
|
||||
// previous runs, so size compounds without bound (see issue #1358). Ignoring
|
||||
// them keeps the backup bounded regardless of how the volumes are mounted.
|
||||
archive.glob(
|
||||
'**/*',
|
||||
{ cwd: uploadsDir, ignore: ['photos/google/**', 'photos/trek/**'], nodir: true, dot: true },
|
||||
{
|
||||
cwd: uploadsDir,
|
||||
ignore: ['photos/google/**', 'photos/trek/**', 'backups/**', 'restore-*/**'],
|
||||
nodir: true,
|
||||
dot: true,
|
||||
},
|
||||
{ prefix: 'uploads' },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -475,7 +475,7 @@ describe('BACKUP-036 createBackup', () => {
|
||||
'**/*',
|
||||
expect.objectContaining({
|
||||
cwd: expect.stringContaining('uploads'),
|
||||
ignore: ['photos/google/**', 'photos/trek/**'],
|
||||
ignore: ['photos/google/**', 'photos/trek/**', 'backups/**', 'restore-*/**'],
|
||||
}),
|
||||
{ prefix: 'uploads' },
|
||||
);
|
||||
@@ -483,6 +483,38 @@ describe('BACKUP-036 createBackup', () => {
|
||||
expect(archiverInstanceMock.directory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('BACKUP-036h — never sweeps backups/ or restore-* into the archive (issue #1358)', async () => {
|
||||
// Regression guard: when data and uploads map to the same directory, the
|
||||
// uploads glob would otherwise pick up the backups/ dir and recursively
|
||||
// embed every prior backup zip, compounding size without bound.
|
||||
fsMock.existsSync.mockImplementation((p: string) => String(p).endsWith('uploads'));
|
||||
fsMock.mkdirSync.mockReturnValue(undefined);
|
||||
|
||||
const writableEvents: Record<string, Function> = {};
|
||||
const fakeWriteStream = {
|
||||
on: vi.fn((event: string, cb: Function) => {
|
||||
writableEvents[event] = cb;
|
||||
}),
|
||||
};
|
||||
fsMock.createWriteStream.mockReturnValue(fakeWriteStream);
|
||||
|
||||
archiverInstanceMock.on.mockImplementation((_e: string, _cb: Function) => {});
|
||||
archiverInstanceMock.pipe.mockReturnValue(undefined);
|
||||
archiverInstanceMock.finalize.mockImplementation(() => {
|
||||
if (writableEvents['close']) writableEvents['close']();
|
||||
});
|
||||
archiverMock.mockReturnValue(archiverInstanceMock);
|
||||
|
||||
fsMock.statSync.mockReturnValue({ size: 1024, birthtime: new Date('2026-04-06T12:00:00Z') });
|
||||
|
||||
await createBackup();
|
||||
|
||||
const globCall = archiverInstanceMock.glob.mock.calls.at(-1);
|
||||
const ignore: string[] = globCall?.[1]?.ignore ?? [];
|
||||
expect(ignore).toContain('backups/**');
|
||||
expect(ignore).toContain('restore-*/**');
|
||||
});
|
||||
|
||||
it('BACKUP-036f — bundles .encryption_key when present and ENCRYPTION_KEY env is unset', async () => {
|
||||
const prevEnvKey = process.env.ENCRYPTION_KEY;
|
||||
delete process.env.ENCRYPTION_KEY;
|
||||
|
||||
Reference in New Issue
Block a user