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';