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:
11
.env.example
11
.env.example
@@ -11,11 +11,12 @@ CRYPTO_SECRET=6KYQBP847D4ATSFA
|
||||
|
||||
# DEBUG=1 # Salt-Decryption testen (ob CRYPTO_SECRET stimmt)
|
||||
|
||||
# Test credentials (für auth-poc.js)
|
||||
INXT_EMAIL=
|
||||
INXT_PASSWORD=
|
||||
# INXT_2FA= # Bei 2FA: aktuellen Code eingeben
|
||||
|
||||
# Browser-Token (für token-test.js und WebDAV) – aus drive.internxt.com localStorage
|
||||
# INXT_TOKEN= # xNewToken
|
||||
# INXT_MNEMONIC= # xMnemonic (für Datei-Entschlüsselung)
|
||||
|
||||
# WebDAV-Credentials (für Duplicati, Explorer etc.)
|
||||
# Ohne Angabe: beliebige Basic-Auth-Credentials werden akzeptiert.
|
||||
# Mit Angabe: nur diese Credentials werden akzeptiert.
|
||||
# WEBDAV_USER=
|
||||
# WEBDAV_PASS=
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,3 +23,6 @@ Thumbs.db
|
||||
# Build / Cache
|
||||
dist/
|
||||
.cache/
|
||||
|
||||
# Externe Repos (nur Referenz)
|
||||
drive-web/
|
||||
|
||||
82
README.md
82
README.md
@@ -1,62 +1,54 @@
|
||||
# Internxt WebDAV Wrapper
|
||||
|
||||
WebDAV-Zugang zu Internxt Drive für Account-Tiers, die keinen CLI- oder Rclone-Zugang haben.
|
||||
WebDAV-Zugang zu Internxt Drive für Account-Tiers ohne CLI- oder Rclone-Native-Zugang.
|
||||
|
||||
## Hintergrund
|
||||
|
||||
Internxt blockiert für bestimmte Account-Typen (z.B. Free, Partner-Accounts) den Zugang über:
|
||||
- Internxt CLI (`cli access not allowed for this user tier`)
|
||||
- Rclone Native-Backend (`rclone access not allowed for this user tier`, Status 402)
|
||||
- Docker-Image `internxt/webdav` (nutzt dieselbe Auth)
|
||||
Internxt blockiert für bestimmte Account-Typen (z.B. Free, Partner) den Zugang über CLI und Rclone.
|
||||
|
||||
**Lösung:** Das Web-UI (drive.internxt.com) funktioniert – es nutzt `login()` mit `clientName: "drive-web"`. Der Wrapper imitiert diese Auth.
|
||||
**Lösung:** Das Web-UI (drive.internxt.com) funktioniert – es nutzt `clientName: "drive-web"`. Dieser Wrapper imitiert diese Auth und bietet einen WebDAV-Server.
|
||||
|
||||
## Auth-Proof-of-Concept
|
||||
## Schnellstart
|
||||
|
||||
1. Abhängigkeiten installieren:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
```bash
|
||||
npm install
|
||||
cp .env.example .env
|
||||
# .env: INXT_TOKEN, INXT_MNEMONIC, CRYPTO_SECRET eintragen (siehe docs/browser-token-auth.md)
|
||||
npm start
|
||||
```
|
||||
|
||||
2. `.env` anlegen (von `.env.example` kopieren):
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
Server läuft auf `http://127.0.0.1:3005`.
|
||||
|
||||
3. Credentials eintragen:
|
||||
```
|
||||
INXT_EMAIL=deine@email.de
|
||||
INXT_PASSWORD=dein_passwort
|
||||
INXT_2FA=123456 # Falls 2FA aktiv
|
||||
```
|
||||
## WebDAV-Funktionen
|
||||
|
||||
4. Auth testen:
|
||||
```bash
|
||||
npm run auth-test
|
||||
```
|
||||
- **PROPFIND** – Verzeichnis auflisten
|
||||
- **MKCOL** – Ordner erstellen
|
||||
- **DELETE** – Dateien/Ordner löschen
|
||||
- **MOVE** – Verschieben/Umbenennen
|
||||
- **GET** – Dateien herunterladen
|
||||
- **PUT** – Dateien hochladen
|
||||
|
||||
Bei Erfolg erscheint „Login erfolgreich!“ – dann kann der WebDAV-Server gebaut werden.
|
||||
## Clients
|
||||
|
||||
- **Duplicati** – Backup-Destination
|
||||
- **rclone** – `rclone config` → WebDAV, URL `http://127.0.0.1:3005`
|
||||
- **Windows Explorer** – Netzlaufwerk verbinden
|
||||
|
||||
## Dokumentation
|
||||
|
||||
- [docs/auth-analysis.md](docs/auth-analysis.md) – Analyse Web vs CLI Auth, clientName-Unterschied
|
||||
| Datei | Beschreibung |
|
||||
|-------|---------------|
|
||||
| [docs/browser-token-auth.md](docs/browser-token-auth.md) | Token aus Browser extrahieren, WebDAV-Credentials |
|
||||
| [docs/webdav-architektur.md](docs/webdav-architektur.md) | Architektur-Übersicht |
|
||||
| [docs/wsl-setup.md](docs/wsl-setup.md) | WSL-Setup (login mit Keys) |
|
||||
| [docs/auth-analysis.md](docs/auth-analysis.md) | Analyse Web vs CLI Auth |
|
||||
| [docs/crypto-secret-extract.md](docs/crypto-secret-extract.md) | CRYPTO_SECRET aus drive.internxt.com ermitteln |
|
||||
|
||||
## Browser-Token-Auth (Alternative)
|
||||
## Scripts
|
||||
|
||||
Falls der API-Login blockiert ist (z.B. Partner-Account):
|
||||
|
||||
1. Auf https://drive.internxt.com einloggen
|
||||
2. DevTools (F12) → Console: `localStorage.getItem('xNewToken')` und `localStorage.getItem('xMnemonic')` ausführen
|
||||
3. Werte in `.env` als `INXT_TOKEN` und `INXT_MNEMONIC` eintragen
|
||||
4. Testen: `npm run token-test`
|
||||
|
||||
Details: [docs/browser-token-auth.md](docs/browser-token-auth.md)
|
||||
|
||||
## WSL (login mit Keys)
|
||||
|
||||
Unter Windows schlägt Kyber-WASM fehl. Unter WSL: [docs/wsl-setup.md](docs/wsl-setup.md)
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
- WebDAV-Server mit Token-Auth implementieren
|
||||
- Storage-Client für Datei-Operationen anbinden
|
||||
| Befehl | Beschreibung |
|
||||
|-------|--------------|
|
||||
| `npm start` | WebDAV-Server starten |
|
||||
| `npm run token-test` | Token prüfen |
|
||||
| `npm run auth-test` | API-Login testen (E-Mail/Passwort) |
|
||||
| `npm run debug-names` | Namensentschlüsselung testen |
|
||||
|
||||
@@ -52,10 +52,16 @@ console.log('Mnemonic:', localStorage.getItem('xMnemonic') || '(nicht gefunden)'
|
||||
INXT_TOKEN=eyJhbGciOiJIUzI1NiIs...
|
||||
INXT_MNEMONIC=word1 word2 word3 ...
|
||||
# Namensentschlüsselung: CRYPTO_SECRET oder CRYPTO_SECRET2 (CLI-Default: 6KYQBP847D4ATSFA)
|
||||
# Falls Datei-/Ordnernamen verschlüsselt angezeigt werden: CRYPTO_SECRET2 aus drive.internxt.com extrahieren
|
||||
CRYPTO_SECRET=6KYQBP847D4ATSFA
|
||||
# Optional: WebDAV-Credentials erzwingen (sonst beliebige Credentials akzeptiert)
|
||||
# WEBDAV_USER=backup
|
||||
# WEBDAV_PASS=geheim
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
## WebDAV-Server starten
|
||||
|
||||
```bash
|
||||
|
||||
49
package-lock.json
generated
49
package-lock.json
generated
@@ -13,8 +13,7 @@
|
||||
"bip39": "^3.1.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.18.0",
|
||||
"webdav-server": "^2.6.0"
|
||||
"express": "^4.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@internxt/lib": {
|
||||
@@ -964,15 +963,6 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
|
||||
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
@@ -1166,43 +1156,6 @@
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/webdav-server": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/webdav-server/-/webdav-server-2.6.2.tgz",
|
||||
"integrity": "sha512-0iHdrOzlKGFD96bTvPF8IIEfxw9Q7jB5LqWqhjyBYsofD6T6mOYqWtAvR88VY9Mq0xeg8bCRHC2Vifc9iuTYuw==",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"mime-types": "^2.1.18",
|
||||
"xml-js-builder": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-js": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"xml-js": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-js-builder": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/xml-js-builder/-/xml-js-builder-1.0.3.tgz",
|
||||
"integrity": "sha512-BoLgG/glT45M0jK5PGh9h+iGrQxa8jJk9ofR63GroRifl2tbGB3/yYiVY3wQWHrZgWWfl9+7fhEB/VoD9mWnSg==",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"xml-js": "^1.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"bip39": "^3.1.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.18.0",
|
||||
"webdav-server": "^2.6.0"
|
||||
"express": "^4.18.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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,10 +618,12 @@ async function handlePut(req, res) {
|
||||
bridgeUser,
|
||||
bridgePass,
|
||||
mnemonic,
|
||||
buffer,
|
||||
buffer: uploadBuffer,
|
||||
});
|
||||
|
||||
const date = new Date().toISOString();
|
||||
|
||||
const doCreate = async () => {
|
||||
await storage.createFileEntryByUuid({
|
||||
fileId,
|
||||
type: type || 'bin',
|
||||
@@ -582,6 +635,32 @@ async function handlePut(req, res) {
|
||||
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) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user