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