fix(admin-service): 将文件存储从本地磁盘改为 MinIO

原实现将 APK/IPA 写入容器内 ./uploads 目录,导致 EACCES 权限错误。
改为通过 MinIO SDK 上传到 oss.gogenex.com / app-releases bucket,
与 docker-compose 中已有的 MINIO_* 环境变量保持一致。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-07 08:15:42 -08:00
parent 6236ff3632
commit b9d2393aa1
1 changed files with 32 additions and 21 deletions

View File

@ -1,19 +1,27 @@
import { Injectable, Logger } from '@nestjs/common';
import * as fs from 'fs/promises';
import * as fsSync from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import * as path from 'path';
import { Client as MinioClient } from 'minio';
import { Readable } from 'stream';
@Injectable()
export class FileStorageService {
private readonly logger = new Logger(FileStorageService.name);
private readonly uploadDir: string;
private readonly minio: MinioClient;
private readonly bucket: string;
constructor() {
this.uploadDir = process.env.UPLOAD_DIR || './uploads';
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',
});
}
/** Generate a unique filename for a new upload */
/** Generate a unique object key for a new upload */
generateObjectName(originalName: string, platform: string, versionName: string): string {
const ext = originalName.split('.').pop() || 'bin';
const timestamp = Date.now();
@ -21,20 +29,24 @@ export class FileStorageService {
return `${platform}-${versionName}-${timestamp}-${random}.${ext}`;
}
/** 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);
/** Upload file buffer to MinIO, return size and sha256 */
async saveFile(buffer: Buffer, objectName: string): Promise<{ size: number; sha256: string }> {
const sha256 = crypto.createHash('sha256').update(buffer).digest('hex');
await fs.writeFile(filePath, buffer);
this.logger.log(`Saved file: ${filename} (${buffer.length} bytes, sha256: ${sha256})`);
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)`);
return { size: buffer.length, sha256 };
}
/** Stream local file to a writable (e.g. Express Response) */
async streamFile(filename: string, destination: NodeJS.WritableStream): Promise<void> {
const filePath = path.join(this.uploadDir, path.basename(filename));
const stream = fsSync.createReadStream(filePath);
/** Stream a MinIO object to a writable (e.g. Express Response) */
async streamFile(objectName: string, destination: NodeJS.WritableStream): Promise<void> {
const stream: Readable = await this.minio.getObject(this.bucket, path.basename(objectName));
await new Promise<void>((resolve, reject) => {
stream.pipe(destination as any);
stream.on('end', resolve);
@ -42,11 +54,10 @@ export class FileStorageService {
});
}
/** 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();
/** 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();
const contentType = ext === '.apk'
? 'application/vnd.android.package-archive'
: 'application/octet-stream';