From e59cbade534a1c2c66bb1658bcf546023ffc69de Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Sat, 17 Jan 2026 16:04:32 +0100 Subject: [PATCH] Webdav --- backend/package.json | 1 + backend/src/webdav/fill-file-sizes.ts | 74 +++++++++++ backend/src/webdav/server.ts | 178 +++++++++++++++++++------- 3 files changed, 210 insertions(+), 43 deletions(-) create mode 100644 backend/src/webdav/fill-file-sizes.ts diff --git a/backend/package.json b/backend/package.json index b17d339..0bbd885 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,6 +5,7 @@ "main": "index.js", "scripts": { "dev": "tsx watch src/index.ts", + "fill": "ts-node src/webdav/fill-file-sizes.ts", "dev:dav": "tsx watch src/webdav/server.ts", "build": "tsc", "start": "node dist/src/index.js", diff --git a/backend/src/webdav/fill-file-sizes.ts b/backend/src/webdav/fill-file-sizes.ts new file mode 100644 index 0000000..7ad3831 --- /dev/null +++ b/backend/src/webdav/fill-file-sizes.ts @@ -0,0 +1,74 @@ +// scripts/fill-file-sizes.ts +import 'dotenv/config'; +import { db } from '../../db'; +import { files } from '../../db/schema'; +import { eq, isNull } from 'drizzle-orm'; +import { HeadObjectCommand } from "@aws-sdk/client-s3"; +import { s3, initS3 } from '../utils/s3'; +import { loadSecrets, secrets } from '../utils/secrets'; + +async function migrate() { + console.log("🚀 Starte Migration der Dateigrößen..."); + + // 1. Setup + await loadSecrets(); + await initS3(); + + // 2. Alle Dateien holen, die noch keine Größe haben (oder alle, um sicherzugehen) + // Wir nehmen erstmal ALLE, um sicherzustellen, dass alles stimmt. + const allFiles = await db.select().from(files); + + console.log(`📦 ${allFiles.length} Dateien in der Datenbank gefunden.`); + + let successCount = 0; + let errorCount = 0; + + // 3. Loop durch alle Dateien + for (const file of allFiles) { + if (!file.path) { + console.log(`⏭️ Überspringe Datei ${file.id} (Kein Pfad)`); + continue; + } + + try { + // S3 fragen (HeadObject lädt nur Metadaten, nicht die ganze Datei -> Schnell) + const command = new HeadObjectCommand({ + Bucket: secrets.S3_BUCKET, // Oder secrets.S3_BUCKET_NAME je nach deiner Config + Key: file.path + }); + + const response = await s3.send(command); + const size = response.ContentLength || 0; + + // In DB speichern + await db.update(files) + .set({ size: size }) + .where(eq(files.id, file.id)); + + process.stdout.write("."); // Fortschrittsanzeige + successCount++; + + } catch (error: any) { + process.stdout.write("X"); + // console.error(`\n❌ Fehler bei ${file.path}: ${error.name}`); + + // Optional: Wenn Datei in S3 fehlt, könnten wir sie markieren oder loggen + if (error.name === 'NotFound') { + // console.error(` -> Datei existiert nicht im Bucket!`); + } + errorCount++; + } + } + + console.log("\n\n------------------------------------------------"); + console.log(`✅ Fertig!`); + console.log(`Updated: ${successCount}`); + console.log(`Fehler: ${errorCount} (Meistens Dateien, die im Bucket fehlen)`); + console.log("------------------------------------------------"); + process.exit(0); +} + +migrate().catch(err => { + console.error("Fataler Fehler:", err); + process.exit(1); +}); \ No newline at end of file diff --git a/backend/src/webdav/server.ts b/backend/src/webdav/server.ts index fe0061f..a983690 100644 --- a/backend/src/webdav/server.ts +++ b/backend/src/webdav/server.ts @@ -1,11 +1,16 @@ import 'dotenv/config'; import { v2 as webdav } from 'webdav-server'; import { db } from '../../db'; -// WICHTIG: 'folders' muss hier importiert werden import { tenants, files, folders } from '../../db/schema'; -import { eq } from 'drizzle-orm'; +import { Readable } from 'stream'; +import { GetObjectCommand, HeadObjectCommand } from "@aws-sdk/client-s3"; +import { s3, initS3 } from '../utils/s3'; +import { secrets, loadSecrets } from '../utils/secrets'; + +// ============================================================================ +// 1. SETUP +// ============================================================================ -// 1. User & Rechte Setup const userManager = new webdav.SimpleUserManager(); const user = userManager.addUser('admin', 'admin', true); @@ -16,90 +21,177 @@ const server = new webdav.WebDAVServer({ httpAuthentication: new webdav.HTTPDigestAuthentication(userManager, 'Default realm'), privilegeManager: privilegeManager, port: 3200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK', + 'Access-Control-Allow-Headers': 'Authorization, Content-Type, Depth, User-Agent, X-Expected-Entity-Length, If-Modified-Since, Cache-Control, Range, Overwrite, Destination', + } }); +// ============================================================================ +// 2. CACHE +// ============================================================================ + +const pathToS3KeyMap = new Map(); +const pathToSizeMap = new Map(); + +// ============================================================================ +// 3. LOGIC +// ============================================================================ + async function startServer() { console.log('------------------------------------------------'); - console.log('[WebDAV] Lade Struktur (Tenants -> Folders -> Files)...'); + console.log('[WebDAV] Starte Server (Filtered Mode)...'); try { - // A. Alle Daten abrufen + await loadSecrets(); + await initS3(); + console.log('[WebDAV] S3 Verbindung OK.'); + + console.log('[WebDAV] Lade Datenbank...'); const allTenants = await db.select().from(tenants); const allFolders = await db.select().from(folders); const allFiles = await db.select().from(files); - console.log(`[WebDAV] Stats: ${allTenants.length} Tenants, ${allFolders.length} Ordner, ${allFiles.length} Dateien.`); + // Zähler für Statistik + let hiddenFilesCount = 0; // -------------------------------------------------------------------- - // REKURSIVE FUNKTION: Baut den Inhalt eines Ordners + // BUILDER // -------------------------------------------------------------------- - const buildFolderContent = (tenantId: string, parentFolderId: string | null) => { + const buildFolderContent = (tenantId: string, parentFolderId: string | null, currentWebDavPath: string) => { const currentDir: any = {}; - // 1. UNTERORDNER finden - // Wir suchen Ordner, die zum aktuellen Tenant gehören UND diesen Ordner als Parent haben - const subFolders = allFolders.filter(f => - f.tenant === tenantId && f.parent === parentFolderId - ); - + // 1. UNTERORDNER + const subFolders = allFolders.filter(f => f.tenant === tenantId && f.parent === parentFolderId); subFolders.forEach(folder => { - // Rekursion: Wir rufen die Funktion für diesen Unterordner erneut auf - // Das Objekt, das zurückkommt, ist der Inhalt des Unterordners - currentDir[folder.name] = buildFolderContent(tenantId, folder.id); + const folderName = folder.name.replace(/\//g, '-'); + const nextPath = `${currentWebDavPath}/${folderName}`; + currentDir[folderName] = buildFolderContent(tenantId, folder.id, nextPath); }); - // 2. DATEIEN finden - // Wir suchen Dateien, die in diesem Ordner liegen - const dirFiles = allFiles.filter(f => - f.tenant === tenantId && f.folder === parentFolderId - ); + // 2. DATEIEN + const dirFiles = allFiles.filter(f => f.tenant === tenantId && f.folder === parentFolderId); dirFiles.forEach(file => { - // Name aus Pfad extrahieren (wie im Frontend) - let fileName = 'Unbenannt.txt'; - if (file.path) fileName = file.path.split('/').pop() || 'Unbenannt.txt'; + // ============================================================ + // ❌ FILTER: DATEIEN OHNE GRÖSSE AUSBLENDEN + // ============================================================ + const fileSize = Number(file.size || 0); + + if (fileSize <= 0) { + // Datei überspringen, wenn 0 Bytes oder null + hiddenFilesCount++; + return; + } + // ============================================================ + + // Name bestimmen + let fileName = 'Unbenannt'; + if (file.path) fileName = file.path.split('/').pop() || 'Unbenannt'; else if (file.name) fileName = file.name; - // Inhalt (Placeholder) - currentDir[fileName] = `Datei: ${fileName}\nID: ${file.id}`; + // A) Eintrag im WebDAV + currentDir[fileName] = `Ref: ${file.id}`; + + // B) Maps füllen + const webDavFullPath = `${currentWebDavPath}/${fileName}`; + + if (file.path) { + pathToS3KeyMap.set(webDavFullPath, file.path); + } + + // C) Größe setzen (wir wissen jetzt sicher, dass sie > 0 ist) + pathToSizeMap.set(webDavFullPath, fileSize); }); return currentDir; }; - // B. Hauptbaum bauen (Einstiegspunkt) + // -------------------------------------------------------------------- + // BAUM ZUSAMMENSETZEN + // -------------------------------------------------------------------- const dbTree: any = {}; allTenants.forEach(tenant => { - const tenantName = tenant.name.replace(/\//g, '-'); + const tName = tenant.name.replace(/\//g, '-'); + const rootPath = `/${tName}`; - // Wir starten die Rekursion am "Root" des Tenants (parentFolderId = null) - const tenantRootContent = buildFolderContent(tenant.id, null); + const content = buildFolderContent(tenant.id, null, rootPath); - // Wenn Tenant komplett leer ist (keine Ordner im Root, keine Files im Root) - if (Object.keys(tenantRootContent).length === 0) { - tenantRootContent['(Leer).txt'] = 'Dieser Tenant ist leer.'; + // Leere Ordner Hinweis (optional) + if (Object.keys(content).length === 0) { + content['(Leer).txt'] = 'Keine gültigen Dateien vorhanden.'; } - - dbTree[tenantName] = tenantRootContent; + dbTree[tName] = content; }); - // Fallback für leere DB if (Object.keys(dbTree).length === 0) { - dbTree['Status.txt'] = 'Keine Tenants gefunden.'; + dbTree['Status.txt'] = 'Datenbank leer.'; } - // C. Baum registrieren - const root = server.rootFileSystem(); - root.addSubTree(server.createExternalContext(), dbTree); + // -------------------------------------------------------------------- + // REGISTRIEREN + // -------------------------------------------------------------------- + const rootFS = server.rootFileSystem(); + rootFS.addSubTree(server.createExternalContext(), dbTree); - // D. Starten + // ==================================================================== + // OVERRIDE 1: DOWNLOAD + // ==================================================================== + (rootFS as any)._openReadStream = async (path: webdav.Path, ctx: any, callback: any) => { + const p = path.toString(); + const s3Key = pathToS3KeyMap.get(p); + + if (s3Key) { + try { + const command = new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: s3Key }); + const response = await s3.send(command); + if (response.Body) return callback(null, response.Body as Readable); + } catch (e: any) { + console.error(`[S3 ERROR] ${e.message}`); + return callback(null, Readable.from([`Error: ${e.message}`])); + } + } + return callback(null, Readable.from(['System File'])); + }; + + // ==================================================================== + // OVERRIDE 2: SIZE + // ==================================================================== + (rootFS as any)._size = async (path: webdav.Path, ctx: any, callback: any) => { + const p = path.toString(); + const cachedSize = pathToSizeMap.get(p); + + if (cachedSize !== undefined) return callback(null, cachedSize); + + // Fallback S3 Check (sollte durch Filter kaum noch vorkommen) + const s3Key = pathToS3KeyMap.get(p); + if (s3Key) { + try { + const command = new HeadObjectCommand({ Bucket: secrets.S3_BUCKET, Key: s3Key }); + const response = await s3.send(command); + const realSize = response.ContentLength || 0; + pathToSizeMap.set(p, realSize); + return callback(null, realSize); + } catch (e) { + return callback(null, 0); + } + } + return callback(null, 0); + }; + + // -------------------------------------------------------------------- + // START + // -------------------------------------------------------------------- server.start(() => { console.log('[WebDAV] 🚀 READY auf http://localhost:3200'); + console.log(`[WebDAV] Sichtbare Dateien: ${pathToS3KeyMap.size}`); + console.log(`[WebDAV] Ausgeblendet (0 Bytes): ${hiddenFilesCount}`); }); } catch (error) { - console.error('[WebDAV] 💥 Fehler:', error); + console.error('[WebDAV] 💥 ERROR:', error); } }