#!/usr/bin/env node
/**
* Refuse to publish a photograph that still knows where it was taken.
*
* Astro strips metadata from every image it processes, so the pictures a
* reader downloads are clean. This check is about the other copy: the original
* sitting in the repository. A private repo is not a secret, backups get
* restored to the wrong places, and repositories occasionally get flipped to
* public by accident. A photo file that carries the exact coordinates of your
* flat should not be in version control in the first place.
*
* Run automatically by `npm run build` and by the deploy script. Exits 1 and
* names the files if any of them carry GPS.
*
* To fix a file, the lossless option is your photo editor: Lightroom,
* Capture One and macOS Photos can all strip location on export. The
* last-resort option is `npm run photos:strip`, which re-encodes.
*/
import { readdir, stat } from 'node:fs/promises';
import { join, relative, extname } from 'node:path';
import exifr from 'exifr';
const ROOT = new URL('..', import.meta.url).pathname;
const SEARCH = ['sites', 'packages'];
const IMAGE_EXT = new Set(['.jpg', '.jpeg', '.tif', '.tiff', '.heic', '.heif', '.png', '.webp']);
const SKIP_DIR = new Set(['node_modules', 'dist', '.astro', '.git']);
async function walk(dir, found = []) {
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return found;
}
for (const entry of entries) {
if (entry.isDirectory()) {
if (SKIP_DIR.has(entry.name)) continue;
await walk(join(dir, entry.name), found);
} else if (IMAGE_EXT.has(extname(entry.name).toLowerCase())) {
found.push(join(dir, entry.name));
}
}
return found;
}
const files = (await Promise.all(SEARCH.map((dir) => walk(join(ROOT, dir))))).flat();
const offenders = [];
for (const file of files) {
try {
// Ask only for the GPS block. If it parses, there is location data.
const gps = await exifr.gps(file);
if (gps && (gps.latitude !== undefined || gps.longitude !== undefined)) {
offenders.push({ file: relative(ROOT, file), gps });
}
} catch {
// Unreadable or metadata-free. Either way, nothing to leak.
}
}
if (offenders.length > 0) {
console.error(`\n✗ ${offenders.length} image(s) carry GPS coordinates:\n`);
for (const { file, gps } of offenders) {
console.error(` ${file}`);
console.error(` ${gps.latitude?.toFixed(5)}, ${gps.longitude?.toFixed(5)}`);
}
console.error(`\n Strip location on export from your photo editor, or run:`);
console.error(` npm run photos:strip\n`);
process.exit(1);
}
console.log(`✓ ${files.length} image(s) checked, no GPS data found`);