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
@@ -41,6 +41,14 @@ vi.mock('../../../src/config', () => ({
updateJwtSecret: () => {},
}));
// Spy on the photo-cache reclaim hook so delete tests assert the wiring without
// touching disk. The removal logic itself is covered in placePhotoCache.test.ts.
const { removeIfUnreferencedSpy } = vi.hoisted(() => ({ removeIfUnreferencedSpy: vi.fn() }));
vi.mock('../../../src/services/placePhotoCache', async (importOriginal) => ({
...(await importOriginal<typeof import('../../../src/services/placePhotoCache')>()),
removeIfUnreferenced: removeIfUnreferencedSpy,
}));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
@@ -252,6 +260,18 @@ describe('deletePlace', () => {
expect(remaining).toHaveLength(1);
expect(remaining[0].id).toBe(p1.id);
});
it('PLACE-SVC-019b — reclaims the photo cache for the deleted place', () => {
removeIfUnreferencedSpy.mockClear();
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const place = createPlace(testDb, trip.id, { name: 'With Photo' }) as any;
testDb.prepare('UPDATE places SET google_place_id = ? WHERE id = ?').run('ChIJgid', place.id);
deletePlace(String(trip.id), String(place.id));
expect(removeIfUnreferencedSpy).toHaveBeenCalledWith('ChIJgid');
});
});
// ── importGpx ─────────────────────────────────────────────────────────────────