Compare commits
2 Commits
378fb59912
...
b463579896
| Author | SHA1 | Date | |
|---|---|---|---|
| b463579896 | |||
| bbf3b899f7 |
@@ -9,7 +9,7 @@ CRYPTO_SECRET=6KYQBP847D4ATSFA
|
|||||||
# Für Namensentschlüsselung (CRYPTO_SECRET2). Falls nicht gesetzt, wird CRYPTO_SECRET verwendet.
|
# Für Namensentschlüsselung (CRYPTO_SECRET2). Falls nicht gesetzt, wird CRYPTO_SECRET verwendet.
|
||||||
# CRYPTO_SECRET2=6KYQBP847D4ATSFA
|
# CRYPTO_SECRET2=6KYQBP847D4ATSFA
|
||||||
|
|
||||||
# DEBUG=1 # Salt-Decryption testen (ob CRYPTO_SECRET stimmt)
|
# DEBUG=1 # Salt-Decryption testen; PUT-Logging (Pfad, Body-Größe, Stacktrace bei Fehlern)
|
||||||
|
|
||||||
# Browser-Token (für token-test.js und WebDAV) – aus drive.internxt.com localStorage
|
# Browser-Token (für token-test.js und WebDAV) – aus drive.internxt.com localStorage
|
||||||
# INXT_TOKEN= # xNewToken
|
# INXT_TOKEN= # xNewToken
|
||||||
|
|||||||
@@ -81,6 +81,21 @@ 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.
|
||||||
|
|
||||||
|
### Restic „object not found“ / 500
|
||||||
|
|
||||||
|
1. **Port prüfen:** rclone-URL muss exakt dem Server-Port entsprechen. Steht in der Konsole z.B. `http://127.0.0.1:3010`, dann in rclone `url = http://127.0.0.1:3010` eintragen.
|
||||||
|
2. **Nur einen Server:** `npm start` beenden (Ctrl+C), dann nur `scripts\start-webdav.cmd` nutzen – sonst antwortet evtl. ein alter Prozess.
|
||||||
|
3. **rclone config:** `rclone config` → Remote `internxt-webdav` → `url` = `http://127.0.0.1:PORT` (PORT aus Server-Start).
|
||||||
|
4. **Logs:** `logs\webdav-errors.log` und `logs\webdav-debug.log` prüfen – dort steht, welche Anfrage 4xx/5xx bekommt.
|
||||||
|
|
||||||
## 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.
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ REM In Duplicati: Einstellungen -> Erweitert -> Scripts -> Vor dem Backup ausfue
|
|||||||
REM Pfad: C:\Pfad\zu\internxt-webdav\scripts\start-webdav.cmd
|
REM Pfad: C:\Pfad\zu\internxt-webdav\scripts\start-webdav.cmd
|
||||||
REM Optional: Port als Argument (z.B. start-webdav.cmd 8080)
|
REM Optional: Port als Argument (z.B. start-webdav.cmd 8080)
|
||||||
|
|
||||||
if "%1"=="" (set PORT=3005) else (set PORT=%1)
|
|
||||||
|
|
||||||
cd /d "%~dp0.."
|
cd /d "%~dp0.."
|
||||||
|
if "%1"=="" (set PORT=3005) else (set PORT=%1)
|
||||||
|
for /f "tokens=2 delims==" %%a in ('findstr /B "PORT=" .env 2^>nul') do set PORT=%%a
|
||||||
|
|
||||||
REM .env und Token pruefen
|
REM .env und Token pruefen
|
||||||
if not exist .env (
|
if not exist .env (
|
||||||
@@ -19,15 +19,18 @@ 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
|
||||||
)
|
)
|
||||||
|
|
||||||
echo Starte WebDAV-Server...
|
if not exist "%~dp0..\logs" mkdir "%~dp0..\logs"
|
||||||
start /B node src/server.js > nul 2>&1
|
set LOGFILE=%~dp0..\logs\webdav.log
|
||||||
|
echo [%date% %time%] WebDAV-Server starten... >> "%LOGFILE%"
|
||||||
|
echo Starte WebDAV-Server... Log: %LOGFILE%
|
||||||
|
start /B node src/server.js >> "%LOGFILE%" 2>&1
|
||||||
|
|
||||||
REM Warten und pruefen ob Server antwortet (OPTIONS braucht keine Auth)
|
REM Warten und pruefen ob Server antwortet (OPTIONS braucht keine Auth)
|
||||||
set RETRIES=0
|
set RETRIES=0
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export async function resolveResource(storage, rootFolderUuid, path) {
|
|||||||
const bucket = file.bucket ?? file.bucket_id;
|
const bucket = file.bucket ?? file.bucket_id;
|
||||||
const fileId = file.fileId ?? file.file_id ?? file.networkFileId;
|
const fileId = file.fileId ?? file.file_id ?? file.networkFileId;
|
||||||
const name = getPlainName(file.name, file.plain_name ?? file.plainName, null, file.folder_id ?? file.folderId);
|
const name = getPlainName(file.name, file.plain_name ?? file.plainName, null, file.folder_id ?? file.folderId);
|
||||||
|
const size = file.size ?? file.file_size ?? 0;
|
||||||
return {
|
return {
|
||||||
uuid: file.uuid,
|
uuid: file.uuid,
|
||||||
type: 'file',
|
type: 'file',
|
||||||
@@ -114,6 +115,7 @@ export async function resolveResource(storage, rootFolderUuid, path) {
|
|||||||
parentUuid: parent.uuid,
|
parentUuid: parent.uuid,
|
||||||
bucket,
|
bucket,
|
||||||
fileId,
|
fileId,
|
||||||
|
size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,14 +129,21 @@ export async function resolveResource(storage, rootFolderUuid, path) {
|
|||||||
return { uuid: folder.uuid, type: 'folder', name, parentUuid: parent.uuid };
|
return { uuid: folder.uuid, type: 'folder', name, parentUuid: parent.uuid };
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = content?.files?.find((f) => {
|
let file = content?.files?.find((f) => {
|
||||||
const name = getPlainName(f.name, f.plain_name ?? f.plainName, null, f.folder_id ?? f.folderId);
|
const name = getPlainName(f.name, f.plain_name ?? f.plainName, null, f.folder_id ?? f.folderId);
|
||||||
return sanitize(name).toLowerCase() === sanitize(childName).toLowerCase();
|
return sanitize(name).toLowerCase() === sanitize(childName).toLowerCase();
|
||||||
});
|
});
|
||||||
|
if (!file && !childName.includes('.')) {
|
||||||
|
file = content?.files?.find((f) => {
|
||||||
|
const name = getPlainName(f.name, f.plain_name ?? f.plainName, null, f.folder_id ?? f.folderId);
|
||||||
|
return sanitize(name).toLowerCase() === sanitize(childName + '.bin').toLowerCase();
|
||||||
|
});
|
||||||
|
}
|
||||||
if (file) {
|
if (file) {
|
||||||
const bucket = file.bucket ?? file.bucket_id;
|
const bucket = file.bucket ?? file.bucket_id;
|
||||||
const fileId = file.fileId ?? file.file_id ?? file.networkFileId;
|
const fileId = file.fileId ?? file.file_id ?? file.networkFileId;
|
||||||
const name = getPlainName(file.name, file.plain_name ?? file.plainName, null, file.folder_id ?? file.folderId);
|
const name = getPlainName(file.name, file.plain_name ?? file.plainName, null, file.folder_id ?? file.folderId);
|
||||||
|
const size = file.size ?? file.file_size ?? 0;
|
||||||
return {
|
return {
|
||||||
uuid: file.uuid,
|
uuid: file.uuid,
|
||||||
type: 'file',
|
type: 'file',
|
||||||
@@ -142,6 +151,7 @@ export async function resolveResource(storage, rootFolderUuid, path) {
|
|||||||
parentUuid: parent.uuid,
|
parentUuid: parent.uuid,
|
||||||
bucket,
|
bucket,
|
||||||
fileId,
|
fileId,
|
||||||
|
size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
197
src/server.js
197
src/server.js
@@ -6,6 +6,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { createClients, refreshUser } from './internxt-client.js';
|
import { createClients, refreshUser } from './internxt-client.js';
|
||||||
import { pathToSegments, segmentsToPath, listFolder, resolveFolder, resolveResource } from './path-resolver.js';
|
import { pathToSegments, segmentsToPath, listFolder, resolveFolder, resolveResource } from './path-resolver.js';
|
||||||
@@ -22,6 +24,47 @@ if (!token) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LOG_DIR = path.join(process.cwd(), 'logs');
|
||||||
|
|
||||||
|
/** Schreibt in logs/webdav-debug.log (separate Datei, kein Konflikt mit stdout→webdav.log) */
|
||||||
|
function logToFile(...args) {
|
||||||
|
const msg = args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') + '\n';
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||||
|
fs.appendFileSync(path.join(LOG_DIR, 'webdav-debug.log'), `[${new Date().toISOString()}] ${msg}`);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Schreibt Fehler in logs/webdav-errors.log (separate Datei, kein Konflikt mit stdout) */
|
||||||
|
function logError(...args) {
|
||||||
|
const msg = args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') + '\n';
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||||
|
fs.appendFileSync(path.join(LOG_DIR, 'webdav-errors.log'), `[${new Date().toISOString()}] ${msg}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('logError failed:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
logError('unhandledRejection', reason);
|
||||||
|
});
|
||||||
|
process.on('uncaughtException', (err) => {
|
||||||
|
logError('uncaughtException', err.message, err.stack);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fehlerdatei beim Start anlegen – prüft, ob dieser Prozess die neue Version läuft
|
||||||
|
logError('Server gestartet (Version mit Fehler-Logging)');
|
||||||
|
|
||||||
|
/** Cache für neu erstellte Dateien – rclone verifiziert per GET direkt nach PUT; API kann verzögert sein */
|
||||||
|
const recentFileCache = new Map();
|
||||||
|
const CACHE_TTL_MS = 60_000;
|
||||||
|
|
||||||
|
function cacheRecentFile(pathKey, resource) {
|
||||||
|
recentFileCache.set(pathKey, resource);
|
||||||
|
setTimeout(() => recentFileCache.delete(pathKey), CACHE_TTL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// WebDAV-Credentials: Wenn gesetzt, werden Client-Credentials dagegen geprüft.
|
// WebDAV-Credentials: Wenn gesetzt, werden Client-Credentials dagegen geprüft.
|
||||||
@@ -67,8 +110,23 @@ function basicAuth(req, res, next) {
|
|||||||
|
|
||||||
app.use(basicAuth);
|
app.use(basicAuth);
|
||||||
|
|
||||||
// Request-Body: PUT als Raw (Datei-Upload), PROPFIND als Text
|
app.use((req, res, next) => {
|
||||||
app.use(express.raw({ type: (req) => req.method === 'PUT', limit: '1gb' }));
|
if (req.url && req.url.includes('restic')) {
|
||||||
|
logToFile('REQ', req.method, req.url);
|
||||||
|
}
|
||||||
|
const origSend = res.send;
|
||||||
|
res.send = function (...args) {
|
||||||
|
if (req.url && req.url.includes('restic')) {
|
||||||
|
logToFile('RES', req.method, req.url, 'status:', res.statusCode);
|
||||||
|
if (res.statusCode >= 400) logError('HTTP', res.statusCode, req.method, req.url);
|
||||||
|
}
|
||||||
|
return origSend.apply(this, args);
|
||||||
|
};
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request-Body: PUT/POST als Raw (Datei-Upload), PROPFIND als Text
|
||||||
|
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' }));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,7 +228,7 @@ async function handlePropfind(req, res) {
|
|||||||
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { users, storage } = createClients(token);
|
const { storage } = createClients(token);
|
||||||
const refresh = await refreshUser(token);
|
const refresh = await refreshUser(token);
|
||||||
const user = refresh.user;
|
const user = refresh.user;
|
||||||
const rootUuid = user?.rootFolderUuid || user?.rootFolderId || user?.root_folder_id;
|
const rootUuid = user?.rootFolderUuid || user?.rootFolderId || user?.root_folder_id;
|
||||||
@@ -179,6 +237,25 @@ async function handlePropfind(req, res) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PROPFIND auf Datei (z.B. rclone-Verifizierung nach PUT)
|
||||||
|
let resource = await resolveResource(storage, rootUuid, path);
|
||||||
|
if (!resource) resource = recentFileCache.get(path);
|
||||||
|
if (resource && resource.type === 'file') {
|
||||||
|
const segments = pathToSegments(path);
|
||||||
|
const fileName = segments[segments.length - 1] || 'file';
|
||||||
|
const items = [{
|
||||||
|
path,
|
||||||
|
name: resource.name || fileName,
|
||||||
|
isCollection: false,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
size: resource.size ?? 0,
|
||||||
|
}];
|
||||||
|
const xml = buildPropfindResponse(baseUrl, items).replace(/\0/g, '');
|
||||||
|
res.set('Content-Type', 'application/xml; charset="utf-8"');
|
||||||
|
res.status(207).send(xml);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const listing = await listFolder(storage, rootUuid, path);
|
const listing = await listFolder(storage, rootUuid, path);
|
||||||
if (!listing) {
|
if (!listing) {
|
||||||
res.status(404).send('Nicht gefunden');
|
res.status(404).send('Nicht gefunden');
|
||||||
@@ -254,7 +331,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 +386,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;
|
||||||
@@ -302,6 +412,10 @@ async function handleMkcol(req, res) {
|
|||||||
await createPromise;
|
await createPromise;
|
||||||
res.status(201).send();
|
res.status(201).send();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err?.message?.toLowerCase().includes('already exists')) {
|
||||||
|
res.status(201).send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error('MKCOL Fehler:', err.message);
|
console.error('MKCOL Fehler:', 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');
|
||||||
@@ -440,12 +554,10 @@ async function handleMove(req, res) {
|
|||||||
* GET Handler – Datei herunterladen
|
* GET Handler – Datei herunterladen
|
||||||
*/
|
*/
|
||||||
async function handleGet(req, res) {
|
async function handleGet(req, res) {
|
||||||
let path = req.url || '/';
|
let path = getPathFromRequest(req);
|
||||||
try {
|
|
||||||
path = decodeURIComponent(path);
|
|
||||||
} catch (_) {}
|
|
||||||
if (!path.startsWith('/')) path = '/' + path;
|
if (!path.startsWith('/')) path = '/' + path;
|
||||||
if (path.endsWith('/')) path = path.slice(0, -1);
|
if (path.endsWith('/')) path = path.slice(0, -1);
|
||||||
|
path = sanitizeForPath(path);
|
||||||
if (path === '/') {
|
if (path === '/') {
|
||||||
res.status(405).send('Verzeichnis kann nicht heruntergeladen werden');
|
res.status(405).send('Verzeichnis kann nicht heruntergeladen werden');
|
||||||
return;
|
return;
|
||||||
@@ -458,7 +570,11 @@ async function handleGet(req, res) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { storage, rootUuid } = await getContext();
|
const { storage, rootUuid } = await getContext();
|
||||||
const resource = await resolveResource(storage, rootUuid, path);
|
let resource = await resolveResource(storage, rootUuid, path);
|
||||||
|
if (!resource) {
|
||||||
|
resource = recentFileCache.get(path);
|
||||||
|
if (resource) logToFile('GET cache hit', path);
|
||||||
|
}
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
res.status(404).send('Nicht gefunden');
|
res.status(404).send('Nicht gefunden');
|
||||||
return;
|
return;
|
||||||
@@ -510,12 +626,10 @@ async function handleGet(req, res) {
|
|||||||
* HEAD Handler – wie GET, aber nur Header
|
* HEAD Handler – wie GET, aber nur Header
|
||||||
*/
|
*/
|
||||||
async function handleHead(req, res) {
|
async function handleHead(req, res) {
|
||||||
let path = req.url || '/';
|
let path = getPathFromRequest(req);
|
||||||
try {
|
|
||||||
path = decodeURIComponent(path);
|
|
||||||
} catch (_) {}
|
|
||||||
if (!path.startsWith('/')) path = '/' + path;
|
if (!path.startsWith('/')) path = '/' + path;
|
||||||
if (path.endsWith('/')) path = path.slice(0, -1);
|
if (path.endsWith('/')) path = path.slice(0, -1);
|
||||||
|
path = sanitizeForPath(path);
|
||||||
if (path === '/') {
|
if (path === '/') {
|
||||||
res.status(405).send();
|
res.status(405).send();
|
||||||
return;
|
return;
|
||||||
@@ -523,7 +637,8 @@ async function handleHead(req, res) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { storage, rootUuid } = await getContext();
|
const { storage, rootUuid } = await getContext();
|
||||||
const resource = await resolveResource(storage, rootUuid, path);
|
let resource = await resolveResource(storage, rootUuid, path);
|
||||||
|
if (!resource) resource = recentFileCache.get(path);
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
res.status(404).send();
|
res.status(404).send();
|
||||||
return;
|
return;
|
||||||
@@ -560,6 +675,10 @@ async function handlePut(req, res) {
|
|||||||
if (path.endsWith('/')) path = path.slice(0, -1);
|
if (path.endsWith('/')) path = path.slice(0, -1);
|
||||||
path = sanitizeForPath(path);
|
path = sanitizeForPath(path);
|
||||||
|
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.log('PUT', path, 'Content-Length:', req.headers['content-length'], 'Body:', req.body?.length ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
if (path === '/') {
|
if (path === '/') {
|
||||||
res.status(403).send('Root kann nicht überschrieben werden');
|
res.status(403).send('Root kann nicht überschrieben werden');
|
||||||
return;
|
return;
|
||||||
@@ -577,12 +696,17 @@ async function handlePut(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
logToFile('PUT try start', path);
|
||||||
const { storage, rootUuid } = await getContext();
|
const { storage, rootUuid } = await getContext();
|
||||||
|
logToFile('PUT getContext OK', path);
|
||||||
const segments = pathToSegments(path);
|
const segments = pathToSegments(path);
|
||||||
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;
|
||||||
@@ -613,15 +737,24 @@ async function handlePut(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { plainName, type } = parseFileName(fileName);
|
const { plainName, type } = parseFileName(fileName);
|
||||||
const fileId = await uploadFileBuffer({
|
let fileId;
|
||||||
bucketId,
|
logToFile('PUT Upload start', path);
|
||||||
bridgeUser,
|
try {
|
||||||
bridgePass,
|
fileId = await uploadFileBuffer({
|
||||||
mnemonic,
|
bucketId,
|
||||||
buffer: uploadBuffer,
|
bridgeUser,
|
||||||
});
|
bridgePass,
|
||||||
|
mnemonic,
|
||||||
|
buffer: uploadBuffer,
|
||||||
|
});
|
||||||
|
} catch (uploadErr) {
|
||||||
|
logError('PUT Upload (Bridge) fehlgeschlagen', path, uploadErr.message);
|
||||||
|
throw uploadErr;
|
||||||
|
}
|
||||||
|
logToFile('PUT Upload OK', path);
|
||||||
|
|
||||||
const date = new Date().toISOString();
|
const date = new Date().toISOString();
|
||||||
|
logToFile('PUT createFileEntry start', path);
|
||||||
|
|
||||||
const doCreate = async () => {
|
const doCreate = async () => {
|
||||||
await storage.createFileEntryByUuid({
|
await storage.createFileEntryByUuid({
|
||||||
@@ -639,7 +772,11 @@ async function handlePut(req, res) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await doCreate();
|
await doCreate();
|
||||||
|
logToFile('PUT createFileEntry OK', path);
|
||||||
|
const fullName = type ? `${plainName}.${type}` : plainName;
|
||||||
|
cacheRecentFile(path, { type: 'file', bucket: bucketId, fileId, name: fullName, size: buffer.length });
|
||||||
} catch (createErr) {
|
} catch (createErr) {
|
||||||
|
logError('PUT createFileEntry fehlgeschlagen', path, createErr.message);
|
||||||
// "File already exists" – Datei per Namen löschen und erneut versuchen
|
// "File already exists" – Datei per Namen löschen und erneut versuchen
|
||||||
if (createErr?.message?.toLowerCase().includes('already exists')) {
|
if (createErr?.message?.toLowerCase().includes('already exists')) {
|
||||||
const [contentPromise] = storage.getFolderContentByUuid({ folderUuid: parent.uuid });
|
const [contentPromise] = storage.getFolderContentByUuid({ folderUuid: parent.uuid });
|
||||||
@@ -664,7 +801,12 @@ async function handlePut(req, res) {
|
|||||||
|
|
||||||
res.status(201).send();
|
res.status(201).send();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('PUT Fehler:', err.message);
|
logError('PUT CATCH', path, err?.message ?? String(err), err?.response?.status, err?.response?.data);
|
||||||
|
const apiErr = err.response?.data ? JSON.stringify(err.response.data) : '';
|
||||||
|
const status = err.response?.status;
|
||||||
|
if (process.env.DEBUG) logError('Stack:', err.stack);
|
||||||
|
console.error('PUT Fehler:', path, err.message, status ? `HTTP ${status}` : '', apiErr || '');
|
||||||
|
if (process.env.DEBUG) console.error(err.stack);
|
||||||
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 +818,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,9 +842,9 @@ 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);
|
logError('PUT unhandled', err?.message, err?.stack);
|
||||||
if (!res.headersSent) res.status(500).send(err.message);
|
if (!res.headersSent) res.status(500).send(err.message);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -734,5 +876,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–4: PROPFIND, MKCOL, DELETE, MOVE, GET, PUT aktiv.');
|
console.log('Phase 1–4: PROPFIND, MKCOL, DELETE, MOVE, GET, PUT aktiv.');
|
||||||
|
console.log(`rclone/restic: URL muss http://127.0.0.1:${PORT} sein (gleicher Port!)`);
|
||||||
console.log('Verwendung: z.B. Windows Explorer → Netzlaufwerk verbinden');
|
console.log('Verwendung: z.B. Windows Explorer → Netzlaufwerk verbinden');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user