import { createReadStream, readFileSync } from 'fs' import type { BackupDestinationType } from './config' // Entschlüsselte (Klartext-)Zugangsdaten — werden vom Aufrufer aus der Konfig entschlüsselt. export interface SftpPlain { host: string; port: number; username: string; password: string; remotePath: string } export interface WebdavPlain { url: string; username: string; password: string; remotePath: string } export type PlainDest = SftpPlain | WebdavPlain export interface RemoteFile { name: string; size?: number; modified?: number } function joinRemote(dir: string, name: string): string { return `${dir.replace(/\/+$/, '')}/${name}` } // ─── SFTP ─────────────────────────────────────────────── async function withSftp(c: SftpPlain, fn: (sftp: any) => Promise): Promise { const SftpClient = (await import('ssh2-sftp-client')).default const sftp = new SftpClient() await sftp.connect({ host: c.host, port: c.port || 22, username: c.username, password: c.password, readyTimeout: 15000 }) try { return await fn(sftp) } finally { try { await sftp.end() } catch { /* ignore */ } } } // ─── WebDAV (Nextcloud) ───────────────────────────────── async function getWebdav(c: WebdavPlain) { const { createClient } = await import('webdav') return createClient(c.url, { username: c.username, password: c.password }) } // ─── Öffentliche, typ-übergreifende API ───────────────── export async function testDestination(type: BackupDestinationType, dest: PlainDest): Promise<{ ok: boolean; message: string }> { try { const marker = Buffer.from(`lageplan-backup-test ${new Date().toISOString()}`) if (type === 'sftp') { const c = dest as SftpPlain await withSftp(c, async (sftp) => { if (!(await sftp.exists(c.remotePath))) await sftp.mkdir(c.remotePath, true) const p = joinRemote(c.remotePath, '.lageplan-test') await sftp.put(marker, p) await sftp.delete(p) }) return { ok: true, message: 'SFTP-Verbindung und Schreibzugriff ok.' } } const c = dest as WebdavPlain const dav = await getWebdav(c) if (!(await dav.exists(c.remotePath))) await dav.createDirectory(c.remotePath, { recursive: true } as any) const p = joinRemote(c.remotePath, '.lageplan-test') await dav.putFileContents(p, marker) await dav.deleteFile(p) return { ok: true, message: 'WebDAV-Verbindung und Schreibzugriff ok.' } } catch (e: any) { return { ok: false, message: e?.message || 'Verbindung fehlgeschlagen.' } } } export async function uploadBackup(type: BackupDestinationType, dest: PlainDest, localPath: string, remoteName: string): Promise { if (type === 'sftp') { const c = dest as SftpPlain await withSftp(c, async (sftp) => { if (!(await sftp.exists(c.remotePath))) await sftp.mkdir(c.remotePath, true) await sftp.put(createReadStream(localPath), joinRemote(c.remotePath, remoteName)) }) return } const c = dest as WebdavPlain const dav = await getWebdav(c) if (!(await dav.exists(c.remotePath))) await dav.createDirectory(c.remotePath, { recursive: true } as any) // Für WebDAV als Buffer hochladen (Backups einer kleinen Feuerwehr sind moderat gross). await dav.putFileContents(joinRemote(c.remotePath, remoteName), readFileSync(localPath), { overwrite: true } as any) } export async function listBackups(type: BackupDestinationType, dest: PlainDest): Promise { if (type === 'sftp') { const c = dest as SftpPlain return withSftp(c, async (sftp) => { if (!(await sftp.exists(c.remotePath))) return [] const list = await sftp.list(c.remotePath) return list.filter((f: any) => f.type === '-').map((f: any) => ({ name: f.name, size: f.size, modified: f.modifyTime })) }) } const c = dest as WebdavPlain const dav = await getWebdav(c) if (!(await dav.exists(c.remotePath))) return [] const items = (await dav.getDirectoryContents(c.remotePath)) as any[] return items.filter(i => i.type === 'file').map(i => ({ name: i.basename, size: i.size, modified: i.lastmod ? Date.parse(i.lastmod) : undefined })) } export async function deleteBackup(type: BackupDestinationType, dest: PlainDest, name: string): Promise { if (type === 'sftp') { const c = dest as SftpPlain await withSftp(c, async (sftp) => { await sftp.delete(joinRemote(c.remotePath, name)) }) return } const c = dest as WebdavPlain const dav = await getWebdav(c) await dav.deleteFile(joinRemote(c.remotePath, name)) }