diff --git a/src/server/index.ts b/src/server/index.ts index fc139ce..74c2cff 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -87,7 +87,16 @@ apiRouter.loadAPIMethods().then(() => { console.log("API OK!") // moved here to ensure it's matched last - app.get("/:fileId", async (ctx) => app.fetch(ctx.req.raw, ctx.env)) + app.get("/:fileId", async (ctx) => + app.fetch( + new Request( + (new URL( + `/api/v1/file/${ctx.req.param("fileId")}`, ctx.req.raw.url)).href, + ctx.req.raw + ), + ctx.env + ) + ) // listen on 3000 or MONOFILE_PORT // moved here to prevent a crash if someone manages to access monofile before api routes are mounted diff --git a/src/server/routes/api/v0/primaryApi.ts b/src/server/routes/api/v0/primaryApi.ts index 1dfe106..d389dbb 100644 --- a/src/server/routes/api/v0/primaryApi.ts +++ b/src/server/routes/api/v0/primaryApi.ts @@ -21,91 +21,15 @@ export let primaryApi = new Hono<{ primaryApi.all("*", getAccount) export default function (files: Files) { - primaryApi.get( - "/file/:fileId", - async (ctx): Promise => { - const fileId = (ctx.req.param() as {fileId: string}).fileId - - let acc = ctx.get("account") as Accounts.Account - - let file = files.files[fileId] - ctx.header("Access-Control-Allow-Origin", "*") - ctx.header("Content-Security-Policy", "sandbox allow-scripts") - ctx.header("Content-Disposition", `${ctx.req.query("attachment") == "1" ? "attachment" : "inline"}; filename="${encodeURI(file.filename.replaceAll("\n","\\n"))}"`) - ctx.header("ETag", file.md5) - //if (file.lastModified) ctx.header("Last-Modified", new Date(file.lastModified).toTimeString()) - - if (file) { - if (file.visibility == "private") { - if (acc?.id != file.owner) { - return ServeError(ctx, 403, "you do not own this file") - } - - if ( - auth.getType(auth.tokenFor(ctx)!) == "App" && - auth - .getPermissions(auth.tokenFor(ctx)!) - ?.includes("private") - ) { - return ServeError(ctx, 403, "insufficient permissions") - } - } - - let range: Range | undefined - - ctx.header("Content-Type", file.mime) - if (file.sizeInBytes) { - ctx.header("Content-Length", file.sizeInBytes.toString()) - - if (file.chunkSize && ctx.req.header("Range")) { - let ranges = RangeParser(file.sizeInBytes, ctx.req.header("Range") || "") - - if (ranges) { - if (typeof ranges == "number") - return ServeError(ctx, ranges == -1 ? 416 : 400, ranges == -1 ? "unsatisfiable ranges" : "invalid ranges") - if (ranges.length > 1) return ServeError(ctx, 400, "multiple ranges not supported") - range = ranges[0] - } - } - } - - if (range) { - ctx.status(206) - ctx.header( - "Content-Length", - (range.end - range.start + 1).toString() - ) - ctx.header( - "Content-Range", - `bytes ${range.start}-${range.end}/${file.sizeInBytes}` - ) - } - - if (ctx.req.method == "HEAD") - return ctx.body(null) - - return files - .readFileStream(fileId, range) - .then(async (stream) => { - let rs = new ReadableStream({ - start(controller) { - stream.once("end", () => controller.close()) - stream.once("error", (err) => controller.error(err)) - }, - cancel(reason) { - stream.destroy(reason instanceof Error ? reason : new Error(reason)) - } - }) - stream.pipe(ctx.env.outgoing) - return new Response(rs, ctx.body(null)) - }) - .catch((err) => { - return ServeError(ctx, err.status, err.message) - }) - } else { - return ServeError(ctx, 404, "file not found") - } - } + primaryApi.get("/:fileId", async (ctx) => + primaryApi.fetch( + new Request( + (new URL( + `/api/v1/file/${ctx.req.param("fileId")}`, ctx.req.raw.url)).href, + ctx.req.raw + ), + ctx.env + ) ) primaryApi.post( diff --git a/src/server/routes/api/v1/api.json b/src/server/routes/api/v1/api.json index 58ccbba..7e5affe 100644 --- a/src/server/routes/api/v1/api.json +++ b/src/server/routes/api/v1/api.json @@ -3,7 +3,14 @@ "baseURL": "/api/v1", "mount": [ "account", - "file", - "session" + "session", + { + "file": "file/index", + "to": "/file" + }, + { + "file": "file/individual", + "to": "/file" + } ] } \ No newline at end of file diff --git a/src/server/routes/api/v1/file.ts b/src/server/routes/api/v1/file.ts deleted file mode 100644 index 88f54b4..0000000 --- a/src/server/routes/api/v1/file.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Hono } from "hono"; -import Files from "../../../lib/files.js"; - -const router = new Hono() - -export default function(files: Files) { - return router -} diff --git a/src/server/routes/api/v1/file/index.ts b/src/server/routes/api/v1/file/index.ts new file mode 100644 index 0000000..d43c846 --- /dev/null +++ b/src/server/routes/api/v1/file/index.ts @@ -0,0 +1,13 @@ +import { Hono } from "hono"; +import Files from "../../../../lib/files.js"; +import { getAccount } from "../../../../lib/middleware.js"; + +const router = new Hono() +router.all("*", getAccount) + +export default function(files: Files) { + + + + return router +} diff --git a/src/server/routes/api/v1/file/individual.ts b/src/server/routes/api/v1/file/individual.ts new file mode 100644 index 0000000..7429974 --- /dev/null +++ b/src/server/routes/api/v1/file/individual.ts @@ -0,0 +1,109 @@ +import { Hono } from "hono" +import * as Accounts from "../../../../lib/accounts.js" +import * as auth from "../../../../lib/auth.js" +import RangeParser, { type Range } from "range-parser" +import ServeError from "../../../../lib/errors.js" +import Files, { WebError } from "../../../../lib/files.js" +import { getAccount, requiresPermissions } from "../../../../lib/middleware.js" +import {Readable} from "node:stream" +import type {ReadableStream as StreamWebReadable} from "node:stream/web" +import formidable from "formidable" +import { HttpBindings } from "@hono/node-server" +import pkg from "../../../../../../package.json" assert {type: "json"} +import { type StatusCode } from "hono/utils/http-status" + +const router = new Hono<{ + Variables: { + account: Accounts.Account + }, + Bindings: HttpBindings +}>() +router.all("*", getAccount) + +export default function(files: Files) { + + router.get("/:id", async (ctx) => { + const fileId = ctx.req.param("id") + + let acc = ctx.get("account") as Accounts.Account + + let file = files.files[fileId] + ctx.header("Accept-Ranges", "bytes") + ctx.header("Access-Control-Allow-Origin", "*") + ctx.header("Content-Security-Policy", "sandbox allow-scripts") + ctx.header("Content-Disposition", `${ctx.req.query("attachment") == "1" ? "attachment" : "inline"}; filename="${encodeURI(file.filename.replaceAll("\n","\\n"))}"`) + ctx.header("ETag", file.md5) + //if (file.lastModified) ctx.header("Last-Modified", new Date(file.lastModified).toTimeString()) + + if (file) { + if (file.visibility == "private") { + if (acc?.id != file.owner) { + return ServeError(ctx, 403, "you do not own this file") + } + + if ( + auth.getType(auth.tokenFor(ctx)!) == "App" && + auth + .getPermissions(auth.tokenFor(ctx)!) + ?.includes("private") + ) { + return ServeError(ctx, 403, "insufficient permissions") + } + } + + let range: Range | undefined + + ctx.header("Content-Type", file.mime) + if (file.sizeInBytes) { + ctx.header("Content-Length", file.sizeInBytes.toString()) + + if (file.chunkSize && ctx.req.header("Range")) { + let ranges = RangeParser(file.sizeInBytes, ctx.req.header("Range") || "") + + if (ranges) { + if (typeof ranges == "number") + return ServeError(ctx, ranges == -1 ? 416 : 400, ranges == -1 ? "unsatisfiable ranges" : "invalid ranges") + if (ranges.length > 1) return ServeError(ctx, 400, "multiple ranges not supported") + range = ranges[0] + + ctx.status(206) + ctx.header( + "Content-Length", + (range.end - range.start + 1).toString() + ) + ctx.header( + "Content-Range", + `bytes ${range.start}-${range.end}/${file.sizeInBytes}` + ) + } + } + } + + if (ctx.req.method == "HEAD") + return ctx.body(null) + + return files + .readFileStream(fileId, range) + .then(async (stream) => { + let rs = new ReadableStream({ + start(controller) { + stream.once("end", () => controller.close()) + stream.once("error", (err) => controller.error(err)) + }, + cancel(reason) { + stream.destroy(reason instanceof Error ? reason : new Error(reason)) + } + }) + stream.pipe(ctx.env.outgoing) + return new Response(rs, ctx.body(null)) + }) + .catch((err) => { + return ServeError(ctx, err.status, err.message) + }) + } else { + return ServeError(ctx, 404, "file not found") + } + }) + + return router +}