Duplicati/rclone-Fixes, Basic Auth, Repo-Aufräumen
- Basic Auth: beliebige oder WEBDAV_USER/WEBDAV_PASS - MKCOL: 201 bei existierendem Ordner (Duplicati-Kompatibilität) - PUT: Leere Dateien, Retry bei File-already-exists - Aufräumen: bridge-test, debug-files entfernt, webdav-server-Dep - .env.example: API-Credentials entfernt, drive-web in gitignore - README: Docs verlinkt, Schnellstart aktualisiert Made-with: Cursor
This commit is contained in:
105
src/server.js
105
src/server.js
@@ -9,6 +9,7 @@ import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import { createClients, refreshUser } from './internxt-client.js';
|
||||
import { pathToSegments, segmentsToPath, listFolder, resolveFolder, resolveResource } from './path-resolver.js';
|
||||
import { getPlainName } from './name-decrypt.js';
|
||||
import { downloadFileStream } from './download.js';
|
||||
import { uploadFileBuffer } from './upload.js';
|
||||
|
||||
@@ -23,6 +24,49 @@ if (!token) {
|
||||
|
||||
const app = express();
|
||||
|
||||
// WebDAV-Credentials: Wenn gesetzt, werden Client-Credentials dagegen geprüft.
|
||||
// Sonst akzeptiert der Server beliebige Basic-Auth-Credentials (für Duplicati etc.).
|
||||
const webdavUser = process.env.WEBDAV_USER;
|
||||
const webdavPass = process.env.WEBDAV_PASS;
|
||||
const authStrict = webdavUser != null && webdavPass != null;
|
||||
|
||||
/**
|
||||
* Basic-Auth-Middleware: Akzeptiert beliebige Credentials oder prüft gegen WEBDAV_USER/WEBDAV_PASS.
|
||||
*/
|
||||
function basicAuth(req, res, next) {
|
||||
if (req.method === 'OPTIONS') return next();
|
||||
|
||||
const auth = req.headers?.authorization;
|
||||
if (!auth || !auth.startsWith('Basic ')) {
|
||||
res.set('WWW-Authenticate', 'Basic realm="Internxt WebDAV"');
|
||||
res.status(401).send('Authentifizierung erforderlich');
|
||||
return;
|
||||
}
|
||||
|
||||
let user, pass;
|
||||
try {
|
||||
const decoded = Buffer.from(auth.slice(6), 'base64').toString('utf8');
|
||||
const colon = decoded.indexOf(':');
|
||||
user = colon >= 0 ? decoded.slice(0, colon) : decoded;
|
||||
pass = colon >= 0 ? decoded.slice(colon + 1) : '';
|
||||
} catch (_) {
|
||||
res.set('WWW-Authenticate', 'Basic realm="Internxt WebDAV"');
|
||||
res.status(401).send('Ungültige Credentials');
|
||||
return;
|
||||
}
|
||||
|
||||
if (authStrict && (user !== webdavUser || pass !== webdavPass)) {
|
||||
res.set('WWW-Authenticate', 'Basic realm="Internxt WebDAV"');
|
||||
res.status(401).send('Ungültige Credentials');
|
||||
return;
|
||||
}
|
||||
|
||||
req.webdavUser = user;
|
||||
next();
|
||||
}
|
||||
|
||||
app.use(basicAuth);
|
||||
|
||||
// Request-Body: PUT als Raw (Datei-Upload), PROPFIND als Text
|
||||
app.use(express.raw({ type: (req) => req.method === 'PUT', limit: '1gb' }));
|
||||
app.use(express.text({ type: 'application/xml', limit: '1kb' }));
|
||||
@@ -243,7 +287,11 @@ async function handleMkcol(req, res) {
|
||||
|
||||
const existing = await resolveResource(storage, rootUuid, path);
|
||||
if (existing) {
|
||||
res.status(405).send('Ressource existiert bereits');
|
||||
if (existing.type === 'folder') {
|
||||
res.status(201).send();
|
||||
return;
|
||||
}
|
||||
res.status(405).send('Ressource existiert bereits (kein Ordner)');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -550,6 +598,9 @@ async function handlePut(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
// Leere Dateien (z.B. Duplicati duplicati-access-privileges-test.tmp): Bridge braucht mind. 1 Byte
|
||||
const uploadBuffer = buffer.length > 0 ? buffer : Buffer.from([0]);
|
||||
|
||||
const refresh = await refreshUser(token);
|
||||
const user = refresh.user;
|
||||
const bridgeUser = user?.bridgeUser || user?.email;
|
||||
@@ -567,21 +618,49 @@ async function handlePut(req, res) {
|
||||
bridgeUser,
|
||||
bridgePass,
|
||||
mnemonic,
|
||||
buffer,
|
||||
buffer: uploadBuffer,
|
||||
});
|
||||
|
||||
const date = new Date().toISOString();
|
||||
await storage.createFileEntryByUuid({
|
||||
fileId,
|
||||
type: type || 'bin',
|
||||
size: buffer.length,
|
||||
plainName,
|
||||
bucket: bucketId,
|
||||
folderUuid: parent.uuid,
|
||||
encryptVersion: '03-aes',
|
||||
modificationTime: date,
|
||||
date,
|
||||
});
|
||||
|
||||
const doCreate = async () => {
|
||||
await storage.createFileEntryByUuid({
|
||||
fileId,
|
||||
type: type || 'bin',
|
||||
size: buffer.length,
|
||||
plainName,
|
||||
bucket: bucketId,
|
||||
folderUuid: parent.uuid,
|
||||
encryptVersion: '03-aes',
|
||||
modificationTime: date,
|
||||
date,
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await doCreate();
|
||||
} catch (createErr) {
|
||||
// "File already exists" – Datei per Namen löschen und erneut versuchen
|
||||
if (createErr?.message?.toLowerCase().includes('already exists')) {
|
||||
const [contentPromise] = storage.getFolderContentByUuid({ folderUuid: parent.uuid });
|
||||
const content = await contentPromise;
|
||||
const fullName = type ? `${plainName}.${type}` : plainName;
|
||||
const dup = (content?.files || []).find((f) => {
|
||||
const apiPlain = f.plain_name ?? f.plainName ?? getPlainName(f.name, null, null, f.folder_id ?? f.folderId);
|
||||
const apiType = f.type ?? '';
|
||||
const apiFull = apiType ? `${apiPlain}.${apiType}` : apiPlain;
|
||||
return apiFull.toLowerCase() === fullName.toLowerCase();
|
||||
});
|
||||
if (dup) {
|
||||
await storage.deleteFileByUuid(dup.uuid);
|
||||
await doCreate();
|
||||
} else {
|
||||
throw createErr;
|
||||
}
|
||||
} else {
|
||||
throw createErr;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).send();
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user