All checks were successful
Test Asgard Runner / test (push) Successful in 2s
Implements GET/POST /api/files proxy for all VueFinder operations
(list, search, preview, download, delete, rename, move, copy, mkdir,
new-file, save, upload). Routes via NATS request-reply to the companion
agent on corrosion.{license_id}.files.cmd with 30s timeout. Gated
behind files.view (GET) and files.manage (POST) permissions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
122 lines
3.6 KiB
TypeScript
122 lines
3.6 KiB
TypeScript
import {
|
|
Controller,
|
|
Get,
|
|
Post,
|
|
Query,
|
|
Body,
|
|
Res,
|
|
UseGuards,
|
|
UseInterceptors,
|
|
UploadedFile,
|
|
HttpException,
|
|
HttpStatus,
|
|
} from '@nestjs/common';
|
|
import { FileInterceptor } from '@nestjs/platform-express';
|
|
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
|
import { Response } from 'express';
|
|
import { CurrentTenant } from '../../common/decorators/current-tenant.decorator';
|
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
|
import { PermissionsGuard } from '../../common/guards/permissions.guard';
|
|
import { FilesService } from './files.service';
|
|
|
|
@ApiTags('Files')
|
|
@ApiBearerAuth()
|
|
@Controller('files')
|
|
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
|
export class FilesController {
|
|
constructor(private readonly filesService: FilesService) {}
|
|
|
|
// VueFinder GET operations: ?q=index (list), ?q=search, ?q=preview, ?q=download
|
|
@Get()
|
|
@RequirePermission('files.view')
|
|
async handleGet(
|
|
@CurrentTenant() licenseId: string,
|
|
@Query('q') operation: string,
|
|
@Query('path') path: string,
|
|
@Query('filter') filter: string,
|
|
@Res({ passthrough: true }) res: Response,
|
|
) {
|
|
switch (operation) {
|
|
case 'index':
|
|
case undefined:
|
|
case '':
|
|
return this.filesService.list(licenseId, path || 'server://');
|
|
case 'search':
|
|
return this.filesService.search(licenseId, path || 'server://', filter);
|
|
case 'preview':
|
|
return this.filesService.preview(licenseId, path);
|
|
case 'download': {
|
|
const result = await this.filesService.download(licenseId, path);
|
|
res.setHeader('Content-Type', 'application/octet-stream');
|
|
res.setHeader(
|
|
'Content-Disposition',
|
|
`attachment; filename="${result.filename}"`,
|
|
);
|
|
res.send(result.content);
|
|
return;
|
|
}
|
|
default:
|
|
throw new HttpException(
|
|
`Unknown operation: ${operation}`,
|
|
HttpStatus.BAD_REQUEST,
|
|
);
|
|
}
|
|
}
|
|
|
|
// VueFinder POST operations: ?q=delete, rename, move, copy, new-folder, new-file, save, upload
|
|
@Post()
|
|
@RequirePermission('files.manage')
|
|
@UseInterceptors(FileInterceptor('upload'))
|
|
async handlePost(
|
|
@CurrentTenant() licenseId: string,
|
|
@Query('q') operation: string,
|
|
@Body() body: Record<string, any>,
|
|
@UploadedFile() file?: Express.Multer.File,
|
|
) {
|
|
switch (operation) {
|
|
case 'delete':
|
|
return this.filesService.delete(licenseId, body.path, body.items);
|
|
case 'rename':
|
|
return this.filesService.rename(
|
|
licenseId,
|
|
body.path,
|
|
body.item,
|
|
body.name,
|
|
);
|
|
case 'move':
|
|
return this.filesService.move(
|
|
licenseId,
|
|
body.path,
|
|
body.items,
|
|
body.destination,
|
|
);
|
|
case 'copy':
|
|
return this.filesService.copy(
|
|
licenseId,
|
|
body.path,
|
|
body.items,
|
|
body.destination,
|
|
);
|
|
case 'new-folder':
|
|
case 'mkdir':
|
|
return this.filesService.createFolder(licenseId, body.path, body.name);
|
|
case 'new-file':
|
|
case 'newfile':
|
|
return this.filesService.createFile(licenseId, body.path, body.name);
|
|
case 'save':
|
|
return this.filesService.save(licenseId, body.path, body.content);
|
|
case 'upload':
|
|
if (!file) {
|
|
throw new HttpException('No file provided', HttpStatus.BAD_REQUEST);
|
|
}
|
|
return this.filesService.upload(licenseId, body.path, file);
|
|
default:
|
|
throw new HttpException(
|
|
`Unknown operation: ${operation}`,
|
|
HttpStatus.BAD_REQUEST,
|
|
);
|
|
}
|
|
}
|
|
}
|