summaryrefslogtreecommitdiff
path: root/main.ts
diff options
context:
space:
mode:
Diffstat (limited to 'main.ts')
-rw-r--r--main.ts72
1 files changed, 45 insertions, 27 deletions
diff --git a/main.ts b/main.ts
index af02c52..607669f 100644
--- a/main.ts
+++ b/main.ts
@@ -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}`,
});
});