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:
2026-02-28 12:21:01 +01:00
parent 406e31f338
commit 7dbc6c8fe4
10 changed files with 147 additions and 294 deletions

View File

@@ -1,106 +0,0 @@
/**
* Bridge-Test: Ruft die Bridge API direkt auf
* Hilft beim Debuggen von 400/401 Fehlern
*
* Aufruf: node src/bridge-test.js
*/
import 'dotenv/config';
import crypto from 'crypto';
import { Users } from '@internxt/sdk/dist/drive/index.js';
const BRIDGE_URL = process.env.BRIDGE_URL || process.env.STORJ_BRIDGE || 'https://gateway.internxt.com/network';
const DRIVE_API_URL = process.env.DRIVE_API_URL || 'https://gateway.internxt.com/drive';
const token = process.env.INXT_TOKEN;
async function sha256Hex(data) {
return crypto.createHash('sha256').update(String(data)).digest('hex');
}
async function main() {
if (!token) {
console.error('INXT_TOKEN fehlt');
process.exit(1);
}
const users = Users.client(DRIVE_API_URL, { clientName: 'drive-web', clientVersion: '1.0' }, {
token,
unauthorizedCallback: () => {
throw new Error('Token ungültig');
},
});
const refresh = await users.refreshUser();
const user = refresh?.user ?? refresh;
const bridgeUser = user?.bridgeUser || user?.email;
const bridgePass = user?.userId;
console.log('User:', user?.email);
console.log('User bucket:', user?.bucket);
console.log('File bucket (from debug):', '695cd596fe2b60d4ec9c3f4e');
console.log('Buckets gleich?', user?.bucket === '695cd596fe2b60d4ec9c3f4e');
console.log('User (alle Keys):', Object.keys(user || {}));
console.log('bridgeUser:', bridgeUser ? bridgeUser.slice(0, 8) + '…' : '(fehlt)');
console.log('bridgePass (userId):', bridgePass ? '***' : '(fehlt)');
console.log('');
if (!bridgeUser || !bridgePass) {
console.error('Bridge-Credentials fehlen (bridgeUser oder userId)');
process.exit(1);
}
// Aus debug-files: bucket=695cd596fe2b60d4ec9c3f4e, fileId=69a2bf36fe5cd3b21d81deb7
const bucketId = '695cd596fe2b60d4ec9c3f4e';
const fileId = '69a2bf36fe5cd3b21d81deb7';
const password = await sha256Hex(bridgePass);
const auth = Buffer.from(`${bridgeUser}:${password}`).toString('base64');
const url = `${BRIDGE_URL}/buckets/${bucketId}/files/${fileId}/info`;
console.log('Request:', url);
console.log('');
// Test 1: Full SDK-Headers (Basic Auth + x-api-version + internxt-*)
console.log('--- Test 1: Full SDK-Headers ---');
const sdkHeaders = {
Authorization: `Basic ${auth}`,
'x-api-version': '2',
'content-type': 'application/json; charset=utf-8',
'internxt-version': '1.0',
'internxt-client': 'drive-web',
};
const res = await fetch(url, { headers: sdkHeaders });
console.log('Status:', res.status, res.statusText);
let text = await res.text();
if (text) {
try {
console.log('Body:', JSON.stringify(JSON.parse(text), null, 2));
} catch {
console.log('Body:', text.slice(0, 500));
}
}
// Test 2: x-token (wie drive-web manchmal)
if (res.status !== 200) {
console.log('');
console.log('--- Test 2: x-token Header ---');
const res2 = await fetch(url, {
headers: { 'x-token': token },
});
console.log('Status:', res2.status, res2.statusText);
text = await res2.text();
if (text) {
try {
console.log('Body:', JSON.stringify(JSON.parse(text), null, 2));
} catch {
console.log('Body:', text.slice(0, 500));
}
}
}
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -1,70 +0,0 @@
/**
* Debug: Zeigt die Struktur der Dateien aus der Drive API
* Hilft beim Debuggen von Bridge 400 (bucket/fileId Format)
*
* Aufruf: node src/debug-files.js [Pfad]
* Beispiel: node src/debug-files.js /
*/
import 'dotenv/config';
import { Storage, Users } from '@internxt/sdk/dist/drive/index.js';
import { resolveFolder } from './path-resolver.js';
const DRIVE_API_URL = process.env.DRIVE_API_URL || 'https://gateway.internxt.com/drive';
const token = process.env.INXT_TOKEN;
if (!token) {
console.error('INXT_TOKEN fehlt');
process.exit(1);
}
const appDetails = { clientName: 'drive-web', clientVersion: '1.0' };
const apiSecurity = {
token,
unauthorizedCallback: () => {
throw new Error('Token ungültig');
},
};
async function main() {
const path = process.argv[2] || '/';
const storage = Storage.client(DRIVE_API_URL, appDetails, apiSecurity);
const users = Users.client(DRIVE_API_URL, appDetails, apiSecurity);
const refresh = await users.refreshUser();
const user = refresh?.user ?? refresh;
const rootUuid = user?.rootFolderUuid || user?.rootFolderId;
if (!rootUuid) {
console.error('Root-Ordner nicht gefunden');
process.exit(1);
}
const folder = await resolveFolder(storage, rootUuid, path);
if (!folder) {
console.error('Pfad nicht gefunden:', path);
process.exit(1);
}
const [contentPromise] = storage.getFolderContentByUuid({ folderUuid: folder.uuid });
const content = await contentPromise;
const files = content?.files || [];
console.log('=== Dateien in', path, '(rohe API-Antwort) ===\n');
for (const f of files) {
console.log('Name:', f.plain_name || f.name || f.plainName);
console.log(' uuid:', f.uuid);
console.log(' bucket:', f.bucket, '(Typ:', typeof f.bucket, ')');
console.log(' bucket_id:', f.bucket_id);
console.log(' fileId:', f.fileId, '(Typ:', typeof f.fileId, ', Länge:', f.fileId?.length, ')');
console.log(' file_id:', f.file_id);
console.log(' networkFileId:', f.networkFileId);
console.log(' size:', f.size);
console.log('');
}
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -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) {

View File

@@ -57,10 +57,6 @@ export async function uploadFileBuffer(params) {
const { bucketId, bridgeUser, bridgePass, mnemonic, buffer, signal } = params;
const fileSize = buffer.length;
if (fileSize === 0) {
throw new Error('Leere Datei kann nicht hochgeladen werden');
}
if (!validateMnemonic(mnemonic)) {
throw new Error('Ungültiges Mnemonic');
}