fix(maps): bound place-photo cache growth (Wikimedia + Google) (#1174)

The place-photo cache (uploads/photos/google) grew unbounded: a Wikimedia
geosearch path cached full-res originals despite requesting a 400px thumb,
the writer applied no size guard, nothing reclaimed orphaned files, and
backups archived the whole re-derivable cache verbatim.

- Prefer the scaled `thumburl` over the full-res `info.url` in the Commons
  geosearch fallback.
- Downscale any cached image to <=800px JPEG via the existing jimp dep,
  with a safe fallback to the original bytes on decode failure.
- Add sweepOrphans() (orphaned meta rows + stray files) wired into the
  scheduler (startup + nightly), and removeIfUnreferenced() called on
  place delete for prompt reclamation.
- Exclude the re-derivable photo/trek caches from backups; restores
  self-heal as the cache dirs are recreated at startup.
This commit is contained in:
jubnl
2026-06-14 23:31:02 +02:00
committed by GitHub
parent 3e9626fce9
commit 8077ffab34
10 changed files with 349 additions and 13 deletions
@@ -25,6 +25,7 @@ const archiverInstanceMock = vi.hoisted(() => ({
pipe: vi.fn(),
file: vi.fn(),
directory: vi.fn(),
glob: vi.fn(),
finalize: vi.fn(),
on: vi.fn(),
}));
@@ -441,7 +442,7 @@ describe('BACKUP-036 createBackup', () => {
);
});
it('BACKUP-036e — includes uploads directory when it exists', async () => {
it('BACKUP-036e — includes uploads but excludes the re-derivable photo caches', async () => {
fsMock.existsSync.mockImplementation((p: string) => {
if (String(p).endsWith('uploads')) return true;
return false;
@@ -467,10 +468,16 @@ describe('BACKUP-036 createBackup', () => {
await createBackup();
expect(archiverInstanceMock.directory).toHaveBeenCalledWith(
expect.stringContaining('uploads'),
'uploads'
expect(archiverInstanceMock.glob).toHaveBeenCalledWith(
'**/*',
expect.objectContaining({
cwd: expect.stringContaining('uploads'),
ignore: ['photos/google/**', 'photos/trek/**'],
}),
{ prefix: 'uploads' },
);
// The re-derivable caches must not be archived verbatim.
expect(archiverInstanceMock.directory).not.toHaveBeenCalled();
});
});