import { Router, Request, Response } from "express"; import { StorageFactory } from "../services/StorageFactory"; import { asyncHandler } from "../middleware/errorHandler"; export const imagesRouter = Router(); /** * GET /api/images/:orgId/:projectId/:category/:filename * Serves images via presigned URLs (redirect approach) */ imagesRouter.get( "/images/:orgId/:projectId/:category/:filename", asyncHandler(async (req: Request, res: Response) => { const { orgId, projectId, category, filename } = req.params; // Validate category if (!["uploads", "generated", "references"].includes(category)) { return res.status(400).json({ success: false, message: "Invalid category", }); } const storageService = await StorageFactory.getInstance(); try { // Check if file exists first (fast check) const exists = await storageService.fileExists( orgId, projectId, category as "uploads" | "generated" | "references", filename, ); if (!exists) { return res.status(404).json({ success: false, message: "File not found", }); } // Determine content type from filename const ext = filename.toLowerCase().split(".").pop(); const contentType = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml", }[ext || ""] || "application/octet-stream"; // Set headers for optimal caching and performance res.setHeader("Content-Type", contentType); res.setHeader("Cache-Control", "public, max-age=86400, immutable"); // 24 hours + immutable res.setHeader("ETag", `"${orgId}-${projectId}-${filename}"`); // Simple ETag // Handle conditional requests (304 Not Modified) const ifNoneMatch = req.headers["if-none-match"]; if (ifNoneMatch === `"${orgId}-${projectId}-${filename}"`) { return res.status(304).end(); // Not Modified } // Stream the file directly through our API (memory efficient) const fileStream = await storageService.streamFile( orgId, projectId, category as "uploads" | "generated" | "references", filename, ); // Handle stream errors fileStream.on("error", (streamError) => { console.error("Stream error:", streamError); if (!res.headersSent) { res.status(500).json({ success: false, message: "Error streaming file", }); } }); // Stream the file without loading into memory fileStream.pipe(res); } catch (error) { console.error("Failed to stream file:", error); return res.status(404).json({ success: false, message: "File not found", }); } }), ); /** * GET /api/images/url/:orgId/:projectId/:category/:filename * Returns a presigned URL instead of redirecting */ imagesRouter.get( "/images/url/:orgId/:projectId/:category/:filename", asyncHandler(async (req: Request, res: Response) => { const { orgId, projectId, category, filename } = req.params; const { expiry = "3600" } = req.query; // Default 1 hour if (!["uploads", "generated", "references"].includes(category)) { return res.status(400).json({ success: false, message: "Invalid category", }); } const storageService = await StorageFactory.getInstance(); try { const presignedUrl = await storageService.getPresignedDownloadUrl( orgId, projectId, category as "uploads" | "generated" | "references", filename, parseInt(expiry as string, 10), ); return res.json({ success: true, url: presignedUrl, expiresIn: parseInt(expiry as string, 10), }); } catch (error) { console.error("Failed to generate presigned URL:", error); return res.status(404).json({ success: false, message: "File not found or access denied", }); } }), );