package attachmentresolver import ( "context" "errors" "fmt" "strings" "time" filemanager "wucher/internal/domain/file_manager" "wucher/internal/shared/pkg/uuidv7" ) type ErrorCode string const ( ErrorInvalidAttachmentUUID ErrorCode = "invalid_attachment_uuid" ErrorInvalidFileUUID ErrorCode = "invalid_file_uuid" ErrorAttachmentNotFound ErrorCode = "attachment_not_found" ErrorRequireValue ErrorCode = "require_value" ErrorFileManagerUnavailable ErrorCode = "file_manager_unavailable" ErrorFileNotReadyForAttachment ErrorCode = "file_not_ready_for_attachment" ErrorCannotResolveFromFile ErrorCode = "cannot_resolve_from_file" ) const ( notReadyRetryAttempts = 2 notReadyRetryDelay = 150 * time.Millisecond ) type ResolveError struct { Code ErrorCode AttachmentField string FileField string Cause error } func (e *ResolveError) Error() string { if e == nil { return "" } switch e.Code { case ErrorInvalidAttachmentUUID: return fmt.Sprintf("%s is invalid UUID", fallbackField(e.AttachmentField, "attachment_id")) case ErrorInvalidFileUUID: return fmt.Sprintf("%s is invalid UUID", fallbackField(e.FileField, "file_id")) case ErrorAttachmentNotFound: return fmt.Sprintf("%s not found", fallbackField(e.AttachmentField, "attachment_id")) case ErrorRequireValue: return fmt.Sprintf( "%s or %s is required", fallbackField(e.AttachmentField, "attachment_id"), fallbackField(e.FileField, "file_id"), ) case ErrorFileManagerUnavailable: return "file manager service is not configured" case ErrorFileNotReadyForAttachment: return "file is not ready for attachment" case ErrorCannotResolveFromFile: return fmt.Sprintf("cannot resolve attachment from %s", fallbackField(e.FileField, "file_id")) default: if e.Cause != nil { return e.Cause.Error() } return "attachment resolve failed" } } func (e *ResolveError) Unwrap() error { return e.Cause } type Params struct { FileManager filemanager.Service RawAttachmentID *string RawFileID *string AttachmentField string FileField string RequireValue bool RefType string RefID string Category *string IsPrimary bool ActorID []byte ValidateAttachmentExists bool AttachmentExists func(ctx context.Context, attachmentID []byte) (bool, error) } func Resolve(ctx context.Context, p Params) ([]byte, error) { if p.RawAttachmentID != nil && strings.TrimSpace(*p.RawAttachmentID) != "" { attachmentID, parseErr := uuidv7.ParseString(strings.TrimSpace(*p.RawAttachmentID)) if parseErr != nil { return nil, &ResolveError{ Code: ErrorInvalidAttachmentUUID, AttachmentField: p.AttachmentField, FileField: p.FileField, Cause: parseErr, } } if p.ValidateAttachmentExists { exists, existsErr := resolveAttachmentExists(ctx, p, attachmentID) if existsErr != nil { return nil, existsErr } if !exists { return nil, &ResolveError{ Code: ErrorAttachmentNotFound, AttachmentField: p.AttachmentField, FileField: p.FileField, } } } return attachmentID, nil } if p.RawFileID != nil && strings.TrimSpace(*p.RawFileID) != "" { if p.FileManager == nil { return nil, &ResolveError{ Code: ErrorFileManagerUnavailable, AttachmentField: p.AttachmentField, FileField: p.FileField, } } fileID, parseErr := uuidv7.ParseString(strings.TrimSpace(*p.RawFileID)) if parseErr != nil { return nil, &ResolveError{ Code: ErrorInvalidFileUUID, AttachmentField: p.AttachmentField, FileField: p.FileField, Cause: parseErr, } } created, createErr := p.FileManager.CreateAttachment(ctx, filemanager.CreateAttachmentParams{ FileID: fileID, RefType: p.RefType, RefID: p.RefID, Category: p.Category, SortOrder: 0, IsPrimary: p.IsPrimary, ActorID: p.ActorID, }) if createErr == nil && created != nil && len(created.ID) == 16 { return created.ID, nil } if createErr != nil && !errors.Is(createErr, filemanager.ErrAttachmentAlreadyExists) && !errors.Is(createErr, filemanager.ErrFileNotReadyForAttachment) { return nil, createErr } if failed, failureErr := fileIsFailed(ctx, p.FileManager, fileID); failureErr != nil { return nil, failureErr } else if failed { return nil, &ResolveError{ Code: ErrorCannotResolveFromFile, AttachmentField: p.AttachmentField, FileField: p.FileField, } } if errors.Is(createErr, filemanager.ErrFileNotReadyForAttachment) { return nil, &ResolveError{ Code: ErrorFileNotReadyForAttachment, AttachmentField: p.AttachmentField, FileField: p.FileField, } } rows, _, listErr := p.FileManager.ListAttachmentsByReference(ctx, filemanager.ListAttachmentsParams{ RefType: p.RefType, RefID: p.RefID, Limit: 100, Offset: 0, }) if listErr != nil { return nil, listErr } for i := range rows { if string(rows[i].FileID) == string(fileID) { return rows[i].ID, nil } } return nil, &ResolveError{ Code: ErrorCannotResolveFromFile, AttachmentField: p.AttachmentField, FileField: p.FileField, } } if p.RequireValue { return nil, &ResolveError{ Code: ErrorRequireValue, AttachmentField: p.AttachmentField, FileField: p.FileField, } } return nil, nil } func resolveAttachmentExists(ctx context.Context, p Params, attachmentID []byte) (bool, error) { if p.AttachmentExists != nil { return p.AttachmentExists(ctx, attachmentID) } if p.FileManager != nil { row, err := p.FileManager.GetAttachmentByID(ctx, attachmentID) if err != nil { return false, err } return row != nil, nil } return true, nil } func fileIsFailed(ctx context.Context, fm filemanager.Service, fileID []byte) (bool, error) { if fm == nil || len(fileID) == 0 { return false, nil } file, err := fm.GetFileByID(ctx, fileID) if err != nil { return false, err } if file == nil { return false, nil } return filemanager.NormalizeFileStatus(file.Status) == filemanager.FileStatusFailed, nil } func fallbackField(v string, fallback string) string { if strings.TrimSpace(v) == "" { return fallback } return strings.TrimSpace(v) } func sleepWithContext(ctx context.Context, d time.Duration) error { timer := time.NewTimer(d) defer timer.Stop() select { case <-ctx.Done(): return ctx.Err() case <-timer.C: return nil } }