feat: add sandbox

This commit is contained in:
2024-02-26 10:48:17 +00:00
parent 5b68230734
commit 85e71c2983
19 changed files with 588 additions and 19 deletions
+2
View File
@@ -2,11 +2,13 @@ import { Router } from "express";
import preview from "./preview";
import trpc from "./trpc/handler";
import { thumbnail } from "./thumbnail";
import sandbox from "./sandbox";
const api = Router();
api.use("/trpc", trpc);
api.use("/preview", preview);
api.use("/sandbox", sandbox);
api.get("/thumbnail/:slug", thumbnail);
+1 -1
View File
@@ -45,7 +45,7 @@ const get = async (req: Request, res: Response) => {
}
if (["css"].includes(ext) && settings.css?.preprocessor === "postcss") {
content = await postcss(fileData, settings.css);
content = await postcss(projectData, fileData);
}
}
+6 -6
View File
@@ -3,16 +3,16 @@ import tailwindcss from "tailwindcss";
import cssnano from "cssnano";
import { FileSchema } from "~/server/db/schema/file";
import { unpackProject } from "~/server/lib/unpack-project";
import { ProjectSettingsSchema } from "~/server/db/schema/project";
import { ProjectSchema } from "~/server/db/schema/project";
export const postcss = async (
fileData: FileSchema,
cfg?: ProjectSettingsSchema["css"]
) => {
export const postcss = async (project: ProjectSchema, fileData: FileSchema) => {
const content = fileData.content || "";
const cfg = project.settings?.css;
try {
const projectDir = await unpackProject({ ext: "ts,tsx,js,jsx,html" });
const projectDir = await unpackProject(project, {
ext: "ts,tsx,js,jsx,html",
});
const plugins: any[] = [];
if (cfg?.tailwindcss) {
+170
View File
@@ -0,0 +1,170 @@
import { Router, type Request } from "express";
import sandbox from "../lib/sandbox";
import db from "~/server/db";
import { and, eq, isNull } from "drizzle-orm";
import { project } from "~/server/db/schema/project";
import { unpackProject } from "../lib/unpack-project";
import { APIError } from "~/lib/api";
const router = Router();
router.post("/:slug/start", async (req, res) => {
const { slug } = req.params as any;
const data = await getProjectBySlug(slug);
if (!data) {
return res.status(404).send("Project not found!");
}
try {
const path = await unpackProject(data);
const result = await sandbox.start(data.slug, path + "/api");
if (!result.ok) {
const body = await result.text();
throw new APIError(body, result.status);
}
res.json({ success: true });
} catch (err) {
const error = err as any;
res
.status(typeof error?.code === "number" ? error.code : 500)
.send(error?.message || "An error occured!");
}
});
router.post("/:slug/stop", async (req, res) => {
const { slug } = req.params as any;
const data = await getProjectBySlug(slug);
if (!data) {
return res.status(404).send("Project not found!");
}
try {
await sandbox.stop(data.slug);
res.json({ success: true });
} catch (err) {
res.status(500).send("An error occured!");
}
});
router.post("/:slug/restart", async (req, res) => {
const { slug } = req.params as any;
const data = await getProjectBySlug(slug);
if (!data) {
return res.status(404).send("Project not found!");
}
try {
await unpackProject(data);
const result = await sandbox.restart(data.slug);
if (!result.ok) {
const body = await result.text();
throw new APIError(body, result.status);
}
res.json({ success: true });
} catch (err) {
const error = err as any;
res
.status(typeof error?.code === "number" ? error.code : 500)
.send(error?.message || "An error occured!");
}
});
router.get("/:slug/stats", async (req, res) => {
const { slug } = req.params as any;
const data = await getProjectBySlug(slug);
if (!data) {
return res.status(404).send("Project not found!");
}
try {
const result = await sandbox.getStats(data.slug);
res.json({ success: true, result });
} catch (err) {
const error = err as any;
res
.status(typeof error.code === "number" ? error.code : 500)
.send(error.message || "An error occured!");
}
});
router.get("/:slug/logs", async (req, res) => {
const { slug } = req.params as any;
const data = await getProjectBySlug(slug);
if (!data) {
return res.status(404).send("Project not found!");
}
try {
const { body, headers } = await sandbox.getLogs(data.slug);
if (!body) {
throw new Error("No body response.");
}
headers.forEach((value, key) => res.setHeader(key, value));
body.pipe(res);
} catch (err) {
res.status(500).send("An error occured!");
}
});
router.get("/:slug/proxy*", async (req, res) => {
const params = req.params as any;
const { slug } = params;
const pathname = params?.[0];
const searchParams = new URLSearchParams();
Object.entries(req.query || {}).forEach(([key, value]) => {
searchParams.set(key, value as string);
});
const data = await getProjectBySlug(slug);
if (!data) {
return res.status(404).send("Project not found!");
}
try {
const { body, headers, status } = await sandbox.proxy(data.slug, {
pathname: pathname + "?" + searchParams.toString(),
headers: getHeadersFromReq(req),
});
if (!body) {
throw new Error("No body response.");
}
headers.forEach((value, key) => res.setHeader(key, value));
res.status(status);
body.pipe(res);
} catch (err) {
const error = err as any;
res
.status(typeof error?.code === "number" ? error.code : 500)
.send(error?.message || "An error occured!");
}
});
function getHeadersFromReq(req: Request) {
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (typeof value === "string") {
headers.set(key, value);
} else if (Array.isArray(value)) {
value.forEach((val) => headers.append(key, val));
}
}
return headers;
}
async function getProjectBySlug(slug: string) {
return db.query.project.findFirst({
where: and(eq(project.slug, slug), isNull(project.deletedAt)),
});
}
export default router;
+1
View File
@@ -5,6 +5,7 @@ import { IS_DEV } from "./lib/consts";
import cookieParser from "cookie-parser";
import api from "./api";
import { authMiddleware } from "./middlewares/auth";
import fetch from "node-fetch";
async function createServer() {
const app = express();
+66
View File
@@ -0,0 +1,66 @@
import fetch, { RequestInit } from "node-fetch";
const createRequest = async (
url: string,
options?: RequestInit & { ignoreError?: boolean }
) => {
const reqUrl = "http://127.0.0.1:8000" + url;
const response = await fetch(reqUrl, options);
if (!response.ok && !options?.ignoreError) {
const err: any = Error(response.statusText);
err.code = response.status;
throw err;
}
return response;
};
const start = async (id: string, path: string) => {
return createRequest("/start", {
body: JSON.stringify({ path, id }),
method: "POST",
ignoreError: true,
});
};
const stop = async (id: string) => {
const res = await createRequest(`/stop/${id}`, { method: "POST" });
return res.json();
};
const restart = async (id: string) => {
return createRequest(`/restart/${id}`, { method: "POST", ignoreError: true });
};
const getStats = async (id: string) => {
const res = await createRequest(`/stats/${id}`);
return res.json();
};
const getLogs = async (id: string) => {
return createRequest(`/logs/${id}`);
};
type ProxyOptions = {
pathname: string;
headers: Headers;
};
const proxy = async (id: string, opt: ProxyOptions) => {
return createRequest(`/proxy/${id}` + opt.pathname, {
headers: opt.headers,
ignoreError: true,
});
};
const sandbox = {
start,
stop,
restart,
getStats,
getLogs,
proxy,
};
export default sandbox;
+3 -1
View File
@@ -5,12 +5,14 @@ import db from "../db";
import { file } from "../db/schema/file";
import { fileExists, getProjectDir } from "./utils";
import { getFileExt } from "~/lib/utils";
import { ProjectSchema } from "../db/schema/project";
type UnpackProjectOptions = {
ext: string;
};
export const unpackProject = async (
projectData: ProjectSchema,
opt: Partial<UnpackProjectOptions> = {}
) => {
const files = await db.query.file.findMany({
@@ -21,7 +23,7 @@ export const unpackProject = async (
),
});
const projectDir = getProjectDir();
const projectDir = getProjectDir(projectData);
if (!fileExists(projectDir)) {
await fs.mkdir(projectDir, { recursive: true });
}
+3 -2
View File
@@ -1,6 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import cuid2 from "@paralleldrive/cuid2";
import { ProjectSchema } from "../db/schema/project";
export const fileExists = (path: string) => {
try {
@@ -11,8 +12,8 @@ export const fileExists = (path: string) => {
}
};
export const getProjectDir = () => {
return path.resolve(process.cwd(), "storage/tmp/project1");
export const getProjectDir = (project: ProjectSchema) => {
return path.resolve(process.cwd(), "storage/tmp", project.slug);
};
export const uid = cuid2.init({
+8 -3
View File
@@ -38,6 +38,7 @@ const fileRouter = router({
create: procedure
.input(
insertFileSchema.pick({
projectId: true,
parentId: true,
filename: true,
isDirectory: true,
@@ -57,7 +58,7 @@ const fileRouter = router({
}
const data: z.infer<typeof insertFileSchema> = {
projectId: 1,
projectId: input.projectId,
parentId: input.parentId,
path: basePath + input.filename,
filename: input.filename,
@@ -69,10 +70,14 @@ const fileRouter = router({
}),
update: procedure
.input(selectFileSchema.partial().required({ id: true }))
.input(selectFileSchema.partial().required({ id: true, projectId: true }))
.mutation(async ({ input }) => {
const fileData = await db.query.file.findFirst({
where: and(eq(file.id, input.id), isNull(file.deletedAt)),
where: and(
eq(file.projectId, input.projectId),
eq(file.id, input.id),
isNull(file.deletedAt)
),
});
if (!fileData) {
throw new TRPCError({ code: "NOT_FOUND" });