Restic-Kompatibilität, POST, rekursives MKCOL, findstr /C: Fix

- Server: POST für Uploads, rekursives MKCOL/PUT, ensureFolderExists
- PUT: fehlende Elternordner werden erstellt
- Scripts: findstr /C: für Literalsuche (Punkt-Konflikt behoben)
- Docs: Restic + rclone Hinweis

Made-with: Cursor
This commit is contained in:
2026-02-28 15:18:57 +01:00
parent 378fb59912
commit bbf3b899f7
4 changed files with 56 additions and 12 deletions

View File

@@ -81,6 +81,14 @@ C:\Pfad\zu\internxt-webdav\scripts\stop-webdav.cmd 8080
Der Server startet im Hintergrund und ist nach ~5 Sekunden bereit. Der Server startet im Hintergrund und ist nach ~5 Sekunden bereit.
## Restic + rclone
```bash
restic -r rclone:internxt-webdav:repo-name init
```
Der Server erstellt fehlende Ordner rekursiv (MKCOL). Bei 500-Fehlern: Server-Log prüfen (`PUT Fehler:`), Token mit `npm run token-refresh` erneuern.
## WebDAV-Credentials (für Duplicati, Explorer) ## WebDAV-Credentials (für Duplicati, Explorer)
Der Server erwartet **Basic Auth**. Ohne `WEBDAV_USER`/`WEBDAV_PASS` in `.env` akzeptiert er **beliebige** Credentials Sie können in Duplicati z.B. Benutzername `backup` und Passwort `geheim` eintragen. Mit `WEBDAV_USER` und `WEBDAV_PASS` werden nur diese Credentials akzeptiert. Der Server erwartet **Basic Auth**. Ohne `WEBDAV_USER`/`WEBDAV_PASS` in `.env` akzeptiert er **beliebige** Credentials Sie können in Duplicati z.B. Benutzername `backup` und Passwort `geheim` eintragen. Mit `WEBDAV_USER` und `WEBDAV_PASS` werden nur diese Credentials akzeptiert.

View File

@@ -19,8 +19,8 @@ if %errorlevel% neq 0 (
exit /b 1 exit /b 1
) )
REM Pruefen ob Server bereits laeuft (0.0.0.0:0 = Listening, sprachunabhaengig) REM Pruefen ob Server bereits laeuft (0.0.0.0:0 = Listening)
netstat -an | findstr ":%PORT% " | findstr "0.0.0.0:0" > nul 2>&1 netstat -an | findstr /C:":%PORT% " | findstr /C:"0.0.0.0:0" > nul 2>&1
if %errorlevel% equ 0 ( if %errorlevel% equ 0 (
echo WebDAV-Server laeuft bereits. echo WebDAV-Server laeuft bereits.
exit /b 0 exit /b 0

View File

@@ -8,9 +8,9 @@ if "%1"=="" (set PORT=3005) else (set PORT=%1)
REM Prozess auf Port finden und beenden REM Prozess auf Port finden und beenden
REM Filter: Port + "0.0.0.0:0" = Listening (sprachunabhaengig) REM Filter: Port + "0.0.0.0:0" = Listening (sprachunabhaengig)
for /f "tokens=5" %%a in ('netstat -ano 2^>nul ^| findstr ":%PORT% " ^| findstr "0.0.0.0:0"') do ( for /f "tokens=5" %%a in ('netstat -ano 2^>nul ^| findstr /C:":%PORT% " ^| findstr /C:"0.0.0.0:0"') do (
taskkill /PID %%a /F > nul 2>&1 taskkill /PID %%a /F > nul 2>&1
echo WebDAV-Server beendet (PID %%a). echo WebDAV-Server beendet - PID %%a
exit /b 0 exit /b 0
) )

View File

@@ -67,8 +67,8 @@ function basicAuth(req, res, next) {
app.use(basicAuth); app.use(basicAuth);
// Request-Body: PUT als Raw (Datei-Upload), PROPFIND als Text // Request-Body: PUT/POST als Raw (Datei-Upload), PROPFIND als Text
app.use(express.raw({ type: (req) => req.method === 'PUT', limit: '1gb' })); app.use(express.raw({ type: (req) => req.method === 'PUT' || req.method === 'POST', limit: '1gb' }));
app.use(express.text({ type: 'application/xml', limit: '1kb' })); app.use(express.text({ type: 'application/xml', limit: '1kb' }));
/** /**
@@ -254,7 +254,37 @@ async function getContext() {
} }
/** /**
* MKCOL Handler Ordner anlegen * Stellt sicher, dass ein Ordnerpfad existiert (erstellt fehlende Eltern rekursiv).
* @returns {Promise<{ uuid: string } | null>} Ordner oder null
*/
async function ensureFolderExists(storage, rootUuid, path) {
const segments = pathToSegments(path);
let currentUuid = rootUuid;
for (const segment of segments) {
const [contentPromise] = storage.getFolderContentByUuid({ folderUuid: currentUuid });
const content = await contentPromise;
const child = content?.children?.find((c) => {
const name = getPlainName(c.name, c.plain_name ?? c.plainName, c.parent_id ?? c.parentId, null);
return sanitizeForPath(name).toLowerCase() === sanitizeForPath(segment).toLowerCase();
});
if (child) {
currentUuid = child.uuid;
} else {
const [createPromise] = storage.createFolderByUuid({
parentFolderUuid: currentUuid,
plainName: segment,
});
const created = await createPromise;
currentUuid = created?.uuid;
if (!currentUuid) return null;
}
}
return { uuid: currentUuid };
}
/**
* MKCOL Handler Ordner anlegen (rekursiv: fehlende Eltern werden erstellt)
*/ */
async function handleMkcol(req, res) { async function handleMkcol(req, res) {
let path = req.url || '/'; let path = req.url || '/';
@@ -279,7 +309,10 @@ async function handleMkcol(req, res) {
try { try {
const { storage, rootUuid } = await getContext(); const { storage, rootUuid } = await getContext();
const parent = await resolveFolder(storage, rootUuid, parentPath); const parent =
parentPath && parentPath !== '/'
? await ensureFolderExists(storage, rootUuid, parentPath)
: { uuid: rootUuid };
if (!parent) { if (!parent) {
res.status(409).send('Übergeordneter Ordner existiert nicht'); res.status(409).send('Übergeordneter Ordner existiert nicht');
return; return;
@@ -582,7 +615,10 @@ async function handlePut(req, res) {
const parentPath = segmentsToPath(segments.slice(0, -1)); const parentPath = segmentsToPath(segments.slice(0, -1));
const fileName = segments[segments.length - 1]; const fileName = segments[segments.length - 1];
const parent = await resolveFolder(storage, rootUuid, parentPath); let parent = await resolveFolder(storage, rootUuid, parentPath);
if (!parent && parentPath && parentPath !== '/') {
parent = await ensureFolderExists(storage, rootUuid, parentPath);
}
if (!parent) { if (!parent) {
res.status(409).send('Zielordner existiert nicht'); res.status(409).send('Zielordner existiert nicht');
return; return;
@@ -664,7 +700,7 @@ async function handlePut(req, res) {
res.status(201).send(); res.status(201).send();
} catch (err) { } catch (err) {
console.error('PUT Fehler:', err.message); console.error('PUT Fehler:', path, err.message);
if (err.message?.includes('Token') || err.response?.status === 401) { if (err.message?.includes('Token') || err.response?.status === 401) {
res.status(401).send('Nicht autorisiert Token erneuern: https://drive.internxt.com'); res.status(401).send('Nicht autorisiert Token erneuern: https://drive.internxt.com');
return; return;
@@ -676,7 +712,7 @@ async function handlePut(req, res) {
// WebDAV Endpoints // WebDAV Endpoints
app.options('*', (req, res) => { app.options('*', (req, res) => {
res.set('DAV', '1, 2'); res.set('DAV', '1, 2');
res.set('Allow', 'OPTIONS, PROPFIND, GET, HEAD, PUT, DELETE, MKCOL, MOVE'); res.set('Allow', 'OPTIONS, PROPFIND, GET, HEAD, PUT, POST, DELETE, MKCOL, MOVE');
res.sendStatus(200); res.sendStatus(200);
}); });
@@ -700,7 +736,7 @@ app.use((req, res, next) => {
}); });
return; return;
} }
if (req.method === 'PUT') { if (req.method === 'PUT' || req.method === 'POST') {
handlePut(req, res).catch((err) => { handlePut(req, res).catch((err) => {
console.error('PUT unhandled:', err); console.error('PUT unhandled:', err);
if (!res.headersSent) res.status(500).send(err.message); if (!res.headersSent) res.status(500).send(err.message);