
- 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>
275 lines
No EOL
7.6 KiB
TypeScript
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');
|
|
}
|
|
}
|
|
} |