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
@@ -538,6 +538,31 @@ describe('fetchWikimediaPhoto (fetch stubbed)', () => {
expect(result!.attribution).toBe('Alice');
});
it('MAPS-036b: geosearch prefers the scaled thumburl over the full-res original', async () => {
const wikiResponse = { ok: true, json: async () => ({ query: { pages: { '-1': {} } } }) };
const commonsResponse = {
ok: true,
json: async () => ({
query: { pages: { '1': {
imageinfo: [{
url: 'https://commons.org/original-16mb.jpg',
thumburl: 'https://commons.org/thumb-400.jpg',
mime: 'image/jpeg',
extmetadata: { Artist: { value: 'Alice' } },
}],
} } },
}),
};
vi.stubGlobal('fetch', vi.fn()
.mockResolvedValueOnce(wikiResponse)
.mockResolvedValueOnce(commonsResponse));
const { fetchWikimediaPhoto } = await import('../../../src/services/mapsService');
const result = await fetchWikimediaPhoto(48.8, 2.3, 'Some Place');
expect(result).toBeDefined();
expect(result!.photoUrl).toBe('https://commons.org/thumb-400.jpg');
expect(result!.attribution).toBe('Alice');
});
it('MAPS-037: returns null when both strategies find nothing', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,