diff --git a/bun.lockb b/bun.lockb index ddbf6da..29ba696 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/pkgs/api/_file.ts b/pkgs/api/_file.ts index b2b9dbb..e981611 100644 --- a/pkgs/api/_file.ts +++ b/pkgs/api/_file.ts @@ -1,18 +1,121 @@ +import mime from "mime"; import { apiContext } from "service-srv"; import { dir } from "utils/dir"; import { g } from "utils/global"; -import { generateIndexHtml } from "../server/serve-web"; -import mime from "mime"; - +import { readdir, stat } from "fs/promises"; +import { basename, dirname } from "path"; +import { + dirAsync, + existsAsync, + moveAsync, + removeAsync, + renameAsync, +} from "fs-jetpack"; export const _ = { url: "/_file/**", async api() { const { req } = apiContext(this); - const rpath = decodeURIComponent(req.params._); + let rpath = decodeURIComponent(req.params._); let res = new Response("NOT FOUND", { status: 404 }); - const path = dir(`${g.datadir}/upload/${rpath}`); + rpath = rpath + .split("/") + .map((e) => e.replace(/\.\./gi, "")) + .filter((e) => !!e) + .join("/"); + + if (Object.keys(req.query_parameters).length > 0) { + await dirAsync(dir(`${g.datadir}/files`)); + const base_dir = dir(`${g.datadir}/files/${rpath}`); + if (typeof req.query_parameters["move"] === "string") { + if (rpath) { + let moveto = req.query_parameters["move"]; + + moveto = moveto + .split("/") + .map((e) => e.replace(/\.\./gi, "")) + .filter((e) => !!e) + .join("/"); + + await moveAsync( + dir(`${g.datadir}/files/${rpath}`), + dir(`${g.datadir}/files/${moveto}/${basename(rpath)}`) + ); + } + + return new Response(JSON.stringify({ status: "ok" }), { + headers: { "content-type": "application/json" }, + }); + } else if (typeof req.query_parameters["del"] === "string") { + if (rpath) { + const base_dir = dir(`${g.datadir}/files/${rpath}`); + if (await existsAsync(base_dir)) { + if ((await readdir(base_dir)).length === 0) { + await removeAsync(base_dir); + } + } + } + + return new Response(JSON.stringify({ status: "ok" }), { + headers: { "content-type": "application/json" }, + }); + } else if (typeof req.query_parameters["rename"] === "string") { + let rename = req.query_parameters["rename"]; + + rename = rename + .split("/") + .map((e) => e.replace(/\.\./gi, "")) + .filter((e) => !!e) + .join("/"); + + let newname = ""; + if (rpath) { + if (await existsAsync(dir(`${g.datadir}/files/${rpath}`))) { + await renameAsync(dir(`${g.datadir}/files/${rpath}`), rename); + } else { + const target = dir( + `${g.datadir}/files/${dirname(rpath)}/${rename}` + ); + await dirAsync(target); + } + newname = `/${dirname(rpath)}/${rename}`; + } + + return new Response(JSON.stringify({ newname }), { + headers: { "content-type": "application/json" }, + }); + } else if (typeof req.query_parameters["dir"] === "string") { + try { + const files = [] as { + name: string; + type: "dir" | "file"; + size: number; + }[]; + await Promise.all( + ( + await readdir(base_dir) + ).map(async (e) => { + const s = await stat(dir(`${g.datadir}/files/${rpath}/${e}`)); + files.push({ + name: e, + type: s.isDirectory() ? "dir" : "file", + size: s.size, + }); + }) + ); + return new Response(JSON.stringify(files), { + headers: { "content-type": "application/json" }, + }); + } catch (e) { + return new Response(JSON.stringify([]), { + headers: { "content-type": "application/json" }, + }); + } + } + } + + const path = dir(`${g.datadir}/files/${rpath}`); const file = Bun.file(path); if (await file.exists()) { diff --git a/pkgs/api/_upload.ts b/pkgs/api/_upload.ts index 119ecb8..df3a394 100644 --- a/pkgs/api/_upload.ts +++ b/pkgs/api/_upload.ts @@ -1,36 +1,74 @@ import mp from "@surfy/multipart-parser"; -import { writeAsync } from "fs-jetpack"; +import { format, parse } from "path"; import { apiContext } from "service-srv"; import { dir } from "utils/dir"; import { g } from "utils/global"; + export const _ = { url: "/_upload", + raw: true, async api(body: any) { const { req } = apiContext(this); - let url = ""; - const raw = await req.arrayBuffer(); const parts = mp(Buffer.from(raw)) as Record< string, { fileName: string; mime: string; type: string; buffer: Buffer } >; + const result: string[] = []; for (const [_, part] of Object.entries(parts)) { - const d = new Date(); - const path = `${d.getFullYear()}-${d.getMonth()}/${d.getDate()}/${d.getTime()}-${part.fileName - ?.replace(/[\W_]+/g, "-") - .toLowerCase()}`; - - url = `/_file/${path}`; - await writeAsync(dir(`${g.datadir}/upload/${path}`), part.buffer); + result.push(await saveFile(req, part.fileName, part.buffer)); } - return url; + return new Response(JSON.stringify(result), { + headers: { "content-type": "application/json" }, + }); }, }; -function toArrayBuffer(buffer: Buffer) { - return buffer.buffer.slice( - buffer.byteOffset, - buffer.byteOffset + buffer.byteLength - ); -} + +const saveFile = async ( + req: Request & { + params: any; + query_parameters: any; + }, + fname: string, + part: any +) => { + const d = new Date(); + let to: string = req.query_parameters["to"] || ""; + if (!to) { + to = `/upload/${d.getFullYear()}-${d.getMonth()}/${d.getDate()}/${d.getTime()}-${fname}`; + } else { + to = to + .split("/") + .map((e) => e.replace(/\.\./gi, "")) + .filter((e) => !!e) + .join("/"); + + to = to.endsWith("/") ? to + fname : to + "/" + fname; + } + to = to.toLowerCase(); + const pto = parse(to); + pto.name = pto.name.replace(/[\W_]+/gi, "-"); + to = format(pto); + + while (await Bun.file(dir(`${g.datadir}/files/${to}`)).exists()) { + const p = parse(to); + const arr = p.name.split("-"); + if (arr.length > 1) { + if (parseInt(arr[arr.length - 1])) { + arr[arr.length - 1] = parseInt(arr[arr.length - 1]) + 1 + ""; + } else { + arr.push("1"); + } + } else { + arr.push("1"); + } + p.name = arr.filter((e) => e).join("-"); + p.base = `${p.name}${p.ext}`; + + to = format(p); + } + await Bun.write(dir(`${g.datadir}/files/${to}`), part); + return to; +}; diff --git a/pkgs/package.json b/pkgs/package.json index f9b42b2..cdd0b07 100644 --- a/pkgs/package.json +++ b/pkgs/package.json @@ -15,6 +15,7 @@ "radix3": "^1.1.0", "typescript": "^5.2.2", "unzipper": "^0.10.14", + "parse-multipart-data": "^1.5.0", "fast-myers-diff": "^3.2.0" } } \ No newline at end of file diff --git a/pkgs/server/create.ts b/pkgs/server/create.ts index 8f05512..908aae5 100644 --- a/pkgs/server/create.ts +++ b/pkgs/server/create.ts @@ -27,6 +27,7 @@ export const createServer = async () => { const route = { url: api._.url, args, + raw: !!api._.raw, fn: api._.api, path: importPath.substring((root || path).length + 1), }; @@ -66,6 +67,7 @@ export const createServer = async () => { g.server = Bun.serve({ port: g.port, + maxRequestBodySize: 1024 * 1024 * 128, async fetch(req) { const url = new URL(req.url); diff --git a/pkgs/server/serve-api.ts b/pkgs/server/serve-api.ts index 6c7d312..87e5fef 100644 --- a/pkgs/server/serve-api.ts +++ b/pkgs/server/serve-api.ts @@ -20,7 +20,7 @@ export const serveAPI = async (url: URL, req: Request) => { return params[e]; }); - if (req.method !== "GET") { + if (req.method !== "GET" && !found.raw) { if (!req.headers.get("content-type")?.startsWith("multipart/form-data")) { try { const json = await req.json(); diff --git a/pkgs/utils/deploy.ts b/pkgs/utils/deploy.ts index fac54c6..8720370 100644 --- a/pkgs/utils/deploy.ts +++ b/pkgs/utils/deploy.ts @@ -17,7 +17,7 @@ export const deploy = { await this.load(this.config.deploy.ts); }, async load(ts: string) { - console.log(`Loading site: ${this.config.site_id} [ts: ${ts}]`); + console.log(`Loading site: ${this.config.site_id} ${ts}`); try { g.deploy.gz = JSON.parse( diff --git a/pkgs/utils/global.ts b/pkgs/utils/global.ts index 3591edc..9d5f141 100644 --- a/pkgs/utils/global.ts +++ b/pkgs/utils/global.ts @@ -10,6 +10,7 @@ import { prodIndex } from "./prod-index"; type SingleRoute = { url: string; args: string[]; + raw: boolean; fn: (...arg: any[]) => Promise; path: string; };