batch upload impl

This commit is contained in:
jay brown
2025-08-06 15:06:18 -07:00
parent fac5615c15
commit 7014fa790b
17 changed files with 1272 additions and 97 deletions
+55 -32
View File
@@ -5,6 +5,7 @@ import (
"log/slog"
"net/http"
"queryorchestration/internal/document/batch/storage"
documentupload "queryorchestration/internal/document/upload"
"github.com/google/uuid"
@@ -128,48 +129,70 @@ func (s *Controllers) ListDocumentBatches(ctx echo.Context, clientId ClientID, p
// UploadDocumentBatch handles ZIP archive upload containing multiple PDFs
func (s *Controllers) UploadDocumentBatch(ctx echo.Context, clientId ClientID) error {
// Get the multipart form
form, err := ctx.MultipartForm()
// Parse multipart form to get ZIP file
file, err := ctx.FormFile("archive")
if err != nil {
slog.Error("unable to get form data", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "unable to get form data")
slog.Error("failed to get uploaded file", "error", err, "client_id", clientId)
return echo.NewHTTPError(http.StatusBadRequest, "file upload required")
}
// Get the archive file
files := form.File["archive"]
if len(files) == 0 {
return echo.NewHTTPError(http.StatusBadRequest, "missing archive file")
} else if len(files) > 1 {
return echo.NewHTTPError(http.StatusBadRequest, "too many files")
}
file := files[0]
// Skip ZIP validation for tests - in production this would check file content
// if file.Header.Get("Content-Type") != "application/zip" {
// return echo.NewHTTPError(http.StatusBadRequest, "file must be a ZIP archive")
// }
// For now, we'll create a batch record with placeholder total documents
// In the real implementation, we would:
// 1. Store the ZIP file to S3
// 2. Start an async process to extract and process PDFs
// 3. Return immediately with 202 Accepted
// Create batch record
batchID, err := s.svc.DocumentBatch.Create(ctx.Request().Context(), clientId, file.Filename, 0)
// Open uploaded file
src, err := file.Open()
if err != nil {
slog.Error("unable to create batch", "error", err)
return echo.NewHTTPError(http.StatusInternalServerError, "unable to create batch")
slog.Error("failed to open uploaded file", "error", err, "client_id", clientId)
return echo.NewHTTPError(http.StatusInternalServerError, "failed to process uploaded file")
}
defer src.Close()
// Initialize storage service with objectstore config
storageService := storage.New(s.cfg)
// Validate ZIP file
err = storageService.ValidateZIPFile(file.Filename, file.Size)
if err != nil {
slog.Error("ZIP validation failed", "filename", file.Filename, "size", file.Size, "error", err, "client_id", clientId)
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid ZIP file: %v", err))
}
// Build response
// Store ZIP file to S3
storageResult, err := storageService.StoreZIPFile(ctx.Request().Context(), string(clientId), file.Filename, src)
if err != nil {
slog.Error("failed to store ZIP file", "error", err, "client_id", clientId)
return echo.NewHTTPError(http.StatusInternalServerError, "failed to store ZIP file")
}
// Create batch record with storage metadata
// Note: totalDocs is unknown at this point, will be determined during extraction in later parts
batchID, err := s.svc.DocumentBatch.CreateWithStorage(
ctx.Request().Context(),
string(clientId),
file.Filename,
0, // totalDocs unknown in Part 1
storageResult.Bucket,
storageResult.Key,
storageResult.SizeBytes,
)
if err != nil {
slog.Error("failed to create batch record", "error", err, "client_id", clientId)
return echo.NewHTTPError(http.StatusInternalServerError, "failed to create batch record")
}
slog.Info("ZIP batch upload completed",
"batch_id", batchID,
"filename", file.Filename,
"size_bytes", storageResult.SizeBytes,
"s3_key", storageResult.Key,
"client_id", clientId,
)
// Return 202 Accepted with batch ID and status URL
statusUrl := fmt.Sprintf("/clients/%s/documents/batches/%s", clientId, batchID)
response := BatchUploadResponse{
BatchId: BatchID(batchID),
Status: BatchStatusProcessing,
StatusUrl: fmt.Sprintf("/client/%s/document/batch/%s", clientId, batchID.String()),
Status: "processing",
StatusUrl: statusUrl,
}
// Return 202 Accepted for async processing
return ctx.JSON(http.StatusAccepted, response)
}