From 1b77a5851d06584c46669e12abfb1704d9a0eb6c Mon Sep 17 00:00:00 2001 From: Rizky Date: Thu, 22 Feb 2024 04:55:01 +0700 Subject: [PATCH] wip fix prasi api --- bun.lockb | Bin 127730 -> 128118 bytes pkgs/api/_file.ts | 113 +++++++++++++++++++++++++++++++++++++-- pkgs/api/_upload.ts | 72 +++++++++++++++++++------ pkgs/package.json | 1 + pkgs/server/create.ts | 2 + pkgs/server/serve-api.ts | 2 +- pkgs/utils/deploy.ts | 2 +- pkgs/utils/global.ts | 1 + 8 files changed, 169 insertions(+), 24 deletions(-) diff --git a/bun.lockb b/bun.lockb index ddbf6dab532715f04739503682544252bf81508b..29ba696031d59eaefa4b87628ffddd635a958a8b 100755 GIT binary patch delta 6446 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 DiqP`T 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; };