Files
shopApiNg/picture-syncer.js

144 lines
5.1 KiB
JavaScript

import fs from 'fs/promises';
import path from 'path';
import sharp from 'sharp';
import { createConnection } from './database.js';
class PictureSyncer {
constructor() {
if (PictureSyncer.instance) {
return PictureSyncer.instance;
}
this.cacheBaseDir = process.env.CACHE_LOCATION || '.';
// Track syncing state per group
this.isSyncing = new Map(); // groupName -> boolean
this.queuedSyncs = new Map(); // groupName -> { imageIds, groupName }
PictureSyncer.instance = this;
}
async syncImages(imageIds, groupName) {
// Check if already syncing this group
if (this.isSyncing.get(groupName)) {
if (this.queuedSyncs.has(groupName)) {
console.log(`🚫 Image sync for '${groupName}' already in progress and queued. Ignoring.`);
return;
}
console.log(`⏳ Image sync for '${groupName}' already in progress. Queuing.`);
this.queuedSyncs.set(groupName, { imageIds, groupName });
return;
}
await this._doSync(imageIds, groupName);
}
async _doSync(imageIds, groupName) {
this.isSyncing.set(groupName, true);
try {
await this._performSync(imageIds, groupName);
} finally {
this.isSyncing.set(groupName, false);
// Process queued sync for this group if any
if (this.queuedSyncs.has(groupName)) {
console.log(`🔄 Processing queued image sync for '${groupName}'...`);
const queued = this.queuedSyncs.get(groupName);
this.queuedSyncs.delete(groupName);
setImmediate(() => this.syncImages(queued.imageIds, queued.groupName));
}
}
}
async _performSync(imageIds, groupName) {
const groupDir = path.join(this.cacheBaseDir, 'img', groupName);
// Ensure directory exists
await fs.mkdir(groupDir, { recursive: true });
// Get existing files
let existingFiles = [];
try {
existingFiles = await fs.readdir(groupDir);
} catch (err) {
// Directory might be empty or new
}
// Filter for image files (assuming we save as {id}.avif)
const existingIds = existingFiles
.filter(f => f.endsWith('.avif'))
.map(f => parseInt(f.replace('.avif', '')));
const validIds = new Set(imageIds.filter(id => id !== null && id !== undefined));
// 1. Delete obsolete images
const toDelete = existingIds.filter(id => !validIds.has(id));
for (const id of toDelete) {
const filePath = path.join(groupDir, `${id}.avif`);
await fs.unlink(filePath);
}
if (toDelete.length > 0) {
console.log(`🗑️ Deleted ${toDelete.length} obsolete images.`);
}
// 2. Download missing images
const toDownload = imageIds.filter(id => id !== null && id !== undefined && !existingIds.includes(id));
if (toDownload.length > 0) {
console.log(`📥 Downloading ${toDownload.length} new images for group '${groupName}'...`);
await this._downloadImages(toDownload, groupDir);
} else {
console.log(`✅ No new images to download for group '${groupName}'.`);
}
}
async _downloadImages(ids, dir) {
let pool;
try {
pool = await createConnection();
// Process in chunks to avoid huge queries
const chunkSize = 50;
for (let i = 0; i < ids.length; i += chunkSize) {
const chunk = ids.slice(i, i + chunkSize);
const list = chunk.join(',');
const result = await pool.request().query(`
SELECT kBild, bBild
FROM tBild
WHERE kBild IN (${list})
`);
for (const record of result.recordset) {
if (record.bBild) {
const filePath = path.join(dir, `${record.kBild}.avif`);
// Resize to 130x130 and convert to AVIF using sharp
await sharp(record.bBild)
.resize(130, 130, {
fit: 'cover',
position: 'center'
})
.avif({ quality: 80 })
.toFile(filePath);
}
}
const processed = Math.min(i + chunkSize, ids.length);
if (processed === ids.length) {
console.log(`✅ Processed ${processed}/${ids.length} images.`);
} else {
console.log(`⏳ Processed ${processed}/${ids.length} images...`);
}
}
} catch (err) {
console.error('❌ Error downloading images:', err);
} finally {
if (pool) {
await pool.close();
}
}
}
}
const instance = new PictureSyncer();
export default instance;