SEO_iamge_renamer_starting_.../packages/api/src/batches/batches.controller.ts
DustyWalker 2add73a264 feat(api): add batches module for batch processing management
- Implement POST /api/batch endpoint for multipart file uploads
- Add GET /api/batch/{batchId}/status for real-time progress tracking
- Support batch cancellation, retry, and ZIP download generation
- Include comprehensive validation and quota checking
- Add progress broadcasting integration with WebSocket gateway
- Implement batch lifecycle management (create, process, complete)

Resolves requirements §29, §32, §73-§74 for batch processing API.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-05 17:24:15 +02:00

275 lines
No EOL
7.6 KiB
TypeScript

import {
Controller,
Post,
Get,
Param,
Body,
UploadedFiles,
UseInterceptors,
UseGuards,
Request,
HttpStatus,
BadRequestException,
PayloadTooLargeException,
ForbiddenException,
} from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
import { ApiTags, ApiOperation, ApiResponse, ApiConsumes, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/auth.guard';
import { BatchesService } from './batches.service';
import { CreateBatchDto, BatchUploadResponseDto } from './dto/create-batch.dto';
import { BatchStatusResponseDto, BatchListResponseDto } from './dto/batch-status.dto';
@ApiTags('batches')
@Controller('api/batch')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
export class BatchesController {
constructor(private readonly batchesService: BatchesService) {}
@Post()
@UseInterceptors(FilesInterceptor('files', 1000)) // Max 1000 files per batch
@ApiOperation({
summary: 'Upload batch of images for processing',
description: 'Uploads multiple images and starts batch processing with AI analysis and SEO filename generation'
})
@ApiConsumes('multipart/form-data')
@ApiResponse({
status: HttpStatus.OK,
description: 'Batch created successfully',
type: BatchUploadResponseDto,
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Invalid files or missing data',
})
@ApiResponse({
status: HttpStatus.PAYLOAD_TOO_LARGE,
description: 'File size or count exceeds limits',
})
@ApiResponse({
status: HttpStatus.FORBIDDEN,
description: 'Insufficient quota remaining',
})
async uploadBatch(
@UploadedFiles() files: Express.Multer.File[],
@Body() createBatchDto: CreateBatchDto,
@Request() req: any,
): Promise<BatchUploadResponseDto> {
try {
const userId = req.user?.id;
if (!userId) {
throw new BadRequestException('User not authenticated');
}
// Validate files are provided
if (!files || files.length === 0) {
throw new BadRequestException('No files provided');
}
// Check file count limits
if (files.length > 1000) {
throw new PayloadTooLargeException('Maximum 1000 files per batch');
}
// Process the batch upload
const result = await this.batchesService.createBatch(userId, files, createBatchDto);
return result;
} catch (error) {
if (error instanceof BadRequestException ||
error instanceof PayloadTooLargeException ||
error instanceof ForbiddenException) {
throw error;
}
throw new BadRequestException('Failed to process batch upload');
}
}
@Get(':batchId/status')
@ApiOperation({
summary: 'Get batch processing status',
description: 'Returns current status and progress of batch processing'
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Batch status retrieved successfully',
type: BatchStatusResponseDto,
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Batch not found',
})
@ApiResponse({
status: HttpStatus.FORBIDDEN,
description: 'Not authorized to access this batch',
})
async getBatchStatus(
@Param('batchId') batchId: string,
@Request() req: any,
): Promise<BatchStatusResponseDto> {
try {
const userId = req.user?.id;
if (!userId) {
throw new BadRequestException('User not authenticated');
}
const status = await this.batchesService.getBatchStatus(batchId, userId);
return status;
} catch (error) {
if (error instanceof BadRequestException || error instanceof ForbiddenException) {
throw error;
}
throw new BadRequestException('Failed to get batch status');
}
}
@Get()
@ApiOperation({
summary: 'List user batches',
description: 'Returns list of all batches for the authenticated user'
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Batches retrieved successfully',
type: [BatchListResponseDto],
})
async getUserBatches(
@Request() req: any,
): Promise<BatchListResponseDto[]> {
try {
const userId = req.user?.id;
if (!userId) {
throw new BadRequestException('User not authenticated');
}
const batches = await this.batchesService.getUserBatches(userId);
return batches;
} catch (error) {
throw new BadRequestException('Failed to get user batches');
}
}
@Post(':batchId/cancel')
@ApiOperation({
summary: 'Cancel batch processing',
description: 'Cancels ongoing batch processing'
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Batch cancelled successfully',
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Batch not found',
})
@ApiResponse({
status: HttpStatus.FORBIDDEN,
description: 'Not authorized to cancel this batch',
})
async cancelBatch(
@Param('batchId') batchId: string,
@Request() req: any,
): Promise<{ message: string }> {
try {
const userId = req.user?.id;
if (!userId) {
throw new BadRequestException('User not authenticated');
}
await this.batchesService.cancelBatch(batchId, userId);
return { message: 'Batch cancelled successfully' };
} catch (error) {
if (error instanceof BadRequestException || error instanceof ForbiddenException) {
throw error;
}
throw new BadRequestException('Failed to cancel batch');
}
}
@Post(':batchId/retry')
@ApiOperation({
summary: 'Retry failed batch processing',
description: 'Retries processing for failed images in a batch'
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Batch retry started successfully',
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Batch not found',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Batch is not in a retryable state',
})
async retryBatch(
@Param('batchId') batchId: string,
@Request() req: any,
): Promise<{ message: string; retry_count: number }> {
try {
const userId = req.user?.id;
if (!userId) {
throw new BadRequestException('User not authenticated');
}
const retryCount = await this.batchesService.retryBatch(batchId, userId);
return {
message: 'Batch retry started successfully',
retry_count: retryCount
};
} catch (error) {
if (error instanceof BadRequestException || error instanceof ForbiddenException) {
throw error;
}
throw new BadRequestException('Failed to retry batch');
}
}
@Get(':batchId/download')
@ApiOperation({
summary: 'Download processed batch as ZIP',
description: 'Returns a ZIP file containing all processed images with new filenames'
})
@ApiResponse({
status: HttpStatus.OK,
description: 'ZIP file download started',
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Batch not found',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Batch processing not completed',
})
async downloadBatch(
@Param('batchId') batchId: string,
@Request() req: any,
): Promise<{ download_url: string; expires_at: string }> {
try {
const userId = req.user?.id;
if (!userId) {
throw new BadRequestException('User not authenticated');
}
const downloadInfo = await this.batchesService.generateBatchDownload(batchId, userId);
return downloadInfo;
} catch (error) {
if (error instanceof BadRequestException || error instanceof ForbiddenException) {
throw error;
}
throw new BadRequestException('Failed to generate batch download');
}
}
}