init push
This commit is contained in:
561
internal/service/helicopter_file_service.go
Normal file
561
internal/service/helicopter_file_service.go
Normal file
@@ -0,0 +1,561 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user