"use strict"; const express = require("express"); const fs = require("fs"); const { sniffImageType } = require("../../core/images/sniff"); const { isHeic } = require("../../core/images/heic"); const { BadRequestError, NotFoundError } = require("../../errors"); const JOB_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; /** * @param {Object} deps * @param {import('../../core/history/history-service').HistoryService} deps.historyService * @param {import('multer').Multer} deps.upload * @param {import('../../logger').Logger} deps.logger */ function createHistoryRoutes({ historyService, upload, logger }) { const router = express.Router(); router.get("/history", (req, res, next) => { try { res.json({ ok: true, versions: historyService.getHistory() }); } catch (err) { next(err); } }); router.post("/history/import", upload.single("photo"), async (req, res, next) => { try { if (!req.file || !req.file.buffer) { throw new BadRequestError("Выберите фотографию для импорта."); } const isHeicFile = isHeic(req.file) || sniffImageType(req.file.buffer) === "heic"; const sniffedType = sniffImageType(req.file.buffer); if (!isHeicFile && !sniffedType) { throw new BadRequestError( "Файл не является поддерживаемым изображением (JPEG, PNG, WebP или HEIC)." ); } const version = await historyService.importPhoto({ buffer: req.file.buffer, mimetype: req.file.mimetype, originalname: req.file.originalname, }); logger.info("Импорт фотографии", { versionId: version.id, ext: version.ext, width: version.width, height: version.height, }); res.json({ ok: true, version }); } catch (err) { next(err); } }); router.delete("/history/:id", async (req, res, next) => { try { const { id } = req.params; if (!JOB_ID_PATTERN.test(id)) { throw new BadRequestError("Некорректный идентификатор версии."); } const existing = historyService.getVersion(id); if (!existing) { throw new NotFoundError("Версия не найдена в истории."); } const result = await historyService.removeCascade(id); logger.audit("Удаление версии истории", { versionId: id, removedCount: result.removed.length, }); res.json({ ok: true, versions: result.remaining }); } catch (err) { next(err); } }); // Alias /image.jpg for backward compatibility -> 302 redirect to actual ext router.get("/history/:id/image.jpg", (req, res, next) => { try { const { id } = req.params; if (!JOB_ID_PATTERN.test(id)) { throw new BadRequestError("Некорректный идентификатор версии."); } const version = historyService.getVersion(id); if (!version) { throw new NotFoundError("Версия не найдена в истории."); } const targetExt = version.ext || "jpg"; if (targetExt !== "jpg") { return res.redirect(302, `/api/history/${id}/image.${targetExt}`); } // If ext is jpg, serve it directly const fileInfo = historyService.getVersionFile(id); fileInfo .then(({ filePath, contentType }) => { res.setHeader("content-type", contentType); res.setHeader("cache-control", "no-store"); res.setHeader( "content-disposition", `inline; filename*=UTF-8''${encodeURIComponent(`version-${id.slice(0, 8)}.jpg`)}` ); fs.createReadStream(filePath).pipe(res); }) .catch(next); } catch (err) { next(err); } }); // Direct route for /history/:id/image.:ext (e.g. image.png, image.webp, image.jpg) router.get("/history/:id/image.:ext", async (req, res, next) => { try { const { id } = req.params; if (!JOB_ID_PATTERN.test(id)) { throw new BadRequestError("Некорректный идентификатор версии."); } const { version, filePath, contentType } = await historyService.getVersionFile(id); res.setHeader("content-type", contentType); res.setHeader("cache-control", "no-store"); res.setHeader( "content-disposition", `inline; filename*=UTF-8''${encodeURIComponent(`version-${id.slice(0, 8)}.${version.ext || "jpg"}`)}` ); fs.createReadStream(filePath).pipe(res); } catch (err) { next(err); } }); return router; } module.exports = { createHistoryRoutes, JOB_ID_PATTERN, };