feat: per-source .md sidecar toggles, Picdrop folder-list diagnostic, fast JPEG fullscreen preview + thumb-first, thumbnail backfill
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
+12
-13
@@ -7,26 +7,24 @@ import { uploadBuffer, loadTargetConfig, type PicdropCfg } from './picdrop';
|
||||
import { loadNasConfig } from './nas';
|
||||
import { buildMetadataMd, sidecarName } from './metadata';
|
||||
|
||||
export interface BackupDest { key: string; label: string; cfg: PicdropCfg; }
|
||||
export interface BackupDest { key: string; label: string; cfg: PicdropCfg; sidecar: boolean; }
|
||||
|
||||
/** Alle aktiven Backup-Ziele einsammeln. */
|
||||
/** Alle aktiven Backup-Ziele einsammeln (inkl. Metadaten-Beileger-Flag pro Ziel). */
|
||||
export async function backupDestinations(): Promise<BackupDest[]> {
|
||||
const dests: BackupDest[] = [];
|
||||
const nas = await loadNasConfig();
|
||||
if (nas) dests.push({ key: 'nas', label: 'NAS', cfg: nas });
|
||||
const targets = await query<any>(`SELECT id, name FROM delivery_targets WHERE is_backup=true`);
|
||||
if (nas) {
|
||||
const s = await one<any>('SELECT nas_metadata_sidecar FROM settings WHERE id=1');
|
||||
dests.push({ key: 'nas', label: 'NAS', cfg: nas, sidecar: !!s?.nas_metadata_sidecar });
|
||||
}
|
||||
const targets = await query<any>(`SELECT id, name, metadata_sidecar FROM delivery_targets WHERE is_backup=true`);
|
||||
for (const t of targets) {
|
||||
const cfg = await loadTargetConfig(t.id);
|
||||
if (cfg) dests.push({ key: t.id, label: t.name, cfg });
|
||||
if (cfg) dests.push({ key: t.id, label: t.name, cfg, sidecar: !!t.metadata_sidecar });
|
||||
}
|
||||
return dests;
|
||||
}
|
||||
|
||||
async function sidecarEnabled(): Promise<boolean> {
|
||||
const s = await one<{ metadata_sidecar: boolean }>('SELECT metadata_sidecar FROM settings WHERE id=1');
|
||||
return !!s?.metadata_sidecar;
|
||||
}
|
||||
|
||||
/** Ein Item auf alle Backup-Ziele spiegeln. */
|
||||
export async function backupItem(itemId: string): Promise<{ mirrored: number; failed: number }> {
|
||||
const dests = await backupDestinations();
|
||||
@@ -36,14 +34,15 @@ export async function backupItem(itemId: string): Promise<{ mirrored: number; fa
|
||||
|
||||
const buf = await getObject(it.result_path);
|
||||
const ym = new Date(it.created_at || Date.now()).toISOString().slice(0, 7);
|
||||
const withMeta = await sidecarEnabled();
|
||||
const md = withMeta ? Buffer.from(await buildMetadataMd(itemId), 'utf8') : null;
|
||||
let md: Buffer | null = null;
|
||||
const anySidecar = dests.some((d) => d.sidecar);
|
||||
if (anySidecar) md = Buffer.from(await buildMetadataMd(itemId), 'utf8');
|
||||
|
||||
let mirrored = 0, failed = 0;
|
||||
for (const d of dests) {
|
||||
try {
|
||||
await uploadBuffer(d.cfg, ym, it.filename || `${it.id}.png`, buf);
|
||||
if (md) await uploadBuffer(d.cfg, ym, sidecarName(it.filename), md);
|
||||
if (d.sidecar && md) await uploadBuffer(d.cfg, ym, sidecarName(it.filename), md);
|
||||
await query(`INSERT INTO item_backups (item_id, target, status) VALUES ($1,$2,'mirrored')
|
||||
ON CONFLICT (item_id, target) DO UPDATE SET status='mirrored', at=now()`, [itemId, d.key]);
|
||||
if (d.key === 'nas') await query(`UPDATE items SET nas_status='mirrored' WHERE id=$1`, [itemId]);
|
||||
|
||||
+19
-10
@@ -24,33 +24,42 @@ async function cfgForKey(key: string | null | undefined): Promise<PicdropCfg | n
|
||||
return loadTargetConfig(key); // UUID eines Zusatzziels
|
||||
}
|
||||
|
||||
/** Ziel-Zugang: 1) Ordner-Ziel 2) Rezept-Ziel 3) Standard-Picdrop (Einstellungen). */
|
||||
async function targetFor(item: { folder_id: string | null; job_id: string }): Promise<PicdropCfg | null> {
|
||||
/** Ziel-Zugang inkl. Quell-Schlüssel: 1) Ordner-Ziel 2) Rezept-Ziel 3) Standard-Picdrop. */
|
||||
async function targetFor(item: { folder_id: string | null; job_id: string }): Promise<{ cfg: PicdropCfg; key: string } | null> {
|
||||
if (item.folder_id) {
|
||||
const f = await one<{ delivery_target_id: string | null }>('SELECT delivery_target_id FROM folders WHERE id=$1', [item.folder_id]).catch(() => null);
|
||||
if (f?.delivery_target_id) { const c = await cfgForKey(f.delivery_target_id); if (c) return c; }
|
||||
if (f?.delivery_target_id) { const c = await cfgForKey(f.delivery_target_id); if (c) return { cfg: c, key: f.delivery_target_id }; }
|
||||
}
|
||||
const j = await one<{ recipe_snapshot: any }>('SELECT recipe_snapshot FROM jobs WHERE id=$1', [item.job_id]);
|
||||
const tid = j?.recipe_snapshot?.delivery_target_id;
|
||||
if (tid) { const c = await cfgForKey(tid); if (c) return c; }
|
||||
return loadConfig(); // Standard-Picdrop
|
||||
if (tid) { const c = await cfgForKey(tid); if (c) return { cfg: c, key: tid }; }
|
||||
const c = await loadConfig();
|
||||
return c ? { cfg: c, key: 'picdrop' } : null;
|
||||
}
|
||||
|
||||
/** Ist der Metadaten-Beileger für diese Quelle aktiv? */
|
||||
async function sidecarForKey(key: string): Promise<boolean> {
|
||||
if (key === 'picdrop') { const s = await one<any>('SELECT picdrop_metadata_sidecar FROM settings WHERE id=1'); return !!s?.picdrop_metadata_sidecar; }
|
||||
if (key === 'nas') { const s = await one<any>('SELECT nas_metadata_sidecar FROM settings WHERE id=1'); return !!s?.nas_metadata_sidecar; }
|
||||
const t = await one<any>('SELECT metadata_sidecar FROM delivery_targets WHERE id=$1', [key]);
|
||||
return !!t?.metadata_sidecar;
|
||||
}
|
||||
|
||||
/** Liefert ein fertiges Item an die passende Picdrop-Galerie aus. */
|
||||
export async function deliverItem(itemId: string): Promise<{ ok: boolean; message: string }> {
|
||||
const it = await one<any>('SELECT id, result_path, filename, folder_id, job_id FROM items WHERE id=$1', [itemId]);
|
||||
if (!it?.result_path) return { ok: false, message: 'Kein Ergebnis vorhanden.' };
|
||||
const cfg = await targetFor(it);
|
||||
if (!cfg) { await query(`UPDATE items SET delivery_status='failed' WHERE id=$1`, [itemId]); return { ok: false, message: 'Kein Auslieferungsziel konfiguriert.' }; }
|
||||
const target = await targetFor(it);
|
||||
if (!target) { await query(`UPDATE items SET delivery_status='failed' WHERE id=$1`, [itemId]); return { ok: false, message: 'Kein Auslieferungsziel konfiguriert.' }; }
|
||||
const { cfg, key } = target;
|
||||
|
||||
await query(`UPDATE items SET delivery_status='pending' WHERE id=$1`, [itemId]);
|
||||
try {
|
||||
const gallery = await galleryFor(it);
|
||||
const buf = await getObject(it.result_path);
|
||||
await uploadBuffer(cfg, gallery, it.filename || `${it.id}.png`, buf);
|
||||
// Optional: Metadaten als begleitende .md-Datei mitschicken.
|
||||
const md = await one<{ metadata_sidecar: boolean }>('SELECT metadata_sidecar FROM settings WHERE id=1');
|
||||
if (md?.metadata_sidecar) {
|
||||
// Optional: Metadaten als begleitende .md-Datei — pro Quelle steuerbar.
|
||||
if (await sidecarForKey(key)) {
|
||||
try {
|
||||
const { buildMetadataMd, sidecarName } = await import('./metadata');
|
||||
await uploadBuffer(cfg, gallery, sidecarName(it.filename), Buffer.from(await buildMetadataMd(itemId), 'utf8'));
|
||||
|
||||
@@ -54,6 +54,26 @@ export async function runRetention(days: number): Promise<{ deleted: number }> {
|
||||
return { deleted: rows.length };
|
||||
}
|
||||
|
||||
/** Erzeugt fehlende Vorschaubilder nach (für schnelle Bibliotheks-Anzeige). */
|
||||
export async function rebuildThumbs(): Promise<{ made: number }> {
|
||||
const { getObject, putObject, thumbKey } = await import('./storage');
|
||||
const sharp = (await import('sharp')).default;
|
||||
const rows = await query<any>(
|
||||
`SELECT id, result_path FROM items WHERE status='done' AND result_path IS NOT NULL AND thumb_path IS NULL`);
|
||||
let made = 0;
|
||||
for (const it of rows) {
|
||||
try {
|
||||
const out = await sharp(await getObject(it.result_path), { failOn: 'none' })
|
||||
.resize(600, 600, { fit: 'inside', withoutEnlargement: true }).webp({ quality: 72 }).toBuffer();
|
||||
const tk = thumbKey(it.id);
|
||||
await putObject(tk, out, 'image/webp');
|
||||
await query(`UPDATE items SET thumb_path=$2 WHERE id=$1`, [it.id, tk]);
|
||||
made++;
|
||||
} catch (e) { console.error('[maintenance] Thumb', it.id, e); }
|
||||
}
|
||||
return { made };
|
||||
}
|
||||
|
||||
/** Löscht nur die Quellbilder fertiger Positionen (Ergebnisse bleiben). */
|
||||
export async function purgeSources(): Promise<{ purged: number }> {
|
||||
const rows = await query<any>(
|
||||
|
||||
Reference in New Issue
Block a user