first commit

This commit is contained in:
2025-06-28 15:16:06 +08:00
parent 0072220d05
commit 3c62871948
27 changed files with 2194 additions and 27 deletions
+12
View File
@@ -0,0 +1,12 @@
export const config = {
port: process.env.PORT || 3000,
auth: {
username: process.env.AUTH_USERNAME || 'admin',
password: process.env.AUTH_PASSWORD || 'password'
},
// For file upload settings
upload: {
maxFileSize: 10 * 1024 * 1024, // 10MB
allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'application/x-msdownload']
}
}
+216
View File
@@ -0,0 +1,216 @@
import * as Minio from 'minio'
import { randomUUID } from 'crypto'
import { minioClient, BUCKET_NAME, initializeMinIO } from './minioClient'
export interface StoredFile {
id: string
filename: string
contentType: string
objectName: string // MinIO 对象名称
size: number
uploadTime: Date
}
export interface FileMetadata {
id: string
originalFilename: string
contentType: string
size: number
uploadTime: Date
objectName: string
}
// 生成对象名称,使用分层结构优化性能
function generateObjectName(type: 'screenshot' | 'version' | 'other', hostname?: string): string {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const uuid = randomUUID()
if (type === 'screenshot' && hostname) {
return `screenshots/${year}/${month}/${day}/${hostname}/${uuid}`
} else if (type === 'version') {
return `versions/${year}/${month}/${uuid}`
} else {
return `files/${year}/${month}/${day}/${uuid}`
}
}
// 确保 MinIO 已初始化
let minioInitialized = false
export async function ensureMinIOReady(): Promise<void> {
if (!minioInitialized) {
const success = await initializeMinIO()
if (!success) {
throw new Error('MinIO 初始化失败')
}
minioInitialized = true
}
}
export async function storeFile(
buffer: Buffer,
filename: string,
contentType: string = 'application/octet-stream',
type: 'screenshot' | 'version' | 'other' = 'other',
hostname?: string
): Promise<StoredFile> {
await ensureMinIOReady()
const id = randomUUID()
const ext = filename.split('.').pop() || ''
const objectName = `${generateObjectName(type, hostname)}.${ext}`
// 设置元数据
const metadata = {
'Content-Type': contentType,
'X-Original-Filename': filename,
'X-File-ID': id,
'X-Upload-Time': new Date().toISOString(),
'X-File-Type': type,
...(hostname && { 'X-Hostname': hostname })
}
try {
// 上传到 MinIO
await minioClient.putObject(BUCKET_NAME, objectName, buffer, buffer.length, metadata)
console.log(`✅ 文件上传成功: ${objectName} (${buffer.length} bytes)`)
return {
id,
filename,
contentType,
objectName,
size: buffer.length,
uploadTime: new Date()
}
} catch (error) {
console.error('❌ 文件上传失败:', error)
throw new Error(`文件上传失败: ${error}`)
}
}
export async function getFile(fileIdOrObjectName: string): Promise<{ buffer: Buffer; contentType: string; filename: string } | null> {
try {
await ensureMinIOReady()
let objectName = fileIdOrObjectName
// 如果看起来是 fileId (UUID格式),我们需要从数据库查找 objectName
// 这里简化处理:如果包含 '/' 就认为是 objectName,否则当作 fileId
if (!fileIdOrObjectName.includes('/')) {
// 这是一个 fileId,需要从数据库查找对应的 objectName
// 注意:这里需要在 API 层面处理,因为不同的模型存储在不同表中
// 暂时直接使用传入的值,让 API 层传递正确的 objectName
console.log(`⚠️ 警告: getFile 收到的可能是 fileId 而不是 objectName: ${fileIdOrObjectName}`)
}
// 获取对象信息
const stat = await minioClient.statObject(BUCKET_NAME, objectName)
// 下载文件
const stream = await minioClient.getObject(BUCKET_NAME, objectName)
const chunks: Buffer[] = []
return new Promise((resolve, reject) => {
stream.on('data', (chunk) => chunks.push(chunk))
stream.on('end', () => {
const buffer = Buffer.concat(chunks)
resolve({
buffer,
contentType: stat.metaData?.['content-type'] || 'application/octet-stream',
filename: stat.metaData?.['x-original-filename'] || objectName.split('/').pop() || 'unknown'
})
})
stream.on('error', (error) => {
console.error('❌ 文件下载失败:', error)
reject(error)
})
})
} catch (error) {
console.error('❌ 获取文件失败:', error)
return null
}
}
// 优化版本:直接通过 objectName 获取文件(用于已知对象名的情况)
export async function getFileByObjectName(objectName: string): Promise<{ buffer: Buffer; contentType: string; filename: string } | null> {
try {
await ensureMinIOReady()
// 获取对象信息
const stat = await minioClient.statObject(BUCKET_NAME, objectName)
// 下载文件
const stream = await minioClient.getObject(BUCKET_NAME, objectName)
const chunks: Buffer[] = []
return new Promise((resolve, reject) => {
stream.on('data', (chunk) => chunks.push(chunk))
stream.on('end', () => {
const buffer = Buffer.concat(chunks)
resolve({
buffer,
contentType: stat.metaData?.['content-type'] || 'application/octet-stream',
filename: stat.metaData?.['x-original-filename'] || objectName.split('/').pop() || 'unknown'
})
})
stream.on('error', (error) => {
console.error('❌ 文件下载失败:', error)
reject(error)
})
})
} catch (error) {
console.error('❌ 获取文件失败:', error)
return null
}
}
export async function deleteFile(objectName: string): Promise<boolean> {
try {
await ensureMinIOReady()
await minioClient.removeObject(BUCKET_NAME, objectName)
console.log(`✅ 文件删除成功: ${objectName}`)
return true
} catch (error) {
console.error('❌ 文件删除失败:', error)
return false
}
}
// 获取文件统计信息
export async function getStorageStats(): Promise<{
totalObjects: number;
totalSize: number;
bucketName: string;
}> {
try {
await ensureMinIOReady()
let totalObjects = 0
let totalSize = 0
const objectsStream = minioClient.listObjects(BUCKET_NAME, '', true)
await new Promise<void>((resolve, reject) => {
objectsStream.on('data', (obj) => {
totalObjects++
totalSize += obj.size || 0
})
objectsStream.on('end', resolve)
objectsStream.on('error', reject)
})
return {
totalObjects,
totalSize,
bucketName: BUCKET_NAME
}
} catch (error) {
console.error('❌ 获取存储统计失败:', error)
return { totalObjects: 0, totalSize: 0, bucketName: BUCKET_NAME }
}
}
+67
View File
@@ -0,0 +1,67 @@
import { NextRequest, NextResponse } from 'next/server'
import { config } from './config'
export function withAuth(handler: (req: NextRequest) => Promise<NextResponse>) {
return async (req: NextRequest) => {
const url = new URL(req.url)
// Skip auth for certain paths
if (
url.pathname.startsWith('/api/') ||
url.pathname.startsWith('/screenshots/') ||
url.pathname.startsWith('/downloads/') ||
url.pathname.includes('install') ||
url.pathname.includes('WinupdateCore') ||
req.method === 'POST'
) {
return handler(req)
}
const authHeader = req.headers.get('authorization')
if (!authHeader) {
return new NextResponse('Authentication required', {
status: 401,
headers: {
'WWW-Authenticate': 'Basic realm="Restricted Access"'
}
})
}
try {
const auth = Buffer.from(authHeader.split(' ')[1], 'base64').toString()
const [username, password] = auth.split(':')
if (username === config.auth.username && password === config.auth.password) {
return handler(req)
} else {
return new NextResponse('Authentication failed', {
status: 401,
headers: {
'WWW-Authenticate': 'Basic realm="Restricted Access"'
}
})
}
} catch {
return new NextResponse('Invalid authentication', {
status: 401,
headers: {
'WWW-Authenticate': 'Basic realm="Restricted Access"'
}
})
}
}
}
export function withCors(handler: (req: NextRequest) => Promise<NextResponse>) {
return async (req: NextRequest) => {
const response = await handler(req)
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
response.headers.set('Access-Control-Expose-Headers', 'Content-Range, X-Content-Range')
return response
}
}
+80
View File
@@ -0,0 +1,80 @@
import * as Minio from 'minio'
// MinIO 配置 - 从环境变量读取
export const minioConfig = {
endPoint: process.env.MINIO_ENDPOINT || 'localhost',
port: parseInt(process.env.MINIO_PORT || '9000'),
useSSL: process.env.MINIO_USE_SSL === 'true',
accessKey: process.env.MINIO_ACCESS_KEY || '',
secretKey: process.env.MINIO_SECRET_KEY || ''
}
export const BUCKET_NAME = process.env.MINIO_BUCKET_NAME || 'winupdate'
// 验证配置
if (!minioConfig.accessKey || !minioConfig.secretKey) {
console.error('❌ MinIO 配置不完整,请检查环境变量 MINIO_ACCESS_KEY 和 MINIO_SECRET_KEY')
}
// 创建 MinIO 客户端
export const minioClient = new Minio.Client(minioConfig)
// 测试连接并确保 bucket 存在
export async function initializeMinIO(): Promise<boolean> {
try {
// 测试连接
await minioClient.listBuckets()
console.log('✅ MinIO 连接成功')
// 检查 bucket 是否存在
const bucketExists = await minioClient.bucketExists(BUCKET_NAME)
if (!bucketExists) {
console.log(`❌ Bucket "${BUCKET_NAME}" 不存在`)
return false
}
console.log(`✅ Bucket "${BUCKET_NAME}" 存在`)
return true
} catch (error) {
console.error('❌ MinIO 连接失败:', error)
return false
}
}
// 测试上传小文件
export async function testMinIOUpload(): Promise<boolean> {
try {
const testData = Buffer.from('Hello MinIO! This is a test file.')
const objectName = `test/test-${Date.now()}.txt`
await minioClient.putObject(BUCKET_NAME, objectName, testData)
console.log(`✅ 测试文件上传成功: ${objectName}`)
// 测试下载
const stream = await minioClient.getObject(BUCKET_NAME, objectName)
const chunks: Buffer[] = []
return new Promise((resolve, reject) => {
stream.on('data', (chunk) => chunks.push(chunk))
stream.on('end', () => {
const downloadedData = Buffer.concat(chunks)
if (downloadedData.toString() === testData.toString()) {
console.log('✅ 测试文件下载成功')
// 清理测试文件
minioClient.removeObject(BUCKET_NAME, objectName).catch(console.error)
resolve(true)
} else {
console.log('❌ 测试文件下载数据不匹配')
resolve(false)
}
})
stream.on('error', (error) => {
console.error('❌ 测试文件下载失败:', error)
reject(error)
})
})
} catch (error) {
console.error('❌ MinIO 测试失败:', error)
return false
}
}
+9
View File
@@ -0,0 +1,9 @@
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
+8
View File
@@ -0,0 +1,8 @@
// Simple notification/logging function
// In production, you might want to integrate with actual push notification service
export function push(message: string) {
console.log(`[NOTIFICATION] ${new Date().toISOString()}: ${message}`)
// Here you could add actual push notification logic
// For example, sending to webhook, email, or push notification service
}