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)
|
if (useRanges)
|
||||||
this.ranges.scan_msg_begin = Math.floor(this.ranges.scan_files_begin / 10),
|
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
|
this.msgIdx = this.ranges.scan_msg_begin
|
||||||
|
|
||||||
|
console.log(this.ranges)
|
||||||
}
|
}
|
||||||
|
|
||||||
async _read() {/*
|
async _read() {/*
|
||||||
|
|
|
@ -32,133 +32,18 @@ export default function (files: Files, apiRoot: Hono) {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
primaryApi.post(
|
primaryApi.post("/upload", async (ctx) =>
|
||||||
"/upload",
|
apiRoot.fetch(
|
||||||
requiresPermissions("upload"),
|
new Request(
|
||||||
(ctx) => { return new Promise((resolve,reject) => {
|
(new URL(
|
||||||
ctx.env.incoming.removeAllListeners("data") // remove hono's buffering
|
`/api/v1/file`, ctx.req.raw.url)).href,
|
||||||
|
{
|
||||||
let errEscalated = false
|
...ctx.req.raw,
|
||||||
function escalate(err:Error) {
|
method: "PUT"
|
||||||
if (errEscalated) return
|
}
|
||||||
errEscalated = true
|
),
|
||||||
|
ctx.env
|
||||||
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)))
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return primaryApi
|
return primaryApi
|
||||||
|
|
|
@ -1,13 +1,153 @@
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono"
|
||||||
import Files from "../../../../lib/files.js";
|
import * as Accounts from "../../../../lib/accounts.js"
|
||||||
import { getAccount } from "../../../../lib/middleware.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)
|
router.all("*", getAccount)
|
||||||
|
|
||||||
export default function(files: Files) {
|
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
|
return router
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,5 +106,7 @@ export default function(files: Files) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.post("/:id")
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,30 +113,14 @@
|
||||||
// quick patch-in to allow for a switch to have everything upload sequentially
|
// 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
|
// switch will have a proper menu option later, for now i'm lazy so it's just gonna be a Secret
|
||||||
let hdl = () => {
|
let hdl = () => {
|
||||||
switch (v.type) {
|
let fd = new FormData()
|
||||||
case "upload":
|
if (v.params.uploadId) fd.append("uploadId", v.params.uploadId)
|
||||||
let fd = new FormData()
|
fd.append("file", v.type == "clone" ? v.url : v.file)
|
||||||
if (v.params.uploadId) fd.append("uploadId", v.params.uploadId)
|
|
||||||
fd.append("file", v.file)
|
|
||||||
|
|
||||||
return handle_fetch_promise(x,fetch("/upload",{
|
return handle_fetch_promise(x,fetch("/api/v1/file",{
|
||||||
method: "POST",
|
method: "PUT",
|
||||||
body: fd
|
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()
|
if (sequential) await hdl()
|
||||||
|
|
Loading…
Reference in a new issue