UNPKG

@quasarbright/projection

Version:

A static site generator that creates a beautiful, interactive gallery to showcase your coding projects. Features search, filtering, tags, responsive design, and an admin UI.

289 lines 10.2 kB
"use strict"; /** * Image manager for handling thumbnail uploads and deletions */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ImageManager = void 0; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const util_1 = require("util"); const mkdir = (0, util_1.promisify)(fs_1.default.mkdir); const writeFile = (0, util_1.promisify)(fs_1.default.writeFile); const unlink = (0, util_1.promisify)(fs_1.default.unlink); const readdir = (0, util_1.promisify)(fs_1.default.readdir); const stat = (0, util_1.promisify)(fs_1.default.stat); const rename = (0, util_1.promisify)(fs_1.default.rename); /** * Supported image file extensions */ const SUPPORTED_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp']; /** * Supported MIME types for image uploads */ const SUPPORTED_MIME_TYPES = [ 'image/png', 'image/jpeg', 'image/gif', 'image/webp' ]; /** * Maximum file size in bytes (5 MB) */ const MAX_FILE_SIZE = 5 * 1024 * 1024; /** * Image manager class for handling thumbnail operations */ class ImageManager { constructor(projectRoot) { this.projectRoot = projectRoot; this.screenshotsDir = path_1.default.join(projectRoot, 'screenshots'); } /** * Ensure the screenshots directory exists */ async ensureScreenshotsDir() { try { await stat(this.screenshotsDir); } catch (error) { if (error.code === 'ENOENT') { // Directory doesn't exist, create it await mkdir(this.screenshotsDir, { recursive: true }); } else { throw error; } } } /** * Validate an uploaded file * @throws Error if validation fails */ validateFile(file) { // Check MIME type if (!SUPPORTED_MIME_TYPES.includes(file.mimetype)) { throw new Error(`Invalid file type: ${file.mimetype}. Supported types: PNG, JPG, JPEG, GIF, WebP`); } // Check file size if (file.size > MAX_FILE_SIZE) { const sizeMB = (file.size / (1024 * 1024)).toFixed(2); const maxSizeMB = (MAX_FILE_SIZE / (1024 * 1024)).toFixed(0); throw new Error(`File too large: ${sizeMB} MB. Maximum size: ${maxSizeMB} MB`); } } /** * Get the file extension from a MIME type */ getExtensionFromMimeType(mimetype) { const mimeToExt = { 'image/png': '.png', 'image/jpeg': '.jpg', 'image/gif': '.gif', 'image/webp': '.webp' }; return mimeToExt[mimetype] || '.jpg'; } /** * Find an existing thumbnail for a project * @returns The filename of the existing thumbnail, or null if not found */ async findExistingThumbnail(projectId) { try { const files = await readdir(this.screenshotsDir); // Look for files matching the project ID with any supported extension for (const ext of SUPPORTED_EXTENSIONS) { const filename = `${projectId}${ext}`; if (files.includes(filename)) { return filename; } } return null; } catch (error) { if (error.code === 'ENOENT') { // Directory doesn't exist yet return null; } throw error; } } /** * Save an image file for a project * @param projectId The project ID * @param file The uploaded file with buffer and mimetype * @returns The relative path to the saved image */ async saveImage(projectId, file) { // Validate the file this.validateFile(file); // Ensure screenshots directory exists await this.ensureScreenshotsDir(); // Delete existing thumbnail if present const existingThumbnail = await this.findExistingThumbnail(projectId); if (existingThumbnail) { const existingPath = path_1.default.join(this.screenshotsDir, existingThumbnail); try { await unlink(existingPath); } catch (error) { // Ignore if file doesn't exist if (error.code !== 'ENOENT') { throw error; } } } // Determine the file extension from MIME type const extension = this.getExtensionFromMimeType(file.mimetype); const filename = `${projectId}${extension}`; const filePath = path_1.default.join(this.screenshotsDir, filename); // Save the new file await writeFile(filePath, file.buffer); // Return the relative path return this.getRelativePath(projectId, extension); } /** * Delete an image file for a project * @param projectId The project ID */ async deleteImage(projectId) { const existingThumbnail = await this.findExistingThumbnail(projectId); if (!existingThumbnail) { // No thumbnail to delete return; } const filePath = path_1.default.join(this.screenshotsDir, existingThumbnail); try { await unlink(filePath); } catch (error) { // Ignore if file doesn't exist if (error.code !== 'ENOENT') { throw error; } } } /** * Save a temporary image file for a project being edited * @param projectId The project ID * @param file The uploaded file with buffer and mimetype * @returns The relative path to the saved temporary image with admin:// prefix */ async saveTempImage(projectId, file) { // Validate the file this.validateFile(file); // Ensure screenshots directory exists await this.ensureScreenshotsDir(); // Delete existing temp file if present await this.deleteTempImage(projectId); // Determine the file extension from MIME type const extension = this.getExtensionFromMimeType(file.mimetype); const filename = `${projectId}.temp${extension}`; const filePath = path_1.default.join(this.screenshotsDir, filename); // Save the temp file await writeFile(filePath, file.buffer); // Return the relative path with admin:// prefix return `admin://${filename}`; } /** * Commit a temporary image (rename from .temp to final name) * @param projectId The project ID */ async commitTempImage(projectId) { try { const files = await readdir(this.screenshotsDir); // Find temp file for this project for (const ext of SUPPORTED_EXTENSIONS) { const tempFilename = `${projectId}.temp${ext}`; if (files.includes(tempFilename)) { const tempPath = path_1.default.join(this.screenshotsDir, tempFilename); // Delete existing final file if present const existingThumbnail = await this.findExistingThumbnail(projectId); if (existingThumbnail) { const existingPath = path_1.default.join(this.screenshotsDir, existingThumbnail); try { await unlink(existingPath); } catch (error) { if (error.code !== 'ENOENT') { throw error; } } } // Rename temp to final const finalFilename = `${projectId}${ext}`; const finalPath = path_1.default.join(this.screenshotsDir, finalFilename); await rename(tempPath, finalPath); return this.getRelativePath(projectId, ext); } } return null; } catch (error) { if (error.code === 'ENOENT') { return null; } throw error; } } /** * Delete a temporary image file * @param projectId The project ID */ async deleteTempImage(projectId) { try { const files = await readdir(this.screenshotsDir); // Find and delete temp file for this project for (const ext of SUPPORTED_EXTENSIONS) { const tempFilename = `${projectId}.temp${ext}`; if (files.includes(tempFilename)) { const tempPath = path_1.default.join(this.screenshotsDir, tempFilename); try { await unlink(tempPath); } catch (error) { if (error.code !== 'ENOENT') { throw error; } } } } } catch (error) { if (error.code !== 'ENOENT') { throw error; } } } /** * Get the relative path for a thumbnail * @param projectId The project ID * @param extension The file extension (including the dot) * @returns The relative path with admin:// prefix (e.g., "admin://project-id.png") */ getRelativePath(projectId, extension) { return `admin://${projectId}${extension}`; } /** * Get the list of supported file extensions */ static getSupportedExtensions() { return [...SUPPORTED_EXTENSIONS]; } /** * Get the list of supported MIME types */ static getSupportedMimeTypes() { return [...SUPPORTED_MIME_TYPES]; } /** * Get the maximum file size in bytes */ static getMaxFileSize() { return MAX_FILE_SIZE; } } exports.ImageManager = ImageManager; //# sourceMappingURL=image-manager.js.map