diff --git a/backend/src/controllers/fileController.js b/backend/src/controllers/fileController.js index 4b9c202..cb644f0 100644 --- a/backend/src/controllers/fileController.js +++ b/backend/src/controllers/fileController.js @@ -170,31 +170,19 @@ function uploadFile(req, res) { /* ── POST /api/files/personal ──────────────────────────────────────────── * Personal image upload for «Мои материалы» (board crops, drawings, textbook * clips). Allowed for ANY authenticated user (incl. students) — unlike the - * teacher library upload above. Image-only; stored is_public=1 so the - * material's download URL resolves (and survives teacher «раздача»). */ + * teacher library upload above. Image-only; saved into uploads/materials and + * served statically (public), so the returned URL renders in , opens in + * a new tab and downloads without an auth header. Returns { url }. */ function uploadPersonalFile(req, res) { if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); - const filePath = path.resolve(UPLOADS_DIR, req.file.filename); + const filePath = path.resolve(UPLOADS_DIR, 'materials', req.file.filename); if (!checkMagicBytes(filePath, req.file.mimetype)) { try { fs.unlinkSync(filePath); } catch {} return res.status(400).json({ error: 'Содержимое файла не соответствует его расширению' }); } - const title = (req.body && req.body.title && req.body.title.trim()) || req.file.originalname || 'Изображение'; - let r; - try { - r = db.prepare(` - INSERT INTO files (title, original_name, stored_name, mimetype, size, is_public, uploaded_by) - VALUES (?, ?, ?, ?, ?, 1, ?) - `).run(title, req.file.originalname, req.file.filename, req.file.mimetype, req.file.size, req.user.id); - } catch (err) { - const fp = path.resolve(UPLOADS_DIR, req.file.filename); - if (fp.startsWith(UPLOADS_DIR + path.sep)) { try { fs.unlinkSync(fp); } catch {} } - throw err; - } - - res.status(201).json({ id: r.lastInsertRowid }); + res.status(201).json({ url: '/uploads/materials/' + req.file.filename }); } /* ── GET /api/files/:id/download ─────────────────────────────────────── */ diff --git a/backend/src/routes/files.js b/backend/src/routes/files.js index cf41c18..b0f4410 100644 --- a/backend/src/routes/files.js +++ b/backend/src/routes/files.js @@ -47,11 +47,23 @@ const upload = multer({ }, }); -/* Personal image upload (Мои материалы): image-only, no library permission. */ +/* Personal image upload (Мои материалы): image-only, no library permission. + * Stored in uploads/materials and served statically (public) so the saved + * /open/download URL works without an auth header — these are personal + * study clips (board crops, drawings, textbook regions), not gated files. */ +const MATERIALS_DIR = path.join(UPLOADS_DIR, 'materials'); +try { require('fs').mkdirSync(MATERIALS_DIR, { recursive: true }); } catch (e) { /* exists */ } const IMG_MIME = ['image/png','image/jpeg','image/gif','image/webp']; const IMG_EXT = new Set(['.png','.jpg','.jpeg','.gif','.webp']); +const materialStorage = multer.diskStorage({ + destination: MATERIALS_DIR, + filename: (_req, file, cb) => { + const ext = path.extname(file.originalname || '') || '.png'; + cb(null, require('crypto').randomBytes(16).toString('hex') + ext); + }, +}); const imageUpload = multer({ - storage, + storage: materialStorage, limits: { fileSize: 20 * 1024 * 1024 }, // 20 MB fileFilter: (_req, file, cb) => { const ext = path.extname(file.originalname || '').toLowerCase(); @@ -65,7 +77,7 @@ router.use(authMiddleware); router.get('/', ctrl.listFiles); router.post('/', requireRole('teacher','admin'), requirePermission('library.upload'), upload.single('file'), fixUtf8Name, ctrl.uploadFile); // Personal materials upload — any authenticated user (covered by router-level authMiddleware) -router.post('/personal', imageUpload.single('file'), fixUtf8Name, ctrl.uploadPersonalFile); +router.post('/personal', imageUpload.single('file'), ctrl.uploadPersonalFile); /* ── folder routes (must be before /:id to avoid conflicts) ── */ router.get('/folders', ctrl.listFolders); diff --git a/backend/src/server.js b/backend/src/server.js index 22942e0..5da9978 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -335,6 +335,7 @@ app.use('/css', express.static(path.join(frontendDir, 'css'), staticCache)); app.use('/img', express.static(path.join(frontendDir, 'img'), staticCache)); app.use('/avatars', express.static(path.join(__dirname, '../uploads/avatars'), { maxAge: '1d' })); app.use('/uploads/flashcards', express.static(path.join(__dirname, '../uploads/flashcards'), { maxAge: '7d' })); +app.use('/uploads/materials', express.static(path.join(__dirname, '../uploads/materials'), { maxAge: '7d' })); // Redirect legacy .html URLs → clean URLs (301) app.use((req, res, next) => { diff --git a/frontend/js/board-clip.js b/frontend/js/board-clip.js index cf1e108..f767789 100644 --- a/frontend/js/board-clip.js +++ b/frontend/js/board-clip.js @@ -21,7 +21,7 @@ const fd = new FormData(); fd.append('file', blob, name); const up = await LS.uploadMaterialFile(fd); - return LS.downloadFileUrl(up.id); + return up.url; } async function persist(meta, kind, url) { diff --git a/frontend/js/material-save.js b/frontend/js/material-save.js index 3725b9a..4cebf54 100644 --- a/frontend/js/material-save.js +++ b/frontend/js/material-save.js @@ -43,7 +43,7 @@ const fd = new FormData(); fd.append('file', o.blob, o.name || 'image.png'); const up = await LS.uploadMaterialFile(fd); - url = LS.downloadFileUrl(up.id); + url = up.url; } if (!url) throw new Error('Нет изображения'); await LS.saveMaterial({ kind: 'image', title: o.title || '', url: url, sourceTitle: o.sourceTitle || null }); diff --git a/frontend/js/textbook-clip.js b/frontend/js/textbook-clip.js index 8c6ef8e..38d4182 100644 --- a/frontend/js/textbook-clip.js +++ b/frontend/js/textbook-clip.js @@ -184,7 +184,7 @@ await LS.saveMaterial({ kind: 'image', title: input.value.trim() || sectionTitle(), - url: LS.downloadFileUrl(up.id), + url: up.url, sourceTitle: chapterTitle() }); toast('Сохранено в «Мои материалы»', 'success'); diff --git a/frontend/my-materials.html b/frontend/my-materials.html index b0cd0b3..b3467da 100644 --- a/frontend/my-materials.html +++ b/frontend/my-materials.html @@ -380,7 +380,7 @@ if (!blob) throw new Error('Не удалось сохранить рисунок'); const fd = new FormData(); fd.append('file', blob, 'drawing.png'); const up = await LS.uploadMaterialFile(fd); - await LS.saveMaterial({ kind: 'image', title: o.title || 'Рисунок', url: LS.downloadFileUrl(up.id), sourceTitle: o.sourceTitle || null }); + await LS.saveMaterial({ kind: 'image', title: o.title || 'Рисунок', url: up.url, sourceTitle: o.sourceTitle || null }); close(); load(); LS.toast('Сохранено в «Мои материалы»', 'success'); } catch (e) { LS.toast(e.message || 'Ошибка', 'error'); btn.disabled = false; } });