UNPKG

peertube-plugin-static-files-light

Version:

PeerTube Plugin zum Hochladen und Verwalten statischer Dateien mit Admin-Statistiken (einfache Version)

485 lines (411 loc) 16.1 kB
const fs = require('fs') const path = require('path') const multer = require('multer') const mime = require('mime-types') const StatsRoutes = require('./routes/stats') async function register({ peertubeHelpers, registerHook, registerSetting, settingsManager, storageManager, videoCategoryManager, videoLicenceManager, videoLanguageManager, getRouter }) { const logger = peertubeHelpers.logger logger.info('🚀 Static Files Plugin wird registriert...') const staticDir = '/app/client/static' // PeerTube static const metadataPath = path.join(staticDir, 'metadata') // /app/client/static/metadata const uploadsPath = path.join(staticDir, 'uploads') // /app/client/static/uploads const imagesPath = path.join(uploadsPath, 'images') // /app/client/static/uploads/images const documentsPath = path.join(uploadsPath, 'documents') // /app/client/static/uploads/documents // Создаем директории try { [uploadsPath, imagesPath, documentsPath, metadataPath].forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) logger.info(`📁 Verzeichnis erstellt: ${path.basename(dir)}`) } }) } catch (error) { logger.error('Fehler beim Erstellen der Verzeichnisse:', error) } // УПРОЩЕННЫЕ ФУНКЦИИ МЕТАДАННЫХ (только файловое хранение) async function saveFileMetadata(filename, metadata) { try { const metadataFile = path.join(metadataPath, `${filename}.json`) fs.writeFileSync(metadataFile, JSON.stringify(metadata, null, 2)) return true } catch (error) { logger.error(`Ошибка сохранения метаданных для ${filename}:`, error) return false } } async function loadFileMetadata(filename) { try { const metadataFile = path.join(metadataPath, `${filename}.json`) if (fs.existsSync(metadataFile)) { const data = fs.readFileSync(metadataFile, 'utf8') return JSON.parse(data) } return null } catch (error) { logger.error(`Ошибка загрузки метаданных для ${filename}:`, error) return null } } // УПРОЩЕННЫЕ НАСТРОЙКИ registerSetting({ name: 'enable-plugin', label: 'Plugin aktivieren', type: 'input-checkbox', default: true, private: false }) registerSetting({ name: 'page-path', label: 'Pfad zur Upload-Seite', type: 'input', default: 'files/upload', private: false }) registerSetting({ name: 'allowed-users', label: 'Berechtigte Benutzer (durch Komma getrennt)', type: 'input-textarea', default: '', descriptionHTML: 'Leer = alle angemeldeten Benutzer', private: false }) registerSetting({ name: 'admin-only', label: 'Nur für Administratoren', type: 'input-checkbox', default: false, descriptionHTML: 'Wenn aktiviert, nur Admins haben Zugriff', private: false }) registerSetting({ name: 'max-file-size', label: 'Maximale Dateigröße (MB)', type: 'input', default: '50', private: false }) // УПРОЩЕННАЯ HTML-НАСТРОЙКА registerSetting({ name: 'admin-link', label: 'Verwaltung', type: 'html', html: ` <div style="text-align: center; padding: 20px; background: #f8f9fa; border-radius: 8px;"> <h4>📁 Datei-Verwaltung</h4> <a href="/p/files/admin" target="_blank" style="background: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px;"> Zur Verwaltung </a> </div> `, private: false }) const router = getRouter() // Stats routes const statsRoutes = new StatsRoutes(peertubeHelpers, settingsManager) statsRoutes.registerRoutes(router) // СТАТИЧЕСКИЙ STORAGE ДЛЯ MULTER const storage = multer.diskStorage({ destination: function (req, file, cb) { const isImage = file.mimetype.startsWith('image/') cb(null, isImage ? imagesPath : documentsPath) }, filename: function (req, file, cb) { const timestamp = Date.now() const originalName = Buffer.from(file.originalname, 'latin1').toString('utf8') const ext = path.extname(originalName) const baseName = path.basename(originalName, ext) const safeBaseName = baseName.replace(/[^a-zA-Z0-9\-_äöüÄÖÜß]/g, '_') cb(null, `${timestamp}_${safeBaseName}${ext}`) } }) // УНИВЕРСАЛЬНЫЙ MIDDLEWARE ДОСТУПА async function checkAccess(req, res, next) { try { const user = await peertubeHelpers.user.getAuthUser(res) if (!user) { return res.status(401).json({ error: 'Authentifizierung erforderlich' }) } const settings = await settingsManager.getSettings([ 'enable-plugin', 'allowed-users', 'admin-only', 'max-file-size' ]) if (!settings['enable-plugin']) { return res.status(403).json({ error: 'Plugin ist deaktiviert' }) } // Только админы if (settings['admin-only'] && user.role !== 0) { return res.status(403).json({ error: 'Nur für Administratoren' }) } // Конкретные пользователи const allowedUsers = settings['allowed-users'] if (allowedUsers && allowedUsers.trim()) { const userList = allowedUsers.split(',').map(u => u.trim()).filter(u => u) if (userList.length > 0 && !userList.includes(user.username)) { return res.status(403).json({ error: 'Benutzer nicht berechtigt' }) } } req.user = user req.settings = settings next() } catch (error) { logger.error('Fehler bei Zugriffsprüfung:', error) res.status(500).json({ error: 'Serverfehler bei Zugriffsprüfung' }) } } // ОБЪЕДИНЕННАЯ ФУНКЦИЯ ЗАГРУЗКИ ФАЙЛОВ async function loadAllFiles() { const files = [] async function loadFilesFromDir(dirPath, category) { if (!fs.existsSync(dirPath)) return const dirFiles = fs.readdirSync(dirPath) for (const filename of dirFiles) { const filePath = path.join(dirPath, filename) const stats = fs.statSync(filePath) let fileInfo = await loadFileMetadata(filename) files.push({ filename: filename, category: category, size: stats.size, uploadDate: fileInfo?.uploadDate || stats.birthtime.toISOString(), uploadedBy: fileInfo?.uploadedBy || 'Unbekannt', mimetype: fileInfo?.mimetype || mime.lookup(filename) || 'application/octet-stream', url: `/static/uploads/${category}/${filename}`, hasMetadata: !!fileInfo }) } } await loadFilesFromDir(imagesPath, 'images') await loadFilesFromDir(documentsPath, 'documents') return files.sort((a, b) => new Date(b.uploadDate) - new Date(a.uploadDate)) } // API ROUTES // Check access router.get('/check-access', checkAccess, async (req, res) => { try { res.json({ allowed: true, user: { username: req.user.username, role: req.user.role, roleText: req.user.role === 0 ? 'Administrator' : req.user.role === 1 ? 'Moderator' : 'Benutzer' }, settings: { maxFileSize: parseInt(req.settings['max-file-size'] || '50'), pagePath: req.settings['page-path'] || 'files/upload' } }) } catch (error) { logger.error('Fehler bei check-access:', error) res.status(500).json({ error: 'Serverfehler' }) } }) // Upload router.post('/upload', checkAccess, (req, res) => { const maxFileSize = parseInt(req.settings['max-file-size'] || '50') * 1024 * 1024 // Создаем динамический upload с нужным лимитом const dynamicUpload = multer({ storage: storage, limits: { fileSize: maxFileSize, fieldSize: maxFileSize }, fileFilter: (req, file, cb) => { const allowedTypes = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/x-icon', 'image/vnd.microsoft.icon', 'application/pdf', 'text/plain', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ] if (allowedTypes.includes(file.mimetype)) { cb(null, true) } else { cb(new Error('Dateityp nicht erlaubt'), false) } } }).single('file') dynamicUpload(req, res, async (err) => { try { if (err) { if (err.code === 'LIMIT_FILE_SIZE') { return res.status(413).json({ error: `Datei zu groß. Maximum: ${req.settings['max-file-size']}MB`, maxSize: req.settings['max-file-size'], errorCode: 'FILE_TOO_LARGE' }) } return res.status(400).json({ error: err.message }) } if (!req.file) { return res.status(400).json({ error: 'Keine Datei hochgeladen' }) } const category = req.file.mimetype.startsWith('image/') ? 'images' : 'documents' const fileUrl = `/static/uploads/${category}/${req.file.filename}` const fileInfo = { filename: req.file.filename, originalname: req.file.originalname, uploadedBy: req.user.username, uploadDate: new Date().toISOString(), category: category, size: req.file.size, mimetype: req.file.mimetype, url: fileUrl } await saveFileMetadata(req.file.filename, fileInfo) res.json({ success: true, message: 'Datei erfolgreich hochgeladen', file: fileInfo }) logger.info(`Datei hochgeladen: ${req.file.originalname} von ${req.user.username}`) } catch (error) { logger.error('Fehler beim Upload:', error) res.status(500).json({ error: 'Fehler beim Hochladen' }) } }) }) // ОБЪЕДИНЕННЫЙ РОУТ ДЛЯ ФАЙЛОВ router.get('/files', checkAccess, async (req, res) => { try { const files = await loadAllFiles() res.json({ files, total: files.length, totalSize: files.reduce((sum, file) => sum + file.size, 0) }) } catch (error) { logger.error('Fehler beim Laden der Dateien:', error) res.status(500).json({ error: 'Fehler beim Laden der Dateien' }) } }) // Admin files (то же самое, но с дополнительной статистикой) router.get('/admin/files', checkAccess, async (req, res) => { try { const files = await loadAllFiles() res.json({ files, stats: { total: files.length, totalSize: files.reduce((sum, file) => sum + file.size, 0), withMetadata: files.filter(f => f.hasMetadata).length, withoutMetadata: files.filter(f => !f.hasMetadata).length } }) } catch (error) { logger.error('Fehler beim Laden der Admin-Dateien:', error) res.status(500).json({ error: 'Fehler beim Laden der Dateien' }) } }) // Cleanup router.post('/admin/cleanup', checkAccess, async (req, res) => { try { let cleanedFiles = 0 async function cleanupDir(dirPath) { if (!fs.existsSync(dirPath)) return const files = fs.readdirSync(dirPath) for (const filename of files) { const hasMetadata = !!(await loadFileMetadata(filename)) if (!hasMetadata) { const filePath = path.join(dirPath, filename) const stats = fs.statSync(filePath) const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) if (stats.birthtime < thirtyDaysAgo) { fs.unlinkSync(filePath) cleanedFiles++ logger.info(`Cleanup: ${filename} gelöscht`) } } } } await cleanupDir(imagesPath) await cleanupDir(documentsPath) res.json({ success: true, message: `${cleanedFiles} verwaiste Dateien wurden aufgeräumt`, cleanedFiles }) } catch (error) { logger.error('Fehler beim Cleanup:', error) res.status(500).json({ error: 'Fehler beim Cleanup' }) } }) // Delete file router.delete('/file/:category/:filename', checkAccess, async (req, res) => { try { const { category, filename } = req.params if (!['images', 'documents'].includes(category)) { return res.status(400).json({ error: 'Ungültige Kategorie' }) } const filePath = path.join(uploadsPath, category, filename) if (!fs.existsSync(filePath)) { return res.status(404).json({ error: 'Datei nicht gefunden' }) } const fileInfo = await loadFileMetadata(filename) // Только админы или владельцы файла могут удалять if (req.user.role !== 0 && fileInfo?.uploadedBy && fileInfo.uploadedBy !== req.user.username) { return res.status(403).json({ error: 'Keine Berechtigung diese Datei zu löschen' }) } fs.unlinkSync(filePath) // Удаляем метаданные const metadataFile = path.join(metadataPath, `${filename}.json`) if (fs.existsSync(metadataFile)) { fs.unlinkSync(metadataFile) } res.json({ success: true, message: 'Datei erfolgreich gelöscht' }) logger.info(`Datei gelöscht: ${filename} von ${req.user.username}`) } catch (error) { logger.error('Fehler beim Löschen:', error) res.status(500).json({ error: 'Fehler beim Löschen der Datei' }) } }) // Serve files router.get('/file/:category/:filename', (req, res) => { try { const { category, filename } = req.params if (!['images', 'documents'].includes(category)) { return res.status(400).json({ error: 'Ungültige Kategorie' }) } const filePath = path.join(uploadsPath, category, filename) if (!fs.existsSync(filePath)) { return res.status(404).json({ error: 'Datei nicht gefunden' }) } const stats = fs.statSync(filePath) const mimeType = mime.lookup(filePath) || 'application/octet-stream' res.setHeader('Content-Type', mimeType) res.setHeader('Content-Length', stats.size) res.setHeader('Cache-Control', 'public, max-age=31536000') if (!req.query.inline) { res.setHeader('Content-Disposition', `attachment; filename="${filename}"`) } const fileStream = fs.createReadStream(filePath) fileStream.pipe(res) fileStream.on('error', (error) => { logger.error('Fehler beim Streamen:', error) if (!res.headersSent) { res.status(500).json({ error: 'Fehler beim Bereitstellen der Datei' }) } }) } catch (error) { logger.error('Fehler beim Bereitstellen der Datei:', error) res.status(500).json({ error: 'Fehler beim Bereitstellen der Datei' }) } }) logger.info('✅ Static Files Plugin erfolgreich registriert') } async function unregister() { console.log('Static Files Plugin wird deregistriert') } module.exports = { register, unregister }