Spaces:
Sleeping
Sleeping
| const express = require('express'); | |
| const router = express.Router(); | |
| const multer = require('multer'); | |
| const WeeklyPracticeFile = require('../models/WeeklyPracticeFile'); | |
| const { authenticateToken } = require('./auth'); | |
| // Use memory storage and persist to MongoDB Grid-like buffer | |
| const upload = multer({ | |
| storage: multer.memoryStorage(), | |
| limits: { fileSize: 25 * 1024 * 1024 } // 25MB | |
| }); | |
| // Helper to read role from headers set by frontend interceptor | |
| const getRole = (req) => req.headers['user-role'] || 'visitor'; | |
| const getUserInfo = (req) => { | |
| try { | |
| return req.headers['user-info'] ? JSON.parse(req.headers['user-info']) : {}; | |
| } catch { return {}; } | |
| }; | |
| // Consistently decode filenames from latin1 -> utf8 (multer provides latin1 strings) | |
| const decodeMaybeLatin1 = (name) => { | |
| try { | |
| if (!name) return name; | |
| return Buffer.from(name, 'latin1').toString('utf8'); | |
| } catch (_) { | |
| return name; | |
| } | |
| }; | |
| const encodeRFC5987 = (str) => { | |
| return encodeURIComponent(str) | |
| .replace(/['()]/g, escape) | |
| .replace(/\*/g, '%2A'); | |
| }; | |
| // List files for a given week | |
| router.get('/week/:weekNumber', authenticateToken, async (req, res) => { | |
| try { | |
| const weekNumber = parseInt(req.params.weekNumber); | |
| const type = req.query.type; // optional: 'source' | 'translation' | |
| const criteria = { weekNumber }; | |
| if (type === 'source' || type === 'translation') criteria.type = type; | |
| const files = await WeeklyPracticeFile.find(criteria).select('-data').sort({ createdAt: 1 }); | |
| res.json({ success: true, files }); | |
| } catch (error) { | |
| console.error('List weekly practice files error:', error); | |
| res.status(500).json({ error: 'Failed to list files' }); | |
| } | |
| }); | |
| // Download file by id | |
| router.get('/:id/download', authenticateToken, async (req, res) => { | |
| try { | |
| const file = await WeeklyPracticeFile.findById(req.params.id); | |
| if (!file) return res.status(404).json({ error: 'File not found' }); | |
| res.setHeader('Content-Type', file.mimeType); | |
| const safeName = decodeMaybeLatin1(file.fileName || 'download'); | |
| const asciiFallback = safeName.replace(/[^\x20-\x7E]/g, '_'); | |
| const utf8Star = encodeRFC5987(safeName); | |
| res.setHeader('Content-Disposition', `attachment; filename="${asciiFallback}"; filename*=UTF-8''${utf8Star}`); | |
| return res.send(file.data); | |
| } catch (error) { | |
| console.error('Download weekly practice file error:', error); | |
| res.status(500).json({ error: 'Failed to download file' }); | |
| } | |
| }); | |
| // Admin uploads source files for a week | |
| router.post('/week/:weekNumber/source', authenticateToken, upload.single('file'), async (req, res) => { | |
| try { | |
| if (getRole(req) !== 'admin') return res.status(403).json({ error: 'Admin access required' }); | |
| const weekNumber = parseInt(req.params.weekNumber); | |
| if (!req.file) return res.status(400).json({ error: 'Missing file' }); | |
| const { description } = req.body || {}; | |
| const saved = await WeeklyPracticeFile.create({ | |
| type: 'source', | |
| weekNumber, | |
| fileName: decodeMaybeLatin1(req.file.originalname), | |
| mimeType: req.file.mimetype, | |
| size: req.file.size, | |
| data: req.file.buffer, | |
| description | |
| }); | |
| res.status(201).json({ success: true, file: { ...saved.toObject(), data: undefined } }); | |
| } catch (error) { | |
| console.error('Upload weekly source file error:', error); | |
| res.status(500).json({ error: 'Failed to upload source file' }); | |
| } | |
| }); | |
| // Students upload translations, optionally referencing a specific sourceFileId | |
| router.post('/week/:weekNumber/translation', authenticateToken, upload.single('file'), async (req, res) => { | |
| try { | |
| const weekNumber = parseInt(req.params.weekNumber); | |
| if (!req.file) return res.status(400).json({ error: 'Missing file' }); | |
| const { description, sourceFileId } = req.body || {}; | |
| const user = getUserInfo(req); | |
| const displayUsername = user?.username || user?.name || user?.displayName || (user?.email ? String(user.email).split('@')[0] : undefined) || 'Unknown User'; | |
| const saved = await WeeklyPracticeFile.create({ | |
| type: 'translation', | |
| weekNumber, | |
| sourceFileId: sourceFileId || undefined, | |
| userId: user._id || undefined, | |
| username: displayUsername, | |
| fileName: decodeMaybeLatin1(req.file.originalname), | |
| mimeType: req.file.mimetype, | |
| size: req.file.size, | |
| data: req.file.buffer, | |
| description | |
| }); | |
| res.status(201).json({ success: true, file: { ...saved.toObject(), data: undefined } }); | |
| } catch (error) { | |
| console.error('Upload weekly translation file error:', error); | |
| res.status(500).json({ error: 'Failed to upload translation file' }); | |
| } | |
| }); | |
| // Admin delete a file by id | |
| router.delete('/:id', authenticateToken, async (req, res) => { | |
| try { | |
| if (getRole(req) !== 'admin') return res.status(403).json({ error: 'Admin access required' }); | |
| const deleted = await WeeklyPracticeFile.findByIdAndDelete(req.params.id); | |
| if (!deleted) return res.status(404).json({ error: 'File not found' }); | |
| res.json({ success: true }); | |
| } catch (error) { | |
| console.error('Delete weekly practice file error:', error); | |
| res.status(500).json({ error: 'Failed to delete file' }); | |
| } | |
| }); | |
| module.exports = router; | |