feat: Bibliothek + Admin
- Bibliothek: grid, before/after slider, rename, download, folder assign, delete, multi-select
- Admin: stats+costs, OpenRouter key (masked/encrypted), budget, defaults, Picdrop config+test,
models (list/add from OpenRouter/set default/delete), users (add/set password)
- api: items/:id PATCH+DELETE, folders CRUD, admin/{settings,stats,models,test-picdrop,users}
- lib/picdrop.ts (SFTP/FTPS test+upload, temp->rename)
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
// Picdrop-Auslieferung via SFTP (ssh2-sftp-client) oder FTPS (basic-ftp).
|
||||
// Konfiguration kommt aus settings (verschlüsseltes Passwort).
|
||||
import posixpath from 'node:path/posix';
|
||||
import { one } from './db';
|
||||
import { decrypt } from './crypto';
|
||||
|
||||
export interface PicdropCfg {
|
||||
host: string; protocol: 'ftps' | 'sftp'; port: number;
|
||||
user: string; password: string; basePath: string;
|
||||
}
|
||||
|
||||
export async function loadConfig(): Promise<PicdropCfg | null> {
|
||||
const s = await one<any>(`SELECT picdrop_host, picdrop_protocol, picdrop_port, picdrop_user,
|
||||
picdrop_password_enc, picdrop_base_path FROM settings WHERE id=1`);
|
||||
if (!s?.picdrop_host || !s?.picdrop_user || !s?.picdrop_password_enc) return null;
|
||||
let password = '';
|
||||
try { password = decrypt(s.picdrop_password_enc); } catch { return null; }
|
||||
return {
|
||||
host: s.picdrop_host, protocol: (s.picdrop_protocol || 'sftp'),
|
||||
port: s.picdrop_port || (s.picdrop_protocol === 'ftps' ? 21 : 22),
|
||||
user: s.picdrop_user, password, basePath: s.picdrop_base_path || '/',
|
||||
};
|
||||
}
|
||||
|
||||
/** Verbindung testen: verbinden + Basisordner listen. */
|
||||
export async function testConnection(cfg: PicdropCfg): Promise<{ ok: boolean; message: string }> {
|
||||
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 });
|
||||
await c.list(cfg.basePath || '/');
|
||||
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 });
|
||||
await c.list(cfg.basePath || '/');
|
||||
c.close();
|
||||
}
|
||||
return { ok: true, message: 'Verbindung erfolgreich.' };
|
||||
} catch (e: any) {
|
||||
return { ok: false, message: e?.message || 'Verbindung fehlgeschlagen.' };
|
||||
}
|
||||
}
|
||||
|
||||
/** Lädt einen Puffer als Datei hoch (temp-Name → umbenennen, kein .filepart). */
|
||||
export async function uploadBuffer(cfg: PicdropCfg, gallery: string, filename: string, buf: Buffer): Promise<void> {
|
||||
const dir = posixpath.join(cfg.basePath || '/', gallery || '');
|
||||
const finalPath = posixpath.join(dir, filename);
|
||||
const tmpPath = posixpath.join(dir, `.tmp-${Date.now()}-${filename}`);
|
||||
const { Readable } = await import('node:stream');
|
||||
|
||||
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: 20000 });
|
||||
try {
|
||||
if (!(await c.exists(dir))) await c.mkdir(dir, true);
|
||||
await c.put(buf, tmpPath);
|
||||
await c.rename(tmpPath, finalPath);
|
||||
} finally { await c.end(); }
|
||||
} else {
|
||||
const { Client } = await import('basic-ftp');
|
||||
const c = new Client(20000);
|
||||
await c.access({ host: cfg.host, port: cfg.port, user: cfg.user, password: cfg.password, secure: true });
|
||||
try {
|
||||
await c.ensureDir(dir);
|
||||
await c.uploadFrom(Readable.from(buf), tmpPath);
|
||||
await c.rename(tmpPath, finalPath);
|
||||
} finally { c.close(); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user