This commit is contained in:
2026-01-17 16:04:32 +01:00
parent 6423886930
commit e59cbade53
3 changed files with 210 additions and 43 deletions

View File

@@ -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",

View File

@@ -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);
});

View File

@@ -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<string, string>();
const pathToSizeMap = new Map<string, number>();
// ============================================================================
// 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);
}
}