Webdav
This commit is contained in:
@@ -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",
|
||||
|
||||
74
backend/src/webdav/fill-file-sizes.ts
Normal file
74
backend/src/webdav/fill-file-sizes.ts
Normal 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);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user