diff options
| author | grothedev <grothedev@gmail.com> | 2025-12-28 21:41:07 -0500 |
|---|---|---|
| committer | grothedev <grothedev@gmail.com> | 2025-12-28 21:41:07 -0500 |
| commit | bd6c3a07a82ba11cf7b0423307229891675e7ed3 (patch) | |
| tree | fce602bc4038f0f79e12f9fb296e3d220915da23 /main.ts | |
| parent | f978ad7db04ced4cbcf04a82bf6f0cc3f4ce66a3 (diff) | |
Diffstat (limited to 'main.ts')
| -rw-r--r-- | main.ts | 72 |
1 files changed, 45 insertions, 27 deletions
@@ -37,26 +37,26 @@ router.get("/css/:filename", async (ctx) => { // Routes router.get("/", async (ctx) => { - logger.info("Page visit", { page: "/", ip: getClientIP(ctx) }); + logger.request("INFO", "Page visit", { page: "/", ip: getClientIP(ctx) }); ctx.response.headers.set("Content-Type", "text/html"); ctx.response.body = await renderUploadPage(false); }); router.get("/chunked", async (ctx) => { - logger.info("Page visit", { page: "/chunked", ip: getClientIP(ctx) }); + logger.request("INFO", "Page visit", { page: "/chunked", ip: getClientIP(ctx) }); ctx.response.headers.set("Content-Type", "text/html"); ctx.response.body = await renderUploadPage(true); }); router.get("/browse", async (ctx) => { - logger.info("Page visit", { page: "/browse", ip: getClientIP(ctx) }); + logger.request("INFO", "Page visit", { page: "/browse", ip: getClientIP(ctx) }); const files = await listUploadedFiles(); ctx.response.headers.set("Content-Type", "text/html"); ctx.response.body = await renderBrowsePage(files); }); router.get("/gallery", async (ctx) => { - logger.info("Page visit", { page: "/gallery", ip: getClientIP(ctx) }); + logger.request("INFO", "Page visit", { page: "/gallery", ip: getClientIP(ctx) }); const files = await listUploadedFiles(); ctx.response.headers.set("Content-Type", "text/html"); ctx.response.body = await renderGalleryPage(files); @@ -66,7 +66,7 @@ router.post("/upload", async (ctx) => { const ip = getClientIP(ctx); if (rateLimiter.isRateLimited(ip)) { - logger.warn("Upload rejected: rate limit", { ip }); + logger.security("WARN", "Upload rejected: rate limit", { ip }); ctx.response.status = Status.TooManyRequests; ctx.response.body = { error: "Rate limit exceeded. Please try again later." }; return; @@ -93,6 +93,7 @@ router.post("/upload", async (ctx) => { // Check file size if (file.size > config.maxFileSize) { const maxMB = Math.floor(config.maxFileSize / (1024 * 1024)); + logger.upload("WARN", "File size exceeded", { filename: file.name, size: file.size, maxSize: config.maxFileSize, ip }); ctx.response.status = Status.BadRequest; ctx.response.body = { error: `File ${file.name} exceeds maximum size of ${maxMB}MB` }; return; @@ -100,6 +101,7 @@ router.post("/upload", async (ctx) => { // Check file type if (!isAllowedFileType(file.type)) { + logger.upload("WARN", "File type not allowed", { filename: file.name, type: file.type, ip }); ctx.response.status = Status.BadRequest; ctx.response.body = { error: `File type ${file.type} is not allowed` }; return; @@ -114,13 +116,13 @@ router.post("/upload", async (ctx) => { await Deno.writeFile(filepath, new Uint8Array(arrayBuffer)); uploadedFiles.push(filename); - logger.info("File uploaded", { filename, size: file.size, ip }); + logger.upload("INFO", "File uploaded successfully", { filename, size: file.size, ip }); } ctx.response.status = Status.OK; ctx.response.body = { files: uploadedFiles }; } catch (error) { - logger.error("Upload error", { error: String(error), ip }); + logger.upload("ERROR", "Upload failed", { error: String(error), ip }); ctx.response.status = Status.InternalServerError; ctx.response.body = { error: "Upload failed" }; } @@ -129,13 +131,6 @@ router.post("/upload", async (ctx) => { router.post("/upload-chunk", async (ctx) => { const ip = getClientIP(ctx); - if (rateLimiter.isRateLimited(ip)) { - logger.warn("Upload rejected: rate limit", { ip }); - ctx.response.status = Status.TooManyRequests; - ctx.response.body = { error: "Rate limit exceeded. Please try again later." }; - return; - } - try { const body = ctx.request.body; const formData = await body.formData(); @@ -153,6 +148,16 @@ router.post("/upload-chunk", async (ctx) => { const storageKey = `${ip}:${filename}`; + // Only apply rate limiting to the first chunk of each file + if (chunkIndex === 0) { + if (rateLimiter.isRateLimited(ip)) { + logger.security("WARN", "Chunked upload rejected: rate limit", { ip, filename }); + ctx.response.status = Status.TooManyRequests; + ctx.response.body = { error: "Rate limit exceeded. Please try again later." }; + return; + } + } + // Initialize storage for this file if (!chunkStorage.has(storageKey)) { chunkStorage.set(storageKey, { chunks: [], totalChunks }); @@ -173,6 +178,7 @@ router.post("/upload-chunk", async (ctx) => { if (totalSize > config.maxFileSize) { chunkStorage.delete(storageKey); const maxMB = Math.floor(config.maxFileSize / (1024 * 1024)); + logger.upload("WARN", "Chunked file size exceeded", { filename, size: totalSize, maxSize: config.maxFileSize, ip }); ctx.response.status = Status.BadRequest; ctx.response.body = { error: `File exceeds maximum size of ${maxMB}MB` }; return; @@ -193,35 +199,38 @@ router.post("/upload-chunk", async (ctx) => { // Clean up chunkStorage.delete(storageKey); - logger.info("Chunked file uploaded", { filename: uniqueFilename, size: totalSize, chunks: totalChunks, ip }); + logger.upload("INFO", "Chunked file uploaded successfully", { filename: uniqueFilename, size: totalSize, chunks: totalChunks, ip }); } ctx.response.status = Status.OK; ctx.response.body = { success: true, chunkIndex, totalChunks }; } catch (error) { - logger.error("Chunk upload error", { error: String(error), ip }); + logger.upload("ERROR", "Chunk upload failed", { error: String(error), ip }); ctx.response.status = Status.InternalServerError; ctx.response.body = { error: "Chunk upload failed" }; } }); -router.get("/download/:filename", async (ctx) => { - const filename = ctx.params.filename; +router.get("/download/:filename+", async (ctx) => { + const encodedFilename = ctx.params.filename; const ip = getClientIP(ctx); - if (!filename) { + if (!encodedFilename) { ctx.response.status = Status.BadRequest; ctx.response.headers.set("Content-Type", "text/html"); ctx.response.body = await renderError("Filename is required", 400); return; } + // Decode the filename to handle special characters and spaces + const filename = decodeURIComponent(encodedFilename); const filepath = `${config.uploadDir}/${filename}`; try { const fileInfo = await Deno.stat(filepath); if (!fileInfo.isFile) { + logger.download("WARN", "File not found (not a file)", { filename, ip }); ctx.response.status = Status.NotFound; ctx.response.headers.set("Content-Type", "text/html"); ctx.response.body = await renderError("File not found", 404); @@ -232,25 +241,30 @@ router.get("/download/:filename", async (ctx) => { const mimeType = getMimeType(filename); const viewableInBrowser = isViewableInBrowser(mimeType); - logger.info(viewableInBrowser ? "File viewed" : "File downloaded", { filename, mimeType, ip }); + logger.download("INFO", viewableInBrowser ? "File viewed" : "File downloaded", { filename, mimeType, size: file.length, ip }); ctx.response.headers.set("Content-Type", mimeType); + // Properly encode filename for Content-Disposition header + // Use RFC 5987 encoding for non-ASCII characters + const asciiFilename = filename.replace(/[^\x00-\x7F]/g, "_"); // Fallback for old browsers + const encodedFilename = encodeURIComponent(filename); + if (viewableInBrowser) { - ctx.response.headers.set("Content-Disposition", `inline; filename="${filename}"`); + ctx.response.headers.set("Content-Disposition", `inline; filename="${asciiFilename}"; filename*=UTF-8''${encodedFilename}`); } else { - ctx.response.headers.set("Content-Disposition", `attachment; filename="${filename}"`); + ctx.response.headers.set("Content-Disposition", `attachment; filename="${asciiFilename}"; filename*=UTF-8''${encodedFilename}`); } ctx.response.body = file; } catch (error) { if (error instanceof Deno.errors.NotFound) { - logger.warn("File not found", { filename, ip }); + logger.download("WARN", "File not found", { filename, ip }); ctx.response.status = Status.NotFound; ctx.response.headers.set("Content-Type", "text/html"); ctx.response.body = await renderError("File not found", 404); } else { - logger.error("Download error", { filename, error: String(error), ip }); + logger.download("ERROR", "Download failed", { filename, error: String(error), ip }); ctx.response.status = Status.InternalServerError; ctx.response.headers.set("Content-Type", "text/html"); ctx.response.body = await renderError("Failed to download file", 500); @@ -263,7 +277,11 @@ app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; - logger.debug(`${ctx.request.method} ${ctx.request.url.pathname}`, { duration: `${ms}ms`, status: ctx.response.status }); + logger.request("DEBUG", `${ctx.request.method} ${ctx.request.url.pathname}`, { + duration: ms, + status: ctx.response.status, + ip: getClientIP(ctx) + }); }); app.use(router.routes()); @@ -273,10 +291,10 @@ app.use(router.allowedMethods()); await ensureUploadDir(); cleanupExpiredFiles(); -logger.info("Server starting", { port: config.port }); +logger.server("INFO", "Server starting", { port: config.port }); app.addEventListener("listen", ({ hostname, port, secure }) => { - logger.info("Server started", { + logger.server("INFO", "Server started", { url: `${secure ? "https" : "http"}://${hostname ?? "localhost"}:${port}`, }); }); |
