mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-21 21:36:26 -08:00
/api/v1/file
This commit is contained in:
parent
4add8a5a8a
commit
a62f1cfbc3
|
@ -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() {/*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -106,5 +106,7 @@ export default function(files: Files) {
|
|||
}
|
||||
})
|
||||
|
||||
router.post("/:id")
|
||||
|
||||
return router
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue