eratic / scripts / photos-check.mjs
384 words
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/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`);

static mirror of HEAD · about · clone: git clone https://git.makefuture.ro/eratic.git