package service import ( "errors" "fmt" "time" "filefast/backend/internal/model" "filefast/backend/internal/store" "github.com/google/uuid" ) type TransferService struct { store *store.MemoryStore } type CreateTransferInput struct { SessionID string `json:"session_id"` Kind string `json:"kind" binding:"required"` Name string `json:"name"` Content string `json:"content"` SizeBytes int64 `json:"size_bytes"` SenderDeviceID string `json:"sender_device_id" binding:"required"` ReceiverDeviceID string `json:"receiver_device_id" binding:"required"` } type UpdateTransferStatusInput struct { CurrentChannel string `json:"current_channel"` FinalStatus string `json:"final_status"` FallbackReason string `json:"fallback_reason"` } func NewTransferService(store *store.MemoryStore) *TransferService { return &TransferService{store: store} } func (s *TransferService) Create(input CreateTransferInput) (model.Transfer, error) { switch input.Kind { case "file": if input.Name == "" { return model.Transfer{}, errors.New("file name is required") } if input.SizeBytes <= 0 { return model.Transfer{}, errors.New("file size must be greater than zero") } case "text": if input.Content == "" { return model.Transfer{}, errors.New("text content is required") } if input.Name == "" { input.Name = "text-message" } default: return model.Transfer{}, errors.New("unsupported transfer kind") } runtime := s.store.RuntimeConfig() now := time.Now() fallbackAllowed := input.Kind == "file" && runtime.MinIOFallbackEnabled strategy := "p2p_turn" if fallbackAllowed { strategy = "p2p_turn_minio" } transfer := model.Transfer{ ID: uuid.NewString(), SessionID: input.SessionID, Kind: input.Kind, Name: input.Name, Content: input.Content, SizeBytes: input.SizeBytes, SenderDeviceID: input.SenderDeviceID, ReceiverDeviceID: input.ReceiverDeviceID, TransferStrategy: strategy, CurrentChannel: model.ChannelP2P, FallbackAllowed: fallbackAllowed, FinalStatus: model.TransferPending, CreatedAt: now, UpdatedAt: now, } return s.store.UpsertTransfer(transfer), nil } func (s *TransferService) UpdateStatus(transferID string, input UpdateTransferStatusInput) (model.Transfer, error) { transfer, ok := s.store.GetTransfer(transferID) if !ok { return model.Transfer{}, errors.New("transfer not found") } if input.CurrentChannel != "" { transfer.CurrentChannel = input.CurrentChannel } if input.FinalStatus != "" { transfer.FinalStatus = input.FinalStatus } if input.FallbackReason != "" { transfer.FallbackReason = input.FallbackReason } transfer.UpdatedAt = time.Now() return s.store.UpsertTransfer(transfer), nil } func (s *TransferService) PrepareFallback(transferID string) (model.Transfer, model.FallbackObject, error) { transfer, ok := s.store.GetTransfer(transferID) if !ok { return model.Transfer{}, model.FallbackObject{}, errors.New("transfer not found") } if !transfer.FallbackAllowed { return model.Transfer{}, model.FallbackObject{}, errors.New("transfer cannot use minio fallback") } if object, ok := s.store.GetFallbackObject(transfer.ID); ok && object.CleanedAt == nil && object.ExpiresAt.After(time.Now()) { return transfer, object, nil } runtime := s.store.RuntimeConfig() now := time.Now() expireAt := now.Add(time.Duration(runtime.MinIORetentionHours) * time.Hour) objectKey := fmt.Sprintf("fallback/%s/%d-%s", now.Format("20060102"), now.Unix(), transfer.ID) transfer.ObjectKey = objectKey transfer.ExpiresAt = &expireAt transfer.UpdatedAt = now transfer = s.store.UpsertTransfer(transfer) object := model.FallbackObject{ TransferID: transfer.ID, ObjectKey: objectKey, SizeBytes: transfer.SizeBytes, CreatedAt: now, ExpiresAt: expireAt, CleanupState: "uploading", } object = s.store.SaveFallbackObject(object) return transfer, object, nil }