mirror of
				https://github.com/khairul169/code-share.git
				synced 2025-11-04 05:31:08 +07:00 
			
		
		
		
	feat: update thumbnail api
This commit is contained in:
		
							parent
							
								
									110bdd88e6
								
							
						
					
					
						commit
						24717db264
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -3,3 +3,4 @@ dist/
 | 
				
			|||||||
build/
 | 
					build/
 | 
				
			||||||
storage/**/*
 | 
					storage/**/*
 | 
				
			||||||
!.gitkeep
 | 
					!.gitkeep
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
				
			|||||||
@ -16,10 +16,14 @@ const ProjectCard = ({ project }: Props) => {
 | 
				
			|||||||
      href={`/${project.slug}`}
 | 
					      href={`/${project.slug}`}
 | 
				
			||||||
      className="border border-white/20 hover:border-white/40 rounded-lg transition-colors overflow-hidden"
 | 
					      className="border border-white/20 hover:border-white/40 rounded-lg transition-colors overflow-hidden"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <img
 | 
					      {project.thumbnail ? (
 | 
				
			||||||
        src={`/api/thumbnail/${project.slug}`}
 | 
					        <img
 | 
				
			||||||
        className="w-full aspect-[3/2] bg-background object-cover"
 | 
					          src={`/api/thumbnail/${project.thumbnail}`}
 | 
				
			||||||
      />
 | 
					          className="w-full aspect-[3/2] bg-background object-cover"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      ) : (
 | 
				
			||||||
 | 
					        <div className="w-full aspect-[3/2] bg-gray-900"></div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div className="py-2 px-3 flex items-center gap-3">
 | 
					      <div className="py-2 px-3 flex items-center gap-3">
 | 
				
			||||||
        <div className="size-8 rounded-full bg-white/80"></div>
 | 
					        <div className="size-8 rounded-full bg-white/80"></div>
 | 
				
			||||||
 | 
				
			|||||||
@ -20,6 +20,8 @@ import StatusBar from "./status-bar";
 | 
				
			|||||||
import { FiTerminal } from "react-icons/fi";
 | 
					import { FiTerminal } from "react-icons/fi";
 | 
				
			||||||
import SettingsDialog from "./settings-dialog";
 | 
					import SettingsDialog from "./settings-dialog";
 | 
				
			||||||
import FileIcon from "~/components/ui/file-icon";
 | 
					import FileIcon from "~/components/ui/file-icon";
 | 
				
			||||||
 | 
					import { api } from "~/lib/api";
 | 
				
			||||||
 | 
					import { useMutation } from "@tanstack/react-query";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Editor = () => {
 | 
					const Editor = () => {
 | 
				
			||||||
  const { project, initialFiles } = useData<Data>();
 | 
					  const { project, initialFiles } = useData<Data>();
 | 
				
			||||||
@ -33,11 +35,17 @@ const Editor = () => {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const openedFilesData = trpc.file.getAll.useQuery(
 | 
					  const openedFilesData = trpc.file.getAll.useQuery(
 | 
				
			||||||
    { projectId: project.id, id: curOpenFiles },
 | 
					    { projectId: project.id!, id: curOpenFiles },
 | 
				
			||||||
    { enabled: curOpenFiles.length > 0, initialData: initialFiles }
 | 
					    { enabled: curOpenFiles.length > 0, initialData: initialFiles }
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const [openedFiles, setOpenedFiles] = useState<any[]>(initialFiles);
 | 
					  const [openedFiles, setOpenedFiles] = useState<any[]>(initialFiles);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const generateThumbnail = useMutation({
 | 
				
			||||||
 | 
					    mutationFn: () => {
 | 
				
			||||||
 | 
					      return api(`/thumbnail/${project.slug!}`, { method: "PATCH" });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const deleteFile = trpc.file.delete.useMutation({
 | 
					  const deleteFile = trpc.file.delete.useMutation({
 | 
				
			||||||
    onSuccess: (file) => {
 | 
					    onSuccess: (file) => {
 | 
				
			||||||
      trpcUtils.file.getAll.invalidate();
 | 
					      trpcUtils.file.getAll.invalidate();
 | 
				
			||||||
@ -76,6 +84,14 @@ const Editor = () => {
 | 
				
			|||||||
  //   api(`/sandbox/${project.slug}/start`, { method: "POST" }).catch(() => {});
 | 
					  //   api(`/sandbox/${project.slug}/start`, { method: "POST" }).catch(() => {});
 | 
				
			||||||
  // }, [project]);
 | 
					  // }, [project]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const itv = setInterval(() => generateThumbnail.mutate(), 60000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      clearInterval(itv);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onOpenFile = useCallback(
 | 
					  const onOpenFile = useCallback(
 | 
				
			||||||
    (fileId: number, autoSwitchTab = true) => {
 | 
					    (fileId: number, autoSwitchTab = true) => {
 | 
				
			||||||
      const idx = curOpenFiles.indexOf(fileId);
 | 
					      const idx = curOpenFiles.indexOf(fileId);
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								rest.http
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								rest.http
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					@baseUrl = http://localhost:3001
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GET {{baseUrl}}/api/thumbnail/t2mo3o3j.jpg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PATCH  {{baseUrl}}/api/thumbnail/t2mo3o3j
 | 
				
			||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { Router } from "express";
 | 
					import { Router } from "express";
 | 
				
			||||||
import preview from "./preview";
 | 
					import preview from "./preview";
 | 
				
			||||||
import trpc from "./trpc/handler";
 | 
					import trpc from "./trpc/handler";
 | 
				
			||||||
import { thumbnail } from "./thumbnail";
 | 
					import thumbnail from "./thumbnail";
 | 
				
			||||||
import sandbox from "./sandbox";
 | 
					import sandbox from "./sandbox";
 | 
				
			||||||
import { nocache } from "../middlewares/nocache";
 | 
					import { nocache } from "../middlewares/nocache";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -10,7 +10,6 @@ const api = Router();
 | 
				
			|||||||
api.use("/trpc", trpc);
 | 
					api.use("/trpc", trpc);
 | 
				
			||||||
api.use("/preview", nocache, preview);
 | 
					api.use("/preview", nocache, preview);
 | 
				
			||||||
api.use("/sandbox", sandbox);
 | 
					api.use("/sandbox", sandbox);
 | 
				
			||||||
 | 
					api.use("/thumbnail", thumbnail);
 | 
				
			||||||
api.get("/thumbnail/:slug", thumbnail);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default api;
 | 
					export default api;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,44 +0,0 @@
 | 
				
			|||||||
import { Request, Response } from "express";
 | 
					 | 
				
			||||||
import { screenshot } from "../lib/screenshot";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const cache = new Map<string, { data: Buffer; timestamp: number }>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const regenerateThumbnail = async (slug: string) => {
 | 
					 | 
				
			||||||
  const curCache = cache.get(slug);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (curCache?.data) {
 | 
					 | 
				
			||||||
    cache.set(slug, {
 | 
					 | 
				
			||||||
      data: curCache.data,
 | 
					 | 
				
			||||||
      timestamp: Date.now(),
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const result = await screenshot(slug);
 | 
					 | 
				
			||||||
  if (!result) {
 | 
					 | 
				
			||||||
    return curCache;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const data = {
 | 
					 | 
				
			||||||
    data: result,
 | 
					 | 
				
			||||||
    timestamp: Date.now(),
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  cache.set(slug, data);
 | 
					 | 
				
			||||||
  return data;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const thumbnail = async (req: Request, res: Response) => {
 | 
					 | 
				
			||||||
  const { slug } = req.params;
 | 
					 | 
				
			||||||
  let cacheData = cache.get(slug);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!cacheData) {
 | 
					 | 
				
			||||||
    cacheData = await regenerateThumbnail(slug);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (cacheData && Date.now() - cacheData.timestamp > 10000) {
 | 
					 | 
				
			||||||
    regenerateThumbnail(slug);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  res.contentType("image/jpeg");
 | 
					 | 
				
			||||||
  res.send(cacheData?.data);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
							
								
								
									
										65
									
								
								server/api/thumbnail/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								server/api/thumbnail/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					import { and, eq, isNull } from "drizzle-orm";
 | 
				
			||||||
 | 
					import { Request, Response, Router } from "express";
 | 
				
			||||||
 | 
					import db from "~/server/db";
 | 
				
			||||||
 | 
					import fs from "fs";
 | 
				
			||||||
 | 
					import { project } from "~/server/db/schema/project";
 | 
				
			||||||
 | 
					import { BASE_URL } from "~/server/lib/consts";
 | 
				
			||||||
 | 
					import { screenshot } from "~/server/lib/screenshot";
 | 
				
			||||||
 | 
					import { getStorageDir } from "~/server/lib/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const thumbnail = async (req: Request, res: Response) => {
 | 
				
			||||||
 | 
					  const { filename } = req.params;
 | 
				
			||||||
 | 
					  const path = getStorageDir("thumbnails", filename);
 | 
				
			||||||
 | 
					  const thumbnail = fs.existsSync(path) ? fs.readFileSync(path) : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!thumbnail) {
 | 
				
			||||||
 | 
					    return res.status(404).send("Thumbnail not found!");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  res.setHeader("Content-Type", "image/jpeg");
 | 
				
			||||||
 | 
					  res.send(thumbnail);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const generate = async (req: Request, res: Response) => {
 | 
				
			||||||
 | 
					  const { slug } = req.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const projectData = await db.query.project.findFirst({
 | 
				
			||||||
 | 
					    where: and(eq(project.slug, slug), isNull(project.deletedAt)),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!projectData) {
 | 
				
			||||||
 | 
					    return res.status(404).send("Project not found!");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const url = `${BASE_URL}/api/preview/${slug}/index.html`;
 | 
				
			||||||
 | 
					    const data = await screenshot(url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!data) {
 | 
				
			||||||
 | 
					      throw new Error("Cannot generate thumbnail!");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const dir = getStorageDir("thumbnails");
 | 
				
			||||||
 | 
					    if (!fs.existsSync(dir)) {
 | 
				
			||||||
 | 
					      fs.mkdirSync(dir, { recursive: true });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const filename = `/${slug}.jpg`;
 | 
				
			||||||
 | 
					    fs.writeFileSync(dir + filename, data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db
 | 
				
			||||||
 | 
					      .update(project)
 | 
				
			||||||
 | 
					      .set({ thumbnail: filename })
 | 
				
			||||||
 | 
					      .where(eq(project.id, projectData.id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json({ filename });
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    res.status(400).send((err as any)?.message || "An error occured!");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const router = Router();
 | 
				
			||||||
 | 
					router.get("/:filename", thumbnail);
 | 
				
			||||||
 | 
					router.patch("/:slug", generate);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default router;
 | 
				
			||||||
@ -16,16 +16,16 @@ CREATE TABLE `files` (
 | 
				
			|||||||
--> statement-breakpoint
 | 
					--> statement-breakpoint
 | 
				
			||||||
CREATE TABLE `projects` (
 | 
					CREATE TABLE `projects` (
 | 
				
			||||||
	`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
 | 
						`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
 | 
				
			||||||
	`fork_id` integer;
 | 
					 | 
				
			||||||
	`user_id` integer NOT NULL,
 | 
						`user_id` integer NOT NULL,
 | 
				
			||||||
 | 
						`fork_id` integer,
 | 
				
			||||||
	`slug` text NOT NULL,
 | 
						`slug` text NOT NULL,
 | 
				
			||||||
	`title` text NOT NULL,
 | 
						`title` text NOT NULL,
 | 
				
			||||||
	`visibility` text DEFAULT 'private',
 | 
						`visibility` text DEFAULT 'private',
 | 
				
			||||||
	`settings` text DEFAULT [object Object],
 | 
						`settings` text DEFAULT [object Object],
 | 
				
			||||||
	`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
 | 
						`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
 | 
				
			||||||
	`deleted_at` text,
 | 
						`deleted_at` text,
 | 
				
			||||||
	FOREIGN KEY (`fork_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE no action,
 | 
						FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
 | 
				
			||||||
	FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
 | 
						FOREIGN KEY (`fork_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE no action
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
--> statement-breakpoint
 | 
					--> statement-breakpoint
 | 
				
			||||||
CREATE TABLE `users` (
 | 
					CREATE TABLE `users` (
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								server/db/drizzle/0001_steep_dragon_man.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								server/db/drizzle/0001_steep_dragon_man.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					ALTER TABLE projects ADD `thumbnail` text;
 | 
				
			||||||
							
								
								
									
										339
									
								
								server/db/drizzle/meta/0001_snapshot.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										339
									
								
								server/db/drizzle/meta/0001_snapshot.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,339 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "version": "5",
 | 
				
			||||||
 | 
					  "dialect": "sqlite",
 | 
				
			||||||
 | 
					  "id": "ac734f8a-134c-4d0e-81a9-7d6025e24acc",
 | 
				
			||||||
 | 
					  "prevId": "4d5af5ab-31e5-4202-9c7e-3264e985bf20",
 | 
				
			||||||
 | 
					  "tables": {
 | 
				
			||||||
 | 
					    "files": {
 | 
				
			||||||
 | 
					      "name": "files",
 | 
				
			||||||
 | 
					      "columns": {
 | 
				
			||||||
 | 
					        "id": {
 | 
				
			||||||
 | 
					          "name": "id",
 | 
				
			||||||
 | 
					          "type": "integer",
 | 
				
			||||||
 | 
					          "primaryKey": true,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": true
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "project_id": {
 | 
				
			||||||
 | 
					          "name": "project_id",
 | 
				
			||||||
 | 
					          "type": "integer",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "parent_id": {
 | 
				
			||||||
 | 
					          "name": "parent_id",
 | 
				
			||||||
 | 
					          "type": "integer",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": false,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "path": {
 | 
				
			||||||
 | 
					          "name": "path",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "filename": {
 | 
				
			||||||
 | 
					          "name": "filename",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "is_directory": {
 | 
				
			||||||
 | 
					          "name": "is_directory",
 | 
				
			||||||
 | 
					          "type": "integer",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false,
 | 
				
			||||||
 | 
					          "default": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "is_file": {
 | 
				
			||||||
 | 
					          "name": "is_file",
 | 
				
			||||||
 | 
					          "type": "integer",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false,
 | 
				
			||||||
 | 
					          "default": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "is_pinned": {
 | 
				
			||||||
 | 
					          "name": "is_pinned",
 | 
				
			||||||
 | 
					          "type": "integer",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false,
 | 
				
			||||||
 | 
					          "default": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "content": {
 | 
				
			||||||
 | 
					          "name": "content",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": false,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "created_at": {
 | 
				
			||||||
 | 
					          "name": "created_at",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false,
 | 
				
			||||||
 | 
					          "default": "CURRENT_TIMESTAMP"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "deleted_at": {
 | 
				
			||||||
 | 
					          "name": "deleted_at",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": false,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "indexes": {
 | 
				
			||||||
 | 
					        "file_path_idx": {
 | 
				
			||||||
 | 
					          "name": "file_path_idx",
 | 
				
			||||||
 | 
					          "columns": [
 | 
				
			||||||
 | 
					            "path"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "isUnique": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "file_name_idx": {
 | 
				
			||||||
 | 
					          "name": "file_name_idx",
 | 
				
			||||||
 | 
					          "columns": [
 | 
				
			||||||
 | 
					            "filename"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "isUnique": false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "foreignKeys": {
 | 
				
			||||||
 | 
					        "files_project_id_projects_id_fk": {
 | 
				
			||||||
 | 
					          "name": "files_project_id_projects_id_fk",
 | 
				
			||||||
 | 
					          "tableFrom": "files",
 | 
				
			||||||
 | 
					          "tableTo": "projects",
 | 
				
			||||||
 | 
					          "columnsFrom": [
 | 
				
			||||||
 | 
					            "project_id"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "columnsTo": [
 | 
				
			||||||
 | 
					            "id"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "onDelete": "no action",
 | 
				
			||||||
 | 
					          "onUpdate": "no action"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "parent_id_fk": {
 | 
				
			||||||
 | 
					          "name": "parent_id_fk",
 | 
				
			||||||
 | 
					          "tableFrom": "files",
 | 
				
			||||||
 | 
					          "tableTo": "files",
 | 
				
			||||||
 | 
					          "columnsFrom": [
 | 
				
			||||||
 | 
					            "parent_id"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "columnsTo": [
 | 
				
			||||||
 | 
					            "id"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "onDelete": "no action",
 | 
				
			||||||
 | 
					          "onUpdate": "no action"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "compositePrimaryKeys": {},
 | 
				
			||||||
 | 
					      "uniqueConstraints": {}
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "projects": {
 | 
				
			||||||
 | 
					      "name": "projects",
 | 
				
			||||||
 | 
					      "columns": {
 | 
				
			||||||
 | 
					        "id": {
 | 
				
			||||||
 | 
					          "name": "id",
 | 
				
			||||||
 | 
					          "type": "integer",
 | 
				
			||||||
 | 
					          "primaryKey": true,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": true
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "user_id": {
 | 
				
			||||||
 | 
					          "name": "user_id",
 | 
				
			||||||
 | 
					          "type": "integer",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "fork_id": {
 | 
				
			||||||
 | 
					          "name": "fork_id",
 | 
				
			||||||
 | 
					          "type": "integer",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": false,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "slug": {
 | 
				
			||||||
 | 
					          "name": "slug",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "title": {
 | 
				
			||||||
 | 
					          "name": "title",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "thumbnail": {
 | 
				
			||||||
 | 
					          "name": "thumbnail",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": false,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "visibility": {
 | 
				
			||||||
 | 
					          "name": "visibility",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": false,
 | 
				
			||||||
 | 
					          "autoincrement": false,
 | 
				
			||||||
 | 
					          "default": "'private'"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "settings": {
 | 
				
			||||||
 | 
					          "name": "settings",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": false,
 | 
				
			||||||
 | 
					          "autoincrement": false,
 | 
				
			||||||
 | 
					          "default": {
 | 
				
			||||||
 | 
					            "css": {
 | 
				
			||||||
 | 
					              "preprocessor": null,
 | 
				
			||||||
 | 
					              "tailwindcss": false
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "js": {
 | 
				
			||||||
 | 
					              "transpiler": null,
 | 
				
			||||||
 | 
					              "packages": []
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "created_at": {
 | 
				
			||||||
 | 
					          "name": "created_at",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false,
 | 
				
			||||||
 | 
					          "default": "CURRENT_TIMESTAMP"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "deleted_at": {
 | 
				
			||||||
 | 
					          "name": "deleted_at",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": false,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "indexes": {
 | 
				
			||||||
 | 
					        "projects_slug_unique": {
 | 
				
			||||||
 | 
					          "name": "projects_slug_unique",
 | 
				
			||||||
 | 
					          "columns": [
 | 
				
			||||||
 | 
					            "slug"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "isUnique": true
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "project_visibility_idx": {
 | 
				
			||||||
 | 
					          "name": "project_visibility_idx",
 | 
				
			||||||
 | 
					          "columns": [
 | 
				
			||||||
 | 
					            "visibility"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "isUnique": false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "foreignKeys": {
 | 
				
			||||||
 | 
					        "projects_user_id_users_id_fk": {
 | 
				
			||||||
 | 
					          "name": "projects_user_id_users_id_fk",
 | 
				
			||||||
 | 
					          "tableFrom": "projects",
 | 
				
			||||||
 | 
					          "tableTo": "users",
 | 
				
			||||||
 | 
					          "columnsFrom": [
 | 
				
			||||||
 | 
					            "user_id"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "columnsTo": [
 | 
				
			||||||
 | 
					            "id"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "onDelete": "no action",
 | 
				
			||||||
 | 
					          "onUpdate": "no action"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "project_fork_id_fk": {
 | 
				
			||||||
 | 
					          "name": "project_fork_id_fk",
 | 
				
			||||||
 | 
					          "tableFrom": "projects",
 | 
				
			||||||
 | 
					          "tableTo": "projects",
 | 
				
			||||||
 | 
					          "columnsFrom": [
 | 
				
			||||||
 | 
					            "fork_id"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "columnsTo": [
 | 
				
			||||||
 | 
					            "id"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "onDelete": "no action",
 | 
				
			||||||
 | 
					          "onUpdate": "no action"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "compositePrimaryKeys": {},
 | 
				
			||||||
 | 
					      "uniqueConstraints": {}
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "users": {
 | 
				
			||||||
 | 
					      "name": "users",
 | 
				
			||||||
 | 
					      "columns": {
 | 
				
			||||||
 | 
					        "id": {
 | 
				
			||||||
 | 
					          "name": "id",
 | 
				
			||||||
 | 
					          "type": "integer",
 | 
				
			||||||
 | 
					          "primaryKey": true,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": true
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "name": {
 | 
				
			||||||
 | 
					          "name": "name",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "email": {
 | 
				
			||||||
 | 
					          "name": "email",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "password": {
 | 
				
			||||||
 | 
					          "name": "password",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "created_at": {
 | 
				
			||||||
 | 
					          "name": "created_at",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": true,
 | 
				
			||||||
 | 
					          "autoincrement": false,
 | 
				
			||||||
 | 
					          "default": "CURRENT_TIMESTAMP"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "deleted_at": {
 | 
				
			||||||
 | 
					          "name": "deleted_at",
 | 
				
			||||||
 | 
					          "type": "text",
 | 
				
			||||||
 | 
					          "primaryKey": false,
 | 
				
			||||||
 | 
					          "notNull": false,
 | 
				
			||||||
 | 
					          "autoincrement": false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "indexes": {
 | 
				
			||||||
 | 
					        "users_email_unique": {
 | 
				
			||||||
 | 
					          "name": "users_email_unique",
 | 
				
			||||||
 | 
					          "columns": [
 | 
				
			||||||
 | 
					            "email"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "isUnique": true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "foreignKeys": {},
 | 
				
			||||||
 | 
					      "compositePrimaryKeys": {},
 | 
				
			||||||
 | 
					      "uniqueConstraints": {}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "enums": {},
 | 
				
			||||||
 | 
					  "_meta": {
 | 
				
			||||||
 | 
					    "schemas": {},
 | 
				
			||||||
 | 
					    "tables": {},
 | 
				
			||||||
 | 
					    "columns": {}
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1 +1,20 @@
 | 
				
			|||||||
{"version":"5","dialect":"sqlite","entries":[{"idx":0,"version":"5","when":1709221275637,"tag":"0000_swift_mandroid","breakpoints":true}]}
 | 
					{
 | 
				
			||||||
 | 
					  "version": "5",
 | 
				
			||||||
 | 
					  "dialect": "sqlite",
 | 
				
			||||||
 | 
					  "entries": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "idx": 0,
 | 
				
			||||||
 | 
					      "version": "5",
 | 
				
			||||||
 | 
					      "when": 1709221275637,
 | 
				
			||||||
 | 
					      "tag": "0000_swift_mandroid",
 | 
				
			||||||
 | 
					      "breakpoints": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "idx": 1,
 | 
				
			||||||
 | 
					      "version": "5",
 | 
				
			||||||
 | 
					      "when": 1709459027055,
 | 
				
			||||||
 | 
					      "tag": "0001_steep_dragon_man",
 | 
				
			||||||
 | 
					      "breakpoints": true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -32,6 +32,7 @@ export const project = sqliteTable(
 | 
				
			|||||||
    forkId: integer("fork_id"),
 | 
					    forkId: integer("fork_id"),
 | 
				
			||||||
    slug: text("slug").notNull().unique(),
 | 
					    slug: text("slug").notNull().unique(),
 | 
				
			||||||
    title: text("title").notNull(),
 | 
					    title: text("title").notNull(),
 | 
				
			||||||
 | 
					    thumbnail: text("thumbnail"),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    visibility: text("visibility", {
 | 
					    visibility: text("visibility", {
 | 
				
			||||||
      enum: ["public", "private", "unlisted"],
 | 
					      enum: ["public", "private", "unlisted"],
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ import puppeteer, { Browser } from "puppeteer";
 | 
				
			|||||||
let browser: Browser | null = null;
 | 
					let browser: Browser | null = null;
 | 
				
			||||||
let closeHandler: any;
 | 
					let closeHandler: any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const screenshot = async (slug: string) => {
 | 
					export const screenshot = async (url: string) => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    if (!browser) {
 | 
					    if (!browser) {
 | 
				
			||||||
      browser = await puppeteer.launch({
 | 
					      browser = await puppeteer.launch({
 | 
				
			||||||
@ -15,12 +15,15 @@ export const screenshot = async (slug: string) => {
 | 
				
			|||||||
    const page = await browser.newPage();
 | 
					    const page = await browser.newPage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await page.setViewport({ width: 512, height: 340 });
 | 
					    await page.setViewport({ width: 512, height: 340 });
 | 
				
			||||||
    await page.goto(`http://localhost:3000/api/preview/${slug}/index.html`, {
 | 
					    await page.goto(url, {
 | 
				
			||||||
      waitUntil: "networkidle0",
 | 
					      waitUntil: "networkidle0",
 | 
				
			||||||
      timeout: 5000,
 | 
					      timeout: 5000,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    const result = await page.screenshot();
 | 
					    const result = await page.screenshot();
 | 
				
			||||||
    await page.close();
 | 
					
 | 
				
			||||||
 | 
					    setTimeout(() => {
 | 
				
			||||||
 | 
					      page.close();
 | 
				
			||||||
 | 
					    }, 500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (closeHandler) {
 | 
					    if (closeHandler) {
 | 
				
			||||||
      clearTimeout(closeHandler);
 | 
					      clearTimeout(closeHandler);
 | 
				
			||||||
@ -30,7 +33,7 @@ export const screenshot = async (slug: string) => {
 | 
				
			|||||||
      browser?.close();
 | 
					      browser?.close();
 | 
				
			||||||
      browser = null;
 | 
					      browser = null;
 | 
				
			||||||
      closeHandler = null;
 | 
					      closeHandler = null;
 | 
				
			||||||
    }, 60000);
 | 
					    }, 30000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return result;
 | 
					    return result;
 | 
				
			||||||
  } catch (err) {
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
				
			|||||||
@ -12,8 +12,12 @@ export const fileExists = (path: string) => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getStorageDir = (...args: string[]) => {
 | 
				
			||||||
 | 
					  return path.join(process.cwd(), "storage", ...args);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getProjectDir = (project: ProjectSchema) => {
 | 
					export const getProjectDir = (project: ProjectSchema) => {
 | 
				
			||||||
  return path.resolve(process.cwd(), "storage/tmp", project.slug);
 | 
					  return path.join(process.cwd(), "storage/tmp", project.slug);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const uid = cuid2.init({
 | 
					export const uid = cuid2.init({
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user