From a62f1cfbc37ffb0d2a3735bbeebc73957c79dbf8 Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Tue, 26 Mar 2024 21:54:55 -0700 Subject: [PATCH] /api/v1/file --- src/server/lib/files.ts | 4 +- src/server/routes/api/v0/primaryApi.ts | 139 ++---------------- src/server/routes/api/v1/file/index.ts | 150 +++++++++++++++++++- src/server/routes/api/v1/file/individual.ts | 2 + src/svelte/elem/UploadWindow.svelte | 30 +--- 5 files changed, 169 insertions(+), 156 deletions(-) diff --git a/src/server/lib/files.ts b/src/server/lib/files.ts index 207a00f..0e97811 100644 --- a/src/server/lib/files.ts +++ b/src/server/lib/files.ts @@ -139,8 +139,10 @@ export class ReadStream extends Readable { if (useRanges) this.ranges.scan_msg_begin = Math.floor(this.ranges.scan_files_begin / 10), - this.ranges.scan_msg_end = Math.ceil(this.ranges.scan_files_end / 10)-1, + this.ranges.scan_msg_end = Math.ceil(this.ranges.scan_files_end / 10), this.msgIdx = this.ranges.scan_msg_begin + + console.log(this.ranges) } async _read() {/* diff --git a/src/server/routes/api/v0/primaryApi.ts b/src/server/routes/api/v0/primaryApi.ts index 992fd76..204d420 100644 --- a/src/server/routes/api/v0/primaryApi.ts +++ b/src/server/routes/api/v0/primaryApi.ts @@ -32,133 +32,18 @@ export default function (files: Files, apiRoot: Hono) { ) ) - primaryApi.post( - "/upload", - requiresPermissions("upload"), - (ctx) => { return new Promise((resolve,reject) => { - ctx.env.incoming.removeAllListeners("data") // remove hono's buffering - - let errEscalated = false - function escalate(err:Error) { - if (errEscalated) return - errEscalated = true - - if ("httpCode" in err) - ctx.status(err.httpCode as StatusCode) - else if (err instanceof WebError) - ctx.status(err.statusCode as StatusCode) - else ctx.status(400) - resolve(ctx.body(err.message)) - } - - let acc = ctx.get("account") as Accounts.Account | undefined - - if (!ctx.req.header("Content-Type")?.startsWith("multipart/form-data")) - return resolve(ctx.body("must be multipart/form-data", 400)) - - if (!ctx.req.raw.body) - return resolve(ctx.body("body must be supplied", 400)) - - let file = files.createWriteStream(acc?.id) - let parser = formidable({ - maxFieldsSize: 65536, - maxFileSize: files.config.maxDiscordFileSize*files.config.maxDiscordFiles, - maxFiles: 1 - }) - - parser.onPart = function(part) { - if (!part.originalFilename || !part.mimetype) { - parser._handlePart(part) - return - } - // lol - if (part.name == "file") { - file.setName(part.originalFilename || "") - file.setType(part.mimetype || "") - - file.on("drain", () => ctx.env.incoming.resume()) - file.on("error", (err) => part.emit("error", err)) - - part.on("data", (data: Buffer) => { - if (!file.write(data)) - ctx.env.incoming.pause() - }) - part.on("end", () => file.end()) - } - } - - parser.on("field", (k,v) => { - if (k == "uploadId") - file.setUploadId(v) - }) - - parser.parse(ctx.env.incoming).catch(e => console.error(e)) - - parser.on('error', (err) => { - escalate(err) - if (!file.destroyed) file.destroy(err) - }) - file.on("error", escalate) - - file.on("finish", async () => { - if (!ctx.env.incoming.readableEnded) await new Promise(res => ctx.env.incoming.once("end", res)) - file.commit() - .then(id => resolve(ctx.body(id!))) - .catch(escalate) - }) - - })} - ) - - primaryApi.post( - "/clone", - requiresPermissions("upload"), - ctx => new Promise(async resolve => { - - let acc = ctx.get("account") as Accounts.Account - - let requestParameters - try { - requestParameters = await ctx.req.json() - } catch (err: any) {return ctx.text(err.toString(), 400)} - - let res = await fetch(requestParameters.url, { - headers: { - "user-agent": `monofile ${pkg.version} (+https://${ctx.req.header("Host")})` - } - }) - if (!res.ok) return ctx.text(`got ${res.status} ${res.statusText}`, 500) - if (!res.body) return ctx.text(`Internal Server Error`, 500) - if ( - res.headers.has("Content-Length") - && !Number.isNaN(parseInt(res.headers.get("Content-Length")!,10)) - && parseInt(res.headers.get("Content-Length")!,10) > files.config.maxDiscordFileSize*files.config.maxDiscordFiles - ) - return ctx.text(`file reports to be too large`, 413) - - let file = files.createWriteStream(acc?.id) - - Readable.fromWeb(res.body as StreamWebReadable) - .pipe(file) - .on("error", (err) => resolve(ctx.text(err.message, err instanceof WebError ? err.statusCode as StatusCode : 500))) - - file - .setName( - requestParameters.url.split("/")[ - requestParameters.url.split("/").length - 1 - ] || "generic" - ) - - if (res.headers.has("content-type")) file.setType(res.headers.get("content-type")!) - if (requestParameters.uploadId) file.setUploadId(requestParameters.uploadId) - - file.once("finish", () => { - file.commit() - .then(id => resolve(ctx.text(id!))) - .catch((err) => resolve(ctx.text(err.message, err instanceof WebError ? err.statusCode as StatusCode : 500))) - }) - - }) + primaryApi.post("/upload", async (ctx) => + apiRoot.fetch( + new Request( + (new URL( + `/api/v1/file`, ctx.req.raw.url)).href, + { + ...ctx.req.raw, + method: "PUT" + } + ), + ctx.env + ) ) return primaryApi diff --git a/src/server/routes/api/v1/file/index.ts b/src/server/routes/api/v1/file/index.ts index d43c846..3a63af8 100644 --- a/src/server/routes/api/v1/file/index.ts +++ b/src/server/routes/api/v1/file/index.ts @@ -1,13 +1,153 @@ -import { Hono } from "hono"; -import Files from "../../../../lib/files.js"; -import { getAccount } from "../../../../lib/middleware.js"; +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() +const router = new Hono<{ + Variables: { + account: Accounts.Account + }, + Bindings: HttpBindings +}>() router.all("*", getAccount) export default function(files: Files) { - + router.on( + ["PUT", "POST"], + "/", + requiresPermissions("upload"), + (ctx) => { return new Promise((resolve,reject) => { + ctx.env.incoming.removeAllListeners("data") // remove hono's buffering + + let errEscalated = false + function escalate(err:Error) { + if (errEscalated) return + errEscalated = true + console.error(err) + + if ("httpCode" in err) + ctx.status(err.httpCode as StatusCode) + else if (err instanceof WebError) + ctx.status(err.statusCode as StatusCode) + else ctx.status(400) + resolve(ctx.body(err.message)) + } + + let acc = ctx.get("account") as Accounts.Account | undefined + + if (!ctx.req.header("Content-Type")?.startsWith("multipart/form-data")) + return resolve(ctx.body("must be multipart/form-data", 400)) + + if (!ctx.req.raw.body) + return resolve(ctx.body("body must be supplied", 400)) + + let file = files.createWriteStream(acc?.id) + let parser = formidable({ + maxFieldsSize: 65536, + maxFileSize: files.config.maxDiscordFileSize*files.config.maxDiscordFiles, + maxFiles: 1 + }) + + let acceptNewData = true + + parser.onPart = function(part) { + if (!part.originalFilename || !part.mimetype) { + parser._handlePart(part) + return + } + // lol + if (part.name == "file") { + if (!acceptNewData || file.writableEnded) + return part.emit("error", new WebError(400, "cannot set file after previously setting up another upload")) + acceptNewData = false + file.setName(part.originalFilename || "") + file.setType(part.mimetype || "") + + file.on("drain", () => ctx.env.incoming.resume()) + file.on("error", (err) => part.emit("error", err)) + + part.on("data", (data: Buffer) => { + if (!file.write(data)) + ctx.env.incoming.pause() + }) + part.on("end", () => file.end()) + } + } + + parser.on("field", async (k,v) => { + if (k == "uploadId") { + if (files.files[v] && ctx.req.method == "POST") + return file.destroy(new WebError(409, "file already exists")) + file.setUploadId(v) + // I'M GONNA KILL MYSELF!!!! + } else if (k == "file") { + if (!acceptNewData || file.writableEnded) + return file.destroy(new WebError(400, "cannot set file after previously setting up another upload")) + acceptNewData = false + + let res = await fetch(v, { + headers: { + "user-agent": `monofile ${pkg.version} (+https://${ctx.req.header("Host")})` + } + }).catch(escalate) + + if (!res) return + + if (!file + .setName( + res.headers.get("Content-Disposition") + ?.match(/filename="(.*)"/)?.[1] + || v.split("/")[ + v.split("/").length - 1 + ] || "generic" + )) return + + if (res.headers.has("Content-Type")) + if (!file.setType(res.headers.get("Content-Type")!)) + return + + if (!res.ok) return file.destroy(new WebError(500, `got ${res.status} ${res.statusText}`)) + if (!res.body) return file.destroy(new WebError(500, `Internal Server Error`)) + if ( + res.headers.has("Content-Length") + && !Number.isNaN(parseInt(res.headers.get("Content-Length")!,10)) + && parseInt(res.headers.get("Content-Length")!,10) > files.config.maxDiscordFileSize*files.config.maxDiscordFiles + ) + return file.destroy(new WebError(413, `file reports to be too large`)) + + Readable.fromWeb(res.body as StreamWebReadable) + .pipe(file) + } + }) + + parser.parse(ctx.env.incoming) + .catch(e => console.error(e)) + + parser.on('error', (err) => { + escalate(err) + if (!file.destroyed) file.destroy(err) + }) + file.on("error", escalate) + + file.on("finish", async () => { + if (!ctx.env.incoming.readableEnded) await new Promise(res => ctx.env.incoming.once("end", res)) + file.commit() + .then(id => resolve(ctx.body(id!))) + .catch(escalate) + }) + + })} + ) return router } diff --git a/src/server/routes/api/v1/file/individual.ts b/src/server/routes/api/v1/file/individual.ts index 3a00086..82dd6b5 100644 --- a/src/server/routes/api/v1/file/individual.ts +++ b/src/server/routes/api/v1/file/individual.ts @@ -106,5 +106,7 @@ export default function(files: Files) { } }) + router.post("/:id") + return router } diff --git a/src/svelte/elem/UploadWindow.svelte b/src/svelte/elem/UploadWindow.svelte index 8e5c332..c41b3c8 100644 --- a/src/svelte/elem/UploadWindow.svelte +++ b/src/svelte/elem/UploadWindow.svelte @@ -113,30 +113,14 @@ // quick patch-in to allow for a switch to have everything upload sequentially // switch will have a proper menu option later, for now i'm lazy so it's just gonna be a Secret let hdl = () => { - switch (v.type) { - case "upload": - let fd = new FormData() - if (v.params.uploadId) fd.append("uploadId", v.params.uploadId) - fd.append("file", v.file) + let fd = new FormData() + if (v.params.uploadId) fd.append("uploadId", v.params.uploadId) + fd.append("file", v.type == "clone" ? v.url : v.file) - return handle_fetch_promise(x,fetch("/upload",{ - method: "POST", - body: fd - })) - break - case "clone": - return handle_fetch_promise( - x, - fetch("/clone", { - method: "POST", - body: JSON.stringify({ - url: v.url, - ...v.params, - }), - }) - ) - break - } + return handle_fetch_promise(x,fetch("/api/v1/file",{ + method: "PUT", + body: fd + })) } if (sequential) await hdl()