feat(BE): add function read and download in document

This commit is contained in:
ragilap
2025-12-31 11:39:53 +07:00
parent 4e2724a702
commit 9d285869f5
5 changed files with 70 additions and 23 deletions
@@ -8,6 +8,7 @@ import (
"mime/multipart" "mime/multipart"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/config"
@@ -29,6 +30,7 @@ type DocumentService interface {
DeleteDocuments(ctx context.Context, ids []uint, removeFromStorage bool) error DeleteDocuments(ctx context.Context, ids []uint, removeFromStorage bool) error
DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, removeFromStorage bool) error DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, removeFromStorage bool) error
PublicURL(document entity.Document) string PublicURL(document entity.Document) string
PresignURL(ctx context.Context, document entity.Document, expires time.Duration) (string, error)
} }
type DocumentUploadRequest struct { type DocumentUploadRequest struct {
@@ -293,6 +295,16 @@ func (s *documentService) PublicURL(document entity.Document) string {
return s.storage.URL(document.Path) return s.storage.URL(document.Path)
} }
func (s *documentService) PresignURL(ctx context.Context, document entity.Document, expires time.Duration) (string, error) {
if s.storage == nil {
return "", errors.New("document storage not configured")
}
if strings.TrimSpace(document.Path) == "" {
return "", errors.New("document path is required")
}
return s.storage.PresignURL(ctx, document.Path, expires)
}
func (s *documentService) generateObjectKey(ext string) (string, error) { func (s *documentService) generateObjectKey(ext string) (string, error) {
normalizedExt := strings.TrimSpace(ext) normalizedExt := strings.TrimSpace(ext)
if normalizedExt != "" && !strings.HasPrefix(normalizedExt, ".") { if normalizedExt != "" && !strings.HasPrefix(normalizedExt, ".") {
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"strings" "strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config" awsconfig "github.com/aws/aws-sdk-go-v2/config"
@@ -17,6 +18,7 @@ type DocumentStorage interface {
Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) (DocumentStorageUploadResult, error) Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) (DocumentStorageUploadResult, error)
Delete(ctx context.Context, key string) error Delete(ctx context.Context, key string) error
URL(key string) string URL(key string) string
PresignURL(ctx context.Context, key string, expires time.Duration) (string, error)
} }
type DocumentStorageUploadResult struct { type DocumentStorageUploadResult struct {
@@ -36,9 +38,10 @@ type S3DocumentStorageConfig struct {
} }
type s3DocumentStorage struct { type s3DocumentStorage struct {
client *s3.Client client *s3.Client
bucket string presignClient *s3.PresignClient
base string bucket string
base string
} }
func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (DocumentStorage, error) { func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (DocumentStorage, error) {
@@ -86,6 +89,7 @@ func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (Doc
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
o.UsePathStyle = cfg.ForcePathStyle o.UsePathStyle = cfg.ForcePathStyle
}) })
presignClient := s3.NewPresignClient(client)
baseURL := strings.TrimSuffix(strings.TrimSpace(cfg.BaseURL), "/") baseURL := strings.TrimSuffix(strings.TrimSpace(cfg.BaseURL), "/")
if baseURL == "" { if baseURL == "" {
@@ -97,9 +101,10 @@ func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (Doc
} }
return &s3DocumentStorage{ return &s3DocumentStorage{
client: client, client: client,
bucket: bucket, presignClient: presignClient,
base: baseURL, bucket: bucket,
base: baseURL,
}, nil }, nil
} }
@@ -158,3 +163,23 @@ func (s *s3DocumentStorage) URL(key string) string {
} }
return fmt.Sprintf("%s/%s", s.base, key) return fmt.Sprintf("%s/%s", s.base, key)
} }
func (s *s3DocumentStorage) PresignURL(ctx context.Context, key string, expires time.Duration) (string, error) {
key = strings.TrimPrefix(strings.TrimSpace(key), "/")
if key == "" {
return "", errors.New("storage key is required")
}
if expires <= 0 {
expires = 15 * time.Minute
}
out, err := s.presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
}, s3.WithPresignExpires(expires))
if err != nil {
return "", err
}
return out.URL, nil
}
@@ -71,13 +71,14 @@ func (u *UniformityController) GetOne(c *fiber.Ctx) error {
withDetails := c.QueryBool("with_details", false) withDetails := c.QueryBool("with_details", false)
calculation := service.UniformityCalculation{} calculation := service.UniformityCalculation{}
var document *entity.Document var document *entity.Document
var documentURL string
var meanWeight float64 var meanWeight float64
if result.MeanUp > 0 { if result.MeanUp > 0 {
meanWeight = math.Round(result.MeanUp / 1.10) meanWeight = math.Round(result.MeanUp / 1.10)
} }
if withDetails { if withDetails {
var err error var err error
calculation, document, err = u.UniformityService.CalculateUniformityFromDocument(c, id) calculation, document, documentURL, err = u.UniformityService.CalculateUniformityFromDocument(c, id)
if err != nil { if err != nil {
return err return err
} }
@@ -111,7 +112,7 @@ func (u *UniformityController) GetOne(c *fiber.Ctx) error {
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Get production uniformity successfully", Message: "Get production uniformity successfully",
Data: dto.ToUniformityDetailDTO(*result, calculation, document, standardDTO), Data: dto.ToUniformityDetailDTO(*result, calculation, document, documentURL, standardDTO),
}) })
} }
@@ -154,7 +155,7 @@ func (u *UniformityController) CreateOne(c *fiber.Ctx) error {
Code: fiber.StatusCreated, Code: fiber.StatusCreated,
Status: "success", Status: "success",
Message: "Create uniformity successfully", Message: "Create uniformity successfully",
Data: dto.ToUniformityDetailDTO(*result, calculation, document, standardDTO), Data: dto.ToUniformityDetailDTO(*result, calculation, document, "", standardDTO),
}) })
} }
@@ -237,7 +238,7 @@ func (u *UniformityController) UpdateOne(c *fiber.Ctx) error {
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Update uniformity successfully", Message: "Update uniformity successfully",
Data: dto.ToUniformityDetailDTO(*result, calculation, document, standardDTO), Data: dto.ToUniformityDetailDTO(*result, calculation, document, "", standardDTO),
}) })
} }
@@ -45,6 +45,7 @@ type UniformityInfoDTO struct {
ProjectFlock string `json:"project_flock"` ProjectFlock string `json:"project_flock"`
Kandang string `json:"kandang"` Kandang string `json:"kandang"`
FileName string `json:"file_name"` FileName string `json:"file_name"`
FileURL string `json:"file_url"`
} }
type UniformityDetailDTO struct { type UniformityDetailDTO struct {
@@ -97,6 +98,7 @@ func ToUniformityDetailDTO(
entityData entity.ProjectFlockKandangUniformity, entityData entity.ProjectFlockKandangUniformity,
calc service.UniformityCalculation, calc service.UniformityCalculation,
document *entity.Document, document *entity.Document,
documentURL string,
standard *UniformityStandardDTO, standard *UniformityStandardDTO,
) UniformityDetailDTO { ) UniformityDetailDTO {
info := UniformityInfoDTO{ info := UniformityInfoDTO{
@@ -105,10 +107,14 @@ func ToUniformityDetailDTO(
ProjectFlock: resolveProjectFlockName(entityData.ProjectFlockKandang), ProjectFlock: resolveProjectFlockName(entityData.ProjectFlockKandang),
Kandang: resolveKandangName(entityData.ProjectFlockKandang), Kandang: resolveKandangName(entityData.ProjectFlockKandang),
FileName: "", FileName: "",
FileURL: "",
} }
if document != nil { if document != nil {
info.FileName = document.Name info.FileName = document.Name
} }
if documentURL != "" {
info.FileURL = documentURL
}
return UniformityDetailDTO{ return UniformityDetailDTO{
Id: entityData.Id, Id: entityData.Id,
@@ -39,7 +39,7 @@ type UniformityService interface {
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error)
ParseBodyWeightExcel(ctx *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error) ParseBodyWeightExcel(ctx *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error)
ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error) ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error)
CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, error) CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, string, error)
} }
type uniformityService struct { type uniformityService struct {
@@ -592,50 +592,53 @@ func (s uniformityService) ComputeUniformity(rows []BodyWeightExcelRow) (Uniform
return computeUniformity(rows) return computeUniformity(rows)
} }
func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, error) { func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, string, error) {
if s.DocumentSvc == nil { if s.DocumentSvc == nil {
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusInternalServerError, "Document service not available") return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
} }
documents, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(uniformityID)) documents, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(uniformityID))
if err != nil { if err != nil {
return UniformityCalculation{}, nil, err return UniformityCalculation{}, nil, "", err
} }
if len(documents) == 0 { if len(documents) == 0 {
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusNotFound, "Uniformity document not found") return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
} }
document := documents[0] document := documents[0]
url := s.DocumentSvc.PublicURL(document) url, err := s.DocumentSvc.PresignURL(c.Context(), document, 15*time.Minute)
if err != nil {
return UniformityCalculation{}, nil, "", err
}
if url == "" { if url == "" {
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusBadRequest, "Uniformity document URL not available") return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusBadRequest, "Uniformity document URL not available")
} }
req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, url, nil) req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, url, nil)
if err != nil { if err != nil {
return UniformityCalculation{}, nil, err return UniformityCalculation{}, nil, "", err
} }
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return UniformityCalculation{}, nil, err return UniformityCalculation{}, nil, "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusBadRequest, "Failed to download uniformity document") return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusBadRequest, "Failed to download uniformity document")
} }
rows, err := parseBodyWeightExcelReader(resp.Body) rows, err := parseBodyWeightExcelReader(resp.Body)
if err != nil { if err != nil {
return UniformityCalculation{}, nil, err return UniformityCalculation{}, nil, "", err
} }
calculation, err := computeUniformity(rows) calculation, err := computeUniformity(rows)
if err != nil { if err != nil {
return UniformityCalculation{}, nil, err return UniformityCalculation{}, nil, "", err
} }
return calculation, &document, nil return calculation, &document, url, nil
} }
func (s *uniformityService) createUniformityApproval( func (s *uniformityService) createUniformityApproval(