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
JavaScript
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
}