import { Injectable, HttpException, HttpStatus, Logger } from '@nestjs/common'; import { NatsService } from '../../services/nats.service'; interface AgentFileResponse { success: boolean; error?: string; data?: unknown; } interface DownloadResult { filename: string; content: unknown; } @Injectable() export class FilesService { private readonly logger = new Logger(FilesService.name); constructor(private readonly natsService: NatsService) {} // Send a file manager command to the companion agent via NATS request-reply. // The agent returns { success: bool, error?: string, data?: VueFinderResponse }. private async sendFileCommand( licenseId: string, payload: Record, ): Promise { const subject = `corrosion.${licenseId}.files.cmd`; try { const response = (await this.natsService.request( subject, payload, 30000, )) as AgentFileResponse | null; // Offline mode — agent unreachable, return null rather than crashing if (response === null) { throw new HttpException( 'Agent not reachable (offline mode)', HttpStatus.SERVICE_UNAVAILABLE, ); } if (!response.success) { throw new HttpException( response.error || 'File operation failed', HttpStatus.BAD_REQUEST, ); } return response.data; } catch (error) { if (error instanceof HttpException) throw error; this.logger.error( `File command failed on subject ${subject}: ${(error as Error).message}`, ); throw new HttpException( 'Agent not reachable or timed out', HttpStatus.SERVICE_UNAVAILABLE, ); } } async list(licenseId: string, path: string): Promise { return this.sendFileCommand(licenseId, { func: 'fm_list', path }); } async search( licenseId: string, path: string, filter: string, ): Promise { return this.sendFileCommand(licenseId, { func: 'fm_search', path, filter }); } async preview(licenseId: string, path: string): Promise { return this.sendFileCommand(licenseId, { func: 'fm_preview', path }); } async download(licenseId: string, path: string): Promise { const result = await this.sendFileCommand(licenseId, { func: 'fm_preview', path, }); const basename = path.split('/').pop() || 'download'; return { filename: basename, content: result }; } async delete( licenseId: string, path: string, items: string[], ): Promise { return this.sendFileCommand(licenseId, { func: 'fm_delete', path, items }); } async rename( licenseId: string, path: string, item: string, name: string, ): Promise { return this.sendFileCommand(licenseId, { func: 'fm_rename', path, items: [item], name, }); } async move( licenseId: string, path: string, items: string[], destination: string, ): Promise { return this.sendFileCommand(licenseId, { func: 'fm_move', path, items, destination, }); } async copy( licenseId: string, path: string, items: string[], destination: string, ): Promise { return this.sendFileCommand(licenseId, { func: 'fm_copy', path, items, destination, }); } async createFolder( licenseId: string, path: string, name: string, ): Promise { return this.sendFileCommand(licenseId, { func: 'fm_mkdir', path, name }); } async createFile( licenseId: string, path: string, name: string, ): Promise { return this.sendFileCommand(licenseId, { func: 'fm_mkfile', path, name }); } async save( licenseId: string, path: string, content: string, ): Promise { return this.sendFileCommand(licenseId, { func: 'fm_save', path, content }); } async upload( licenseId: string, path: string, file: Express.Multer.File, ): Promise { // Encode binary content as base64 so it survives JSON serialization over NATS const content = file.buffer.toString('base64'); return this.sendFileCommand(licenseId, { func: 'fm_upload', path, filename: file.originalname, content, }); } }