Files
dickendock/backend/src/tools/dateien/routes.js

362 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const db = require('../../db');
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 getSetting = key => db.prepare('SELECT value FROM admin_settings WHERE key=?').get(key)?.value;
const storage = multer.diskStorage({
destination: UPLOAD_DIR,
filename: (req, file, cb) => cb(null, `${Date.now()}-${Math.random().toString(36).slice(2)}${path.extname(file.originalname)}`),
});
const getUpload = () => multer({
storage,
limits: { fileSize: parseInt(getSetting('file_max_size_mb') || '50') * 1024 * 1024 },
fileFilter: (req, file, cb) => {
const allowed = (getSetting('file_allowed_ext') || '').split(',').map(e => e.trim().toLowerCase()).filter(Boolean);
if (!allowed.length) return cb(null, true);
const ext = path.extname(file.originalname).toLowerCase();
allowed.includes(ext) ? cb(null, true) : cb(new Error(`Dateityp nicht erlaubt: ${ext}`));
},
});
// Walk folder tree upward to check if any ancestor is shared with user
function isInSharedFolder(folderId, uid) {
let cur = folderId;
while (cur) {
if (db.prepare('SELECT id FROM folder_shares WHERE folder_id=? AND shared_with=?').get(cur, uid)) return true;
const p = db.prepare('SELECT parent_id FROM folders WHERE id=?').get(cur);
cur = p?.parent_id || null;
}
return false;
}
const enrichFolder = (folder) => ({
...folder,
fileCount: db.prepare('SELECT COUNT(*) c FROM files WHERE folder_id=?').get(folder.id).c,
subCount: db.prepare('SELECT COUNT(*) c FROM folders WHERE parent_id=?').get(folder.id).c,
totalSize: db.prepare('SELECT COALESCE(SUM(size),0) s FROM files WHERE folder_id=?').get(folder.id).s,
});
// ── Ordner ────────────────────────────────────────────────────────────────────
router.get('/folders', authenticate, (req, res) => {
const uid = req.user.id;
const parentId = req.query.parent_id ? parseInt(req.query.parent_id) : null;
// Own folders in this directory
const own = parentId
? db.prepare('SELECT * FROM folders WHERE user_id=? AND parent_id=? ORDER BY name').all(uid, parentId)
: db.prepare('SELECT * FROM folders WHERE user_id=? AND parent_id IS NULL ORDER BY name').all(uid);
// Shared folders
let shared = [];
if (!parentId) {
shared = db.prepare(`
SELECT f.*, u.username as owner,
GROUP_CONCAT(su.username) as shared_with_names,
MIN(fs.created_at) as shared_at
FROM folders f JOIN users u ON u.id=f.user_id
JOIN folder_shares fs ON fs.folder_id=f.id
JOIN users su ON su.id=fs.shared_with
WHERE fs.shared_with=?
GROUP BY f.id ORDER BY f.name
`).all(uid);
} else if (isInSharedFolder(parentId, uid)) {
const parentFolder = db.prepare('SELECT * FROM folders WHERE id=?').get(parentId);
if (parentFolder) {
shared = db.prepare('SELECT f.*, u.username as owner FROM folders f JOIN users u ON u.id=f.user_id WHERE f.parent_id=? AND f.user_id!=? ORDER BY f.name').all(parentId, uid);
}
}
// Folders shared BY me (root only)
const sharedByMe = parentId ? [] : db.prepare(`
SELECT DISTINCT f.*, u.username as owner,
GROUP_CONCAT(su.username) as shared_with_names,
MIN(fs.created_at) as shared_at
FROM folders f JOIN users u ON u.id=f.user_id
JOIN folder_shares fs ON fs.folder_id=f.id
JOIN users su ON su.id=fs.shared_with
WHERE f.user_id=?
GROUP BY f.id ORDER BY f.name
`).all(uid);
res.json({ own: own.map(enrichFolder), shared: shared.map(enrichFolder), sharedByMe: sharedByMe.map(enrichFolder) });
});
router.post('/folders', authenticate, (req, res) => {
const { name, parent_id } = req.body;
if (!name?.trim()) return res.status(400).json({ error: 'Name erforderlich' });
const parentId = parent_id ? parseInt(parent_id) : null;
if (parentId) {
const parent = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(parentId, req.user.id);
if (!parent) return res.status(404).json({ error: 'Überordner nicht gefunden' });
}
const r = db.prepare('INSERT INTO folders (user_id,name,parent_id) VALUES (?,?,?)').run(req.user.id, name.trim(), parentId);
res.json(enrichFolder(db.prepare('SELECT * FROM folders WHERE id=?').get(r.lastInsertRowid)));
});
router.delete('/folders/:id', authenticate, (req, res) => {
const folder = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
if (!folder) return res.status(404).json({ error: 'Nicht gefunden' });
// Delete all files inside
const files = db.prepare('SELECT * FROM files WHERE folder_id=? AND user_id=?').all(folder.id, req.user.id);
for (const f of files) { try { fs.unlinkSync(path.join(UPLOAD_DIR, f.filename)); } catch {} }
db.prepare('DELETE FROM files WHERE folder_id=? AND user_id=?').run(folder.id, req.user.id);
db.prepare('DELETE FROM folders WHERE id=?').run(folder.id);
res.json({ success: true });
});
router.get('/folders/:id/shares', authenticate, (req, res) => {
const folder = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
if (!folder) return res.status(404).json({ error: 'Nicht gefunden' });
res.json(db.prepare('SELECT u.id,u.username,fs.created_at as shared_at FROM folder_shares fs JOIN users u ON u.id=fs.shared_with WHERE fs.folder_id=?').all(folder.id));
});
router.post('/folders/:id/share', authenticate, (req, res) => {
const folder = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
if (!folder) return res.status(404).json({ error: 'Nicht gefunden' });
const target = db.prepare('SELECT * FROM users WHERE username=?').get(req.body.username);
if (!target) return res.status(404).json({ error: 'Benutzer nicht gefunden' });
if (target.id === req.user.id) return res.status(400).json({ error: 'Kann nicht mit dir selbst teilen' });
db.prepare('INSERT OR IGNORE INTO folder_shares (folder_id,shared_by,shared_with) VALUES (?,?,?)').run(folder.id, req.user.id, target.id);
res.json({ success: true });
});
router.delete('/folders/:id/share/:userId', authenticate, (req, res) => {
const folder = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
if (!folder) return res.status(404).json({ error: 'Nicht gefunden' });
db.prepare('DELETE FROM folder_shares WHERE folder_id=? AND shared_with=?').run(folder.id, req.params.userId);
res.json({ success: true });
});
// ── Dateien ───────────────────────────────────────────────────────────────────
router.get('/', authenticate, (req, res) => {
const uid = req.user.id;
const folderId = req.query.folder_id ? parseInt(req.query.folder_id) : null;
// Own files in this directory
const own = folderId
? db.prepare('SELECT f.*, u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.user_id=? AND f.folder_id=? ORDER BY f.created_at DESC').all(uid, folderId)
: db.prepare('SELECT f.*, u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.user_id=? AND f.folder_id IS NULL ORDER BY f.created_at DESC').all(uid);
// Files shared BY me
const sharedByMe = folderId ? [] : db.prepare(`
SELECT DISTINCT f.*, u.username as owner,
GROUP_CONCAT(su.username) as shared_with_names,
MIN(s.created_at) as shared_at,
MAX(s.accessed_at) as accessed_at,
MAX(CASE WHEN s.password_hash IS NOT NULL THEN 1 ELSE 0 END) as has_password
FROM files f JOIN users u ON u.id=f.user_id
JOIN file_shares s ON s.file_id=f.id
JOIN users su ON su.id=s.shared_with
WHERE f.user_id=?
GROUP BY f.id ORDER BY f.created_at DESC
`).all(uid);
// Files shared with me
let shared = [];
if (!folderId) {
shared = db.prepare(`
SELECT f.*, u.username as owner, sb.username as shared_by_name,
CASE WHEN s.password_hash IS NOT NULL THEN 1 ELSE 0 END as has_password
FROM files f JOIN users u ON u.id=f.user_id
JOIN file_shares s ON s.file_id=f.id
JOIN users sb ON sb.id=s.shared_by
WHERE s.shared_with=?
ORDER BY f.created_at DESC
`).all(uid);
} else if (isInSharedFolder(folderId, uid)) {
const folder = db.prepare('SELECT * FROM folders WHERE id=?').get(folderId);
if (folder) {
shared = db.prepare('SELECT f.*, u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.folder_id=? AND f.user_id=? ORDER BY f.created_at DESC').all(folderId, folder.user_id);
}
}
res.json({ own, shared, sharedByMe });
});
router.post('/', authenticate, (req, res) => {
const uid = req.user.id;
const maxSizeMb = parseInt(getSetting('file_max_size_mb') || '50');
const totalLimitMb= parseInt(getSetting('file_total_size_mb') || '500');
const usedBytes = db.prepare('SELECT COALESCE(SUM(size),0) AS s FROM files WHERE user_id=?').get(uid).s;
if (usedBytes >= totalLimitMb * 1024 * 1024)
return res.status(400).json({ error: `Gesamtlimit von ${totalLimitMb} MB erreicht` });
getUpload().single('file')(req, res, err => {
if (err) return res.status(400).json({ error: err.message });
if (!req.file) return res.status(400).json({ error: 'Keine Datei' });
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));
});
});
// ── 3D-Modelle Hilfsendpoints ─────────────────────────────────────────────────
// Sucht einen Ordner ohne ihn anzulegen gibt null zurück wenn nicht gefunden
router.get('/find-folder', authenticate, (req, res) => {
const uid = req.user.id;
const { name, parent_id } = req.query;
if (!name?.trim()) return res.json(null);
const parentId = parent_id ? parseInt(parent_id) : null;
const folder = parentId
? db.prepare('SELECT * FROM folders WHERE user_id=? AND name=? AND parent_id=?').get(uid, name.trim(), parentId)
: db.prepare('SELECT * FROM folders WHERE user_id=? AND name=? AND parent_id IS NULL').get(uid, name.trim());
res.json(folder ? enrichFolder(folder) : null);
});
// Stellt sicher dass ein Ordner existiert (Name + optionaler parent), gibt ihn zurück
router.post('/ensure-folder', authenticate, (req, res) => {
const uid = req.user.id;
const { name, parent_id } = req.body;
if (!name?.trim()) return res.status(400).json({ error: 'Name erforderlich' });
const parentId = parent_id ? parseInt(parent_id) : null;
// Suche existierenden Ordner
const existing = parentId
? db.prepare('SELECT * FROM folders WHERE user_id=? AND name=? AND parent_id=?').get(uid, name.trim(), parentId)
: db.prepare('SELECT * FROM folders WHERE user_id=? AND name=? AND parent_id IS NULL').get(uid, name.trim());
if (existing) return res.json(enrichFolder(existing));
const r = db.prepare('INSERT INTO folders (user_id,name,parent_id) VALUES (?,?,?)').run(uid, name.trim(), parentId);
res.json(enrichFolder(db.prepare('SELECT * FROM folders WHERE id=?').get(r.lastInsertRowid)));
});
// Upload 3D-Dateien (ohne Ext-Beschränkung, für den Kalkulator)
const upload3d = multer({
storage,
limits: { fileSize: parseInt(process.env.MAX_3D_SIZE_MB || '200') * 1024 * 1024 },
});
router.post('/upload-3d', authenticate, (req, res) => {
upload3d.array('files', 20)(req, res, err => {
if (err) return res.status(400).json({ error: err.message });
if (!req.files?.length) return res.status(400).json({ error: 'Keine Dateien' });
const uid = req.user.id;
const folderId = req.body.folder_id ? parseInt(req.body.folder_id) : null;
const inserted = [];
for (const f of req.files) {
const r = db.prepare('INSERT INTO files (user_id,filename,originalname,mimetype,size,folder_id) VALUES (?,?,?,?,?,?)')
.run(uid, f.filename, f.originalname, f.mimetype || 'application/octet-stream', f.size, folderId);
inserted.push(r.lastInsertRowid);
}
res.json({ uploaded: inserted.length });
});
});
router.get('/storage', authenticate, (req, res) => {
const uid = req.user.id;
const used = db.prepare('SELECT COALESCE(SUM(size),0) AS s FROM files WHERE user_id=?').get(uid).s;
const count = db.prepare('SELECT COUNT(*) AS c FROM files WHERE user_id=?').get(uid).c;
const maxSizeMb = parseInt(getSetting('file_max_size_mb') || '50');
const totalLimitMb = parseInt(getSetting('file_total_size_mb') || '500');
res.json({ used, count, maxSizeMb, totalLimitMb });
});
router.get('/:id/download', authenticate, async (req, res) => {
const uid = req.user.id;
const file = db.prepare('SELECT * FROM files WHERE id=?').get(req.params.id);
if (!file) return res.status(404).json({ error: 'Nicht gefunden' });
const isOwner = file.user_id === uid;
const share = db.prepare('SELECT * FROM file_shares WHERE file_id=? AND shared_with=?').get(file.id, uid);
const inSharedDir = file.folder_id && isInSharedFolder(file.folder_id, uid);
if (!isOwner && !share && !inSharedDir) return res.status(403).json({ error: 'Kein Zugriff' });
// Passwortprüfung für Shares (Owner braucht kein Passwort)
if (!isOwner && share?.password_hash) {
const bcrypt = require('bcryptjs');
const pw = req.query.password || req.headers['x-share-password'] || '';
const ok = pw && await bcrypt.compare(pw, share.password_hash);
if (!ok) return res.status(403).json({
error: 'Falsches Passwort',
passwordRequired: !pw // true = noch kein PW eingegeben, false = falsches PW
});
}
const fp = path.join(UPLOAD_DIR, file.filename);
if (!fs.existsSync(fp)) return res.status(404).json({ error: 'Datei fehlt' });
// Zugriff tracken (immer aktualisieren damit es sicher gesetzt wird)
if (share) {
db.prepare("UPDATE file_shares SET accessed_at = datetime('now','localtime') WHERE file_id=? AND shared_with=?")
.run(file.id, uid);
}
res.download(fp, file.originalname);
});
router.delete('/:id', authenticate, (req, res) => {
const file = db.prepare('SELECT * FROM files WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
if (!file) return res.status(404).json({ error: 'Nicht gefunden' });
try { fs.unlinkSync(path.join(UPLOAD_DIR, file.filename)); } catch {}
db.prepare('DELETE FROM files WHERE id=?').run(file.id);
res.json({ success: true });
});
router.get('/:id/shares', authenticate, (req, res) => {
const file = db.prepare('SELECT * FROM files WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
if (!file) return res.status(404).json({ error: 'Nicht gefunden' });
res.json(db.prepare(`
SELECT u.id, u.username, s.created_at as shared_at,
s.accessed_at,
CASE WHEN s.password_hash IS NOT NULL THEN 1 ELSE 0 END as has_password
FROM file_shares s JOIN users u ON u.id=s.shared_with
WHERE s.file_id=?
`).all(file.id));
});
router.post('/:id/share', authenticate, async (req, res) => {
const file = db.prepare('SELECT * FROM files WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
if (!file) return res.status(404).json({ error: 'Nicht gefunden' });
const target = db.prepare('SELECT * FROM users WHERE username=?').get(req.body.username);
if (!target) return res.status(404).json({ error: 'Benutzer nicht gefunden' });
if (target.id === req.user.id) return res.status(400).json({ error: 'Kann nicht mit dir selbst teilen' });
let passwordHash = null;
if (req.body.password) {
const bcrypt = require('bcryptjs');
passwordHash = await bcrypt.hash(req.body.password, 10);
}
const existing = db.prepare('SELECT id FROM file_shares WHERE file_id=? AND shared_with=?').get(file.id, target.id);
if (existing) {
db.prepare('UPDATE file_shares SET password_hash=?, accessed_at=NULL WHERE file_id=? AND shared_with=?')
.run(passwordHash, file.id, target.id);
} else {
db.prepare('INSERT INTO file_shares (file_id,shared_by,shared_with,password_hash) VALUES (?,?,?,?)')
.run(file.id, req.user.id, target.id, passwordHash);
}
res.json({ success: true });
});
router.delete('/:id/share/:userId', authenticate, (req, res) => {
const file = db.prepare('SELECT * FROM files WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
if (!file) return res.status(404).json({ error: 'Nicht gefunden' });
db.prepare('DELETE FROM file_shares WHERE file_id=? AND shared_with=?').run(file.id, req.params.userId);
res.json({ success: true });
});
// Settings
router.get('/settings', authenticate, requireAdmin, (req, res) => {
const s = db.prepare('SELECT * FROM admin_settings').all();
const obj = {}; for (const r of s) obj[r.key] = r.value;
res.json(obj);
});
router.put('/settings', authenticate, requireAdmin, (req, res) => {
for (const key of ['file_max_size_mb','file_total_size_mb','file_allowed_ext']) {
if (req.body[key] !== undefined)
db.prepare('INSERT OR REPLACE INTO admin_settings (key,value) VALUES (?,?)').run(key, String(req.body[key]));
}
res.json({ success: true });
});
module.exports = router;