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 4751ec5..97e93ac 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,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 { - 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 { + const stream: Readable = await this.minio.getObject(this.bucket, path.basename(objectName)); await new Promise((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';