From 7bb89b87ae4093422f4877d78a592c49a1bf63d9 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 7 Mar 2026 08:20:05 -0800 Subject: [PATCH] =?UTF-8?q?fix(admin-service):=20=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E7=A3=81=E7=9B=98=E5=AD=98=E5=82=A8=20+=20Docker=20volume=20?= =?UTF-8?q?=E8=A7=A3=E5=86=B3=20EACCES=20=E6=9D=83=E9=99=90=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回退 MinIO 方案,改用本地磁盘存储(与 git 历史及 rwadurian 一致)。 根本原因:容器内无权在 ./uploads 创建目录(EACCES)。 修复方案: - UPLOAD_DIR 默认改为 /app/uploads(明确绝对路径) - docker-compose 添加命名卷 admin-uploads 挂载到 /app/uploads - 挂载卷由 Docker 管理,容器拥有写权限 Co-Authored-By: Claude Sonnet 4.6 --- backend/docker-compose.yml | 4 ++ .../services/file-storage.service.ts | 53 ++++++++----------- 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 23bf736..500ad62 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -361,6 +361,9 @@ services: - MINIO_BUCKET=app-releases - OSS_BASE_URL=https://oss.gogenex.com - JWT_ACCESS_SECRET=dev-access-secret-change-in-production + - UPLOAD_DIR=/app/uploads + volumes: + - admin-uploads:/app/uploads depends_on: postgres: condition: service_healthy @@ -559,6 +562,7 @@ volumes: postgres_data: redis_data: kafka_data: + admin-uploads: networks: genex-network: diff --git a/backend/services/admin-service/src/application/services/file-storage.service.ts b/backend/services/admin-service/src/application/services/file-storage.service.ts index 97e93ac..da12173 100644 --- a/backend/services/admin-service/src/application/services/file-storage.service.ts +++ b/backend/services/admin-service/src/application/services/file-storage.service.ts @@ -1,27 +1,19 @@ import { Injectable, Logger } from '@nestjs/common'; -import * as crypto from 'crypto'; +import * as fs from 'fs/promises'; +import * as fsSync from 'fs'; import * as path from 'path'; -import { Client as MinioClient } from 'minio'; -import { Readable } from 'stream'; +import * as crypto from 'crypto'; @Injectable() export class FileStorageService { private readonly logger = new Logger(FileStorageService.name); - private readonly minio: MinioClient; - private readonly bucket: string; + private readonly uploadDir: string; constructor() { - this.bucket = process.env.MINIO_BUCKET || 'app-releases'; - this.minio = new MinioClient({ - endPoint: process.env.MINIO_ENDPOINT || 'localhost', - port: parseInt(process.env.MINIO_PORT || '9000', 10), - useSSL: process.env.MINIO_USE_SSL === 'true', - accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin', - secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin', - }); + this.uploadDir = process.env.UPLOAD_DIR || '/app/uploads'; } - /** Generate a unique object key for a new upload */ + /** Generate a unique filename for a new upload */ generateObjectName(originalName: string, platform: string, versionName: string): string { const ext = originalName.split('.').pop() || 'bin'; const timestamp = Date.now(); @@ -29,24 +21,20 @@ export class FileStorageService { return `${platform}-${versionName}-${timestamp}-${random}.${ext}`; } - /** Upload file buffer to MinIO, return size and sha256 */ - async saveFile(buffer: Buffer, objectName: string): Promise<{ size: number; sha256: string }> { + /** Save file buffer to local disk, return size and sha256 */ + async saveFile(buffer: Buffer, filename: string): Promise<{ size: number; sha256: string }> { + await fs.mkdir(this.uploadDir, { recursive: true }); + const filePath = path.join(this.uploadDir, filename); const sha256 = crypto.createHash('sha256').update(buffer).digest('hex'); - const ext = path.extname(objectName).toLowerCase(); - const contentType = ext === '.apk' - ? 'application/vnd.android.package-archive' - : 'application/octet-stream'; - - await this.minio.putObject(this.bucket, objectName, buffer, buffer.length, { - 'Content-Type': contentType, - }); - this.logger.log(`Uploaded to MinIO: ${this.bucket}/${objectName} (${buffer.length} bytes)`); + await fs.writeFile(filePath, buffer); + this.logger.log(`Saved file: ${filename} (${buffer.length} bytes, sha256: ${sha256})`); return { size: buffer.length, sha256 }; } - /** Stream a MinIO object to a writable (e.g. Express Response) */ - async streamFile(objectName: string, destination: NodeJS.WritableStream): Promise { - const stream: Readable = await this.minio.getObject(this.bucket, path.basename(objectName)); + /** Stream local file to a writable (e.g. Express Response) */ + async streamFile(filename: string, destination: NodeJS.WritableStream): Promise { + const filePath = path.join(this.uploadDir, path.basename(filename)); + const stream = fsSync.createReadStream(filePath); await new Promise((resolve, reject) => { stream.pipe(destination as any); stream.on('end', resolve); @@ -54,10 +42,11 @@ export class FileStorageService { }); } - /** Get object size and content-type from MinIO */ - async statFile(objectName: string): Promise<{ size: number; contentType: string }> { - const stat = await this.minio.statObject(this.bucket, path.basename(objectName)); - const ext = path.extname(objectName).toLowerCase(); + /** Get file size and content-type from local disk */ + async statFile(filename: string): Promise<{ size: number; contentType: string }> { + const filePath = path.join(this.uploadDir, path.basename(filename)); + const stat = await fs.stat(filePath); + const ext = path.extname(filename).toLowerCase(); const contentType = ext === '.apk' ? 'application/vnd.android.package-archive' : 'application/octet-stream';