import { config } from "./config.ts"; import { logger } from "./logger.ts"; export async function ensureUploadDir() { try { await Deno.mkdir(config.uploadDir, { recursive: true }); } catch (error) { if (!(error instanceof Deno.errors.AlreadyExists)) { throw error; } } } export async function getUniqueFilename(filename: string): Promise { const filepath = `${config.uploadDir}/${filename}`; try { await Deno.stat(filepath); // File exists, append epoch seconds const epoch = Math.floor(Date.now() / 1000); const parts = filename.split("."); if (parts.length > 1) { const ext = parts.pop(); return `${parts.join(".")}_${epoch}.${ext}`; } return `${filename}_${epoch}`; } catch (error) { if (error instanceof Deno.errors.NotFound) { return filename; } throw error; } } export async function listUploadedFiles() { const files: Array<{ name: string; size: number; uploaded: Date }> = []; try { for await (const entry of Deno.readDir(config.uploadDir)) { if (entry.isFile) { const stat = await Deno.stat(`${config.uploadDir}/${entry.name}`); files.push({ name: entry.name, size: stat.size, uploaded: stat.mtime || new Date(), }); } } } catch (error) { if (!(error instanceof Deno.errors.NotFound)) { logger.server("ERROR", "Error listing files", { error: String(error) }); } } return files.sort((a, b) => b.uploaded.getTime() - a.uploaded.getTime()); } export async function cleanupExpiredFiles() { if (config.fileExpiration === 0) return; const now = Date.now(); let cleaned = 0; try { for await (const entry of Deno.readDir(config.uploadDir)) { if (entry.isFile) { const filepath = `${config.uploadDir}/${entry.name}`; const stat = await Deno.stat(filepath); const age = now - (stat.mtime?.getTime() || 0); if (age > config.fileExpiration) { await Deno.remove(filepath); cleaned++; logger.cleanup("INFO", "Expired file removed", { filename: entry.name, ageMs: age }); } } } } catch (error) { logger.cleanup("ERROR", "Error during file cleanup", { error: String(error) }); } if (cleaned > 0) { logger.cleanup("INFO", "File cleanup completed", { filesRemoved: cleaned }); } } // Run cleanup every hour setInterval(() => cleanupExpiredFiles(), 60 * 60 * 1000); export function isAllowedFileType(contentType: string): boolean { if (config.allowedFileTypes.includes("*/*")) { return true; } return config.allowedFileTypes.includes(contentType); } export function formatFileSize(bytes: number): string { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; } export function getMimeType(filename: string): string { const ext = filename.split(".").pop()?.toLowerCase(); const mimeTypes: Record = { // Images "jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "gif": "image/gif", "webp": "image/webp", "svg": "image/svg+xml", "bmp": "image/bmp", "ico": "image/x-icon", // Videos "mp4": "video/mp4", "webm": "video/webm", "ogv": "video/ogg", "avi": "video/x-msvideo", "mov": "video/quicktime", "mkv": "video/x-matroska", "m4v": "video/x-m4v", "mpeg": "video/mpeg", "mpg": "video/mpeg", // Audio "mp3": "audio/mpeg", "wav": "audio/wav", "ogg": "audio/ogg", "m4a": "audio/mp4", "flac": "audio/flac", // Documents "pdf": "application/pdf", "txt": "text/plain", "html": "text/html", "css": "text/css", "js": "application/javascript", "json": "application/json", // Archives "zip": "application/zip", "tar": "application/x-tar", "gz": "application/gzip", }; return mimeTypes[ext || ""] || "application/octet-stream"; } export function isViewableInBrowser(mimeType: string): boolean { return mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType === "application/pdf" || mimeType === "text/plain"; }