From 5c3fafff1ba772bc4e440cbe4ec521e7f10f6a9d Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 21 Jul 2026 10:21:27 +0200 Subject: [PATCH] feat: Add PdfOperationsController with dual-input merge endpoints - Add PdfOperationsController.cs with route '/api/pdf/operations' - MergeFromFiles: POST /merge (multipart/form-data) - accepts List - MergeFromBase64: POST /merge (application/json) - accepts MergePdfsRequest DTO - Returns merged PDF as FileContentResult (application/pdf) - Supports optional page ranges via JSON endpoint only (multipart binding complex) - XML documentation with response codes (200, 400, 500) - Uses primary constructor pattern --- .../Controllers/PdfOperationsController.cs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 DocumentOperator.API/Controllers/PdfOperationsController.cs diff --git a/DocumentOperator.API/Controllers/PdfOperationsController.cs b/DocumentOperator.API/Controllers/PdfOperationsController.cs new file mode 100644 index 0000000..663572a --- /dev/null +++ b/DocumentOperator.API/Controllers/PdfOperationsController.cs @@ -0,0 +1,120 @@ +using DocumentOperator.Application.MergePdfs; +using DocumentOperator.Domain.Common.Exceptions; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace DocumentOperator.API.Controllers; + +/// +/// Controller for PDF operations (merge, stamp, annotate). +/// +[ApiController] +[Route("api/pdf/operations")] +public class PdfOperationsController(IMediator mediator) : ControllerBase +{ + /// + /// Merges multiple PDF files into a single PDF. + /// Supports multipart/form-data file upload. + /// + /// PDF files to merge (minimum 2 required) + /// Cancellation token + /// Merged PDF file + /// PDFs merged successfully - returns merged PDF + /// Invalid input (fewer than 2 files, corrupted PDF) + /// Internal server error during PDF processing + [HttpPost("merge")] + [Consumes("multipart/form-data")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task MergeFromFiles( + [FromForm] List files, + CancellationToken cancellationToken) + { + // Convert IFormFile[] to Stream[] (use OpenReadStream directly - no buffering) + var streams = files.Select(f => f.OpenReadStream()).ToList(); + + // Send command to MediatR (no page ranges for now - multipart binding is complex) + var command = new MergePdfsCommand + { + PdfStreams = streams, + PageRanges = null + }; + + byte[] mergedPdf = await mediator.Send(command, cancellationToken); + + // Return merged PDF + return File(mergedPdf, "application/pdf", "merged.pdf"); + } + + /// + /// Merges multiple PDF files into a single PDF. + /// Supports Base64-encoded PDFs via JSON payload. + /// + /// Request containing Base64-encoded PDFs and optional page ranges + /// Cancellation token + /// Merged PDF file + /// PDFs merged successfully - returns merged PDF + /// Invalid input (Base64 format error, fewer than 2 files, corrupted PDF, invalid page range) + /// Internal server error during PDF processing + [HttpPost("merge")] + [Consumes("application/json")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task MergeFromBase64( + [FromBody] MergePdfsRequest request, + CancellationToken cancellationToken) + { + // Convert Base64[] to MemoryStream[] + List streams = new(); + try + { + foreach (var base64Pdf in request.Base64Pdfs) + { + byte[] pdfBytes = Convert.FromBase64String(base64Pdf); + streams.Add(new MemoryStream(pdfBytes)); + } + } + catch (FormatException ex) + { + // Dispose opened streams on error + foreach (var stream in streams) stream.Dispose(); + throw new BadRequestException("Invalid Base64 format: " + ex.Message); + } + + var command = new MergePdfsCommand + { + PdfStreams = streams, + PageRanges = request.PageRanges + }; + + byte[] mergedPdf = await mediator.Send(command, cancellationToken); + + // Cleanup streams (important for MemoryStreams we created) + foreach (var stream in streams) stream.Dispose(); + + return File(mergedPdf, "application/pdf", "merged.pdf"); + } +} + +/// +/// Request DTO for Base64-encoded PDF merge operation +/// +public record MergePdfsRequest +{ + /// + /// Array of Base64-encoded PDF files (minimum 2 required) + /// + /// ["JVBERi0xLjQK...", "JVBERi0xLjQK..."] + public List Base64Pdfs { get; init; } = new(); + + /// + /// Optional page ranges per PDF (null = all pages). + /// Format: "1-3,5" means pages 1, 2, 3, and 5. + /// If provided, array length must match Base64Pdfs length. + /// + /// ["1-2", "1,3,5", null] + public List? PageRanges { get; init; } +}