178 lines
5.6 KiB
JavaScript
178 lines
5.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Sync ftp-images -> public/images and report changes.
|
|
* Usage: node scripts/sync-ftp-images.mjs [--dry-run]
|
|
*
|
|
* Mapping:
|
|
* ftp-images/bilder-bp/ -> public/images/dishes/ (menu uses /images/dishes/...)
|
|
* ftp-images/<other>/ -> public/images/<other>/
|
|
*/
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import crypto from "node:crypto";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.resolve(__dirname, "..");
|
|
const DRY_RUN = process.argv.includes("--dry-run");
|
|
const FTP_ROOT = path.join(ROOT, "ftp-images");
|
|
const PUBLIC_IMAGES = path.join(ROOT, "public", "images");
|
|
const LOG_FILE = path.join(ROOT, "sync-images-log.txt");
|
|
const RESULT_FILE = path.join(ROOT, "sync-result.json");
|
|
|
|
/** FTP subfolder name -> public/images subfolder name */
|
|
const DEST_ALIASES = {
|
|
"bilder-bp": "dishes",
|
|
};
|
|
|
|
const IMAGE_EXT = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif", ".svg", ".avif"]);
|
|
|
|
const logLines = [];
|
|
const log = (...args) => {
|
|
const line = args.map(String).join(" ");
|
|
logLines.push(line);
|
|
console.log(...args);
|
|
};
|
|
|
|
function hashFile(filePath) {
|
|
const data = fs.readFileSync(filePath);
|
|
return crypto.createHash("md5").update(data).digest("hex");
|
|
}
|
|
|
|
function walk(dir) {
|
|
const out = [];
|
|
if (!fs.existsSync(dir)) return out;
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) out.push(...walk(full));
|
|
else if (entry.isFile()) out.push(full);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function rel(from, to) {
|
|
return path.relative(from, to).split(path.sep).join("/");
|
|
}
|
|
|
|
function discoverSources() {
|
|
if (!fs.existsSync(FTP_ROOT)) return [];
|
|
const pairs = [];
|
|
for (const entry of fs.readdirSync(FTP_ROOT, { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) continue;
|
|
const destName = DEST_ALIASES[entry.name] ?? entry.name;
|
|
pairs.push({
|
|
src: path.join(FTP_ROOT, entry.name),
|
|
dest: path.join(PUBLIC_IMAGES, destName),
|
|
srcLabel: `ftp-images/${entry.name}`,
|
|
destLabel: `public/images/${destName}`,
|
|
});
|
|
}
|
|
return pairs;
|
|
}
|
|
|
|
function syncPair({ src, dest, srcLabel, destLabel }) {
|
|
fs.mkdirSync(dest, { recursive: true });
|
|
|
|
const srcFiles = walk(src).filter((f) => IMAGE_EXT.has(path.extname(f).toLowerCase()));
|
|
const destFiles = walk(dest).filter((f) => IMAGE_EXT.has(path.extname(f).toLowerCase()));
|
|
const srcSet = new Set(srcFiles.map((f) => rel(src, f)));
|
|
|
|
const copied = [];
|
|
const updated = [];
|
|
const unchanged = [];
|
|
const removed = [];
|
|
|
|
for (const file of srcFiles) {
|
|
const relPath = rel(src, file);
|
|
const target = path.join(dest, relPath);
|
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
|
|
if (!fs.existsSync(target)) {
|
|
if (!DRY_RUN) fs.copyFileSync(file, target);
|
|
copied.push(relPath);
|
|
continue;
|
|
}
|
|
|
|
if (hashFile(file) !== hashFile(target)) {
|
|
if (!DRY_RUN) fs.copyFileSync(file, target);
|
|
updated.push(relPath);
|
|
} else {
|
|
unchanged.push(relPath);
|
|
}
|
|
}
|
|
|
|
for (const file of destFiles) {
|
|
const relPath = rel(dest, file);
|
|
if (!srcSet.has(relPath)) {
|
|
if (!DRY_RUN) fs.unlinkSync(file);
|
|
removed.push(relPath);
|
|
}
|
|
}
|
|
|
|
log(`\n=== ${srcLabel} -> ${destLabel} ===`);
|
|
log(` new: ${copied.length} updated: ${updated.length} unchanged: ${unchanged.length} removed: ${removed.length}`);
|
|
for (const f of copied) log(` + ${f}`);
|
|
for (const f of updated) log(` ~ ${f}`);
|
|
for (const f of removed) log(` - ${f}`);
|
|
|
|
return { copied, updated, unchanged, removed, src: srcLabel, dest: destLabel };
|
|
}
|
|
|
|
function checkMenuReferences() {
|
|
const menuFile = path.join(ROOT, "infrastructure", "menu", "static-menu-data.ts");
|
|
if (!fs.existsSync(menuFile)) {
|
|
log("\nMenu file not found; skipping reference check.");
|
|
return { unique: [], missing: [] };
|
|
}
|
|
|
|
const text = fs.readFileSync(menuFile, "utf8");
|
|
const refs = [...text.matchAll(/["'`](\/images\/[^"'`]+)["'`]/g)].map((m) => m[1]);
|
|
const unique = [...new Set(refs)];
|
|
const missing = unique.filter((ref) => !fs.existsSync(path.join(ROOT, "public", ref.replace(/^\//, ""))));
|
|
|
|
log(`\n=== Menu references: ${unique.length} unique, ${missing.length} missing ===`);
|
|
for (const m of missing) log(` ! ${m}`);
|
|
return { unique, missing };
|
|
}
|
|
|
|
function writeOutputs(result) {
|
|
if (!DRY_RUN) {
|
|
fs.writeFileSync(LOG_FILE, logLines.join("\n") + "\n");
|
|
fs.writeFileSync(RESULT_FILE, JSON.stringify(result, null, 2) + "\n");
|
|
log(`\nWrote ${path.relative(ROOT, LOG_FILE)}`);
|
|
log(`Wrote ${path.relative(ROOT, RESULT_FILE)}`);
|
|
}
|
|
}
|
|
|
|
if (!fs.existsSync(FTP_ROOT)) {
|
|
console.error(`ftp-images not found at ${FTP_ROOT}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
log(DRY_RUN ? "DRY RUN\n" : "Syncing...\n");
|
|
|
|
const pairs = discoverSources();
|
|
if (pairs.length === 0) {
|
|
log("No subfolders found in ftp-images/");
|
|
writeOutputs({ totals: { copied: 0, updated: 0, unchanged: 0, removed: 0 }, pairs: [], menu: { unique: [], missing: [] } });
|
|
process.exit(0);
|
|
}
|
|
|
|
const pairResults = [];
|
|
const totals = { copied: 0, updated: 0, unchanged: 0, removed: 0 };
|
|
|
|
for (const pair of pairs) {
|
|
const result = syncPair(pair);
|
|
pairResults.push(result);
|
|
totals.copied += result.copied.length;
|
|
totals.updated += result.updated.length;
|
|
totals.unchanged += result.unchanged.length;
|
|
totals.removed += result.removed.length;
|
|
}
|
|
|
|
log(`\n=== TOTALS: ${totals.copied} new, ${totals.updated} updated, ${totals.unchanged} unchanged, ${totals.removed} removed ===`);
|
|
|
|
const menu = checkMenuReferences();
|
|
writeOutputs({ dryRun: DRY_RUN, totals, pairs: pairResults, menu });
|
|
|
|
if (menu.missing.length) process.exitCode = 1; |