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/
 | 
			
		||||
storage/**/*
 | 
			
		||||
!.gitkeep
 | 
			
		||||
.env
 | 
			
		||||
 | 
			
		||||
@ -16,10 +16,14 @@ const ProjectCard = ({ project }: Props) => {
 | 
			
		||||
      href={`/${project.slug}`}
 | 
			
		||||
      className="border border-white/20 hover:border-white/40 rounded-lg transition-colors overflow-hidden"
 | 
			
		||||
    >
 | 
			
		||||
      <img
 | 
			
		||||
        src={`/api/thumbnail/${project.slug}`}
 | 
			
		||||
        className="w-full aspect-[3/2] bg-background object-cover"
 | 
			
		||||
      />
 | 
			
		||||
      {project.thumbnail ? (
 | 
			
		||||
        <img
 | 
			
		||||
          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="size-8 rounded-full bg-white/80"></div>
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,8 @@ import StatusBar from "./status-bar";
 | 
			
		||||
import { FiTerminal } from "react-icons/fi";
 | 
			
		||||
import SettingsDialog from "./settings-dialog";
 | 
			
		||||
import FileIcon from "~/components/ui/file-icon";
 | 
			
		||||
import { api } from "~/lib/api";
 | 
			
		||||
import { useMutation } from "@tanstack/react-query";
 | 
			
		||||
 | 
			
		||||
const Editor = () => {
 | 
			
		||||
  const { project, initialFiles } = useData<Data>();
 | 
			
		||||
@ -33,11 +35,17 @@ const Editor = () => {
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const openedFilesData = trpc.file.getAll.useQuery(
 | 
			
		||||
    { projectId: project.id, id: curOpenFiles },
 | 
			
		||||
    { projectId: project.id!, id: curOpenFiles },
 | 
			
		||||
    { enabled: curOpenFiles.length > 0, initialData: initialFiles }
 | 
			
		||||
  );
 | 
			
		||||
  const [openedFiles, setOpenedFiles] = useState<any[]>(initialFiles);
 | 
			
		||||
 | 
			
		||||
  const generateThumbnail = useMutation({
 | 
			
		||||
    mutationFn: () => {
 | 
			
		||||
      return api(`/thumbnail/${project.slug!}`, { method: "PATCH" });
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const deleteFile = trpc.file.delete.useMutation({
 | 
			
		||||
    onSuccess: (file) => {
 | 
			
		||||
      trpcUtils.file.getAll.invalidate();
 | 
			
		||||
@ -76,6 +84,14 @@ const Editor = () => {
 | 
			
		||||
  //   api(`/sandbox/${project.slug}/start`, { method: "POST" }).catch(() => {});
 | 
			
		||||
  // }, [project]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const itv = setInterval(() => generateThumbnail.mutate(), 60000);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      clearInterval(itv);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const onOpenFile = useCallback(
 | 
			
		||||
    (fileId: number, autoSwitchTab = true) => {
 | 
			
		||||
      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 preview from "./preview";
 | 
			
		||||
import trpc from "./trpc/handler";
 | 
			
		||||
import { thumbnail } from "./thumbnail";
 | 
			
		||||
import thumbnail from "./thumbnail";
 | 
			
		||||
import sandbox from "./sandbox";
 | 
			
		||||
import { nocache } from "../middlewares/nocache";
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,6 @@ const api = Router();
 | 
			
		||||
api.use("/trpc", trpc);
 | 
			
		||||
api.use("/preview", nocache, preview);
 | 
			
		||||
api.use("/sandbox", sandbox);
 | 
			
		||||
 | 
			
		||||
api.get("/thumbnail/:slug", thumbnail);
 | 
			
		||||
api.use("/thumbnail", thumbnail);
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
CREATE TABLE `projects` (
 | 
			
		||||
	`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
 | 
			
		||||
	`fork_id` integer;
 | 
			
		||||
	`user_id` integer NOT NULL,
 | 
			
		||||
	`fork_id` integer,
 | 
			
		||||
	`slug` text NOT NULL,
 | 
			
		||||
	`title` text NOT NULL,
 | 
			
		||||
	`visibility` text DEFAULT 'private',
 | 
			
		||||
	`settings` text DEFAULT [object Object],
 | 
			
		||||
	`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
 | 
			
		||||
	`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
 | 
			
		||||
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"),
 | 
			
		||||
    slug: text("slug").notNull().unique(),
 | 
			
		||||
    title: text("title").notNull(),
 | 
			
		||||
    thumbnail: text("thumbnail"),
 | 
			
		||||
 | 
			
		||||
    visibility: text("visibility", {
 | 
			
		||||
      enum: ["public", "private", "unlisted"],
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import puppeteer, { Browser } from "puppeteer";
 | 
			
		||||
let browser: Browser | null = null;
 | 
			
		||||
let closeHandler: any;
 | 
			
		||||
 | 
			
		||||
export const screenshot = async (slug: string) => {
 | 
			
		||||
export const screenshot = async (url: string) => {
 | 
			
		||||
  try {
 | 
			
		||||
    if (!browser) {
 | 
			
		||||
      browser = await puppeteer.launch({
 | 
			
		||||
@ -15,12 +15,15 @@ export const screenshot = async (slug: string) => {
 | 
			
		||||
    const page = await browser.newPage();
 | 
			
		||||
 | 
			
		||||
    await page.setViewport({ width: 512, height: 340 });
 | 
			
		||||
    await page.goto(`http://localhost:3000/api/preview/${slug}/index.html`, {
 | 
			
		||||
    await page.goto(url, {
 | 
			
		||||
      waitUntil: "networkidle0",
 | 
			
		||||
      timeout: 5000,
 | 
			
		||||
    });
 | 
			
		||||
    const result = await page.screenshot();
 | 
			
		||||
    await page.close();
 | 
			
		||||
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      page.close();
 | 
			
		||||
    }, 500);
 | 
			
		||||
 | 
			
		||||
    if (closeHandler) {
 | 
			
		||||
      clearTimeout(closeHandler);
 | 
			
		||||
@ -30,7 +33,7 @@ export const screenshot = async (slug: string) => {
 | 
			
		||||
      browser?.close();
 | 
			
		||||
      browser = null;
 | 
			
		||||
      closeHandler = null;
 | 
			
		||||
    }, 60000);
 | 
			
		||||
    }, 30000);
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
  } 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) => {
 | 
			
		||||
  return path.resolve(process.cwd(), "storage/tmp", project.slug);
 | 
			
		||||
  return path.join(process.cwd(), "storage/tmp", project.slug);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const uid = cuid2.init({
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user