feat: Add PdfOperationsController with dual-input merge endpoints
- Add PdfOperationsController.cs with route '/api/pdf/operations' - MergeFromFiles: POST /merge (multipart/form-data) - accepts List<IFormFile> - 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
This commit is contained in:
120
DocumentOperator.API/Controllers/PdfOperationsController.cs
Normal file
120
DocumentOperator.API/Controllers/PdfOperationsController.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller for PDF operations (merge, stamp, annotate).
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/pdf/operations")]
|
||||||
|
public class PdfOperationsController(IMediator mediator) : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Merges multiple PDF files into a single PDF.
|
||||||
|
/// Supports multipart/form-data file upload.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="files">PDF files to merge (minimum 2 required)</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Merged PDF file</returns>
|
||||||
|
/// <response code="200">PDFs merged successfully - returns merged PDF</response>
|
||||||
|
/// <response code="400">Invalid input (fewer than 2 files, corrupted PDF)</response>
|
||||||
|
/// <response code="500">Internal server error during PDF processing</response>
|
||||||
|
[HttpPost("merge")]
|
||||||
|
[Consumes("multipart/form-data")]
|
||||||
|
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||||
|
public async Task<IActionResult> MergeFromFiles(
|
||||||
|
[FromForm] List<IFormFile> 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Merges multiple PDF files into a single PDF.
|
||||||
|
/// Supports Base64-encoded PDFs via JSON payload.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request containing Base64-encoded PDFs and optional page ranges</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Merged PDF file</returns>
|
||||||
|
/// <response code="200">PDFs merged successfully - returns merged PDF</response>
|
||||||
|
/// <response code="400">Invalid input (Base64 format error, fewer than 2 files, corrupted PDF, invalid page range)</response>
|
||||||
|
/// <response code="500">Internal server error during PDF processing</response>
|
||||||
|
[HttpPost("merge")]
|
||||||
|
[Consumes("application/json")]
|
||||||
|
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||||
|
public async Task<IActionResult> MergeFromBase64(
|
||||||
|
[FromBody] MergePdfsRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Convert Base64[] to MemoryStream[]
|
||||||
|
List<Stream> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request DTO for Base64-encoded PDF merge operation
|
||||||
|
/// </summary>
|
||||||
|
public record MergePdfsRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Array of Base64-encoded PDF files (minimum 2 required)
|
||||||
|
/// </summary>
|
||||||
|
/// <example>["JVBERi0xLjQK...", "JVBERi0xLjQK..."]</example>
|
||||||
|
public List<string> Base64Pdfs { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>["1-2", "1,3,5", null]</example>
|
||||||
|
public List<string?>? PageRanges { get; init; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user