|
| 1 | +import type { FileProxy } from "../db/schema" |
| 2 | +import { readFile } from "node:fs/promises" |
| 3 | +import { join } from "node:path" |
| 4 | +import { normalizePath } from "./normalize-path" |
| 5 | + |
| 6 | +export async function resolveFileProxy( |
| 7 | + proxy: FileProxy, |
| 8 | + file_path: string, |
| 9 | +): Promise<Response> { |
| 10 | + const normalizedPath = normalizePath(file_path) |
| 11 | + const pattern = proxy.matching_pattern |
| 12 | + |
| 13 | + // Extract the relative path after the pattern prefix |
| 14 | + // Pattern: "prefix/*" -> prefix is "prefix/" |
| 15 | + const prefix = pattern.slice(0, -1) // Remove "*" to get "prefix/" |
| 16 | + const relativePath = normalizedPath.slice(prefix.length) |
| 17 | + |
| 18 | + if (proxy.proxy_type === "disk") { |
| 19 | + return resolveDiskProxy(proxy.disk_path, relativePath) |
| 20 | + } else { |
| 21 | + return resolveHttpProxy(proxy.http_target_url, relativePath) |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +async function resolveDiskProxy( |
| 26 | + diskPath: string, |
| 27 | + relativePath: string, |
| 28 | +): Promise<Response> { |
| 29 | + const fullPath = join(diskPath, relativePath) |
| 30 | + |
| 31 | + try { |
| 32 | + const content = await readFile(fullPath) |
| 33 | + const fileName = relativePath.split("/").pop() || "file" |
| 34 | + const contentType = getContentType(fileName) |
| 35 | + |
| 36 | + return new Response(content, { |
| 37 | + headers: { |
| 38 | + "Content-Type": contentType, |
| 39 | + "Content-Disposition": `attachment; filename="${fileName}"`, |
| 40 | + "Content-Length": content.byteLength.toString(), |
| 41 | + }, |
| 42 | + }) |
| 43 | + } catch (error: any) { |
| 44 | + if (error.code === "ENOENT") { |
| 45 | + return new Response("File not found", { status: 404 }) |
| 46 | + } |
| 47 | + console.error("Disk proxy error:", error) |
| 48 | + return new Response("Failed to read file from disk", { status: 500 }) |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +async function resolveHttpProxy( |
| 53 | + httpTargetUrl: string, |
| 54 | + relativePath: string, |
| 55 | +): Promise<Response> { |
| 56 | + // Ensure the URL doesn't have double slashes |
| 57 | + const baseUrl = httpTargetUrl.endsWith("/") |
| 58 | + ? httpTargetUrl.slice(0, -1) |
| 59 | + : httpTargetUrl |
| 60 | + const targetUrl = `${baseUrl}/${relativePath}` |
| 61 | + |
| 62 | + try { |
| 63 | + const response = await fetch(targetUrl) |
| 64 | + |
| 65 | + if (!response.ok) { |
| 66 | + return new Response(`HTTP proxy returned status ${response.status}`, { |
| 67 | + status: response.status, |
| 68 | + }) |
| 69 | + } |
| 70 | + |
| 71 | + // Pass through the response body and relevant headers |
| 72 | + const headers = new Headers() |
| 73 | + const contentType = response.headers.get("Content-Type") |
| 74 | + if (contentType) { |
| 75 | + headers.set("Content-Type", contentType) |
| 76 | + } |
| 77 | + const contentLength = response.headers.get("Content-Length") |
| 78 | + if (contentLength) { |
| 79 | + headers.set("Content-Length", contentLength) |
| 80 | + } |
| 81 | + const fileName = relativePath.split("/").pop() || "file" |
| 82 | + headers.set("Content-Disposition", `attachment; filename="${fileName}"`) |
| 83 | + |
| 84 | + return new Response(response.body, { |
| 85 | + status: response.status, |
| 86 | + headers, |
| 87 | + }) |
| 88 | + } catch (error) { |
| 89 | + console.error("HTTP proxy error:", error) |
| 90 | + return new Response("Failed to fetch file from HTTP proxy", { status: 502 }) |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +function getContentType(fileName: string): string { |
| 95 | + const ext = fileName.split(".").pop()?.toLowerCase() |
| 96 | + const mimeTypes: Record<string, string> = { |
| 97 | + txt: "text/plain", |
| 98 | + html: "text/html", |
| 99 | + css: "text/css", |
| 100 | + js: "application/javascript", |
| 101 | + json: "application/json", |
| 102 | + xml: "application/xml", |
| 103 | + png: "image/png", |
| 104 | + jpg: "image/jpeg", |
| 105 | + jpeg: "image/jpeg", |
| 106 | + gif: "image/gif", |
| 107 | + svg: "image/svg+xml", |
| 108 | + pdf: "application/pdf", |
| 109 | + zip: "application/zip", |
| 110 | + } |
| 111 | + return mimeTypes[ext || ""] || "application/octet-stream" |
| 112 | +} |
0 commit comments