feat: WebDAV Sync-Button - alle lokalen Dateien auf Synology kopieren
This commit is contained in:
@@ -226,4 +226,44 @@ router.post('/webdav/test', authenticate, requireAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── WebDAV Sync: alle lokalen Dateien auf NAS kopieren ────────────────────
|
||||||
|
router.post('/webdav/sync', authenticate, requireAdmin, async (req, res) => {
|
||||||
|
const webdav = require('../tools/dateien/webdav');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads';
|
||||||
|
|
||||||
|
const cfg = webdav.getConfig();
|
||||||
|
if (!cfg.enabled) return res.status(400).json({ error: 'WebDAV nicht aktiviert' });
|
||||||
|
|
||||||
|
// Alle Dateien aus DB holen mit Username
|
||||||
|
const files = db.prepare(`
|
||||||
|
SELECT f.*, u.username FROM files f
|
||||||
|
JOIN users u ON u.id = f.user_id
|
||||||
|
ORDER BY f.user_id, f.id
|
||||||
|
`).all();
|
||||||
|
|
||||||
|
const results = { ok: 0, failed: 0, errors: [] };
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const localPath = path.join(UPLOAD_DIR, file.filename);
|
||||||
|
if (!fs.existsSync(localPath)) {
|
||||||
|
results.failed++;
|
||||||
|
results.errors.push(`${file.originalname}: lokale Datei fehlt`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const buf = fs.readFileSync(localPath);
|
||||||
|
const davPath = webdav.userPath(cfg, file.username) + '/' + file.originalname;
|
||||||
|
await webdav.uploadFile(buf, davPath, file.mimetype || 'application/octet-stream');
|
||||||
|
results.ok++;
|
||||||
|
} catch(e) {
|
||||||
|
results.failed++;
|
||||||
|
results.errors.push(`${file.originalname}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ total: files.length, ...results });
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -2702,6 +2702,8 @@ function WebDavSettings({ toast }) {
|
|||||||
const [cfg, setCfg] = useState({ url:'', user:'', password:'', basePath:'/dickendock', enabled:false });
|
const [cfg, setCfg] = useState({ url:'', user:'', password:'', basePath:'/dickendock', enabled:false });
|
||||||
const [testing, setTesting] = useState(false);
|
const [testing, setTesting] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [syncing, setSyncing] = useState(false);
|
||||||
|
const [syncResult, setSyncResult] = useState(null);
|
||||||
const [showPw, setShowPw] = useState(false);
|
const [showPw, setShowPw] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -2780,7 +2782,7 @@ function WebDavSettings({ toast }) {
|
|||||||
Synology WebDAV aktivieren: Systemsteuerung → Dateidienste → WebDAV
|
Synology WebDAV aktivieren: Systemsteuerung → Dateidienste → WebDAV
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{display:'flex',gap:8}}>
|
<div style={{display:'flex',gap:8,flexWrap:'wrap',marginBottom:12}}>
|
||||||
<button onClick={test} disabled={testing || !cfg.url}
|
<button onClick={test} disabled={testing || !cfg.url}
|
||||||
style={{...S.btn('#60a5fa'), opacity:(testing||!cfg.url)?0.5:1}}>
|
style={{...S.btn('#60a5fa'), opacity:(testing||!cfg.url)?0.5:1}}>
|
||||||
{testing ? '⏳ Teste...' : '🔌 Verbindung testen'}
|
{testing ? '⏳ Teste...' : '🔌 Verbindung testen'}
|
||||||
@@ -2790,6 +2792,49 @@ function WebDavSettings({ toast }) {
|
|||||||
{saving ? '⏳...' : '💾 Speichern'}
|
{saving ? '⏳...' : '💾 Speichern'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Sync Button */}
|
||||||
|
{cfg.enabled && (
|
||||||
|
<div style={{borderTop:'1px solid rgba(255,255,255,0.07)',paddingTop:14}}>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10,marginBottom:8}}>
|
||||||
|
BESTEHENDE DATEIEN SYNCHRONISIEREN
|
||||||
|
</div>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10,marginBottom:10,lineHeight:1.5}}>
|
||||||
|
Kopiert alle lokalen Dateien aus dem Docker-Container auf die Synology.
|
||||||
|
Bereits vorhandene Dateien werden überschrieben.
|
||||||
|
</div>
|
||||||
|
<button onClick={async()=>{
|
||||||
|
setSyncing(true); setSyncResult(null);
|
||||||
|
try {
|
||||||
|
const d = await api('/admin/webdav/sync', { body: {} });
|
||||||
|
setSyncResult(d);
|
||||||
|
} catch(e) { setSyncResult({ error: e.message }); }
|
||||||
|
finally { setSyncing(false); }
|
||||||
|
}} disabled={syncing}
|
||||||
|
style={{...S.btn('#f59e0b'), opacity:syncing?0.5:1, marginBottom:10}}>
|
||||||
|
{syncing ? '⏳ Synchronisiere...' : '📤 Alles auf Synology kopieren'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{syncResult && !syncResult.error && (
|
||||||
|
<div style={{padding:'10px 12px',background:'rgba(74,222,128,0.08)',border:'1px solid rgba(74,222,128,0.3)',borderRadius:8,fontFamily:'monospace',fontSize:11}}>
|
||||||
|
<div style={{color:'#4ade80',marginBottom:4}}>
|
||||||
|
✓ Sync abgeschlossen: {syncResult.ok}/{syncResult.total} erfolgreich
|
||||||
|
</div>
|
||||||
|
{syncResult.failed > 0 && (
|
||||||
|
<div style={{color:'#f87171'}}>
|
||||||
|
✗ {syncResult.failed} fehlgeschlagen
|
||||||
|
{syncResult.errors?.slice(0,5).map((e,i) => (
|
||||||
|
<div key={i} style={{color:'rgba(248,113,113,0.7)',fontSize:9,marginTop:2}}>· {e}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{syncResult?.error && (
|
||||||
|
<div style={{color:'#f87171',fontFamily:'monospace',fontSize:11}}>✗ {syncResult.error}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user