@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
JavaScript
;
/**
* 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