checkpoint
This commit is contained in:
parent
f591d261eb
commit
9a0523876c
|
|
@ -0,0 +1,35 @@
|
|||
import { gunzipSync } from "bun";
|
||||
import { unpack } from "msgpackr";
|
||||
import { apiContext } from "utils/api-context";
|
||||
import { execQuery } from "utils/query";
|
||||
|
||||
const g = global as any;
|
||||
export const _ = {
|
||||
url: "/_dbs/*",
|
||||
raw: true,
|
||||
async api() {
|
||||
const ctx = apiContext(this);
|
||||
const { req, res } = ctx;
|
||||
if (typeof g.db !== "undefined") {
|
||||
if (req.params._ === "check") {
|
||||
return { mode: "encrypted" };
|
||||
}
|
||||
|
||||
try {
|
||||
const body = unpack(gunzipSync(await req.arrayBuffer()));
|
||||
|
||||
try {
|
||||
const result = await execQuery(body, g.db);
|
||||
return result;
|
||||
} catch (e: any) {
|
||||
console.log("_dbs error", body, e.message);
|
||||
res.sendStatus(500);
|
||||
res.send(e.message);
|
||||
}
|
||||
} catch (e) {
|
||||
res.sendStatus(500);
|
||||
res.send('{status: "unauthorized"}');
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
import { $ } from "bun";
|
||||
import { readdirSync } from "fs";
|
||||
import { readAsync, removeAsync, writeAsync } from "fs-jetpack";
|
||||
import { apiContext } from "utils/api-context";
|
||||
import { config as _config, type SiteConfig } from "utils/config";
|
||||
import { fs } from "utils/fs";
|
||||
import { genEnv, parseEnv } from "utils/parse-env";
|
||||
|
||||
export const _ = {
|
||||
url: "/_deploy",
|
||||
async api(
|
||||
action: (
|
||||
| { type: "check" }
|
||||
| { type: "db-update"; url: string; orm: SiteConfig["db"]["orm"] }
|
||||
| { type: "db-pull" }
|
||||
| { type: "db-gen" }
|
||||
| { type: "db-ver" }
|
||||
| { type: "db-sync"; url: string }
|
||||
| { type: "restart" }
|
||||
| { type: "domain-add"; domain: string }
|
||||
| { type: "domain-del"; domain: string }
|
||||
| { type: "deploy-del"; ts: string }
|
||||
| { type: "deploy"; load_from?: string }
|
||||
| { type: "deploy-status" }
|
||||
| { type: "redeploy"; ts: string }
|
||||
) & {
|
||||
id_site: string;
|
||||
}
|
||||
) {
|
||||
const { res, req } = apiContext(this);
|
||||
const deploy = _config.current?.deploy!;
|
||||
const config = _config.current!;
|
||||
|
||||
if (typeof req.query_parameters["export"] !== "undefined") {
|
||||
return new Response(
|
||||
Bun.file(fs.path(`site:deploy/current/${deploy.current}.gz`))
|
||||
);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case "check":
|
||||
const deploys = readdirSync(fs.path("site:deploy/history"));
|
||||
|
||||
return {
|
||||
now: Date.now(),
|
||||
current: deploy.current,
|
||||
deploys: deploys
|
||||
.filter((e) => e.endsWith(".gz"))
|
||||
.map((e) => parseInt(e.replace(".gz", ""))),
|
||||
db: {
|
||||
url: config.db.url,
|
||||
orm: config.db.orm,
|
||||
},
|
||||
};
|
||||
case "db-ver": {
|
||||
return (await fs.read(`site:app/db/version`, "string")) || "";
|
||||
}
|
||||
case "db-sync": {
|
||||
const res = await fetch(action.url);
|
||||
const text = await res.text();
|
||||
if (text) {
|
||||
await Bun.write(fs.path("site:app/db/prisma/schema.prisma"), text);
|
||||
await Bun.write(
|
||||
fs.path(`site:app/db/version`),
|
||||
Date.now().toString()
|
||||
);
|
||||
}
|
||||
return "ok";
|
||||
}
|
||||
case "db-update":
|
||||
if (action.url) {
|
||||
config.db.url = action.url;
|
||||
config.db.orm = action.orm;
|
||||
const env = genEnv({
|
||||
...parseEnv(await Bun.file(fs.path("site:app/db/.env")).text()),
|
||||
DATABASE_URL: action.url,
|
||||
});
|
||||
await Bun.write(fs.path("site:app/db/.env"), env);
|
||||
}
|
||||
return "ok";
|
||||
case "db-gen":
|
||||
{
|
||||
await $`bun prisma generate`.cwd(fs.path("site:app/db"));
|
||||
|
||||
res.send("ok");
|
||||
setTimeout(() => {
|
||||
// restartServer();
|
||||
}, 300);
|
||||
}
|
||||
break;
|
||||
case "db-pull":
|
||||
{
|
||||
let env = await readAsync(fs.path("site:app/db/.env"));
|
||||
if (env) {
|
||||
const ENV = parseEnv(env);
|
||||
if (typeof ENV.DATABASE_URL === "string") {
|
||||
const type = ENV.DATABASE_URL.split("://").shift();
|
||||
if (type) {
|
||||
await writeAsync(
|
||||
fs.path("site:app/db/prisma/schema.prisma"),
|
||||
`\
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "${type}"
|
||||
url = env("DATABASE_URL")
|
||||
}`
|
||||
);
|
||||
|
||||
try {
|
||||
await Bun.write(
|
||||
fs.path("site:app/db/.env"),
|
||||
`DATABASE_URL=${ENV.DATABASE_URL}`
|
||||
);
|
||||
await $`bun install`.cwd(fs.path("site:app/db"));
|
||||
await $`bun prisma db pull --force`.cwd(
|
||||
fs.path("site:app/db")
|
||||
);
|
||||
await $`bun prisma generate`.cwd(fs.path("site:app/db"));
|
||||
await Bun.write(
|
||||
fs.path(`site:app/db/version`),
|
||||
Date.now().toString()
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
res.send("ok");
|
||||
setTimeout(() => {
|
||||
// restartServer();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "restart":
|
||||
{
|
||||
res.send("ok");
|
||||
setTimeout(() => {
|
||||
// restartServer();
|
||||
}, 300);
|
||||
}
|
||||
break;
|
||||
case "deploy-del":
|
||||
{
|
||||
await removeAsync(fs.path(`site:deploy/history/${action.ts}.gz`));
|
||||
const deploys = readdirSync(fs.path(`site:deploy/history`));
|
||||
|
||||
return {
|
||||
now: Date.now(),
|
||||
current: deploy.current,
|
||||
deploys: deploys
|
||||
.filter((e) => e.endsWith(".gz"))
|
||||
.map((e) => parseInt(e.replace(".gz", ""))),
|
||||
};
|
||||
}
|
||||
break;
|
||||
case "deploy-status":
|
||||
break;
|
||||
case "deploy":
|
||||
{
|
||||
await _config.init("site:site.json");
|
||||
await _config.set("site_id", action.id_site);
|
||||
fs.init(config);
|
||||
|
||||
return {
|
||||
now: Date.now(),
|
||||
current: deploy.current,
|
||||
deploys: config.deploy.history,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case "redeploy":
|
||||
{
|
||||
// deploy.config.deploy.ts = action.ts;
|
||||
// await deploy.saveConfig();
|
||||
// await deploy.load(action.ts);
|
||||
// const deploys = fs.readdirSync(dir(`/app/web/deploy`));
|
||||
|
||||
// return {
|
||||
// now: Date.now(),
|
||||
// current: parseInt(deploy.config.deploy.ts),
|
||||
// deploys: deploys
|
||||
// .filter((e) => e.endsWith(".gz"))
|
||||
// .map((e) => parseInt(e.replace(".gz", ""))),
|
||||
// };
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const downloadFile = async (
|
||||
url: string,
|
||||
filePath: string,
|
||||
progress?: (rec: number, total: number) => void
|
||||
) => {
|
||||
try {
|
||||
const _url = new URL(url);
|
||||
if (_url.hostname === "localhost") {
|
||||
_url.hostname = "127.0.0.1";
|
||||
}
|
||||
// g.log.info(`Downloading ${url} to ${filePath}`);
|
||||
const res = await fetch(_url as any);
|
||||
if (res.body) {
|
||||
const file = Bun.file(filePath);
|
||||
|
||||
const writer = file.writer();
|
||||
const reader = res.body.getReader();
|
||||
|
||||
// Step 3: read the data
|
||||
let receivedLength = 0; // received that many bytes at the moment
|
||||
let chunks = []; // array of received binary chunks (comprises the body)
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
writer.end();
|
||||
break;
|
||||
}
|
||||
|
||||
chunks.push(value);
|
||||
writer.write(value);
|
||||
receivedLength += value.length;
|
||||
|
||||
if (progress) {
|
||||
progress(
|
||||
receivedLength,
|
||||
parseInt(res.headers.get("content-length") || "0")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
import mime from "mime";
|
||||
import { apiContext } from "utils/api-context";
|
||||
import { dir } from "utils/dir";
|
||||
import { g } from "utils/global";
|
||||
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);
|
||||
let rpath = decodeURIComponent(req.params._);
|
||||
rpath = rpath
|
||||
.split("/")
|
||||
.map((e) => e.replace(/\.\./gi, ""))
|
||||
.filter((e) => !!e)
|
||||
.join("/");
|
||||
|
||||
let res = new Response("NOT FOUND", { status: 404 });
|
||||
|
||||
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)) {
|
||||
const s = await stat(base_dir);
|
||||
if (s.isDirectory()) {
|
||||
if ((await readdir(base_dir)).length === 0) {
|
||||
await removeAsync(base_dir);
|
||||
}
|
||||
} else {
|
||||
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(null), {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const path = dir(`${g.datadir}/files/${rpath}`);
|
||||
const file = Bun.file(path);
|
||||
|
||||
if (await file.exists()) {
|
||||
res = new Response(file);
|
||||
} else {
|
||||
res = new Response("NOT FOUND", { status: 404 });
|
||||
}
|
||||
|
||||
const arr = path.split("-");
|
||||
const ext = arr.pop();
|
||||
const fname = arr.join("-") + "." + ext;
|
||||
const ctype = mime.getType(fname);
|
||||
if (ctype) {
|
||||
res.headers.set("content-disposition", `inline; filename="${fname}"`);
|
||||
res.headers.set("content-type", ctype);
|
||||
}
|
||||
|
||||
res.headers.set("Access-Control-Allow-Origin", "*");
|
||||
res.headers.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT");
|
||||
res.headers.set("Access-Control-Allow-Headers", "content-type");
|
||||
res.headers.set("Access-Control-Allow-Credentials", "true");
|
||||
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import mime from "mime";
|
||||
import { apiContext } from "utils/api-context";
|
||||
import { fs } from "utils/fs";
|
||||
|
||||
export const _ = {
|
||||
url: "/_finfo/**",
|
||||
async api() {
|
||||
const { req } = apiContext(this);
|
||||
let rpath = decodeURIComponent(req.params._);
|
||||
rpath = rpath
|
||||
.split("/")
|
||||
.map((e) => e.replace(/\.\./gi, ""))
|
||||
.filter((e) => !!e)
|
||||
.join("/");
|
||||
|
||||
let res = new Response("NOT FOUND", { status: 404 });
|
||||
|
||||
const path = fs.path(`upload:${rpath}`);
|
||||
const file = Bun.file(path);
|
||||
|
||||
if (await file.exists()) {
|
||||
const arr = (path.split("/").pop() || "").split("-");
|
||||
const ctype = mime.getType(path);
|
||||
const ext = mime.getExtension(ctype || "");
|
||||
const fname = arr.join("-") + "." + ext;
|
||||
|
||||
res = new Response(
|
||||
JSON.stringify({
|
||||
filename: fname,
|
||||
fullpath: path,
|
||||
size: formatFileSize(file.size),
|
||||
mime: ctype,
|
||||
ext,
|
||||
}),
|
||||
{
|
||||
headers: { "content-type": "application/json" },
|
||||
}
|
||||
);
|
||||
} else {
|
||||
res = new Response("null", {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
function formatFileSize(bytes: number) {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { apiContext } from "utils/api-context";
|
||||
|
||||
const g = global as unknown as {
|
||||
_font_cache: Record<string, { body: any; headers: any }>;
|
||||
};
|
||||
|
||||
if (!g._font_cache) {
|
||||
g._font_cache = {};
|
||||
}
|
||||
|
||||
export const _ = {
|
||||
url: "/_font/**",
|
||||
async api() {
|
||||
const { req } = apiContext(this);
|
||||
const pathname = req.url.split("/_font").pop() || "";
|
||||
const cache = g._font_cache[pathname];
|
||||
if (cache) {
|
||||
if (req.headers.get("accept-encoding")?.includes("gzip")) {
|
||||
return new Response(Bun.gzipSync(cache.body), {
|
||||
headers: {
|
||||
"content-type": cache.headers["content-type"],
|
||||
"content-encoding": "gzip",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return new Response(cache.body, {
|
||||
headers: {
|
||||
"content-type": cache.headers["content-type"],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
let f: Response = null as any;
|
||||
let raw = false;
|
||||
if (pathname?.startsWith("/s/")) {
|
||||
f = await fetch(`https://fonts.gstatic.com${pathname}`);
|
||||
raw = true;
|
||||
} else {
|
||||
f = await fetch(`https://fonts.googleapis.com${pathname}`);
|
||||
}
|
||||
if (f) {
|
||||
let body = null as any;
|
||||
|
||||
if (!raw) {
|
||||
body = await f.text();
|
||||
body = body.replaceAll("https://fonts.gstatic.com", "/_font");
|
||||
} else {
|
||||
body = await f.arrayBuffer();
|
||||
}
|
||||
|
||||
g._font_cache[pathname] = { body, headers: {} };
|
||||
f.headers.forEach((v, k) => {
|
||||
g._font_cache[pathname].headers[k] = v;
|
||||
});
|
||||
|
||||
const res = new Response(body);
|
||||
|
||||
res.headers.set("content-type", f.headers.get("content-type") || "");
|
||||
return res;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import { dirAsync } from "fs-jetpack";
|
||||
import { apiContext } from "utils/api-context";
|
||||
import { stat } from "fs/promises";
|
||||
import { dir } from "utils/dir";
|
||||
import { g } from "utils/global";
|
||||
import { dirname, parse } from "path";
|
||||
import sharp from "sharp";
|
||||
|
||||
const modified = {} as Record<string, number>;
|
||||
|
||||
export const _ = {
|
||||
url: "/_img/**",
|
||||
async api() {
|
||||
const { req } = apiContext(this);
|
||||
let res = new Response("NOT FOUND", { status: 404 });
|
||||
|
||||
const w = parseInt(req.query_parameters.w);
|
||||
const h = parseInt(req.query_parameters.h);
|
||||
const fit = req.query_parameters.fit;
|
||||
let force = typeof req.query_parameters.force === "string";
|
||||
|
||||
let rpath = decodeURIComponent(req.params._);
|
||||
rpath = rpath
|
||||
.split("/")
|
||||
.map((e) => e.replace(/\.\./gi, ""))
|
||||
.filter((e) => !!e)
|
||||
.join("/");
|
||||
|
||||
try {
|
||||
const filepath = dir(`${g.datadir}/files/${rpath}`);
|
||||
const st = await stat(filepath);
|
||||
if (st.isFile()) {
|
||||
if (
|
||||
!modified[filepath] ||
|
||||
(modified[filepath] && modified[filepath] !== st.mtimeMs)
|
||||
) {
|
||||
modified[filepath] = st.mtimeMs;
|
||||
force = true;
|
||||
}
|
||||
|
||||
if (!w && !h) {
|
||||
const file = Bun.file(filepath);
|
||||
return new Response(file);
|
||||
} else {
|
||||
const original = Bun.file(filepath);
|
||||
|
||||
const p = parse(filepath);
|
||||
if (p.ext === ".svg") {
|
||||
return new Response(original);
|
||||
}
|
||||
|
||||
let path = `${w ? `w-${w}` : ""}${h ? `h-${h}` : ``}${
|
||||
fit ? `-${fit}` : ""
|
||||
}`;
|
||||
let file_name = dir(
|
||||
`${g.datadir}/files/upload/thumb/${path}/${rpath}.webp`
|
||||
);
|
||||
let file = Bun.file(file_name);
|
||||
if (!(await file.exists())) {
|
||||
await dirAsync(dirname(file_name));
|
||||
force = true;
|
||||
}
|
||||
|
||||
if (force) {
|
||||
const img = sharp(await original.arrayBuffer());
|
||||
const arg: any = { fit: fit || "inside" };
|
||||
if (w) {
|
||||
arg.width = w;
|
||||
}
|
||||
if (h) {
|
||||
arg.height = h;
|
||||
}
|
||||
let out = img.resize(arg).webp({ quality: 75 });
|
||||
out = out.webp();
|
||||
|
||||
await Bun.write(file_name, new Uint8Array(await out.toBuffer()));
|
||||
file = Bun.file(file_name);
|
||||
}
|
||||
|
||||
return new Response(file);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
return new Response("ERROR:" + e.message, { status: 404 });
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { BunSqliteKeyValue } from "pkgs/utils/kv";
|
||||
import { apiContext } from "utils/api-context";
|
||||
import { dir } from "utils/dir";
|
||||
import { g } from "utils/global";
|
||||
|
||||
export const _ = {
|
||||
url: "/_kv",
|
||||
raw: true,
|
||||
async api(mode: "get" | "set" | "del", key: string, value?: any) {
|
||||
const { req } = apiContext(this);
|
||||
|
||||
if (!g.kv) {
|
||||
g.kv = new BunSqliteKeyValue(dir(`${g.datadir}/db-kv.sqlite`));
|
||||
}
|
||||
|
||||
try {
|
||||
const parts = (await req.json()) as [string, string, any];
|
||||
switch (parts[0]) {
|
||||
case "set": {
|
||||
if (typeof parts[1] === "string") {
|
||||
g.kv.set(parts[1], parts[2]);
|
||||
|
||||
return new Response(JSON.stringify({ status: "ok" }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ status: "failed", reason: "no key or body" }),
|
||||
{
|
||||
headers: { "content-type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
case "get": {
|
||||
if (parts[2]) {
|
||||
g.kv.set(parts[1], parts[2]);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(g.kv.get(parts[1]) || null), {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
case "del": {
|
||||
if (parts[1]) {
|
||||
g.kv.delete(parts[1]);
|
||||
|
||||
return new Response(JSON.stringify({ status: "ok" }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return new Response(JSON.stringify({ status: "failed" }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import { Database } from "bun:sqlite";
|
||||
import admin from "firebase-admin";
|
||||
import { listAsync } from "fs-jetpack";
|
||||
import { apiContext } from "utils/api-context";
|
||||
|
||||
import { dir } from "utils/dir";
|
||||
import { g } from "utils/global";
|
||||
|
||||
export const _ = {
|
||||
url: "/_notif/:action/:token",
|
||||
async api(
|
||||
action: string,
|
||||
data:
|
||||
| { type: "register"; token: string; id: string }
|
||||
| { type: "send"; id: string; body: string; title: string; data?: any }
|
||||
) {
|
||||
const { req } = apiContext(this);
|
||||
|
||||
if (action === "list") {
|
||||
return await listAsync(dir("public"));
|
||||
}
|
||||
|
||||
if (!g.firebaseInit) {
|
||||
g.firebaseInit = true;
|
||||
|
||||
try {
|
||||
g.firebase = admin.initializeApp({
|
||||
credential: admin.credential.cert(dir("public/firebase-admin.json")),
|
||||
});
|
||||
g.notif = {
|
||||
db: new Database(dir(`${g.datadir}/notif.sqlite`)),
|
||||
};
|
||||
|
||||
g.notif.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS notif (
|
||||
token TEXT PRIMARY KEY,
|
||||
id TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (g.firebase) {
|
||||
switch (action) {
|
||||
case "register":
|
||||
{
|
||||
if (data && data.type === "register" && data.id) {
|
||||
if (data.token) {
|
||||
const q = g.notif.db.query(
|
||||
`SELECT * FROM notif WHERE token = '${data.token}'`
|
||||
);
|
||||
const result = q.all();
|
||||
if (result.length > 0) {
|
||||
g.notif.db.exec(
|
||||
`UPDATE notif SET id = '${data.id}' WHERE token = '${data.token}'`
|
||||
);
|
||||
} else {
|
||||
g.notif.db.exec(
|
||||
`INSERT INTO notif VALUES ('${data.token}', '${data.id}')`
|
||||
);
|
||||
}
|
||||
|
||||
return { result: "OK" };
|
||||
} else {
|
||||
return { error: "missing token" };
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "send":
|
||||
{
|
||||
if (data && data.type === "send") {
|
||||
const q = g.notif.db.query<{ token: string }, any>(
|
||||
`SELECT * FROM notif WHERE id = '${data.id}'`
|
||||
);
|
||||
let result = q.all();
|
||||
for (const c of result) {
|
||||
try {
|
||||
await g.firebase.messaging().send({
|
||||
notification: { body: data.body, title: data.title },
|
||||
data: data.data,
|
||||
token: c.token,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
result = result.filter((v) => v.token !== c.token);
|
||||
}
|
||||
}
|
||||
|
||||
return { result: "OK", totalDevice: result.length };
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { error: "missing ./firebase-admin.json" };
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
import { apiContext, createResponse } from "service-srv";
|
||||
import { SinglePage, g } from "utils/global";
|
||||
import { gzipAsync } from "utils/gzip";
|
||||
import { getContent } from "../server/prep-api-ts";
|
||||
import mime from "mime";
|
||||
|
||||
const cache = {
|
||||
route: null as any,
|
||||
comps: {} as Record<string, any>,
|
||||
};
|
||||
export const _ = {
|
||||
url: "/_prasi/**",
|
||||
async api() {
|
||||
const { req, res } = apiContext(this);
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Headers", "content-type");
|
||||
const gz = g.deploy.content;
|
||||
const parts = req.params._.split("/");
|
||||
|
||||
const action = {
|
||||
_: () => {
|
||||
res.send({ prasi: "v2" });
|
||||
},
|
||||
compress: async () => {
|
||||
const last = parts.pop();
|
||||
if (last === "all") {
|
||||
g.compress.mode = "all";
|
||||
}
|
||||
if (last === "only-gz") {
|
||||
g.compress.mode = "only-gz";
|
||||
}
|
||||
if (last === "off") {
|
||||
g.compress.mode = "off";
|
||||
}
|
||||
},
|
||||
code: async () => {
|
||||
if (gz) {
|
||||
const path = parts.slice(1).join("/");
|
||||
if (gz.code.site[path]) {
|
||||
const type = mime.getType(path);
|
||||
if (type) res.setHeader("content-type", type);
|
||||
res.send(
|
||||
gz.code.site[path],
|
||||
req.headers.get("accept-encoding") || ""
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
route: async () => {
|
||||
if (gz) {
|
||||
if (cache.route) return await responseCompressed(req, cache.route);
|
||||
|
||||
let layout = null as null | SinglePage;
|
||||
for (const l of gz.layouts) {
|
||||
if (!layout) layout = l;
|
||||
if (l.is_default_layout) layout = l;
|
||||
}
|
||||
|
||||
cache.route = JSON.stringify({
|
||||
site: { ...gz.site, api_url: (gz.site as any)?.config?.api_url },
|
||||
urls: gz.pages.map((e) => {
|
||||
return { id: e.id, url: e.url };
|
||||
}),
|
||||
layout: {
|
||||
id: layout?.id,
|
||||
root: layout?.content_tree,
|
||||
},
|
||||
});
|
||||
|
||||
return await responseCompressed(req, cache.route);
|
||||
}
|
||||
},
|
||||
page: async () => {
|
||||
const page = g.deploy.pages[parts[1]];
|
||||
if (page) {
|
||||
const res = createResponse(
|
||||
JSON.stringify({
|
||||
id: page.id,
|
||||
root: page.content_tree,
|
||||
url: page.url,
|
||||
}),
|
||||
{
|
||||
cache_accept: req.headers.get("accept-encoding") || "",
|
||||
high_compression: true,
|
||||
}
|
||||
);
|
||||
return res;
|
||||
}
|
||||
},
|
||||
pages: async () => {
|
||||
const pages = [];
|
||||
if (req.params.ids) {
|
||||
for (const id of req.params.ids) {
|
||||
const page = g.deploy.pages[id];
|
||||
if (page) {
|
||||
pages.push({
|
||||
id: page.id,
|
||||
root: page.content_tree,
|
||||
url: page.url,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await responseCompressed(req, JSON.stringify(pages));
|
||||
},
|
||||
comp: async () => {
|
||||
const comps = {} as Record<string, any>;
|
||||
|
||||
const pending = new Set<string>();
|
||||
if (req.params.ids) {
|
||||
for (const id of req.params.ids) {
|
||||
const comp = g.deploy.comps[id];
|
||||
if (comp) {
|
||||
comps[id] = comp;
|
||||
} else if (cache.comps[id]) {
|
||||
comps[id] = cache.comps[id];
|
||||
} else {
|
||||
pending.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pending.size > 0) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://prasi.avolut.com/prod/452e91b8-c474-4ed2-9c43-447ac8778aa8/_prasi/comp`,
|
||||
{ method: "POST", body: JSON.stringify({ ids: [...pending] }) }
|
||||
);
|
||||
for (const [k, v] of Object.entries((await res.json()) as any)) {
|
||||
cache.comps[k] = v;
|
||||
comps[k] = v;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return createResponse(JSON.stringify(comps), {
|
||||
cache_accept: req.headers.get("accept-encoding") || "",
|
||||
high_compression: true,
|
||||
});
|
||||
},
|
||||
"load.json": async () => {
|
||||
res.setHeader("content-type", "application/json");
|
||||
res.send(
|
||||
await getContent("load.json"),
|
||||
req.headers.get("accept-encoding") || ""
|
||||
);
|
||||
},
|
||||
"load.js": async () => {
|
||||
res.setHeader("content-type", "text/javascript");
|
||||
|
||||
const url = req.query_parameters["url"]
|
||||
? JSON.stringify(req.query_parameters["url"])
|
||||
: "undefined";
|
||||
|
||||
if (req.query_parameters["dev"]) {
|
||||
res.send(
|
||||
await getContent("load.js.dev", url),
|
||||
req.headers.get("accept-encoding") || ""
|
||||
);
|
||||
} else {
|
||||
res.send(
|
||||
await getContent("load.js.prod", url),
|
||||
req.headers.get("accept-encoding") || ""
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const pathname: keyof typeof action = parts[0] as any;
|
||||
const run = action[pathname];
|
||||
|
||||
if (run) {
|
||||
return await run();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const responseCompressed = async (req: Request, body: string) => {
|
||||
if (req.headers.get("accept-encoding")?.includes("gz")) {
|
||||
return new Response(await gzipAsync(body), {
|
||||
headers: { "content-encoding": "gzip" },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(body);
|
||||
};
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { apiContext } from "utils/api-context";
|
||||
|
||||
export const _ = {
|
||||
url: "/_proxy/**",
|
||||
raw: true,
|
||||
async api() {
|
||||
const { req } = apiContext(this);
|
||||
|
||||
try {
|
||||
const raw_url = decodeURIComponent(req.params["_"]);
|
||||
const url = new URL(raw_url) as URL;
|
||||
const body = await req.arrayBuffer();
|
||||
const headers = {} as Record<string, string>;
|
||||
req.headers.forEach((v, k) => {
|
||||
if (k.startsWith("sec-")) return;
|
||||
if (k.startsWith("connection")) return;
|
||||
if (k.startsWith("dnt")) return;
|
||||
if (k.startsWith("host")) return;
|
||||
headers[k] = v;
|
||||
});
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: req.method || "POST",
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
if (res.headers.get("content-encoding")) {
|
||||
res.headers.delete("content-encoding");
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
status: "failed",
|
||||
reason: e.message,
|
||||
}),
|
||||
{
|
||||
status: 403,
|
||||
headers: { "content-type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import mp from "@surfy/multipart-parser";
|
||||
import { dirAsync, existsAsync } from "fs-jetpack";
|
||||
import { format, parse, dirname } from "path";
|
||||
import { apiContext } from "utils/api-context";
|
||||
import { dir } from "utils/dir";
|
||||
import { g } from "utils/global";
|
||||
|
||||
export const _ = {
|
||||
url: "/_upload",
|
||||
raw: true,
|
||||
async api(body: any) {
|
||||
const { req } = apiContext(this);
|
||||
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)) {
|
||||
result.push(await saveFile(req, part.fileName, part.buffer));
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
if (await existsAsync(dirname(to))) {
|
||||
dirAsync(dirname(to));
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import { $ } from "bun";
|
||||
import Database from "bun:sqlite";
|
||||
import { copyAsync } from "fs-jetpack";
|
||||
import mime from "mime";
|
||||
import { deploy } from "utils/deploy";
|
||||
import { dir } from "utils/dir";
|
||||
import { g, SinglePage } from "utils/global";
|
||||
import { getContent } from "../server/prep-api-ts";
|
||||
|
||||
export const _ = {
|
||||
url: "/_zip",
|
||||
raw: true,
|
||||
async api() {
|
||||
await $`rm bundle*`.nothrow().quiet().cwd(`${g.datadir}`);
|
||||
await copyAsync(
|
||||
dir(`pkgs/empty_bundle.sqlite`),
|
||||
dir(`${g.datadir}/bundle.sqlite`)
|
||||
);
|
||||
const db = new Database(dir(`${g.datadir}/bundle.sqlite`));
|
||||
|
||||
const ts = g.deploy.config.deploy.ts;
|
||||
const add = ({
|
||||
path,
|
||||
type,
|
||||
content,
|
||||
}: {
|
||||
path: string;
|
||||
type: string;
|
||||
content: string | Buffer;
|
||||
}) => {
|
||||
if (path) {
|
||||
const query = db.query(
|
||||
"INSERT INTO files (path, type, content) VALUES ($path, $type, $content)"
|
||||
);
|
||||
query.run({
|
||||
$path: path.startsWith(g.datadir)
|
||||
? path.substring(`${g.datadir}/bundle`.length)
|
||||
: path,
|
||||
$type: type,
|
||||
$content: content,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
add({ path: "version", type: "", content: deploy.config.deploy.ts + "" });
|
||||
add({ path: "site_id", type: "", content: deploy.config.site_id + "" });
|
||||
add({
|
||||
path: "base_url",
|
||||
type: "",
|
||||
content: g.deploy.content?.site?.config?.api_url || "",
|
||||
});
|
||||
const gz = g.deploy.content;
|
||||
|
||||
if (gz) {
|
||||
let layout = null as null | SinglePage;
|
||||
for (const l of gz.layouts) {
|
||||
if (!layout) layout = l;
|
||||
if (l.is_default_layout) layout = l;
|
||||
}
|
||||
|
||||
let api_url = (gz.site as any)?.config?.api_url;
|
||||
|
||||
add({
|
||||
path: "route",
|
||||
type: "",
|
||||
content: JSON.stringify({
|
||||
site: {
|
||||
...gz.site,
|
||||
api_url,
|
||||
},
|
||||
urls: gz.pages.map((e) => {
|
||||
return { id: e.id, url: e.url };
|
||||
}),
|
||||
layout: {
|
||||
id: layout?.id,
|
||||
root: layout?.content_tree,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
add({
|
||||
path: "load-js",
|
||||
type: "",
|
||||
content: await getContent("load.js.prod", `"${api_url}"`),
|
||||
});
|
||||
}
|
||||
|
||||
for (const [directory, files] of Object.entries(g.deploy.content || {})) {
|
||||
if (directory !== "code" && directory !== "site") {
|
||||
for (const comp of Object.values(files) as any) {
|
||||
let filepath = `${g.datadir}/bundle/${directory}/${comp.id}.json`;
|
||||
|
||||
add({
|
||||
path: filepath,
|
||||
type: mime.getType(filepath) || "text/plain",
|
||||
content: JSON.stringify(comp),
|
||||
});
|
||||
}
|
||||
} else if (directory === "site") {
|
||||
const filepath = `${g.datadir}/bundle/${directory}.json`;
|
||||
add({
|
||||
path: filepath,
|
||||
type: mime.getType(filepath) || "text/plain",
|
||||
content: JSON.stringify(files),
|
||||
});
|
||||
} else {
|
||||
for (const [filename, content] of Object.entries(files)) {
|
||||
let filepath = `${g.datadir}/bundle/${directory}/${filename}`;
|
||||
|
||||
if (content instanceof Buffer || typeof content === "string") {
|
||||
add({
|
||||
path: filepath,
|
||||
type: mime.getType(filepath) || "text/plain",
|
||||
content,
|
||||
});
|
||||
} else {
|
||||
for (const [k, v] of Object.entries(content || {})) {
|
||||
filepath = `${g.datadir}/bundle/${directory}/${filename}/${k}`;
|
||||
if (v instanceof Buffer || typeof v === "string") {
|
||||
add({
|
||||
path: filepath,
|
||||
type: mime.getType(filepath) || "text/plain",
|
||||
content: v,
|
||||
});
|
||||
} else {
|
||||
add({
|
||||
path: filepath,
|
||||
type: mime.getType(filepath) || "text/plain",
|
||||
content: JSON.stringify(v),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await $`zip "bundle-${ts}.zip" bundle.sqlite`
|
||||
.nothrow()
|
||||
.quiet()
|
||||
.cwd(`${g.datadir}`);
|
||||
return new Response(Bun.file(`${g.datadir}/bundle-${ts}.zip`));
|
||||
},
|
||||
};
|
||||
|
|
@ -4,9 +4,12 @@ import type { ServerCtx } from "utils/server-ctx";
|
|||
import { prasiContent } from "../content/content";
|
||||
import { prasi_content_deploy } from "../content/content-deploy";
|
||||
import { prasi_content_ipc } from "../content/content-ipc";
|
||||
import { fs } from "utils/fs";
|
||||
|
||||
startup("site", async () => {
|
||||
await config.init("site:site.json");
|
||||
fs.init(config.current!);
|
||||
|
||||
if (g.mode === "site") {
|
||||
g.prasi = g.ipc ? prasi_content_ipc : prasi_content_deploy;
|
||||
|
||||
|
|
@ -19,6 +22,18 @@ startup("site", async () => {
|
|||
|
||||
const startSiteServer = async () => {
|
||||
if (g.mode === "site") {
|
||||
let port = 0;
|
||||
if (g.ipc) {
|
||||
try {
|
||||
const runtime = (await fs.read("site:runtime.json", "json")) as {
|
||||
port: number;
|
||||
};
|
||||
port = runtime.port;
|
||||
} catch (e) {}
|
||||
} else {
|
||||
port = config.current?.port || 3000;
|
||||
}
|
||||
|
||||
g.server = Bun.serve({
|
||||
async fetch(req, server) {
|
||||
const content = prasiContent();
|
||||
|
|
@ -40,7 +55,13 @@ const startSiteServer = async () => {
|
|||
}
|
||||
},
|
||||
websocket: { message(ws, message) {} },
|
||||
port: 0,
|
||||
port,
|
||||
reusePort: true,
|
||||
});
|
||||
|
||||
if (g.ipc && g.server.port) {
|
||||
await g.ipc.backend?.server?.init?.({ port: g.server.port });
|
||||
await fs.write("site:runtime.json", { port: g.server.port });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import { prasi_content_deploy } from "./content/content-deploy";
|
|||
import { ensureDBReady } from "./db/ensure";
|
||||
import { ensureServerReady } from "./server/ensure";
|
||||
import { startServer } from "./server/start";
|
||||
import { removeAsync } from "fs-jetpack";
|
||||
|
||||
const is_dev = process.argv.includes("--dev");
|
||||
const is_ipc = process.argv.includes("--ipc");
|
||||
startup("supervisor", async () => {
|
||||
console.log(`${c.green}Prasi Server:${c.esc} ${fs.path("site:")}`);
|
||||
await config.init("site:site.json");
|
||||
await removeAsync(fs.path(`site:runtime.json`));
|
||||
fs.init(config.current!);
|
||||
|
||||
if (!is_ipc) {
|
||||
const site_id = config.get("site_id") as string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
import mime from "mime";
|
||||
import { binaryExtensions } from "./binary-ext";
|
||||
|
||||
const parseQueryParams = (ctx: any) => {
|
||||
const pageHref = ctx.req.url;
|
||||
const searchParams = new URLSearchParams(
|
||||
pageHref.substring(pageHref.indexOf("?"))
|
||||
);
|
||||
const result: any = {};
|
||||
searchParams.forEach((v, k) => {
|
||||
result[k] = v;
|
||||
});
|
||||
|
||||
return result as any;
|
||||
};
|
||||
export const apiContext = (ctx: any) => {
|
||||
ctx.req.params = ctx.params;
|
||||
|
||||
if (ctx.params["_0"]) {
|
||||
ctx.params["_"] = ctx.params["_0"];
|
||||
delete ctx.params["_0"];
|
||||
}
|
||||
|
||||
ctx.req.query_parameters = parseQueryParams(ctx);
|
||||
return {
|
||||
req: ctx.req as Request & { params: any; query_parameters: any },
|
||||
res: {
|
||||
...ctx.res,
|
||||
send: (body) => {
|
||||
ctx.res = createResponse(body, { res: ctx.res });
|
||||
},
|
||||
sendStatus: (code: number) => {
|
||||
ctx.res._status = code;
|
||||
},
|
||||
setHeader: (key: string, value: string) => {
|
||||
ctx.res.headers.append(key, value);
|
||||
},
|
||||
} as Response & {
|
||||
send: (body?: string | object) => void;
|
||||
setHeader: (key: string, value: string) => void;
|
||||
sendStatus: (code: number) => void;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
(BigInt.prototype as any).toJSON = function (): string {
|
||||
return `BigInt::` + this.toString();
|
||||
};
|
||||
|
||||
export const createResponse = (
|
||||
body: any,
|
||||
opt?: {
|
||||
headers?: any;
|
||||
res?: any;
|
||||
rewrite?: (arg: {
|
||||
body: Bun.BodyInit;
|
||||
headers: Headers | any;
|
||||
}) => Bun.BodyInit;
|
||||
}
|
||||
) => {
|
||||
const status =
|
||||
typeof opt?.res?._status === "number" ? opt?.res?._status : undefined;
|
||||
|
||||
const content_type = opt?.headers?.["content-type"];
|
||||
const is_binary = binaryExtensions.includes(
|
||||
mime.getExtension(content_type) || ""
|
||||
);
|
||||
const headers = { ...(opt?.headers || {}) } as Record<string, string>;
|
||||
|
||||
let pre_content = body;
|
||||
if (opt?.rewrite) {
|
||||
pre_content = opt.rewrite({ body: pre_content, headers });
|
||||
}
|
||||
|
||||
let content: any =
|
||||
typeof pre_content === "string" || is_binary
|
||||
? pre_content
|
||||
: JSON.stringify(pre_content);
|
||||
|
||||
let res = new Response(
|
||||
content,
|
||||
status
|
||||
? {
|
||||
status,
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
|
||||
for (const [k, v] of Object.entries(headers)) {
|
||||
res.headers.append(k, v);
|
||||
}
|
||||
const cur = opt?.res as Response;
|
||||
if (cur) {
|
||||
cur.headers.forEach((value, key) => {
|
||||
res.headers.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof body === "object" && !res.headers.has("content-type")) {
|
||||
res.headers.append("content-type", "application/json");
|
||||
}
|
||||
|
||||
res.headers.append(
|
||||
"Strict-Transport-Security",
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
);
|
||||
|
||||
res.headers.append("X-Content-Type-Options", "nosniff");
|
||||
|
||||
return res;
|
||||
};
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
export const binaryExtensions = [
|
||||
"3dm",
|
||||
"3ds",
|
||||
"3g2",
|
||||
"3gp",
|
||||
"7z",
|
||||
"a",
|
||||
"aac",
|
||||
"adp",
|
||||
"afdesign",
|
||||
"afphoto",
|
||||
"afpub",
|
||||
"ai",
|
||||
"aif",
|
||||
"aiff",
|
||||
"alz",
|
||||
"ape",
|
||||
"apk",
|
||||
"appimage",
|
||||
"ar",
|
||||
"arj",
|
||||
"asf",
|
||||
"au",
|
||||
"avi",
|
||||
"bak",
|
||||
"baml",
|
||||
"bh",
|
||||
"bin",
|
||||
"bk",
|
||||
"bmp",
|
||||
"btif",
|
||||
"bz2",
|
||||
"bzip2",
|
||||
"cab",
|
||||
"caf",
|
||||
"cgm",
|
||||
"class",
|
||||
"cmx",
|
||||
"cpio",
|
||||
"cr2",
|
||||
"cur",
|
||||
"dat",
|
||||
"dcm",
|
||||
"deb",
|
||||
"dex",
|
||||
"djvu",
|
||||
"dll",
|
||||
"dmg",
|
||||
"dng",
|
||||
"doc",
|
||||
"docm",
|
||||
"docx",
|
||||
"dot",
|
||||
"dotm",
|
||||
"dra",
|
||||
"DS_Store",
|
||||
"dsk",
|
||||
"dts",
|
||||
"dtshd",
|
||||
"dvb",
|
||||
"dwg",
|
||||
"dxf",
|
||||
"ecelp4800",
|
||||
"ecelp7470",
|
||||
"ecelp9600",
|
||||
"egg",
|
||||
"eol",
|
||||
"eot",
|
||||
"epub",
|
||||
"exe",
|
||||
"f4v",
|
||||
"fbs",
|
||||
"fh",
|
||||
"fla",
|
||||
"flac",
|
||||
"flatpak",
|
||||
"fli",
|
||||
"flv",
|
||||
"fpx",
|
||||
"fst",
|
||||
"fvt",
|
||||
"g3",
|
||||
"gh",
|
||||
"gif",
|
||||
"graffle",
|
||||
"gz",
|
||||
"gzip",
|
||||
"h261",
|
||||
"h263",
|
||||
"h264",
|
||||
"icns",
|
||||
"ico",
|
||||
"ief",
|
||||
"img",
|
||||
"ipa",
|
||||
"iso",
|
||||
"jar",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"jpgv",
|
||||
"jpm",
|
||||
"jxr",
|
||||
"key",
|
||||
"ktx",
|
||||
"lha",
|
||||
"lib",
|
||||
"lvp",
|
||||
"lz",
|
||||
"lzh",
|
||||
"lzma",
|
||||
"lzo",
|
||||
"m3u",
|
||||
"m4a",
|
||||
"m4v",
|
||||
"mar",
|
||||
"mdi",
|
||||
"mht",
|
||||
"mid",
|
||||
"midi",
|
||||
"mj2",
|
||||
"mka",
|
||||
"mkv",
|
||||
"mmr",
|
||||
"mng",
|
||||
"mobi",
|
||||
"mov",
|
||||
"movie",
|
||||
"mp3",
|
||||
"mp4",
|
||||
"mp4a",
|
||||
"mpeg",
|
||||
"mpg",
|
||||
"mpga",
|
||||
"mxu",
|
||||
"nef",
|
||||
"npx",
|
||||
"numbers",
|
||||
"nupkg",
|
||||
"o",
|
||||
"odp",
|
||||
"ods",
|
||||
"odt",
|
||||
"oga",
|
||||
"ogg",
|
||||
"ogv",
|
||||
"otf",
|
||||
"ott",
|
||||
"pages",
|
||||
"pbm",
|
||||
"pcx",
|
||||
"pdb",
|
||||
"pdf",
|
||||
"pea",
|
||||
"pgm",
|
||||
"pic",
|
||||
"png",
|
||||
"pnm",
|
||||
"pot",
|
||||
"potm",
|
||||
"potx",
|
||||
"ppa",
|
||||
"ppam",
|
||||
"ppm",
|
||||
"pps",
|
||||
"ppsm",
|
||||
"ppsx",
|
||||
"ppt",
|
||||
"pptm",
|
||||
"pptx",
|
||||
"psd",
|
||||
"pya",
|
||||
"pyc",
|
||||
"pyo",
|
||||
"pyv",
|
||||
"qt",
|
||||
"rar",
|
||||
"ras",
|
||||
"raw",
|
||||
"resources",
|
||||
"rgb",
|
||||
"rip",
|
||||
"rlc",
|
||||
"rmf",
|
||||
"rmvb",
|
||||
"rpm",
|
||||
"rtf",
|
||||
"rz",
|
||||
"s3m",
|
||||
"s7z",
|
||||
"scpt",
|
||||
"sgi",
|
||||
"shar",
|
||||
"snap",
|
||||
"sil",
|
||||
"sketch",
|
||||
"slk",
|
||||
"smv",
|
||||
"snk",
|
||||
"so",
|
||||
"stl",
|
||||
"suo",
|
||||
"sub",
|
||||
"swf",
|
||||
"tar",
|
||||
"tbz",
|
||||
"tbz2",
|
||||
"tga",
|
||||
"tgz",
|
||||
"thmx",
|
||||
"tif",
|
||||
"tiff",
|
||||
"tlz",
|
||||
"ttc",
|
||||
"ttf",
|
||||
"txz",
|
||||
"udf",
|
||||
"uvh",
|
||||
"uvi",
|
||||
"uvm",
|
||||
"uvp",
|
||||
"uvs",
|
||||
"uvu",
|
||||
"viv",
|
||||
"vob",
|
||||
"war",
|
||||
"wav",
|
||||
"wax",
|
||||
"wbmp",
|
||||
"wdp",
|
||||
"weba",
|
||||
"webm",
|
||||
"webp",
|
||||
"whl",
|
||||
"wim",
|
||||
"wm",
|
||||
"wma",
|
||||
"wmv",
|
||||
"wmx",
|
||||
"woff",
|
||||
"woff2",
|
||||
"wrm",
|
||||
"wvx",
|
||||
"xbm",
|
||||
"xif",
|
||||
"xla",
|
||||
"xlam",
|
||||
"xls",
|
||||
"xlsb",
|
||||
"xlsm",
|
||||
"xlsx",
|
||||
"xlt",
|
||||
"xltm",
|
||||
"xltx",
|
||||
"xm",
|
||||
"xmind",
|
||||
"xpi",
|
||||
"xpm",
|
||||
"xwd",
|
||||
"xz",
|
||||
"z",
|
||||
"zip",
|
||||
"zipx",
|
||||
];
|
||||
|
|
@ -2,6 +2,7 @@ import { dirAsync } from "fs-jetpack";
|
|||
import { fs } from "./fs";
|
||||
import get from "lodash.get";
|
||||
import set from "lodash.set";
|
||||
import { readdirSync } from "fs";
|
||||
|
||||
export const config = {
|
||||
async init(path: string) {
|
||||
|
|
@ -10,8 +11,16 @@ export const config = {
|
|||
}
|
||||
|
||||
const result = await fs.read(path, "json");
|
||||
this.current = result as typeof default_config;
|
||||
if (!this.current) {
|
||||
this.current = result as typeof default_config;
|
||||
}
|
||||
this.file_path = path;
|
||||
|
||||
const deploys = readdirSync(fs.path(`site:deploy/history`));
|
||||
this.current.deploy.history = deploys
|
||||
.filter((e) => e.endsWith(".gz"))
|
||||
.map((e) => parseInt(e.replace(".gz", "")));
|
||||
|
||||
return result as typeof default_config;
|
||||
},
|
||||
get(path: string) {
|
||||
|
|
@ -32,7 +41,7 @@ const default_config = {
|
|||
db: { orm: "prisma" as "prisma" | "prasi", url: "" },
|
||||
deploy: {
|
||||
current: 0,
|
||||
history: [],
|
||||
history: [] as number[],
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { mkdirSync, statSync } from "fs";
|
|||
import { copyAsync } from "fs-jetpack";
|
||||
import { g } from "./global";
|
||||
import { dirname, join } from "path";
|
||||
import type { SiteConfig } from "./config";
|
||||
const internal = Symbol("internal");
|
||||
|
||||
export const fs = {
|
||||
|
|
@ -75,13 +76,15 @@ export const fs = {
|
|||
createPath: true,
|
||||
});
|
||||
},
|
||||
init() {
|
||||
init(config: SiteConfig) {
|
||||
this[internal].prefix.site = join(g.dir.root, "site");
|
||||
this[internal].prefix.upload = config.upload_path;
|
||||
this[internal].prefix.internal = join(process.cwd(), "internal");
|
||||
},
|
||||
[internal]: {
|
||||
prefix: {
|
||||
site: "",
|
||||
upload: "",
|
||||
internal: "",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Server } from "bun";
|
||||
import { join, resolve } from "path";
|
||||
import type { SiteConfig } from "./config";
|
||||
import { config, type SiteConfig } from "./config";
|
||||
import { fs } from "./fs";
|
||||
import type { PrasiSpawn, spawn } from "./spawn";
|
||||
import type { prasi_content_ipc } from "../content/content-ipc";
|
||||
|
|
@ -75,7 +75,6 @@ export const startup = (mode: "supervisor" | "site", fn: () => void) => {
|
|||
} else {
|
||||
g.dir.root = join(process.cwd(), "..", "..");
|
||||
}
|
||||
fs.init();
|
||||
|
||||
fn();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
/** We don't normalize anything, so it is just strings and strings. */
|
||||
export type Data = Record<string, string>;
|
||||
|
||||
/** We typecast the value as a string so that it is compatible with envfiles. */
|
||||
export type Input = Record<string, any>;
|
||||
|
||||
// perhaps in the future we can use @bevry/json's toJSON and parseJSON and JSON.stringify to support more advanced types
|
||||
|
||||
function removeQuotes(str: string) {
|
||||
// Check if the string starts and ends with single or double quotes
|
||||
if (
|
||||
(str.startsWith('"') && str.endsWith('"')) ||
|
||||
(str.startsWith("'") && str.endsWith("'"))
|
||||
) {
|
||||
// Remove the quotes
|
||||
return str.slice(1, -1);
|
||||
}
|
||||
// If the string is not wrapped in quotes, return it as is
|
||||
return str;
|
||||
}
|
||||
|
||||
/** Parse an envfile string. */
|
||||
export function parseEnv(src: string): Data {
|
||||
const result: Data = {};
|
||||
const lines = splitInLines(src);
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^([^=:#]+?)[=:]((.|\n)*)/);
|
||||
if (match) {
|
||||
const key = match[1].trim();
|
||||
const value = removeQuotes(match[2].trim());
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Turn an object into an envfile string. */
|
||||
export function genEnv(obj: Input): string {
|
||||
let result = "";
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (key) {
|
||||
const line = `${key}=${jsonValueToEnv(value)}`;
|
||||
result += line + "\n";
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function splitInLines(src: string): string[] {
|
||||
return src
|
||||
.replace(/("[\s\S]*?")/g, (_m, cg) => {
|
||||
return cg.replace(/\n/g, "%%LINE-BREAK%%");
|
||||
})
|
||||
.split("\n")
|
||||
.filter((i) => Boolean(i.trim()))
|
||||
.map((i) => i.replace(/%%LINE-BREAK%%/g, "\n"));
|
||||
}
|
||||
|
||||
function jsonValueToEnv(value: any): string {
|
||||
let processedValue = String(value);
|
||||
processedValue = processedValue.replace(/\n/g, "\\n");
|
||||
processedValue = processedValue.includes("\\n")
|
||||
? `"${processedValue}"`
|
||||
: processedValue;
|
||||
return processedValue;
|
||||
}
|
||||
Loading…
Reference in New Issue