mirror of
https://github.com/khairul169/code-share.git
synced 2025-04-28 08:39:35 +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