Fix harmonize:images thumb rebuild and allow oversized museum scans.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila KhodjaefandCursor committed 2026-07-27 12:23:25 +03:00
1 parent 6656f91f25
commit 89e39d408a
8 files changed
+92 -13

No files matched your search

+75 -4
View File
@@ -1,24 +1,33 @@
/**
* Bidirectional image harmonize (dev <-> prod) by file mtime.
* Bidirectional image harmonize (dev <-> prod) by file mtime,
* then regenerate painting + portrait thumbnails on both image roots.
*
* Usage:
* npm run harmonize:images
* npm run harmonize:images -- --dry-run
* npm run harmonize:images -- --skip-thumbnails
*/
const fs = require('fs');
const path = require('path');
const { rootDir, confirmProdAction } = require('./db-env');
const { spawnSync } = require('child_process');
const { rootDir, confirmProdAction, PROD_DB_NAME } = require('./db-env');
const { loadHarmonizeConfig, prodImagesPath } = require('./lib/harmonize-config');
const { printCliResult } = require('./lib/cli-result');
const IMAGE_SUBDIRS = ['portraits', 'paintings'];
const MTIME_TOLERANCE_MS = 1000;
const THUMB_SCRIPTS = [
'regenerate-thumbnails.js',
'regenerate-portrait-thumbs.js',
];
function parseArgs(argv) {
const configArg = argv.find((a) => a.startsWith('--config='));
return {
dryRun: argv.includes('--dry-run'),
verbose: argv.includes('--verbose'),
skipThumbnails: argv.includes('--skip-thumbnails'),
configPath: configArg ? configArg.slice('--config='.length) : null,
};
}
@@ -78,6 +87,36 @@ function copyFile(src, dest, dryRun) {
fs.copyFileSync(src, dest);
}
/**
* Rebuild thumbs from full images for one environment.
* Child scripts honour existing env (dotenv will not override DB_NAME / IMAGE_DIR).
*/
function regenerateThumbnails({ label, imageDir, database }) {
console.log(`\n--- Thumbnails (${label}) ---`);
console.log(` IMAGE_DIR=${imageDir}`);
console.log(` DB_NAME=${database}`);
const env = {
...process.env,
IMAGE_DIR: imageDir,
DB_NAME: database,
};
for (const file of THUMB_SCRIPTS) {
const scriptPath = path.join(__dirname, file);
console.log(` Running ${file}…`);
const result = spawnSync(process.execPath, [scriptPath], {
cwd: rootDir,
stdio: 'inherit',
env,
});
const code = result.status ?? 1;
if (code !== 0) {
throw new Error(`Thumbnail regeneration failed (${label}): ${file} exited ${code}`);
}
}
}
function timestampSlug(date = new Date()) {
const pad = (n) => String(n).padStart(2, '0');
return [
@@ -107,7 +146,7 @@ async function main() {
if (!dryRun && process.env.CONFIRM_PROD !== '1') {
await confirmProdAction(
'Harmonize will copy image files between dev and prod (prod files may be overwritten).',
'Harmonize will copy image files between dev and prod, then regenerate painting and portrait thumbnails on both sides (prod files and gallery_prod paths may be updated).',
);
}
@@ -165,9 +204,35 @@ async function main() {
const reportDir = path.join(rootDir, 'db', 'SyncReports');
fs.mkdirSync(reportDir, { recursive: true });
const reportPath = path.join(reportDir, `harmonize_images_${timestampSlug()}.json`);
const thumbDetails = [];
if (!dryRun && !options.skipThumbnails) {
require('dotenv').config({ path: path.join(rootDir, '.env') });
const devDb = process.env.DB_NAME || 'gallery_dev';
regenerateThumbnails({
label: 'dev',
imageDir: devDir,
database: devDb,
});
thumbDetails.push(`Dev thumbs regenerated (${devDb} @ ${devDir})`);
regenerateThumbnails({
label: 'prod',
imageDir: prodDir,
database: PROD_DB_NAME,
});
thumbDetails.push(`Prod thumbs regenerated (${PROD_DB_NAME} @ ${prodDir})`);
} else if (options.skipThumbnails) {
thumbDetails.push('Thumbnails skipped (--skip-thumbnails)');
} else {
thumbDetails.push('Thumbnails skipped (dry-run)');
}
fs.writeFileSync(reportPath, JSON.stringify({
generatedAt: new Date().toISOString(),
dryRun,
skipThumbnails: options.skipThumbnails,
devDir,
prodDir,
stats: {
@@ -175,6 +240,7 @@ async function main() {
prodToDev: stats.prodToDev,
skipped: stats.skipped,
},
thumbnails: thumbDetails,
actions: options.verbose ? stats.actions : stats.actions.slice(0, 500),
}, null, 2));
@@ -183,7 +249,12 @@ async function main() {
script: 'harmonize-images',
ok: true,
summary: `Image harmonize complete${mode}: dev→prod ${stats.devToProd}, prod→dev ${stats.prodToDev}, skipped ${stats.skipped}.`,
details: [`Dev: ${devDir}`, `Prod: ${prodDir}`, `Report: ${reportPath}`],
details: [
`Dev: ${devDir}`,
`Prod: ${prodDir}`,
`Report: ${reportPath}`,
...thumbDetails,
],
});
}
+7 -1
View File
@@ -1691,7 +1691,13 @@ async function generateThumbnailFromFull(fullDest, thumbDest, width = THUMB_WIDT
if (!fs.existsSync(fullDest)) return false;
const ext = path.extname(thumbDest).toLowerCase();
let pipeline = sharp(fullDest).rotate().resize({ width, withoutEnlargement: true });
// Museum scans can exceed Sharp's default ~268MP input cap; we only resize down.
let pipeline = sharp(fullDest, {
limitInputPixels: false,
sequentialRead: true,
})
.rotate()
.resize({ width, withoutEnlargement: true });
if (ext === '.png') {
pipeline = pipeline.png({ quality: 85 });
+1 -1
View File
@@ -1,6 +1,6 @@
const fs = require('fs');
const path = require('path');
const { rootDir } = require('./db-env');
const { rootDir } = require('../db-env');
const DEFAULT_CONFIG_PATH = path.join(rootDir, 'infra', 'deploy', 'harmonize.config.json');
const FALLBACK_CONFIG_PATH = path.join(rootDir, 'infra', 'deploy', 'devtoprod.config.json');
+1 -1
View File
@@ -51,7 +51,7 @@ async function unlinkOldThumbRel(thumbRel, imageRel) {
}
async function aspectRatio(filePath) {
const meta = await sharp(filePath).metadata();
const meta = await sharp(filePath, { limitInputPixels: false }).metadata();
if (!meta.width || !meta.height) return null;
return meta.width / meta.height;
}