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:
@@ -8,7 +8,7 @@ const json = (b: unknown, s = 200) =>
|
||||
new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } });
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
const rows = await query<any>(`SELECT id, name, protocol, host, port, username, base_path, is_backup,
|
||||
const rows = await query<any>(`SELECT id, name, protocol, host, port, username, base_path, is_backup, metadata_sidecar,
|
||||
(password_enc IS NOT NULL) AS password_set FROM delivery_targets ORDER BY name`);
|
||||
return json({ targets: rows });
|
||||
};
|
||||
@@ -27,10 +27,10 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
}
|
||||
if (!b.name?.trim()) return json({ error: 'Name fehlt.' }, 400);
|
||||
const row = await one<any>(
|
||||
`INSERT INTO delivery_targets (name, protocol, host, port, username, password_enc, base_path, is_backup)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id`,
|
||||
`INSERT INTO delivery_targets (name, protocol, host, port, username, password_enc, base_path, is_backup, metadata_sidecar)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING id`,
|
||||
[b.name.trim(), b.protocol || 'sftp', b.host || null, b.port || null, b.username || null,
|
||||
b.password ? encrypt(String(b.password)) : null, b.base_path || null, !!b.is_backup]);
|
||||
b.password ? encrypt(String(b.password)) : null, b.base_path || null, !!b.is_backup, !!b.metadata_sidecar]);
|
||||
return json({ id: row!.id });
|
||||
};
|
||||
|
||||
@@ -44,6 +44,7 @@ export const PATCH: APIRoute = async ({ request }) => {
|
||||
if (c in b) set(c, b[c] === '' ? null : b[c]);
|
||||
}
|
||||
if ('is_backup' in b) set('is_backup', !!b.is_backup);
|
||||
if ('metadata_sidecar' in b) set('metadata_sidecar', !!b.metadata_sidecar);
|
||||
if (b.password) set('password_enc', encrypt(String(b.password)));
|
||||
if (!sets.length) return json({ ok: true });
|
||||
args.push(b.id);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import posixpath from 'node:path/posix';
|
||||
import { loadConfig } from '../../../lib/picdrop';
|
||||
|
||||
export const prerender = false;
|
||||
const json = (b: unknown, s = 200) =>
|
||||
new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } });
|
||||
|
||||
/** Diagnose: listet einen Picdrop-Ordner (Standard: Basisordner). Body { path? } */
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
const b = await request.json().catch(() => ({} as any));
|
||||
const cfg = await loadConfig();
|
||||
if (!cfg) return json({ error: 'Picdrop nicht konfiguriert.' }, 400);
|
||||
const path = b.path || cfg.basePath || '/';
|
||||
try {
|
||||
if (cfg.protocol === 'sftp') {
|
||||
const SftpClient = (await import('ssh2-sftp-client')).default;
|
||||
const c = new SftpClient();
|
||||
await c.connect({ host: cfg.host, port: cfg.port, username: cfg.user, password: cfg.password, readyTimeout: 15000 });
|
||||
try {
|
||||
const list = await c.list(path);
|
||||
return json({ path, entries: list.map((e: any) => ({ name: e.name, type: e.type, size: e.size })) });
|
||||
} finally { await c.end(); }
|
||||
} else {
|
||||
const { Client } = await import('basic-ftp');
|
||||
const c = new Client(15000);
|
||||
await c.access({ host: cfg.host, port: cfg.port, user: cfg.user, password: cfg.password, secure: true });
|
||||
try {
|
||||
const list = await c.list(path);
|
||||
return json({ path, entries: list.map((e: any) => ({ name: e.name, type: e.type, size: e.size })) });
|
||||
} finally { c.close(); }
|
||||
}
|
||||
} catch (e: any) {
|
||||
return json({ path, error: e?.message || 'Listing fehlgeschlagen.' });
|
||||
}
|
||||
};
|
||||
@@ -24,6 +24,7 @@ export const GET: APIRoute = async () => {
|
||||
// Speicherverwaltung
|
||||
keep_sources: s.keep_sources, make_thumbnails: s.make_thumbnails, retention_days: s.retention_days,
|
||||
metadata_sidecar: s.metadata_sidecar, api_token: s.api_token,
|
||||
picdrop_metadata_sidecar: s.picdrop_metadata_sidecar, nas_metadata_sidecar: s.nas_metadata_sidecar,
|
||||
// Rechte / Datenschutz / NSFW
|
||||
library_visibility: s.library_visibility, anonymous_generations: s.anonymous_generations,
|
||||
private_allowed: s.private_allowed, allow_nsfw: s.allow_nsfw,
|
||||
@@ -43,10 +44,11 @@ export const PATCH: APIRoute = async ({ request }) => {
|
||||
if (b.nas_password) set('nas_password_enc', encrypt(String(b.nas_password)));
|
||||
if (b.generate_api_token) set('api_token', randomToken());
|
||||
const boolCols = ['keep_sources', 'make_thumbnails', 'nas_enabled', 'metadata_sidecar',
|
||||
'anonymous_generations', 'private_allowed', 'allow_nsfw'];
|
||||
'picdrop_metadata_sidecar', 'nas_metadata_sidecar', 'anonymous_generations', 'private_allowed', 'allow_nsfw'];
|
||||
for (const col of ['picdrop_host', 'picdrop_protocol', 'picdrop_port', 'picdrop_user', 'picdrop_base_path',
|
||||
'picdrop_default_gallery', 'default_dpi', 'default_crop_mode', 'concurrency', 'cricut_sheet_cm', 'monthly_budget', 'n8n_webhook_url',
|
||||
'keep_sources', 'make_thumbnails', 'retention_days', 'metadata_sidecar', 'api_token',
|
||||
'picdrop_metadata_sidecar', 'nas_metadata_sidecar',
|
||||
'library_visibility', 'anonymous_generations', 'private_allowed', 'allow_nsfw',
|
||||
'nas_enabled', 'nas_host', 'nas_protocol', 'nas_port', 'nas_user', 'nas_base_path']) {
|
||||
if (col in b) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { storageStats, runRetention, purgeSources, mirrorAllToNas } from '../../../lib/maintenance';
|
||||
import { storageStats, runRetention, purgeSources, mirrorAllToNas, rebuildThumbs } from '../../../lib/maintenance';
|
||||
import { one } from '../../../lib/db';
|
||||
|
||||
export const prerender = false;
|
||||
@@ -13,6 +13,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
const b = await request.json().catch(() => ({}));
|
||||
if (b.action === 'purge_sources') return json(await purgeSources());
|
||||
if (b.action === 'mirror_all') return json(await mirrorAllToNas());
|
||||
if (b.action === 'rebuild_thumbs') return json(await rebuildThumbs());
|
||||
if (b.action === 'retention') {
|
||||
const s = await one<{ retention_days: number | null }>('SELECT retention_days FROM settings WHERE id=1');
|
||||
if (!s?.retention_days) return json({ error: 'Keine Aufbewahrungsfrist gesetzt.' }, 400);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import sharp from 'sharp';
|
||||
import { one } from '../../../../lib/db';
|
||||
import { getObject } from '../../../../lib/storage';
|
||||
|
||||
@@ -28,13 +29,19 @@ export const GET: APIRoute = async ({ params, url, locals }) => {
|
||||
: item?.result_path;
|
||||
if (!path) return new Response('Nicht gefunden', { status: 404 });
|
||||
try {
|
||||
const buf = await getObject(path);
|
||||
let buf = await getObject(path);
|
||||
const download = q.get('download') === '1';
|
||||
const isWebp = path.endsWith('.webp');
|
||||
let contentType = path.endsWith('.webp') ? 'image/webp' : 'image/png';
|
||||
// Schnelle Vollbild-Vorschau: nicht-transparente Ergebnisse als JPEG (deutlich kleiner,
|
||||
// schneller auf Mobil) — reicht fürs „In Fotos sichern". Der PNG-Download bleibt unverändert.
|
||||
if (q.get('preview') === '1' && !download && !item?.has_alpha) {
|
||||
try { buf = await sharp(buf, { failOn: 'none' }).jpeg({ quality: 90, mozjpeg: true }).toBuffer(); contentType = 'image/jpeg'; }
|
||||
catch { /* Fallback: Original */ }
|
||||
}
|
||||
return new Response(buf, {
|
||||
headers: {
|
||||
'Content-Type': isWebp ? 'image/webp' : 'image/png',
|
||||
'Cache-Control': 'private, max-age=300',
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'private, max-age=600',
|
||||
...(download ? { 'Content-Disposition': `attachment; filename="${item.filename || 'klarbild.png'}"` } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user