This commit is contained in:
Rizky
2024-01-23 23:03:13 +07:00
commit 1bd21cf6a7
44 changed files with 2920 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
import { apiContext } from "service-srv";
import { g } from "utils/global";
export const _ = {
url: "/_api_frm",
async api() {
const { req, res } = apiContext(this);
let allowUrl = req.headers.get("origin") || req.headers.get("referer");
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT");
res.setHeader("Access-Control-Allow-Headers", "content-type");
res.setHeader("Access-Control-Allow-Credentials", "true");
if (allowUrl) {
res.setHeader("Access-Control-Allow-Origin", allowUrl);
}
res.setHeader("content-type", "text/html");
res.setHeader("etag", g.frm.etag);
res.send(`<script>${g.frm.js}</script>`);
},
};
+20
View File
@@ -0,0 +1,20 @@
import { apiContext } from "service-srv";
import { execQuery } from "utils/query";
export const _ = {
url: "/_dbs/:dbName/:action",
async api(dbName: any, action?: string) {
const { req, res } = apiContext(this);
const body = req.params;
try {
const result = await execQuery(body, db);
res.send(result);
} catch (e: any) {
res.sendStatus(500);
res.send(e.message);
console.error(e);
}
},
};
+228
View File
@@ -0,0 +1,228 @@
import { $ } from "execa";
import * as fs from "fs";
import { dirAsync, removeAsync } from "fs-jetpack";
import { apiContext } from "service-srv";
import { dir } from "utils/dir";
import { g } from "utils/global";
import { restartServer } from "utils/restart";
import { loadWebCache } from "../server/load-web";
export const _ = {
url: "/_deploy",
async api(
action: (
| { type: "check" }
| { type: "db-update"; url: string }
| { type: "db-pull" }
| { type: "restart" }
| { type: "domain-add"; domain: string }
| { type: "domain-del"; domain: string }
| { type: "deploy-del"; ts: string }
| { type: "deploy"; dlurl: string }
| { type: "deploy-status" }
| { type: "redeploy"; ts: string }
) & {
id_site: string;
}
) {
const { res } = apiContext(this);
if (!g.web[action.id_site]) {
g.web[action.id_site] = {
current: 0,
domains: [],
deploying: null,
router: null,
deploys: [],
site_id: action.id_site,
cacheKey: 0,
cache: null,
};
}
const path = dir(`app/web/${action.id_site}`);
await dirAsync(path);
const web = g.web[action.id_site];
if (!web.domains) {
web.domains = [];
}
switch (action.type) {
case "check":
return {
now: Date.now(),
current: web.current,
deploys: web.deploys,
domains: web.domains,
db: {
url: g.dburl || "-",
},
};
case "db-update":
if (action.url) {
g.dburl = action.url;
await Bun.write(
dir("app/db/.env"),
`\
DATABASE_URL="${action.url}"
`
);
}
return "ok";
case "db-pull":
{
await $({ cwd: dir("app/db") })`bun prisma db pull`;
await $({ cwd: dir("app/db") })`bun prisma generate`;
res.send("ok");
setTimeout(() => {
restartServer();
}, 300);
}
break;
case "restart":
{
res.send("ok");
setTimeout(() => {
restartServer();
}, 300);
}
break;
case "domain-add":
{
web.domains.push(action.domain);
await Bun.write(`${path}/domains.json`, JSON.stringify(web.domains));
g.domains = null;
res.send("ok");
}
break;
case "domain-del":
{
web.domains = web.domains.filter((e) => e !== action.domain);
await Bun.write(`${path}/domains.json`, web.domains);
g.domains = null;
res.send("ok");
}
break;
case "deploy-del":
{
web.deploys = web.deploys.filter((e) => e !== parseInt(action.ts));
try {
await removeAsync(`${path}/deploys/${action.ts}`);
} catch (e) {}
return {
now: Date.now(),
current: web.current,
deploys: web.deploys,
};
}
break;
case "deploy-status":
break;
case "deploy":
{
await fs.promises.mkdir(`${path}/deploys`, { recursive: true });
const cur = Date.now();
const filePath = `${path}/deploys/${cur}`;
web.deploying = {
status: "generating",
received: 0,
total: 0,
};
if (
await downloadFile(action.dlurl, filePath, (rec, total) => {
web.deploying = {
status: "transfering",
received: rec,
total: total,
};
})
) {
web.deploying.status = "deploying";
await fs.promises.writeFile(`${path}/current`, cur.toString());
web.current = cur;
web.deploys.push(cur);
await loadWebCache(web.site_id, web.current);
}
web.deploying = null;
return {
now: Date.now(),
current: web.current,
deploys: web.deploys,
};
}
break;
case "redeploy":
{
const cur = parseInt(action.ts);
const lastcur = web.current;
try {
if (web.deploys.find((e) => e === cur)) {
web.current = cur;
await fs.promises.writeFile(`${path}/current`, cur.toString());
await loadWebCache(web.site_id, web.current);
}
} catch (e) {
web.current = lastcur;
web.deploys = web.deploys.filter((e) => e !== parseInt(action.ts));
await removeAsync(`${path}/deploys/${action.ts}`);
}
return {
now: Date.now(),
current: web.current,
deploys: web.deploys,
};
}
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";
}
const res = await fetch(_url);
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;
}
};
+70
View File
@@ -0,0 +1,70 @@
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";
export const _ = {
url: "/_file/**",
async api() {
const { req } = apiContext(this);
const rpath = decodeURIComponent(req.params._);
let res = new Response("NOT FOUND", { status: 404 });
try {
if (rpath.startsWith("site")) {
if (rpath === "site-html") {
res = new Response(generateIndexHtml("[[base_url]]", "[[site_id]]"));
}
if (rpath === "site-zip") {
const path = dir(`app/static/site.zip`);
res = new Response(Bun.file(path));
}
if (rpath === "site-md5") {
const path = dir(`app/static/md5`);
res = new Response(Bun.file(path));
}
} else if (rpath.startsWith("current-")) {
if (rpath.startsWith("current-md5-")) {
const site_id = rpath.substring("current-md5-".length);
const path = dir(`app/web/${site_id}/current`);
res = new Response(Bun.file(path));
} else {
const site_id = rpath.substring("current-".length);
const path = dir(`app/web/${site_id}/current`);
const id = await Bun.file(path).text();
if (id) {
const path = dir(`app/web/${site_id}/deploys/${id}`);
res = new Response(Bun.file(path));
}
}
}
} catch (e) {
res = new Response("NOT FOUND", { status: 404 });
}
const path = dir(`${g.datadir}/upload/${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-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;
},
};
+95
View File
@@ -0,0 +1,95 @@
import { Database } from "bun:sqlite";
import admin from "firebase-admin";
import { apiContext } from "service-srv";
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 (!g.firebaseInit) {
g.firebaseInit = true;
try {
g.firebase = admin.initializeApp({
credential: admin.credential.cert(dir("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" };
},
};
+143
View File
@@ -0,0 +1,143 @@
import { readAsync } from "fs-jetpack";
import { apiContext } from "service-srv";
import { g } from "utils/global";
import { dir } from "utils/dir";
const cache = {
dev: "",
prod: "",
};
export const _ = {
url: "/_prasi/**",
async api() {
const { req, res } = apiContext(this);
res.setHeader("Access-Control-Allow-Origin", "*");
const action = {
_: () => {
res.send({ prasi: "v2" });
},
"load.json": async () => {
res.setHeader("content-type", "application/json");
res.send(
JSON.stringify({
apiEntry: getApiEntry(),
apiTypes: (await getApiTypes()) || "",
prismaTypes: {
"prisma.d.ts": await getPrisma("prisma"),
"runtime/index.d.ts": await getPrisma("runtime"),
"runtime/library.d.ts": await getPrisma("library"),
},
})
);
},
"load.js": async () => {
res.setHeader("content-type", "text/javascript");
const url = req.query_parameters["url"]
? JSON.stringify(req.query_parameters["url"])
: "undefined";
if (!cache.dev) {
cache.dev = `\
(() => {
const baseurl = new URL(location.href);
baseurl.pathname = '';
const url = ${url} || baseurl.toString();
const w = window;
if (!w.prasiApi) {
w.prasiApi = {};
}
w.prasiApi[url] = {
apiEntry: ${JSON.stringify(getApiEntry())},
apiTypes: ${JSON.stringify((await getApiTypes()) || "")},
prismaTypes: {
"prisma.d.ts": ${await getPrisma("prisma")},
"runtime/index.d.ts": ${await getPrisma("runtime")},
"runtime/library.d.ts": ${await getPrisma("library")},
},
};
})();`;
cache.prod = `\
(() => {
const baseurl = new URL(location.href);
baseurl.pathname = '';
const url = ${url} || baseurl.toString();
const w = window;
if (!w.prasiApi) {
w.prasiApi = {};
}
w.prasiApi[url] = {
apiEntry: ${JSON.stringify(getApiEntry())},
};
})();`;
}
if (req.query_parameters["dev"]) {
res.send(cache.dev);
} else {
res.send(cache.prod);
}
},
};
const pathname: keyof typeof action = req.params._.split("/")[0] as any;
const run = action[pathname];
if (run) {
await run();
}
},
};
export const getApiEntry = () => {
const res: any = {};
for (const [k, v] of Object.entries(g.api)) {
const name = k.substring(0, k.length - 3);
res[name] = { ...v, name, path: `app/srv/api/${v.path}` };
}
return res;
};
const getApiTypes = async () => {
return (
`\
declare module "gen/srv/api/entry" {
export * as srv from "gen/srv/api/srv";
}
` +
((await readAsync(dir("app/srv/exports.d.ts"))) || "")
.replace(/\"app\/srv\/api/gi, '"srv/api')
.replace(
'declare module "app/srv/exports"',
'declare module "gen/srv/api/srv"'
)
);
};
const getPrisma = async (path: string) => {
if (path === "prisma")
return JSON.stringify(
(
(await readAsync(dir("node_modules/.prisma/client/index.d.ts"))) || ""
).replace(`@prisma/client/runtime/library`, `./runtime/library`)
);
if (path === "runtime")
return JSON.stringify(
await readAsync(
dir("node_modules/@prisma/client/runtime/index-browser.d.ts")
)
);
if (path === "library")
return JSON.stringify(
await readAsync(dir("node_modules/@prisma/client/runtime/library.d.ts"))
);
return JSON.stringify({});
};
+41
View File
@@ -0,0 +1,41 @@
import { g } from "utils/global";
import { gzipAsync } from "utils/gzip";
export const _ = {
url: "/_proxy/*",
async api(arg: {
url: string;
method: "POST" | "GET";
headers: any;
body: any;
}) {
const res = await fetch(
arg.url,
arg.body
? {
method: arg.method || "POST",
headers: arg.headers,
body: arg.body,
}
: {
headers: arg.headers,
}
);
let body: any = null;
const headers: any = {};
res.headers.forEach((v, k) => {
headers[k] = v;
});
body = await res.arrayBuffer();
if (headers["content-encoding"] === "gzip") {
body = await gzipAsync(new Uint8Array(body));
} else {
delete headers["content-encoding"];
}
return new Response(body, { headers });
},
};
+36
View File
@@ -0,0 +1,36 @@
import mp from "@surfy/multipart-parser";
import { writeAsync } from "fs-jetpack";
import { apiContext } from "service-srv";
import { dir } from "utils/dir";
import { g } from "utils/global";
export const _ = {
url: "/_upload",
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 }
>;
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);
}
return url;
},
};
function toArrayBuffer(buffer: Buffer) {
return buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength
);
}
+79
View File
@@ -0,0 +1,79 @@
import mime from "mime";
import { apiContext } from "service-srv";
import { g } from "utils/global";
import { getApiEntry } from "./_prasi";
export const _ = {
url: "/_web/:id/**",
async api(id: string, _: string) {
const { req, res } = apiContext(this);
const web = g.web[id];
if (web) {
const cache = web.cache;
if (cache) {
const parts = _.split("/");
switch (parts[0]) {
case "site": {
res.setHeader("content-type", "application/json");
if (req.query_parameters["prod"]) {
return {
site: cache.site,
pages: cache.pages.map((e) => {
return {
id: e.id,
url: e.url,
};
}),
api: getApiEntry(),
};
} else {
return cache.site;
}
}
case "pages": {
res.setHeader("content-type", "application/json");
return cache.pages.map((e) => {
return {
id: e.id,
url: e.url,
};
});
}
case "page": {
res.setHeader("content-type", "application/json");
return cache.pages.find((e) => e.id === parts[1]);
}
case "npm-site": {
let path = parts.slice(1).join("/");
res.setHeader("content-type", mime.getType(path) || "text/plain");
if (path === "site.js") {
path = "index.js";
}
return cache.npm.site[path];
}
case "npm-page": {
const page_id = parts[1];
if (cache.npm.pages[page_id]) {
let path = parts.slice(2).join("/");
res.setHeader("content-type", mime.getType(path) || "text/plain");
if (path === "page.js") {
path = "index.js";
}
return cache.npm.pages[page_id][path];
}
res.setHeader("content-type", "text/javascript");
}
case "comp": {
res.setHeader("content-type", "application/json");
return cache.comps.find((e) => e.id === parts[1]);
}
}
}
}
return req.params;
},
};