Phase 2: MKCOL, DELETE, MOVE implementiert

- resolveResource() für Pfad→UUID (Datei/Ordner)
- MKCOL: Ordner anlegen (createFolderByUuid)
- DELETE: Datei/Ordner löschen
- MOVE: Verschieben + Umbenennen mit Destination-Header

Made-with: Cursor
This commit is contained in:
2026-02-28 11:06:56 +01:00
parent 7c1866e6fc
commit 43b814d984
4 changed files with 245 additions and 9 deletions

View File

@@ -59,7 +59,7 @@ INXT_MNEMONIC=word1 word2 word3 ...
npm start npm start
``` ```
Server läuft auf `http://127.0.0.1:3005`. Phase 1 (PROPFIND) ist aktiv Verzeichnisinhalt kann aufgelistet werden. Windows Explorer: Netzlaufwerk verbinden → `http://127.0.0.1:3005`. Server läuft auf `http://127.0.0.1:3005`. Phase 1+2 aktiv: PROPFIND, MKCOL (Ordner anlegen), DELETE, MOVE. Windows Explorer: Netzlaufwerk verbinden → `http://127.0.0.1:3005`.
## Hinweise ## Hinweise

View File

@@ -51,7 +51,7 @@ Die drive-web nutzt `Network.client` (Bridge) und `NetworkFacade` für Up-/Downl
## Implementierungsreihenfolge ## Implementierungsreihenfolge
1. **Phase 1:** PROPFIND (Verzeichnis auflisten) ✅ implementiert 1. **Phase 1:** PROPFIND (Verzeichnis auflisten) ✅ implementiert
2. **Phase 2:** MKCOL, DELETE, MOVE 2. **Phase 2:** MKCOL, DELETE, MOVE ✅ implementiert
3. **Phase 3:** GET (Download) Bridge + Entschlüsselung 3. **Phase 3:** GET (Download) Bridge + Entschlüsselung
4. **Phase 4:** PUT (Upload) Verschlüsselung + Bridge 4. **Phase 4:** PUT (Upload) Verschlüsselung + Bridge

View File

@@ -15,7 +15,7 @@ export function pathToSegments(path) {
* Baut Pfad aus Segmenten. * Baut Pfad aus Segmenten.
*/ */
export function segmentsToPath(segments) { export function segmentsToPath(segments) {
return '/' + (Array.isArray(segments) ? segments.join('/') : ''); return '/' + (Array.isArray(segments) ? segments.filter(Boolean).join('/') : '');
} }
/** /**
@@ -70,3 +70,39 @@ export async function listFolder(storage, rootFolderUuid, path) {
return { folders, files }; return { folders, files };
} }
/**
* Resolved einen WebDAV-Pfad zu einer Datei oder einem Ordner.
* @returns {Promise<{ uuid: string, type: 'folder'|'file', name: string, parentUuid?: string } | null>}
*/
export async function resolveResource(storage, rootFolderUuid, path) {
const segments = pathToSegments(path);
if (segments.length === 0) {
return { uuid: rootFolderUuid, type: 'folder', name: 'Drive' };
}
const parentPath = segments.slice(0, -1);
const childName = segments[segments.length - 1];
const parent = await resolveFolder(storage, rootFolderUuid, segmentsToPath(parentPath));
if (!parent) return null;
const [contentPromise] = storage.getFolderContentByUuid({ folderUuid: parent.uuid });
const content = await contentPromise;
const folder = content?.children?.find(
(c) => (c.plain_name || c.name || '').toLowerCase() === childName.toLowerCase()
);
if (folder) {
return { uuid: folder.uuid, type: 'folder', name: folder.plain_name || folder.name, parentUuid: parent.uuid };
}
const file = content?.files?.find(
(f) => (f.plain_name || f.name || f.plainName || '').toLowerCase() === childName.toLowerCase()
);
if (file) {
return { uuid: file.uuid, type: 'file', name: file.plain_name || file.name || file.plainName, parentUuid: parent.uuid };
}
return null;
}

View File

@@ -1,5 +1,5 @@
/** /**
* WebDAV-Server für Internxt Drive (Phase 1: PROPFIND) * WebDAV-Server für Internxt Drive (Phase 1+2: PROPFIND, MKCOL, DELETE, MOVE)
* *
* Nutzt Browser-Token (INXT_TOKEN, INXT_MNEMONIC) aus .env. * Nutzt Browser-Token (INXT_TOKEN, INXT_MNEMONIC) aus .env.
* Siehe docs/browser-token-auth.md * Siehe docs/browser-token-auth.md
@@ -8,7 +8,7 @@
import 'dotenv/config'; import 'dotenv/config';
import express from 'express'; import express from 'express';
import { createClients, refreshUser } from './internxt-client.js'; import { createClients, refreshUser } from './internxt-client.js';
import { pathToSegments, listFolder } from './path-resolver.js'; import { pathToSegments, segmentsToPath, listFolder, resolveFolder, resolveResource } from './path-resolver.js';
const PORT = parseInt(process.env.PORT || '3005', 10); const PORT = parseInt(process.env.PORT || '3005', 10);
const token = process.env.INXT_TOKEN; const token = process.env.INXT_TOKEN;
@@ -158,6 +158,197 @@ async function handlePropfind(req, res) {
} }
} }
/**
* Holt Root-UUID und Storage (mit Token-Check).
*/
async function getContext() {
const { storage } = createClients(token);
const refresh = await refreshUser(token);
const user = refresh.user;
const rootUuid = user?.rootFolderUuid || user?.rootFolderId || user?.root_folder_id;
if (!rootUuid) throw new Error('Root-Ordner nicht gefunden');
return { storage, rootUuid };
}
/**
* MKCOL Handler Ordner anlegen
*/
async function handleMkcol(req, res) {
let path = req.url || '/';
try {
path = decodeURIComponent(path);
} catch (_) {}
if (!path.startsWith('/')) path = '/' + path;
if (path === '/') {
res.status(403).send('Root kann nicht erstellt werden');
return;
}
if (path.endsWith('/')) path = path.slice(0, -1);
const segments = pathToSegments(path);
if (segments.length === 0) {
res.status(403).send('Root bereits vorhanden');
return;
}
const parentPath = segmentsToPath(segments.slice(0, -1));
const newName = segments[segments.length - 1];
try {
const { storage, rootUuid } = await getContext();
const parent = await resolveFolder(storage, rootUuid, parentPath);
if (!parent) {
res.status(409).send('Übergeordneter Ordner existiert nicht');
return;
}
const existing = await resolveResource(storage, rootUuid, path);
if (existing) {
res.status(405).send('Ressource existiert bereits');
return;
}
const [createPromise] = storage.createFolderByUuid({
parentFolderUuid: parent.uuid,
plainName: newName,
});
await createPromise;
res.status(201).send();
} catch (err) {
console.error('MKCOL Fehler:', err.message);
if (err.message?.includes('Token') || err.response?.status === 401) {
res.status(401).send('Nicht autorisiert');
return;
}
res.status(500).send(err.message || 'Interner Fehler');
}
}
/**
* DELETE Handler Datei oder Ordner löschen
*/
async function handleDelete(req, res) {
let path = req.url || '/';
try {
path = decodeURIComponent(path);
} catch (_) {}
if (!path.startsWith('/')) path = '/' + path;
if (path.endsWith('/')) path = path.slice(0, -1);
if (path === '/') {
res.status(403).send('Root kann nicht gelöscht werden');
return;
}
try {
const { storage, rootUuid } = await getContext();
const resource = await resolveResource(storage, rootUuid, path);
if (!resource) {
res.status(404).send('Nicht gefunden');
return;
}
if (resource.type === 'folder') {
await storage.deleteFolderByUuid(resource.uuid);
} else {
await storage.deleteFileByUuid(resource.uuid);
}
res.status(204).send();
} catch (err) {
console.error('DELETE Fehler:', err.message);
if (err.message?.includes('Token') || err.response?.status === 401) {
res.status(401).send('Nicht autorisiert');
return;
}
res.status(500).send(err.message || 'Interner Fehler');
}
}
/**
* MOVE Handler Datei oder Ordner verschieben/umbenennen
*/
async function handleMove(req, res) {
let path = req.url || '/';
const destinationHeader = req.headers['destination'];
if (!destinationHeader) {
res.status(400).send('Destination-Header fehlt');
return;
}
try {
path = decodeURIComponent(path);
} catch (_) {}
if (!path.startsWith('/')) path = '/' + path;
if (path.endsWith('/')) path = path.slice(0, -1);
if (path === '/') {
res.status(403).send('Root kann nicht verschoben werden');
return;
}
let destPath;
try {
const destUrl = new URL(destinationHeader);
destPath = decodeURIComponent(destUrl.pathname || '/');
} catch (_) {
res.status(400).send('Ungültige Destination-URL');
return;
}
if (!destPath.startsWith('/')) destPath = '/' + destPath;
if (destPath.endsWith('/')) destPath = destPath.slice(0, -1);
const overwrite = (req.headers['overwrite'] || 'T').toUpperCase() === 'T';
try {
const { storage, rootUuid } = await getContext();
const source = await resolveResource(storage, rootUuid, path);
if (!source) {
res.status(404).send('Quelle nicht gefunden');
return;
}
const destSegments = pathToSegments(destPath);
const destParentPath = segmentsToPath(destSegments.slice(0, -1));
const destName = destSegments[destSegments.length - 1];
const destParent = await resolveFolder(storage, rootUuid, destParentPath);
if (!destParent) {
res.status(409).send('Zielordner existiert nicht');
return;
}
const existingDest = await resolveResource(storage, rootUuid, destPath);
if (existingDest) {
if (!overwrite) {
res.status(412).send('Ziel existiert, Overwrite nicht erlaubt');
return;
}
if (existingDest.type === 'folder') {
await storage.deleteFolderByUuid(existingDest.uuid);
} else {
await storage.deleteFileByUuid(existingDest.uuid);
}
}
const payload = { destinationFolder: destParent.uuid };
if (destName && destName !== source.name) {
payload.name = destName;
}
if (source.type === 'folder') {
await storage.moveFolderByUuid(source.uuid, payload);
} else {
await storage.moveFileByUuid(source.uuid, payload);
}
res.status(201).send();
} catch (err) {
console.error('MOVE Fehler:', err.message);
if (err.message?.includes('Token') || err.response?.status === 401) {
res.status(401).send('Nicht autorisiert');
return;
}
res.status(500).send(err.message || 'Interner Fehler');
}
}
// WebDAV Endpoints // WebDAV Endpoints
app.options('*', (req, res) => { app.options('*', (req, res) => {
res.set('DAV', '1, 2'); res.set('DAV', '1, 2');
@@ -180,15 +371,24 @@ app.use((req, res, next) => {
return; return;
} }
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
res.status(501).send('DELETE noch nicht implementiert Phase 2'); handleDelete(req, res).catch((err) => {
console.error('DELETE unhandled:', err);
if (!res.headersSent) res.status(500).send(err.message);
});
return; return;
} }
if (req.method === 'MKCOL') { if (req.method === 'MKCOL') {
res.status(501).send('MKCOL noch nicht implementiert Phase 2'); handleMkcol(req, res).catch((err) => {
console.error('MKCOL unhandled:', err);
if (!res.headersSent) res.status(500).send(err.message);
});
return; return;
} }
if (req.method === 'MOVE') { if (req.method === 'MOVE') {
res.status(501).send('MOVE noch nicht implementiert Phase 2'); handleMove(req, res).catch((err) => {
console.error('MOVE unhandled:', err);
if (!res.headersSent) res.status(500).send(err.message);
});
return; return;
} }
next(); next();
@@ -196,6 +396,6 @@ app.use((req, res, next) => {
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Internxt WebDAV Server http://127.0.0.1:${PORT}`); console.log(`Internxt WebDAV Server http://127.0.0.1:${PORT}`);
console.log('Phase 1: PROPFIND (Verzeichnis auflisten) aktiv.'); console.log('Phase 1+2: PROPFIND, MKCOL, DELETE, MOVE aktiv.');
console.log('Verwendung: z.B. Windows Explorer → Netzlaufwerk verbinden'); console.log('Verwendung: z.B. Windows Explorer → Netzlaufwerk verbinden');
}); });