/api/v1/file

This commit is contained in:
May 2024-03-26 21:54:55 -07:00
parent 4add8a5a8a
commit a62f1cfbc3
5 changed files with 169 additions and 156 deletions

View file

@ -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() {/*

View file

@ -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))
primaryApi.post("/upload", async (ctx) =>
apiRoot.fetch(
new Request(
(new URL(
`/api/v1/file`, ctx.req.raw.url)).href,
{
...ctx.req.raw,
method: "PUT"
}
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)
})
})}
),
ctx.env
)
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)))
})
})
)
return primaryApi

View file

@ -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
}

View file

@ -106,5 +106,7 @@ export default function(files: Files) {
}
})
router.post("/:id")
return router
}

View file

@ -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)
fd.append("file", v.type == "clone" ? v.url : v.file)
return handle_fetch_promise(x,fetch("/upload",{
method: "POST",
return handle_fetch_promise(x,fetch("/api/v1/file",{
method: "PUT",
body: fd
}))
break
case "clone":
return handle_fetch_promise(
x,
fetch("/clone", {
method: "POST",
body: JSON.stringify({
url: v.url,
...v.params,
}),
})
)
break
}
}
if (sequential) await hdl()