562 lines
17 KiB
Go
562 lines
17 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
filemanager "wucher/internal/domain/file_manager"
|
|
helicopterfile "wucher/internal/domain/helicopter_file"
|
|
helicopterfilerealtime "wucher/internal/realtime/helicopterfile"
|
|
"wucher/internal/shared/pkg/attachmentresolver"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
)
|
|
|
|
type HelicopterFileService struct {
|
|
repo helicopterfile.Repository
|
|
fileManager filemanager.Service
|
|
events helicopterFileEventPublisher
|
|
}
|
|
|
|
func NewHelicopterFileService(repo helicopterfile.Repository) *HelicopterFileService {
|
|
return &HelicopterFileService{repo: repo}
|
|
}
|
|
|
|
type helicopterFileEventPublisher interface {
|
|
PublishToUsers(userUUIDs []string, event helicopterfilerealtime.Event) int
|
|
}
|
|
|
|
func (s *HelicopterFileService) WithFileManagerService(fileManagerSvc filemanager.Service) *HelicopterFileService {
|
|
s.fileManager = fileManagerSvc
|
|
return s
|
|
}
|
|
|
|
func (s *HelicopterFileService) WithHelicopterFileEvents(publisher helicopterFileEventPublisher) *HelicopterFileService {
|
|
s.events = publisher
|
|
return s
|
|
}
|
|
|
|
func (s *HelicopterFileService) Create(ctx context.Context, row *helicopterfile.HelicopterFile) error {
|
|
if row != nil && row.Position <= 0 {
|
|
next, err := s.nextPositionByHelicopterSection(ctx, row.HelicopterID, row.Section)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
row.Position = next
|
|
}
|
|
return s.repo.Create(ctx, row)
|
|
}
|
|
|
|
func (s *HelicopterFileService) Update(ctx context.Context, row *helicopterfile.HelicopterFile) error {
|
|
return s.repo.Update(ctx, row)
|
|
}
|
|
|
|
func (s *HelicopterFileService) Delete(ctx context.Context, id []byte) error {
|
|
return s.repo.Delete(ctx, id)
|
|
}
|
|
|
|
func (s *HelicopterFileService) GetByID(ctx context.Context, id []byte) (*helicopterfile.HelicopterFile, error) {
|
|
return s.repo.GetByID(ctx, id)
|
|
}
|
|
|
|
func (s *HelicopterFileService) List(ctx context.Context, filter string, sort string, limit, offset int) ([]helicopterfile.HelicopterFile, int64, error) {
|
|
return s.repo.List(ctx, filter, normalizeHelicopterFileSort(sort), limit, offset)
|
|
}
|
|
|
|
func (s *HelicopterFileService) ListByHelicopter(ctx context.Context, helicopterID []byte) ([]helicopterfile.HelicopterFile, error) {
|
|
return s.repo.ListByHelicopter(ctx, helicopterID)
|
|
}
|
|
|
|
func (s *HelicopterFileService) ListByHelicopterAndSection(ctx context.Context, helicopterID []byte, section string) ([]helicopterfile.HelicopterFile, error) {
|
|
return s.repo.ListByHelicopterAndSection(ctx, helicopterID, section)
|
|
}
|
|
|
|
func (s *HelicopterFileService) HelicopterExists(ctx context.Context, helicopterID []byte) (bool, error) {
|
|
return s.repo.HelicopterExists(ctx, helicopterID)
|
|
}
|
|
|
|
func (s *HelicopterFileService) AttachmentExists(ctx context.Context, attachmentID []byte) (bool, error) {
|
|
return s.repo.AttachmentExists(ctx, attachmentID)
|
|
}
|
|
|
|
func (s *HelicopterFileService) RequestUploadIntentsBulk(ctx context.Context, inputs []helicopterfile.UploadIntentBulkInput, actorID []byte) []helicopterfile.UploadIntentBulkItemResult {
|
|
items := make([]helicopterfile.UploadIntentBulkItemResult, len(inputs))
|
|
if s.fileManager == nil {
|
|
for i := range items {
|
|
items[i] = helicopterfile.UploadIntentBulkItemResult{
|
|
Index: i,
|
|
Success: false,
|
|
Error: &helicopterfile.BulkItemError{
|
|
Status: 400,
|
|
Title: "Upload intent failed",
|
|
Detail: "file manager service is not configured",
|
|
},
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
for i := range inputs {
|
|
item := inputs[i]
|
|
typ := strings.TrimSpace(item.Type)
|
|
if typ != "helicopter_file_upload" {
|
|
items[i] = uploadIntentBulkError(i, 422, "Validation error", "type must be helicopter_file_upload")
|
|
continue
|
|
}
|
|
|
|
name := strings.TrimSpace(item.Name)
|
|
if name == "" {
|
|
items[i] = uploadIntentBulkError(i, 422, "Validation error", "name is required")
|
|
continue
|
|
}
|
|
|
|
contentType := strings.TrimSpace(item.ContentType)
|
|
if contentType == "" {
|
|
items[i] = uploadIntentBulkError(i, 422, "Validation error", "content_type is required")
|
|
continue
|
|
}
|
|
|
|
if item.SizeBytes < 0 {
|
|
items[i] = uploadIntentBulkError(i, 422, "Validation error", "size_bytes must be greater than or equal to 0")
|
|
continue
|
|
}
|
|
|
|
grant, grantErr := s.fileManager.RequestFileUploadIntent(ctx, filemanager.RequestFileUploadIntentParams{
|
|
Name: name,
|
|
ContentType: contentType,
|
|
SizeBytes: item.SizeBytes,
|
|
ActorID: actorID,
|
|
})
|
|
if grantErr != nil {
|
|
items[i] = uploadIntentBulkError(i, 400, "Upload intent failed", grantErr.Error())
|
|
continue
|
|
}
|
|
|
|
items[i] = helicopterfile.UploadIntentBulkItemResult{
|
|
Index: i,
|
|
Success: true,
|
|
Grant: grant,
|
|
}
|
|
}
|
|
|
|
return items
|
|
}
|
|
|
|
func (s *HelicopterFileService) CreateFromUploadIntentsBulk(ctx context.Context, inputs []helicopterfile.CreateBulkInput, actorID []byte) []helicopterfile.CreateBulkItemResult {
|
|
items := make([]helicopterfile.CreateBulkItemResult, len(inputs))
|
|
if s.fileManager == nil {
|
|
for i := range items {
|
|
items[i] = createBulkError(i, 400, "Create failed", "file manager service is not configured")
|
|
}
|
|
return items
|
|
}
|
|
|
|
for i := range inputs {
|
|
item := inputs[i]
|
|
typ := strings.TrimSpace(item.Type)
|
|
if typ != "helicopter_file_create" && typ != "helicopter_file" {
|
|
items[i] = createBulkError(i, 422, "Validation error", "type must be helicopter_file_create or helicopter_file")
|
|
continue
|
|
}
|
|
|
|
section := strings.TrimSpace(item.Section)
|
|
if !isValidHelicopterFileSection(section) {
|
|
items[i] = createBulkError(i, 422, "Validation error", "section is invalid")
|
|
continue
|
|
}
|
|
|
|
helicopterID, err := uuidv7.ParseString(strings.TrimSpace(item.HelicopterID))
|
|
if err != nil {
|
|
items[i] = createBulkError(i, 422, "Validation error", "helicopter_id is invalid UUID")
|
|
continue
|
|
}
|
|
exists, err := s.repo.HelicopterExists(ctx, helicopterID)
|
|
if err != nil {
|
|
items[i] = createBulkError(i, 400, "Create failed", err.Error())
|
|
continue
|
|
}
|
|
if !exists {
|
|
items[i] = createBulkError(i, 422, "Validation error", "helicopter_id not found")
|
|
continue
|
|
}
|
|
|
|
uploadIntentID, parseErr := uuidv7.ParseString(strings.TrimSpace(item.UploadIntentUUID))
|
|
if parseErr != nil {
|
|
items[i] = createBulkError(i, 422, "Validation error", "upload_intent_uuid is invalid UUID")
|
|
continue
|
|
}
|
|
|
|
fileName := strings.TrimSpace(item.FileName)
|
|
if fileName == "" {
|
|
items[i] = createBulkError(i, 422, "Validation error", "file_name is required")
|
|
continue
|
|
}
|
|
|
|
var folderID []byte
|
|
folderProvided := false
|
|
if item.FolderUUID != nil {
|
|
folderProvided = true
|
|
folderUUID := strings.TrimSpace(*item.FolderUUID)
|
|
if folderUUID != "" {
|
|
parsedFolderID, folderErr := uuidv7.ParseString(folderUUID)
|
|
if folderErr != nil {
|
|
items[i] = createBulkError(i, 422, "Validation error", "folder_uuid is invalid UUID")
|
|
continue
|
|
}
|
|
folderID = parsedFolderID
|
|
}
|
|
}
|
|
|
|
createdFile, createErr := s.fileManager.CreateFileNodeFromUploadIntent(ctx, filemanager.CreateFileFromUploadIntentParams{
|
|
UploadIntentID: uploadIntentID,
|
|
FolderID: folderID,
|
|
FolderProvided: folderProvided,
|
|
Name: fileName,
|
|
ActorID: actorID,
|
|
IsTemplate: true,
|
|
})
|
|
if createErr != nil {
|
|
if errors.Is(createErr, filemanager.ErrDuplicateName) {
|
|
targetFolderID := folderID
|
|
if !folderProvided || len(targetFolderID) == 0 {
|
|
root, rootErr := s.fileManager.GetOrCreateSystemRootFolder(ctx)
|
|
if rootErr != nil || root == nil {
|
|
detail := "unable to resolve target folder"
|
|
if rootErr != nil {
|
|
detail = rootErr.Error()
|
|
}
|
|
items[i] = createBulkError(i, 400, "Create failed", detail)
|
|
continue
|
|
}
|
|
targetFolderID = root.ID
|
|
}
|
|
|
|
nameNormalized := strings.ToLower(filepath.Base(strings.TrimSpace(fileName)))
|
|
if nameNormalized == "" {
|
|
items[i] = createBulkError(i, 422, "Validation error", "file_name is invalid")
|
|
continue
|
|
}
|
|
existingFile, existingErr := s.fileManager.GetFileByFolderAndName(ctx, targetFolderID, nameNormalized)
|
|
if existingErr != nil || existingFile == nil {
|
|
detail := "duplicate file exists but cannot be resolved"
|
|
if existingErr != nil {
|
|
detail = existingErr.Error()
|
|
}
|
|
items[i] = createBulkError(i, 400, "Create failed", detail)
|
|
continue
|
|
}
|
|
createdFile = existingFile
|
|
} else {
|
|
items[i] = createBulkError(i, 400, "Create failed", createErr.Error())
|
|
continue
|
|
}
|
|
}
|
|
if createdFile == nil {
|
|
items[i] = createBulkError(i, 400, "Create failed", "unable to create file from upload intent")
|
|
continue
|
|
}
|
|
|
|
position := 0
|
|
if item.Position != nil {
|
|
position = *item.Position
|
|
}
|
|
isMandatory := false
|
|
if item.IsMandatory != nil {
|
|
isMandatory = *item.IsMandatory
|
|
}
|
|
row := &helicopterfile.HelicopterFile{
|
|
Section: section,
|
|
Position: position,
|
|
IsMandatory: isMandatory,
|
|
HelicopterID: helicopterID,
|
|
SourceFileID: append([]byte(nil), createdFile.ID...),
|
|
CreatedBy: actorID,
|
|
UpdatedBy: actorID,
|
|
}
|
|
if row.Position <= 0 {
|
|
next, nextErr := s.nextPositionByHelicopterSection(ctx, row.HelicopterID, row.Section)
|
|
if nextErr != nil {
|
|
items[i] = createBulkError(i, 400, "Create failed", nextErr.Error())
|
|
continue
|
|
}
|
|
row.Position = next
|
|
}
|
|
if err := s.repo.Create(ctx, row); err != nil {
|
|
items[i] = createBulkError(i, 400, "Create failed", err.Error())
|
|
continue
|
|
}
|
|
|
|
createdRow, getErr := s.repo.GetByID(ctx, row.ID)
|
|
if getErr == nil && createdRow != nil {
|
|
items[i] = helicopterfile.CreateBulkItemResult{
|
|
Index: i,
|
|
Success: true,
|
|
Row: createdRow,
|
|
}
|
|
continue
|
|
}
|
|
|
|
items[i] = helicopterfile.CreateBulkItemResult{
|
|
Index: i,
|
|
Success: true,
|
|
Row: row,
|
|
}
|
|
}
|
|
|
|
return items
|
|
}
|
|
|
|
func (s *HelicopterFileService) FinalizePendingAttachments(ctx context.Context, limit int) (int, error) {
|
|
if s == nil || s.repo == nil {
|
|
return 0, errors.New("helicopter file service is not configured")
|
|
}
|
|
if s.fileManager == nil {
|
|
return 0, nil
|
|
}
|
|
|
|
rows, err := s.repo.ListPendingAttachments(ctx, limit)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return s.finalizePendingAttachmentRows(ctx, rows), nil
|
|
}
|
|
|
|
func (s *HelicopterFileService) FinalizePendingAttachmentsBySourceFileUUID(ctx context.Context, sourceFileUUID string, limit int) (int, error) {
|
|
if s == nil || s.repo == nil {
|
|
return 0, errors.New("helicopter file service is not configured")
|
|
}
|
|
if s.fileManager == nil {
|
|
return 0, nil
|
|
}
|
|
|
|
sourceFileUUID = strings.TrimSpace(sourceFileUUID)
|
|
if sourceFileUUID == "" {
|
|
return 0, nil
|
|
}
|
|
sourceFileID, err := uuidv7.ParseString(sourceFileUUID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
rows, err := s.repo.ListPendingAttachmentsBySourceFileID(ctx, sourceFileID, limit)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return s.finalizePendingAttachmentRows(ctx, rows), nil
|
|
}
|
|
|
|
func (s *HelicopterFileService) finalizePendingAttachmentRows(ctx context.Context, rows []helicopterfile.HelicopterFile) int {
|
|
finalized := 0
|
|
for i := range rows {
|
|
row := &rows[i]
|
|
if len(row.ID) != 16 || len(row.SourceFileID) != 16 || len(row.FileAttachmentID) == 16 {
|
|
continue
|
|
}
|
|
|
|
attachmentID, ok, err := s.finalizePendingAttachment(ctx, row)
|
|
if err != nil || !ok || len(attachmentID) != 16 {
|
|
continue
|
|
}
|
|
finalized++
|
|
}
|
|
return finalized
|
|
}
|
|
|
|
func (s *HelicopterFileService) finalizePendingAttachment(ctx context.Context, row *helicopterfile.HelicopterFile) ([]byte, bool, error) {
|
|
if s == nil || row == nil || s.fileManager == nil {
|
|
return nil, false, nil
|
|
}
|
|
if len(row.SourceFileID) != 16 || len(row.ID) != 16 || len(row.HelicopterID) != 16 {
|
|
return nil, false, nil
|
|
}
|
|
|
|
sourceFileUUID := uuidBytesToStringOrEmpty(row.SourceFileID)
|
|
if sourceFileUUID == "" {
|
|
return nil, false, nil
|
|
}
|
|
helicopterUUID := uuidBytesToStringOrEmpty(row.HelicopterID)
|
|
if helicopterUUID == "" {
|
|
return nil, false, nil
|
|
}
|
|
category := "helicopter_file"
|
|
attachmentID, err := attachmentresolver.Resolve(ctx, attachmentresolver.Params{
|
|
FileManager: s.fileManager,
|
|
RawFileID: &sourceFileUUID,
|
|
|
|
AttachmentField: "file_attachment_id",
|
|
FileField: "source_file_id",
|
|
RequireValue: true,
|
|
|
|
RefType: "helicopter",
|
|
RefID: helicopterUUID,
|
|
Category: &category,
|
|
|
|
IsPrimary: false,
|
|
ActorID: append([]byte(nil), row.UpdatedBy...),
|
|
})
|
|
if err != nil {
|
|
var resolveErr *attachmentresolver.ResolveError
|
|
if errors.As(err, &resolveErr) && resolveErr.Code == attachmentresolver.ErrorFileNotReadyForAttachment {
|
|
return nil, false, nil
|
|
}
|
|
return nil, false, err
|
|
}
|
|
if len(attachmentID) != 16 {
|
|
return nil, false, nil
|
|
}
|
|
|
|
row.FileAttachmentID = append([]byte(nil), attachmentID...)
|
|
row.SourceFileID = nil
|
|
row.UpdatedAt = time.Now().UTC()
|
|
if len(row.UpdatedBy) == 0 {
|
|
row.UpdatedBy = append([]byte(nil), row.CreatedBy...)
|
|
}
|
|
if err := s.repo.Update(ctx, row); err != nil {
|
|
return nil, false, err
|
|
}
|
|
s.publishHelicopterFileState(row, attachmentID)
|
|
if err := s.markAttachmentFileActive(ctx, attachmentID, row.UpdatedBy); err != nil {
|
|
return attachmentID, true, err
|
|
}
|
|
return attachmentID, true, nil
|
|
}
|
|
|
|
func (s *HelicopterFileService) publishHelicopterFileState(row *helicopterfile.HelicopterFile, fileID []byte) {
|
|
if s == nil || s.events == nil || row == nil || len(row.ID) != 16 || len(row.HelicopterID) != 16 {
|
|
return
|
|
}
|
|
|
|
actorID := row.UpdatedBy
|
|
if len(actorID) != 16 {
|
|
actorID = row.CreatedBy
|
|
}
|
|
if len(actorID) != 16 {
|
|
return
|
|
}
|
|
|
|
actorUUID := uuidBytesToStringOrEmpty(actorID)
|
|
helicopterUUID := uuidBytesToStringOrEmpty(row.HelicopterID)
|
|
helicopterFileUUID := uuidBytesToStringOrEmpty(row.ID)
|
|
attachmentUUID := uuidBytesToStringOrEmpty(fileID)
|
|
if attachmentUUID == "" {
|
|
attachmentUUID = uuidBytesToStringOrEmpty(row.FileAttachmentID)
|
|
}
|
|
if actorUUID == "" || helicopterUUID == "" || helicopterFileUUID == "" || attachmentUUID == "" {
|
|
return
|
|
}
|
|
|
|
updatedAt := row.UpdatedAt.UTC()
|
|
if updatedAt.IsZero() {
|
|
updatedAt = time.Now().UTC()
|
|
}
|
|
|
|
sourceFile := row.SourceFile
|
|
name := ""
|
|
mimeType := ""
|
|
if sourceFile != nil {
|
|
name = strings.TrimSpace(sourceFile.Name)
|
|
mimeType = strings.TrimSpace(sourceFile.MimeType)
|
|
}
|
|
|
|
s.events.PublishToUsers([]string{actorUUID}, helicopterfilerealtime.Event{
|
|
EventType: helicopterfilerealtime.DefaultEventType,
|
|
HelicopterID: helicopterUUID,
|
|
HelicopterFileID: helicopterFileUUID,
|
|
FileID: attachmentUUID,
|
|
Section: strings.TrimSpace(row.Section),
|
|
Name: name,
|
|
MimeType: mimeType,
|
|
AttachmentPending: len(row.FileAttachmentID) != 16 && len(row.SourceFileID) == 16,
|
|
Position: row.Position,
|
|
IsMandatory: row.IsMandatory,
|
|
UpdatedAt: updatedAt.Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
func (s *HelicopterFileService) markAttachmentFileActive(ctx context.Context, attachmentID []byte, actorID []byte) error {
|
|
if s == nil || s.fileManager == nil || len(attachmentID) != 16 {
|
|
return nil
|
|
}
|
|
lifecycle, ok := s.fileManager.(filemanager.LifecycleManager)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
attachment, err := s.fileManager.GetAttachmentByID(ctx, attachmentID)
|
|
if err != nil || attachment == nil || len(attachment.FileID) != 16 {
|
|
return err
|
|
}
|
|
return lifecycle.MarkFileActive(ctx, attachment.FileID, actorID)
|
|
}
|
|
|
|
func (s *HelicopterFileService) nextPositionByHelicopterSection(ctx context.Context, helicopterID []byte, section string) (int, error) {
|
|
rows, err := s.repo.ListByHelicopterAndSection(ctx, helicopterID, section)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
maxPos := 0
|
|
for i := range rows {
|
|
if rows[i].Position > maxPos {
|
|
maxPos = rows[i].Position
|
|
}
|
|
}
|
|
return maxPos + 1, nil
|
|
}
|
|
|
|
func uploadIntentBulkError(index int, status int, title, detail string) helicopterfile.UploadIntentBulkItemResult {
|
|
return helicopterfile.UploadIntentBulkItemResult{
|
|
Index: index,
|
|
Success: false,
|
|
Error: &helicopterfile.BulkItemError{
|
|
Status: status,
|
|
Title: title,
|
|
Detail: detail,
|
|
},
|
|
}
|
|
}
|
|
|
|
func createBulkError(index int, status int, title, detail string) helicopterfile.CreateBulkItemResult {
|
|
return helicopterfile.CreateBulkItemResult{
|
|
Index: index,
|
|
Success: false,
|
|
Error: &helicopterfile.BulkItemError{
|
|
Status: status,
|
|
Title: title,
|
|
Detail: detail,
|
|
},
|
|
}
|
|
}
|
|
|
|
func isValidHelicopterFileSection(section string) bool {
|
|
switch strings.TrimSpace(section) {
|
|
case helicopterfile.SectionBeforeFirstFlightInspection,
|
|
helicopterfile.SectionFlightPreparationAndWB,
|
|
helicopterfile.SectionAfterLastFlightInspection:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func uuidBytesToStringOrEmpty(id []byte) string {
|
|
s, err := uuidv7.BytesToString(id)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return s
|
|
}
|
|
|
|
func normalizeHelicopterFileSort(sort string) string {
|
|
if normalized := normalizeSortByRules(strings.TrimSpace(sort),
|
|
sortExpr("section ASC, position ASC, created_at ASC", "section DESC, position DESC, created_at DESC", "section"),
|
|
sortField("position"),
|
|
sortField("is_mandatory"),
|
|
sortField("created_at"),
|
|
sortField("updated_at"),
|
|
); normalized != "" {
|
|
return normalized
|
|
}
|
|
return "section ASC, position ASC, created_at ASC"
|
|
}
|