252 lines
6.4 KiB
Go
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
|
|
}
|
|
}
|