first commit
This commit is contained in:
12
.dockerignore
Normal file
12
.dockerignore
Normal 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
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# deps
|
||||||
|
node_modules/
|
||||||
|
*.db
|
||||||
3
.prettierrc
Normal file
3
.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"semi": false
|
||||||
|
}
|
||||||
16
Dockerfile
Normal file
16
Dockerfile
Normal 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
10
drizzle.config.ts
Normal 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
1670
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal 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
40
src/database.ts
Normal 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
225
src/index.ts
Normal 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
20
src/schema.ts
Normal 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
20
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user