mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-25 15:06:26 -08:00
files API
This commit is contained in:
parent
3e834bfda2
commit
e01788de4f
59
src/server/lib/apply.ts
Normal file
59
src/server/lib/apply.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import type Files from "./files.js"
|
||||
import type { FilePointer } from "./files.js"
|
||||
import * as Accounts from "./accounts.js"
|
||||
import { FileSchemas } from "./schemas/index.js"
|
||||
|
||||
export type Update = Pick<FilePointer, "visibility" | "filename" | "tag">
|
||||
& {
|
||||
owner: string | null,
|
||||
id: string
|
||||
}
|
||||
|
||||
export function applyTagMask(tags: string[], mask: Record<string, boolean>) {
|
||||
return Object.entries(Object.assign(
|
||||
Object.fromEntries(tags.map(e => [e, true])),
|
||||
mask
|
||||
)).filter(e => e[1]).map(e => e[0])
|
||||
}
|
||||
|
||||
export const operations : Exclude<({
|
||||
[K in keyof Update]: [K,
|
||||
((files: Files, passed: Update[K], id: string, file: FilePointer) => void)
|
||||
| true
|
||||
]
|
||||
})[keyof Update], undefined>[] = [
|
||||
["filename", true],
|
||||
["visibility", true],
|
||||
["tag", true],
|
||||
["owner", (files: Files, owner: string|null, id: string, file: FilePointer) => {
|
||||
files.chown(id, owner || undefined, true)
|
||||
return
|
||||
}],
|
||||
["id", (files: Files, newId: string, oldId: string, file: FilePointer) => {
|
||||
files.mv(oldId, newId, true)
|
||||
return
|
||||
}]
|
||||
]
|
||||
|
||||
export default function apply(
|
||||
files: Files,
|
||||
uploadId: string,
|
||||
source: Partial<Update>,
|
||||
noWrite: boolean = false
|
||||
) {
|
||||
let file = files.db.data[uploadId]
|
||||
let issues = operations.map(([k, v]) => {
|
||||
if (source[k] === undefined) return
|
||||
if (v == true)
|
||||
//@ts-ignore SHUTUPSHUTUPSHUTUP
|
||||
file[k] = source[k]
|
||||
else
|
||||
//@ts-ignore oh my god you shut up too
|
||||
v(files, source[k], uploadId, file)
|
||||
}).filter(e => Boolean(e))
|
||||
|
||||
if (!noWrite) {
|
||||
Accounts.Db.save()
|
||||
files.db.save()
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import { Client as API, convertSnowflakeToDate } from "./DiscordAPI/index.js"
|
|||
import type { APIAttachment } from "discord-api-types/v10"
|
||||
import config, { Configuration } from "./config.js"
|
||||
import "dotenv/config"
|
||||
import apply from "./apply.js"
|
||||
|
||||
import * as Accounts from "./accounts.js"
|
||||
import { z } from "zod"
|
||||
|
@ -695,6 +696,11 @@ export default class Files {
|
|||
|
||||
async mv(uploadId: string, newId: string, noWrite: boolean = false) {
|
||||
let target = this.db.data[uploadId]
|
||||
|
||||
if (newId in this.db.data) {
|
||||
throw new Error("overwriting another file with this move")
|
||||
}
|
||||
|
||||
if (target.owner) {
|
||||
let owner = Accounts.getFromId(target.owner)
|
||||
if (owner) {
|
||||
|
@ -710,4 +716,6 @@ export default class Files {
|
|||
if (!noWrite)
|
||||
return this.db.save()
|
||||
}
|
||||
|
||||
apply = apply.bind(null, this)
|
||||
}
|
|
@ -184,6 +184,13 @@ export function scheme(scheme: z.ZodTypeAny, transformer: (ctx: Context) => Prom
|
|||
}
|
||||
}
|
||||
|
||||
// this is bad but idgaf
|
||||
export function runtimeEvaluatedScheme(sch: (c: Context) => z.ZodTypeAny, transformer?: Parameters<typeof scheme>[1]): RequestHandler {
|
||||
return async function(ctx, next) {
|
||||
return scheme(sch(ctx),transformer)(ctx, next)
|
||||
}
|
||||
}
|
||||
|
||||
// Not really middleware but a utility
|
||||
|
||||
export const login = async (ctx: Context, account: Accounts.AccountResolvable) => {
|
||||
|
@ -212,7 +219,7 @@ export const verifyPoi = (user: string, poi?: string, wantsMfaPoi: boolean = fal
|
|||
export const mirror = (apiRoot: Hono, ctx: Context, url: string, init: Partial<RequestInit>) => apiRoot.fetch(
|
||||
new Request(
|
||||
(new URL(url, ctx.req.raw.url)).href,
|
||||
Object.assign(ctx.req.raw,init)
|
||||
init.body ? {...ctx.req.raw, headers: ctx.req.raw.headers, ...init} : Object.assign(ctx.req.raw, init)
|
||||
),
|
||||
ctx.env
|
||||
)
|
|
@ -6,14 +6,14 @@ export const FileId = z.string()
|
|||
.max(config.maxUploadIdLength,"file ID too long")
|
||||
.min(1, "you... *need* a file ID")
|
||||
export const FileVisibility = z.enum(["public", "anonymous", "private"])
|
||||
export const FileTag = z.string().toLowerCase().max(30, "tag length too long")
|
||||
export const FileTag = z.string().toLowerCase().regex(/^[a-z\-]+$/, "invalid characters").max(30, "tag length too long")
|
||||
export const FilePointer = z.object({
|
||||
filename: z.string().max(256, "filename too long"),
|
||||
filename: z.string().max(512, "filename too long"),
|
||||
mime: z.string().max(256, "mimetype too long"),
|
||||
messageids: z.array(z.string()),
|
||||
owner: z.optional(z.string()),
|
||||
sizeInBytes: z.optional(z.number()),
|
||||
tag: z.optional(FileTag),
|
||||
tag: z.optional(FileTag.array().max(5)),
|
||||
visibility: z.optional(FileVisibility).default("public"),
|
||||
chunkSize: z.optional(z.number()),
|
||||
lastModified: z.optional(z.number()),
|
||||
|
|
|
@ -4,7 +4,7 @@ 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, requiresAccount, requiresScopes, scheme } from "../../../../lib/middleware.js"
|
||||
import { getAccount, requiresAccount, requiresScopes, runtimeEvaluatedScheme, scheme } from "../../../../lib/middleware.js"
|
||||
import {Readable} from "node:stream"
|
||||
import type {ReadableStream as StreamWebReadable} from "node:stream/web"
|
||||
import formidable from "formidable"
|
||||
|
@ -14,6 +14,8 @@ import { type StatusCode } from "hono/utils/http-status"
|
|||
import { z } from "zod"
|
||||
import { FileSchemas } from "../../../../lib/schemas/index.js"
|
||||
import config from "../../../../lib/config.js"
|
||||
import { BulkFileUpdate, BulkUnprivilegedFileUpdate } from "./schemes.js"
|
||||
import { applyTagMask } from "../../../../lib/apply.js"
|
||||
|
||||
const router = new Hono<{
|
||||
Variables: {
|
||||
|
@ -158,5 +160,81 @@ export default function(files: Files) {
|
|||
})}
|
||||
)
|
||||
|
||||
// THIS IS SHIT!!!
|
||||
router.patch("/", requiresAccount, runtimeEvaluatedScheme(
|
||||
(c) => c.get("account").admin ? BulkFileUpdate : BulkUnprivilegedFileUpdate
|
||||
), (ctx) => {
|
||||
let actor = ctx.get("account")
|
||||
let update = ctx.get("parsedScheme") as z.infer<typeof BulkFileUpdate>
|
||||
let to = Array.from(new Set(update.to).values())
|
||||
let todo = update.do
|
||||
|
||||
for (let k of to) {
|
||||
if (!(k in files.db.data))
|
||||
return ServeError(ctx, 404, `file ${k} doesn't exist`)
|
||||
if (!actor.admin && files.db.data[k].owner != actor.id)
|
||||
return ServeError(ctx, 403, `you don't own file ${k}`)
|
||||
}
|
||||
|
||||
let applied: Record<string, string[]> = {}
|
||||
|
||||
if (typeof todo !== "string" && "tag" in todo)
|
||||
for (let e of to) {
|
||||
applied[e] = applyTagMask(
|
||||
files.db.data[e].tag || [],
|
||||
todo.tag as Exclude<typeof todo.tag, undefined>
|
||||
)
|
||||
if (applied[e].length > 5)
|
||||
return ServeError(ctx, 400, `too many tags for file ID ${e}`)
|
||||
}
|
||||
|
||||
|
||||
to.forEach(
|
||||
todo == "delete"
|
||||
? e => files.unlink(e, true)
|
||||
: e => files.apply(e, {
|
||||
...todo,
|
||||
...("tag" in todo ? {
|
||||
tag: applied[e]
|
||||
} : {})
|
||||
} as Omit<typeof todo, "tag"> & { tag: string[] }, true)
|
||||
)
|
||||
|
||||
files.db.save()
|
||||
Accounts.Db.save()
|
||||
|
||||
return ctx.text("ok")
|
||||
})
|
||||
|
||||
router.get("/", requiresAccount,
|
||||
/*scheme(
|
||||
z.object({
|
||||
page: z.string().refine(e => !Number.isNaN(parseInt(e,10))),
|
||||
amount: z.string().refine(e => !Number.isNaN(parseInt(e,10))),
|
||||
changedOn: z.string().refine(e => !Number.isNaN(parseInt(e,10)))
|
||||
}).partial(),
|
||||
c=>c.req.query()
|
||||
),*/ (ctx,next) => {
|
||||
let queryStr = ctx.req.query()
|
||||
let accId = queryStr.account
|
||||
let actor = ctx.get("account")
|
||||
|
||||
let target = accId
|
||||
? (
|
||||
accId == "me"
|
||||
? actor
|
||||
: Accounts.resolve(accId)
|
||||
)
|
||||
: null
|
||||
|
||||
if (!actor.admin && target != actor)
|
||||
return ServeError(ctx, 403, "can't control other users")
|
||||
let d = Object.entries(files.db.data)
|
||||
.map(([id, file]) => ({...file, messageids: undefined, id}))
|
||||
.filter(e => (!target || e.owner == target.id))
|
||||
|
||||
return ctx.json(d)
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ 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, requiresScopes } from "../../../../lib/middleware.js"
|
||||
import { getAccount, mirror, requiresScopes } from "../../../../lib/middleware.js"
|
||||
import {Readable} from "node:stream"
|
||||
import type {ReadableStream as StreamWebReadable} from "node:stream/web"
|
||||
import formidable from "formidable"
|
||||
|
@ -17,7 +17,7 @@ const router = new Hono<{
|
|||
},
|
||||
Bindings: HttpBindings
|
||||
}>()
|
||||
router.all("*", getAccount)
|
||||
router.use(getAccount)
|
||||
|
||||
export default function(files: Files, apiRoot: Hono) {
|
||||
|
||||
|
@ -134,5 +134,25 @@ export default function(files: Files, apiRoot: Hono) {
|
|||
)
|
||||
})
|
||||
|
||||
router.delete("/:id", async (ctx) =>
|
||||
mirror(apiRoot, ctx, "/api/v1/file", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({
|
||||
do: "delete",
|
||||
to: [ctx.req.param("id")]
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
router.patch("/:id", async (ctx) =>
|
||||
mirror(apiRoot, ctx, "/api/v1/file", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({
|
||||
do: await ctx.req.json(),
|
||||
to: [ctx.req.param("id")]
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
return router
|
||||
}
|
||||
|
|
29
src/server/routes/api/v1/file/schemes.ts
Normal file
29
src/server/routes/api/v1/file/schemes.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { z } from "zod";
|
||||
import { FileSchemas } from "../../../../lib/schemas/index.js";
|
||||
|
||||
export const FilePatch = FileSchemas.FilePointer
|
||||
.pick({ filename: true, visibility: true })
|
||||
.extend({
|
||||
id: z.string(),
|
||||
owner: z.string().nullable(),
|
||||
tag: z.record(FileSchemas.FileTag, z.boolean())
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const FileUpdate = z.union([
|
||||
z.literal("delete"),
|
||||
FilePatch
|
||||
])
|
||||
export const UnprivilegedFileUpdate = z.union([
|
||||
z.literal("delete"),
|
||||
FilePatch.omit({ id: true, owner: true })
|
||||
])
|
||||
|
||||
export const BulkFileUpdate = z.object({
|
||||
do: FileUpdate,
|
||||
to: FileSchemas.FileId.array()
|
||||
})
|
||||
export const BulkUnprivilegedFileUpdate = z.object({
|
||||
do: UnprivilegedFileUpdate,
|
||||
to: FileSchemas.FileId.array()
|
||||
})
|
Loading…
Reference in a new issue