first commit

This commit is contained in:
2025-10-21 12:38:08 +08:00
commit cfe28c316d
11 changed files with 2041 additions and 0 deletions

12
.dockerignore Normal file
View File

@@ -0,0 +1,12 @@
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.nyc_output
coverage
.nyc_output
.coverage
.coverage/
*.log

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# deps
node_modules/
*.db

3
.prettierrc Normal file
View File

@@ -0,0 +1,3 @@
{
"semi": false
}

16
Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
FROM node:24-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
ENV NODE_ENV=production
CMD ["npm", "start"]

10
drizzle.config.ts Normal file
View File

@@ -0,0 +1,10 @@
import type { Config } from 'drizzle-kit'
export default {
schema: './src/schema.ts',
out: './drizzle',
dialect: 'sqlite',
dbCredentials: {
url: './uploads.db',
},
} satisfies Config

1670
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "server",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@hono/node-server": "^1.19.5",
"better-sqlite3": "^12.4.1",
"drizzle-kit": "^0.31.5",
"drizzle-orm": "^0.44.6",
"hono": "^4.10.1"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}
}

40
src/database.ts Normal file
View File

@@ -0,0 +1,40 @@
import Database from 'better-sqlite3'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import { join } from 'path'
import * as schema from './schema'
const dbPath = join(process.cwd(), 'uploads.db')
const sqlite = new Database(dbPath)
const db = drizzle(sqlite, { schema })
// 初始化数据库表
export async function initDatabase() {
// 创建projects表
await db.run(`
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
entry_point TEXT NOT NULL,
is_active BOOLEAN DEFAULT 1,
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
// 创建files表
await db.run(`
CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
original_name TEXT NOT NULL,
content TEXT NOT NULL,
size INTEGER NOT NULL,
project_id INTEGER NOT NULL,
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES projects (id)
)
`)
}
export default db
export { schema }

225
src/index.ts Normal file
View File

@@ -0,0 +1,225 @@
import { Hono } from "hono"
import { serve } from "@hono/node-server"
import { cors } from "hono/cors"
import db, { schema, initDatabase } from "./database"
import { eq, desc } from "drizzle-orm"
import { randomBytes } from "crypto"
const app = new Hono()
// 启用CORS
app.use("*", cors())
// 生成安全的slug
const generateSlug = (): string => {
return randomBytes(6).toString('hex')
}
// 辅助函数根据slug查找项目
const findProjectBySlug = async (slug: string) => {
const project = await db
.select()
.from(schema.projects)
.where(eq(schema.projects.slug, slug))
.limit(1)
return project[0] || null
}
// 辅助函数:创建项目
const createProject = async (name: string, entryPoint: string) => {
const slug = generateSlug()
const projectResult = await db
.insert(schema.projects)
.values({
slug,
name,
entryPoint,
})
.returning({ id: schema.projects.id, slug: schema.projects.slug })
return projectResult[0]
}
// 辅助函数:存储文件
const storeFile = async (
file: globalThis.File,
projectId: number,
filename: string
) => {
const content = await file.text()
await db.insert(schema.files).values({
filename,
originalName: file.name,
content,
size: file.size,
projectId,
})
}
// 辅助函数:统一错误处理
const handleError = (
c: any,
error: any,
message: string,
statusCode: number = 500
) => {
console.error(message, error)
return c.json({ error: message }, statusCode)
}
// 获取项目列表
app.get("/api/projects", async (c) => {
try {
const projectList = await db
.select()
.from(schema.projects)
.orderBy(desc(schema.projects.uploadedAt))
return c.json(projectList)
} catch (error) {
return handleError(c, error, "获取项目列表失败")
}
})
// 代理路由:将 /projects/:slug 重定向到 /api/projects/:slug
app.get("/projects/:slug", async (c) => {
const slug = c.req.param("slug")
return c.redirect(`/api/projects/${slug}`)
})
// 获取项目内容(托管)
app.get("/api/projects/:slug", async (c) => {
try {
const slug = c.req.param("slug")
const project = await findProjectBySlug(slug)
if (!project) {
return c.json({ error: "项目不存在" }, 404)
}
if (!project.isActive) {
return c.json({ error: "项目已停用" }, 403)
}
// 查找项目中的文件
const targetFile = await db
.select()
.from(schema.files)
.where(eq(schema.files.projectId, project.id))
.limit(1)
if (!targetFile.length) {
return c.json({ error: "项目文件不存在" }, 404)
}
return new Response(targetFile[0].content, {
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-cache"
},
})
} catch (error) {
return handleError(c, error, "获取项目内容失败")
}
})
// 项目上传API
app.post("/api/upload", async (c) => {
try {
const formData = await c.req.formData()
const file = formData.get("file") as globalThis.File
const projectName = formData.get("projectName") as string
if (!file) {
return c.json({ error: "没有选择文件" }, 400)
}
if (!file.name.endsWith(".html")) {
return c.json({ error: "只支持HTML文件" }, 400)
}
if (!projectName) {
return c.json({ error: "项目名称不能为空" }, 400)
}
const project = await createProject(projectName, file.name)
const filename = `file_${Date.now()}.html`
await storeFile(file, project.id, filename)
return c.json({
id: project.id,
slug: project.slug,
name: projectName,
message: "HTML文件上传成功",
url: `/api/projects/${project.slug}/`,
})
} catch (error) {
return handleError(c, error, "上传失败")
}
})
// 切换项目激活状态
app.patch("/api/projects/:slug/toggle", async (c) => {
try {
const slug = c.req.param("slug")
const project = await findProjectBySlug(slug)
if (!project) {
return c.json({ error: "项目不存在" }, 404)
}
// 切换激活状态
const newStatus = !project.isActive
await db
.update(schema.projects)
.set({ isActive: newStatus })
.where(eq(schema.projects.slug, slug))
return c.json({
message: `项目已${newStatus ? '激活' : '停用'}`,
isActive: newStatus
})
} catch (error) {
return handleError(c, error, "切换项目状态失败")
}
})
// 删除项目
app.delete("/api/projects/:slug", async (c) => {
try {
const slug = c.req.param("slug")
const project = await findProjectBySlug(slug)
if (!project) {
return c.json({ error: "项目不存在" }, 404)
}
// 删除项目相关的所有文件
await db.delete(schema.files).where(eq(schema.files.projectId, project.id))
// 删除项目
await db.delete(schema.projects).where(eq(schema.projects.slug, slug))
return c.json({ message: "项目删除成功" })
} catch (error) {
return handleError(c, error, "删除项目失败")
}
})
// 启动服务器
const port = 3000
// 初始化数据库
initDatabase()
.then(() => {
console.log("数据库初始化完成")
console.log(`服务器运行在 http://localhost:${port}`)
serve({
fetch: app.fetch,
port,
})
})
.catch((error) => {
console.error("数据库初始化失败:", error)
process.exit(1)
})

20
src/schema.ts Normal file
View File

@@ -0,0 +1,20 @@
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const projects = sqliteTable('projects', {
id: integer('id').primaryKey({ autoIncrement: true }),
slug: text('slug').notNull().unique(),
name: text('name').notNull(),
entryPoint: text('entry_point').notNull(),
isActive: integer('is_active', { mode: 'boolean' }).default(true),
uploadedAt: text('uploaded_at').default('CURRENT_TIMESTAMP')
})
export const files = sqliteTable('files', {
id: integer('id').primaryKey({ autoIncrement: true }),
filename: text('filename').notNull(),
originalName: text('original_name').notNull(),
content: text('content').notNull(),
size: integer('size').notNull(),
projectId: integer('project_id').notNull().references(() => projects.id),
uploadedAt: text('uploaded_at').default('CURRENT_TIMESTAMP')
})

20
tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": ["src/**/*", "drizzle.config.ts"],
"exclude": ["node_modules", "dist"]
}