Files
2026-07-16 22:16:45 +07:00

252 lines
6.4 KiB
Go

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
}
}