Add background PDF generation for published document versions

Move PDF generation from synchronous publish flow to a background polling
job. Published versions with file_id IS NULL are picked up by the job,
which generates the PDF, uploads to S3, and links the file. Export PDF
now serves stored files for published versions (with optional signature
page and watermark) and generates on the fly for drafts.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-04-17 08:15:57 +02:00
parent ff175e3d0d
commit 25c590ffe6
13 changed files with 795 additions and 70 deletions

View File

@@ -17,11 +17,10 @@ package trust
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"errors"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/docgen"
@@ -29,7 +28,7 @@ import (
"go.probo.inc/probo/pkg/html2pdf"
"go.probo.inc/probo/pkg/mail"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/watermarkpdf"
"go.probo.inc/probo/pkg/pdfutils"
)
type (
@@ -82,7 +81,7 @@ func (s *DocumentService) ExportPDF(
return nil, fmt.Errorf("cannot export document PDF: %w", err)
}
watermarkedPDF, err := watermarkpdf.AddConfidentialWithTimestamp(pdfData, email)
watermarkedPDF, err := pdfutils.AddConfidentialWithTimestamp(pdfData, email)
if err != nil {
return nil, fmt.Errorf("cannot add watermark to PDF: %w", err)
}
@@ -141,8 +140,7 @@ func (s *DocumentService) exportPDFData(
) ([]byte, error) {
document := &coredata.Document{}
version := &coredata.DocumentVersion{}
organization := &coredata.Organization{}
var approverNames []string
fileRecord := &coredata.File{}
err := s.svc.pg.WithConn(
ctx,
@@ -163,6 +161,54 @@ func (s *DocumentService) exportPDFData(
return fmt.Errorf("cannot load latest published document version: %w", err)
}
if version.FileID == nil {
return nil
}
if err := fileRecord.LoadByID(ctx, conn, s.svc.scope, *version.FileID); err != nil {
return fmt.Errorf("cannot load document version file: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
if version.FileID != nil {
pdfData, err := s.svc.fileManager.GetFileBytes(ctx, fileRecord)
if err != nil {
return nil, fmt.Errorf("cannot fetch document PDF file: %w", err)
}
return pdfData, nil
}
// TODO: remove on-the-fly fallback once all published versions have a stored PDF.
pdfData, err := s.generatePDFOnTheFly(ctx, document, version)
if err != nil {
return nil, fmt.Errorf("cannot generate PDF on the fly: %w", err)
}
return pdfData, nil
}
// generatePDFOnTheFly generates a PDF from scratch for versions that don't have
// a stored file yet. Can be removed once all published versions have been
// processed by the document PDF worker.
func (s *DocumentService) generatePDFOnTheFly(
ctx context.Context,
document *coredata.Document,
version *coredata.DocumentVersion,
) ([]byte, error) {
organization := &coredata.Organization{}
var approverNames []string
err := s.svc.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
lastQuorum := &coredata.DocumentVersionApprovalQuorum{}
if err := lastQuorum.LoadLastByDocumentVersionID(ctx, conn, s.svc.scope, version.ID); err != nil {
if !errors.Is(err, coredata.ErrResourceNotFound) {

View File

@@ -25,7 +25,7 @@ import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
"go.probo.inc/probo/pkg/watermarkpdf"
"go.probo.inc/probo/pkg/pdfutils"
)
type ReportService struct {
@@ -112,7 +112,7 @@ func (s ReportService) ExportPDF(
return nil, fmt.Errorf("cannot export report PDF: %w", err)
}
watermarkedPDF, err := watermarkpdf.AddConfidentialWithTimestamp(pdfData, email)
watermarkedPDF, err := pdfutils.AddConfidentialWithTimestamp(pdfData, email)
if err != nil {
return nil, fmt.Errorf("cannot add watermark to PDF: %w", err)
}

View File

@@ -25,7 +25,7 @@ import (
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/watermarkpdf"
"go.probo.inc/probo/pkg/pdfutils"
)
type TrustCenterFileService struct {
@@ -104,7 +104,7 @@ func (s *TrustCenterFileService) ExportFile(
}
if mimeType == "application/pdf" {
watermarkedPDF, err := watermarkpdf.AddConfidentialWithTimestamp(fileData, email)
watermarkedPDF, err := pdfutils.AddConfidentialWithTimestamp(fileData, email)
if err != nil {
return nil, "", fmt.Errorf("cannot add watermark to PDF: %w", err)
}