feat: Synology WebDAV Integration - Admin-Einstellungen + Datei-Spiegelung
This commit is contained in:
@@ -8,13 +8,17 @@ const { authenticate, requireAdmin } = require('../../middleware/auth');
|
||||
const router = express.Router();
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads';
|
||||
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
||||
const webdav = require('./webdav');
|
||||
|
||||
const getSetting = key => db.prepare('SELECT value FROM admin_settings WHERE key=?').get(key)?.value;
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
const diskStorage = multer.diskStorage({
|
||||
destination: UPLOAD_DIR,
|
||||
filename: (req, file, cb) => cb(null, `${Date.now()}-${Math.random().toString(36).slice(2)}${path.extname(file.originalname)}`),
|
||||
});
|
||||
const memStorage = multer.memoryStorage();
|
||||
|
||||
const storage = diskStorage; // Standard: Disk
|
||||
const getUpload = () => multer({
|
||||
storage,
|
||||
limits: { fileSize: parseInt(getSetting('file_max_size_mb') || '50') * 1024 * 1024 },
|
||||
@@ -194,7 +198,19 @@ router.post('/', authenticate, (req, res) => {
|
||||
const folderId = req.body.folder_id ? parseInt(req.body.folder_id) : null;
|
||||
const r = db.prepare('INSERT INTO files (user_id,filename,originalname,mimetype,size,folder_id) VALUES (?,?,?,?,?,?)')
|
||||
.run(uid, req.file.filename, req.file.originalname, req.file.mimetype||'application/octet-stream', req.file.size, folderId);
|
||||
res.json(db.prepare('SELECT f.*,u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.id=?').get(r.lastInsertRowid));
|
||||
const newFile = db.prepare('SELECT f.*,u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.id=?').get(r.lastInsertRowid);
|
||||
|
||||
// WebDAV: Datei asynchron spiegeln (Fehler ignorieren, lokale Kopie bleibt)
|
||||
const cfg = webdav.getConfig();
|
||||
if (cfg.enabled) {
|
||||
const user = db.prepare('SELECT username FROM users WHERE id=?').get(uid);
|
||||
const davPath = webdav.userPath(cfg, user.username) + '/' + req.file.originalname;
|
||||
fs.readFile(path.join(UPLOAD_DIR, req.file.filename), (err, buf) => {
|
||||
if (!err) webdav.uploadFile(buf, davPath, req.file.mimetype).catch(e => console.error('[webdav upload]', e.message));
|
||||
});
|
||||
}
|
||||
|
||||
res.json(newFile);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
165
backend/src/tools/dateien/webdav.js
Normal file
165
backend/src/tools/dateien/webdav.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* WebDAV Storage Backend für Synology NAS
|
||||
* Abstrahiert alle Datei-Operationen – lokal oder WebDAV
|
||||
*/
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const db = require('../../db');
|
||||
|
||||
function getSetting(key) {
|
||||
return db.prepare('SELECT value FROM admin_settings WHERE key=?').get(key)?.value || '';
|
||||
}
|
||||
|
||||
function getConfig() {
|
||||
return {
|
||||
url: getSetting('webdav_url'), // z.B. http://192.168.1.100:5005
|
||||
user: getSetting('webdav_user'),
|
||||
password: getSetting('webdav_password'),
|
||||
basePath: getSetting('webdav_base_path') || '/dickendock', // z.B. /dickendock
|
||||
enabled: getSetting('webdav_enabled') === '1',
|
||||
};
|
||||
}
|
||||
|
||||
function authHeader(cfg) {
|
||||
return 'Basic ' + Buffer.from(`${cfg.user}:${cfg.password}`).toString('base64');
|
||||
}
|
||||
|
||||
// WebDAV Request ausführen
|
||||
function davRequest(method, davPath, opts = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cfg = getConfig();
|
||||
const base = new URL(cfg.url);
|
||||
const fullPath = base.pathname.replace(/\/$/, '') + davPath;
|
||||
const mod = base.protocol === 'https:' ? https : http;
|
||||
|
||||
const headers = {
|
||||
'Authorization': authHeader(cfg),
|
||||
'Accept': '*/*',
|
||||
...opts.headers,
|
||||
};
|
||||
|
||||
const reqOpts = {
|
||||
hostname: base.hostname,
|
||||
port: base.port || (base.protocol === 'https:' ? 443 : 80),
|
||||
path: fullPath,
|
||||
method,
|
||||
headers,
|
||||
rejectUnauthorized: false, // Synology Self-Signed Certs
|
||||
};
|
||||
|
||||
if (opts.body) {
|
||||
headers['Content-Length'] = Buffer.byteLength(opts.body);
|
||||
}
|
||||
|
||||
const req = mod.request(reqOpts, res => {
|
||||
let data = '';
|
||||
res.setEncoding('binary');
|
||||
res.on('data', d => data += d);
|
||||
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: data }));
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
if (opts.body) req.write(opts.body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Öffentliche API ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Ordner auf WebDAV anlegen (inkl. alle Parent-Ordner)
|
||||
*/
|
||||
async function mkdirp(davPath) {
|
||||
const parts = davPath.split('/').filter(Boolean);
|
||||
let current = '';
|
||||
for (const part of parts) {
|
||||
current += '/' + part;
|
||||
const r = await davRequest('MKCOL', current);
|
||||
if (r.status !== 201 && r.status !== 405) { // 405 = schon vorhanden
|
||||
throw new Error(`MKCOL ${current} → ${r.status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User-Basispfad auf WebDAV: /dickendock/{username}
|
||||
*/
|
||||
function userPath(cfg, username) {
|
||||
return cfg.basePath.replace(/\/$/, '') + '/' + username.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Datei auf WebDAV hochladen
|
||||
* @param {Buffer} buffer - Dateiinhalt
|
||||
* @param {string} davPath - Zielpfad auf WebDAV
|
||||
* @param {string} mimeType
|
||||
*/
|
||||
async function uploadFile(buffer, davPath, mimeType = 'application/octet-stream') {
|
||||
await mkdirp(davPath.split('/').slice(0, -1).join('/'));
|
||||
const r = await new Promise((resolve, reject) => {
|
||||
const cfg = getConfig();
|
||||
const base = new URL(cfg.url);
|
||||
const fullPath = base.pathname.replace(/\/$/, '') + davPath;
|
||||
const mod = base.protocol === 'https:' ? https : http;
|
||||
|
||||
const headers = {
|
||||
'Authorization': authHeader(cfg),
|
||||
'Content-Type': mimeType,
|
||||
'Content-Length': buffer.length,
|
||||
};
|
||||
|
||||
const req = mod.request({
|
||||
hostname: base.hostname,
|
||||
port: base.port || (base.protocol === 'https:' ? 443 : 80),
|
||||
path: fullPath,
|
||||
method: 'PUT',
|
||||
headers,
|
||||
rejectUnauthorized: false,
|
||||
}, res => { res.resume(); resolve({ status: res.statusCode }); });
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(buffer);
|
||||
req.end();
|
||||
});
|
||||
|
||||
if (r.status !== 201 && r.status !== 204) {
|
||||
throw new Error(`PUT ${davPath} → ${r.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Datei von WebDAV downloaden (gibt Buffer zurück)
|
||||
*/
|
||||
async function downloadFile(davPath) {
|
||||
const r = await davRequest('GET', davPath);
|
||||
if (r.status !== 200) throw new Error(`GET ${davPath} → ${r.status}`);
|
||||
return Buffer.from(r.body, 'binary');
|
||||
}
|
||||
|
||||
/**
|
||||
* Datei/Ordner auf WebDAV löschen
|
||||
*/
|
||||
async function deleteFile(davPath) {
|
||||
const r = await davRequest('DELETE', davPath);
|
||||
if (r.status !== 204 && r.status !== 200 && r.status !== 404) {
|
||||
throw new Error(`DELETE ${davPath} → ${r.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verbindung testen
|
||||
*/
|
||||
async function testConnection() {
|
||||
const cfg = getConfig();
|
||||
if (!cfg.url || !cfg.user || !cfg.password) throw new Error('WebDAV nicht konfiguriert');
|
||||
const r = await davRequest('PROPFIND', cfg.basePath || '/', {
|
||||
headers: { 'Depth': '0', 'Content-Type': 'application/xml' },
|
||||
body: '<?xml version="1.0"?><propfind xmlns="DAV:"><prop><displayname/></prop></propfind>',
|
||||
});
|
||||
if (r.status !== 207 && r.status !== 200) throw new Error(`Verbindung fehlgeschlagen: HTTP ${r.status}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = { getConfig, userPath, mkdirp, uploadFile, downloadFile, deleteFile, testConnection };
|
||||
Reference in New Issue
Block a user