tender single document download
This commit is contained in:
@@ -453,12 +453,14 @@ func (h *TenderHandler) GetDocuments(c echo.Context) error {
|
|||||||
return response.Success(c, documents, "Tender documents retrieved successfully")
|
return response.Success(c, documents, "Tender documents retrieved successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadDocuments downloads all scraped documents for a tender
|
// DownloadDocuments downloads scraped documents for a tender
|
||||||
// @Summary Download scraped documents for a tender
|
// @Summary Download scraped documents for a tender
|
||||||
// @Description Stream all scraped tender documents from storage as a ZIP archive
|
// @Description Stream all scraped tender documents as a ZIP archive, or pass filename to download a single document
|
||||||
// @Tags Tenders
|
// @Tags Tenders
|
||||||
// @Produce application/zip
|
// @Produce application/zip
|
||||||
|
// @Produce application/octet-stream
|
||||||
// @Param id path string true "Tender ID"
|
// @Param id path string true "Tender ID"
|
||||||
|
// @Param filename query string false "Document filename (from GET /documents); when set, returns that file instead of a ZIP"
|
||||||
// @Success 200 {file} file
|
// @Success 200 {file} file
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 404 {object} response.APIResponse
|
// @Failure 404 {object} response.APIResponse
|
||||||
@@ -470,6 +472,10 @@ func (h *TenderHandler) DownloadDocuments(c echo.Context) error {
|
|||||||
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty")
|
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if filename := strings.TrimSpace(c.QueryParam("filename")); filename != "" {
|
||||||
|
return h.streamDocumentDownload(c, id, filename)
|
||||||
|
}
|
||||||
|
|
||||||
download, err := h.service.DownloadDocuments(c.Request().Context(), id)
|
download, err := h.service.DownloadDocuments(c.Request().Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrTenderDocumentNotFound) {
|
if errors.Is(err, ErrTenderDocumentNotFound) {
|
||||||
@@ -493,6 +499,30 @@ func (h *TenderHandler) DownloadDocuments(c echo.Context) error {
|
|||||||
return c.Stream(http.StatusOK, download.ContentType, download.Reader)
|
return c.Stream(http.StatusOK, download.ContentType, download.Reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *TenderHandler) streamDocumentDownload(c echo.Context, tenderID, filename string) error {
|
||||||
|
download, err := h.service.DownloadDocument(c.Request().Context(), tenderID, filename)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrTenderDocumentNotFound) {
|
||||||
|
return response.NotFound(c, "Tender document not found")
|
||||||
|
}
|
||||||
|
if errors.Is(err, ErrTenderDocumentStorageUnavailable) {
|
||||||
|
return response.InternalServerError(c, "Tender document storage is unavailable")
|
||||||
|
}
|
||||||
|
return response.InternalServerError(c, "Failed to download tender document")
|
||||||
|
}
|
||||||
|
defer download.Reader.Close()
|
||||||
|
|
||||||
|
headers := c.Response().Header()
|
||||||
|
headers.Set(echo.HeaderContentDisposition, mime.FormatMediaType("attachment", map[string]string{
|
||||||
|
"filename": download.Filename,
|
||||||
|
}))
|
||||||
|
if download.Size > 0 {
|
||||||
|
headers.Set(echo.HeaderContentLength, strconv.FormatInt(download.Size, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Stream(http.StatusOK, download.ContentType, download.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
// GetAISummary retrieves the AI-generated overall summary for a tender
|
// GetAISummary retrieves the AI-generated overall summary for a tender
|
||||||
// @Summary Get AI-generated tender summary
|
// @Summary Get AI-generated tender summary
|
||||||
// @Description Retrieve the AI-generated overall summary for a tender from the AI pipeline storage
|
// @Description Retrieve the AI-generated overall summary for a tender from the AI pipeline storage
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"mime"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -53,6 +54,7 @@ type Service interface {
|
|||||||
GetDocumentSummaries(ctx context.Context, id string) ([]DocumentSummary, error)
|
GetDocumentSummaries(ctx context.Context, id string) ([]DocumentSummary, error)
|
||||||
GetDocuments(ctx context.Context, id string) (*TenderDocumentsResponse, error)
|
GetDocuments(ctx context.Context, id string) (*TenderDocumentsResponse, error)
|
||||||
DownloadDocuments(ctx context.Context, id string) (*TenderDocumentDownload, error)
|
DownloadDocuments(ctx context.Context, id string) (*TenderDocumentDownload, error)
|
||||||
|
DownloadDocument(ctx context.Context, id, filename string) (*TenderDocumentDownload, error)
|
||||||
|
|
||||||
// AI summarization operations
|
// AI summarization operations
|
||||||
GetAISummary(ctx context.Context, id string) (*AISummaryResponse, error)
|
GetAISummary(ctx context.Context, id string) (*AISummaryResponse, error)
|
||||||
@@ -624,6 +626,96 @@ func (s *tenderService) DownloadDocuments(ctx context.Context, id string) (*Tend
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DownloadDocument streams a single scraped document for a tender.
|
||||||
|
func (s *tenderService) DownloadDocument(ctx context.Context, id, filename string) (*TenderDocumentDownload, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, fmt.Errorf("tender ID cannot be empty")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(filename) == "" {
|
||||||
|
return nil, fmt.Errorf("document filename cannot be empty")
|
||||||
|
}
|
||||||
|
if s.aiSummarizerStorage == nil {
|
||||||
|
return nil, ErrTenderDocumentStorageUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
tender, err := s.repository.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to get tender for document download", map[string]interface{}{
|
||||||
|
"tender_id": id,
|
||||||
|
"filename": filename,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to get tender: %w", err)
|
||||||
|
}
|
||||||
|
if tender == nil {
|
||||||
|
return nil, fmt.Errorf("tender not found")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(tender.ContractFolderID) == "" {
|
||||||
|
return nil, ErrTenderDocumentNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
documents, err := s.listDocumentsForTender(ctx, tender)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to list tender documents for download", map[string]interface{}{
|
||||||
|
"tender_id": id,
|
||||||
|
"filename": filename,
|
||||||
|
"contract_folder_id": tender.ContractFolderID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to list tender documents: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, ok := findStoredDocumentByFilename(documents, filename)
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrTenderDocumentNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := s.aiSummarizerStorage.DownloadDocument(ctx, doc.ObjectName)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to open tender document from storage", map[string]interface{}{
|
||||||
|
"tender_id": id,
|
||||||
|
"filename": filename,
|
||||||
|
"object_name": doc.ObjectName,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to download document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadName := path.Base(doc.DocumentName)
|
||||||
|
contentType := mime.TypeByExtension(filepath.Ext(downloadName))
|
||||||
|
if contentType == "" {
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Tender document download prepared", map[string]interface{}{
|
||||||
|
"tender_id": id,
|
||||||
|
"filename": downloadName,
|
||||||
|
"size": doc.Size,
|
||||||
|
})
|
||||||
|
|
||||||
|
return &TenderDocumentDownload{
|
||||||
|
Reader: reader,
|
||||||
|
Filename: downloadName,
|
||||||
|
ContentType: contentType,
|
||||||
|
Size: doc.Size,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findStoredDocumentByFilename(documents []ai_summarizer.StoredDocument, filename string) (ai_summarizer.StoredDocument, bool) {
|
||||||
|
filename = strings.TrimSpace(filename)
|
||||||
|
if filename == "" {
|
||||||
|
return ai_summarizer.StoredDocument{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
wantBase := path.Base(filename)
|
||||||
|
for _, doc := range documents {
|
||||||
|
if doc.DocumentName == filename || path.Base(doc.DocumentName) == wantBase {
|
||||||
|
return doc, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ai_summarizer.StoredDocument{}, false
|
||||||
|
}
|
||||||
|
|
||||||
func (s *tenderService) writeDocumentsArchive(ctx context.Context, writer *io.PipeWriter, documents []ai_summarizer.StoredDocument) {
|
func (s *tenderService) writeDocumentsArchive(ctx context.Context, writer *io.PipeWriter, documents []ai_summarizer.StoredDocument) {
|
||||||
zipWriter := zip.NewWriter(writer)
|
zipWriter := zip.NewWriter(writer)
|
||||||
usedNames := make(map[string]int, len(documents))
|
usedNames := make(map[string]int, len(documents))
|
||||||
|
|||||||
Reference in New Issue
Block a user