From 39cb48a6567342d0471eba870a4d148c98578864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20T=C3=A4kker?= Date: Tue, 29 Sep 2026 16:22:53 +0200 Subject: [PATCH] Remove legacy static media route --- .env.example | 1 - README.md | 3 +- nuxt.config.ts | 1 - server/routes/static/[...path].ts | 225 ------------------------------ server/utils/backend-config.ts | 1 - 5 files changed, 1 insertion(+), 230 deletions(-) delete mode 100644 server/routes/static/[...path].ts diff --git a/.env.example b/.env.example index 4f7c098..70fa10f 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,5 @@ # Copy this file to .env for local development. Use real, private values locally. NUXT_MONGODB_URI=mongodb://127.0.0.1:27017/music-v2 NUXT_UPLOAD_DIR=./upload -NUXT_MEDIA_BASE_URL=https://s3-seaweedfs.lucasskt.dk/music NUXT_PUBLIC_MEDIA_BASE_URL=https://s3-seaweedfs.lucasskt.dk/music NUXT_TOKEN_EXPIRE=1h diff --git a/README.md b/README.md index 29625e5..35d2ee6 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,10 @@ The private settings are declared in `nuxt.config.ts` and can be set with matchi | --- | --- | --- | | `NUXT_MONGODB_URI` | Yes | MongoDB connection URI. | | `NUXT_UPLOAD_DIR` | Yes for upload/media operations | Writable path where uploaded media is stored. Prefer an absolute path. | -| `NUXT_MEDIA_BASE_URL` | No | Optional anonymous media read base URL. Object paths are appended as-is, for example `https://s3-seaweedfs.lucasskt.dk/music`. When configured, media reads use this URL first and fall back to local uploads when the object is missing. | | `NUXT_PUBLIC_MEDIA_BASE_URL` | Yes for browser media URLs | Public base URL used by `GET_FILE` and `GET_AUDIO_FILE` to build direct bucket URLs. | | `NUXT_TOKEN_EXPIRE` | No | Session lifetime accepted by `parse-duration`, such as `1h` or `7d`; defaults to `1h`. | -For a short migration window, the server also accepts the standalone backend names `MONGODB_URI`, `UPLOAD_DIR`, `MEDIA_BASE_URL`, and `TOKEN_EXPIRE` as fallbacks. Prefer the `NUXT_` names for all new deployments. `VITE_PUBLIC_BACKEND` is no longer used: browser API requests are same-origin. +For a short migration window, the server also accepts the standalone backend names `MONGODB_URI`, `UPLOAD_DIR`, and `TOKEN_EXPIRE` as fallbacks. Prefer the `NUXT_` names for all new deployments. `VITE_PUBLIC_BACKEND` is no longer used: browser API requests are same-origin. Nuxt loads `.env` while running its CLI for development and local production preview, but a built server does not load `.env`. Configure these variables in the production process manager or hosting environment. See the [Nuxt runtime config guide](https://nuxt.com/docs/4.x/guide/going-further/runtime-config) and [deployment guide](https://nuxt.com/docs/4.x/getting-started/deployment). diff --git a/nuxt.config.ts b/nuxt.config.ts index 1c913f6..a837b4b 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -13,7 +13,6 @@ export default defineNuxtConfig({ // are resolved by server utilities at runtime, never during this build. mongodbUri: "", uploadDir: "", - mediaBaseUrl: "", tokenExpire: "", public: { mediaBaseUrl: "", diff --git a/server/routes/static/[...path].ts b/server/routes/static/[...path].ts deleted file mode 100644 index 7dec333..0000000 --- a/server/routes/static/[...path].ts +++ /dev/null @@ -1,225 +0,0 @@ -import { createReadStream } from "node:fs" -import { stat } from "node:fs/promises" -import { Readable } from "node:stream" -import path from "node:path" -import { - assertMethod, - createError, - getRouterParam, - getRequestHeader, - sendStream, - setHeader, - setResponseStatus, -} from "h3" -import { getBackendRuntimeConfig } from "../../utils/backend-config" -import { - InvalidUploadPathError, - resolveUploadFile, - UploadFileNotFoundError, -} from "../../utils/upload-files" - -const contentTypes: Record = { - ".aac": "audio/aac", - ".flac": "audio/flac", - ".jpeg": "image/jpeg", - ".jpg": "image/jpeg", - ".m4a": "audio/mp4", - ".mp3": "audio/mpeg", - ".mp4": "video/mp4", - ".ogg": "audio/ogg", - ".png": "image/png", - ".wav": "audio/wav", - ".webp": "image/webp", -} - -function sendRangeNotSatisfiable( - event: Parameters[0], - size: number, -) { - setResponseStatus(event, 416) - setHeader(event, "Content-Range", `bytes */${size}`) - setHeader(event, "Accept-Ranges", "bytes") - setHeader(event, "Content-Length", 0) - return null -} - -function parseRange(rangeHeader: string | undefined, size: number) { - let start = 0 - let end = size - 1 - if (!rangeHeader) return { start, end, partial: false } - const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim()) - if (!match || (!match[1] && !match[2]) || size === 0) return null - if (!match[1]) { - const suffixLength = Number(match[2]) - if (!Number.isSafeInteger(suffixLength) || suffixLength <= 0) - return null - start = Math.max(size - suffixLength, 0) - } else { - start = Number(match[1]) - if (!Number.isSafeInteger(start)) return null - if (match[2]) { - end = Number(match[2]) - if (!Number.isSafeInteger(end)) return null - } - } - if (start >= size || end < start) return null - return { start, end: Math.min(end, size - 1), partial: true } -} - -function remoteObjectUrl(baseUrl: string, relativePath: string) { - if ( - !relativePath || - relativePath.includes("\\") || - relativePath - .split("/") - .some((part) => !part || part === "." || part === "..") - ) { - throw new InvalidUploadPathError() - } - const base = baseUrl.replace(/\/+$/, "") - return `${base}/${relativePath.split("/").map(encodeURIComponent).join("/")}` -} - -async function serveRemote( - event: Parameters[0], - url: string, - extension: string, -) { - const method = event.node.req.method === "HEAD" ? "HEAD" : "GET" - const head = await fetch(url, { method: "HEAD" }) - if (head.status === 404) return { found: false as const, body: null } - if (!head.ok) - throw createError({ - statusCode: 502, - statusMessage: `Media bucket returned ${head.status}`, - }) - const size = Number(head.headers.get("content-length")) - if (!Number.isSafeInteger(size) || size < 0) - throw createError({ - statusCode: 502, - statusMessage: "Media bucket returned invalid content length", - }) - setHeader( - event, - "Accept-Ranges", - head.headers.get("accept-ranges") || "bytes", - ) - setHeader( - event, - "Content-Type", - head.headers.get("content-type") || - contentTypes[extension] || - "application/octet-stream", - ) - const range = parseRange(getRequestHeader(event, "range"), size) - if (!range) - return { - found: true as const, - body: sendRangeNotSatisfiable(event, size), - } - const length = size === 0 ? 0 : range.end - range.start + 1 - if (range.partial) { - setResponseStatus(event, 206) - setHeader( - event, - "Content-Range", - `bytes ${range.start}-${range.end}/${size}`, - ) - } - setHeader(event, "Content-Length", length) - if (method === "HEAD" || length === 0) - return { found: true as const, body: null } - const response = await fetch(url, { - headers: { Range: `bytes=${range.start}-${range.end}` }, - }) - if (response.status !== 206 || !response.body) - throw createError({ - statusCode: 502, - statusMessage: - "Media bucket failed to return the requested byte range", - }) - return { - found: true as const, - body: sendStream( - event, - Readable.fromWeb( - response.body as import("node:stream/web").ReadableStream, - ), - ), - } -} - -export default defineEventHandler(async (event) => { - assertMethod(event, ["GET", "HEAD"]) - const relativePath = getRouterParam(event, "path") - if (!relativePath) { - setResponseStatus(event, 404) - return null - } - const config = getBackendRuntimeConfig(event) - if (config.mediaBaseUrl) { - const url = remoteObjectUrl(config.mediaBaseUrl, relativePath) - const remote = await serveRemote( - event, - url, - path.extname(relativePath).toLowerCase(), - ) - if (remote.found) return remote.body - // Missing bucket objects fall through to local media during the read migration. - } - - let file: { path: string } - try { - file = await resolveUploadFile(event, relativePath) - } catch (error) { - if ( - error instanceof InvalidUploadPathError || - error instanceof UploadFileNotFoundError - ) { - setResponseStatus(event, 404) - return null - } - throw error - } - let metadata - try { - metadata = await stat(file.path) - } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") { - setResponseStatus(event, 404) - return null - } - throw error - } - if (!metadata.isFile()) { - setResponseStatus(event, 404) - return null - } - setHeader(event, "Accept-Ranges", "bytes") - setHeader( - event, - "Content-Type", - contentTypes[path.extname(file.path).toLowerCase()] ?? - "application/octet-stream", - ) - const range = parseRange(getRequestHeader(event, "range"), metadata.size) - if (!range) return sendRangeNotSatisfiable(event, metadata.size) - const length = metadata.size === 0 ? 0 : range.end - range.start + 1 - if (range.partial) { - setResponseStatus(event, 206) - setHeader( - event, - "Content-Range", - `bytes ${range.start}-${range.end}/${metadata.size}`, - ) - } - setHeader(event, "Content-Length", length) - if (event.node.req.method === "HEAD" || length === 0) return null - - console.log(file.path) - - return sendStream( - event, - createReadStream(file.path, { start: range.start, end: range.end }), - ) -}) diff --git a/server/utils/backend-config.ts b/server/utils/backend-config.ts index ede6703..8806571 100644 --- a/server/utils/backend-config.ts +++ b/server/utils/backend-config.ts @@ -11,7 +11,6 @@ export function getBackendRuntimeConfig(event?: H3Event) { return { mongodbUri: config.mongodbUri || process.env.MONGODB_URI || "", uploadDir: config.uploadDir || process.env.UPLOAD_DIR || "", - mediaBaseUrl: config.mediaBaseUrl || process.env.MEDIA_BASE_URL || "", tokenExpire: config.tokenExpire || process.env.TOKEN_EXPIRE || "1h", } }