first commit
This commit is contained in:
@@ -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']
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user