init push
This commit is contained in:
251
internal/shared/pkg/attachmentresolver/resolver.go
Normal file
251
internal/shared/pkg/attachmentresolver/resolver.go
Normal file
@@ -0,0 +1,251 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user