diff options
Diffstat (limited to 'fileUtils.ts')
| -rw-r--r-- | fileUtils.ts | 161 |
1 files changed, 161 insertions, 0 deletions
diff --git a/fileUtils.ts b/fileUtils.ts new file mode 100644 index 0000000..6360a95 --- /dev/null +++ b/fileUtils.ts @@ -0,0 +1,161 @@ +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<string> { + 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.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.info("Expired file removed", { filename: entry.name, age }); + } + } + } + } catch (error) { + logger.error("Error during file cleanup", { error: String(error) }); + } + + if (cleaned > 0) { + logger.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<string, string> = { + // 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", + "ogg": "video/ogg", + "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"; +} |
