package store import ( "database/sql" "errors" "fmt" "time" ) type UploadRecord struct { UploadID string Binding []byte Asset []byte Grant []byte ObjectKey string State string OSSID string CreatedAt time.Time CompletedAt *time.Time } func (s *Store) LoadUpload(uploadID string) (UploadRecord, error) { if uploadID == "" { return UploadRecord{}, errors.New("upload ID is required") } s.mu.Lock() defer s.mu.Unlock() var record UploadRecord var createdAt string var completedAt sql.NullString var ossID sql.NullString err := s.db.QueryRow(`SELECT upload_id, binding, asset, grant, object_key, state, oss_id, created_at, completed_at FROM uploads WHERE upload_id = ?`, uploadID).Scan( &record.UploadID, &record.Binding, &record.Asset, &record.Grant, &record.ObjectKey, &record.State, &ossID, &createdAt, &completedAt, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return UploadRecord{}, err } return UploadRecord{}, fmt.Errorf("load upload: %w", err) } record.OSSID = ossID.String parsed, err := time.Parse(time.RFC3339Nano, createdAt) if err != nil { return UploadRecord{}, fmt.Errorf("parse upload created_at: %w", err) } record.CreatedAt = parsed if completedAt.Valid && completedAt.String != "" { parsed, err := time.Parse(time.RFC3339Nano, completedAt.String) if err != nil { return UploadRecord{}, fmt.Errorf("parse upload completed_at: %w", err) } record.CompletedAt = &parsed } return record, nil } func (s *Store) ReplaceUploadGrant(uploadID, objectKey string, grant []byte) error { if uploadID == "" || objectKey == "" || len(grant) == 0 { return errors.New("upload ID, object key and grant are required") } s.mu.Lock() defer s.mu.Unlock() result, err := s.db.Exec(`UPDATE uploads SET grant = ?, object_key = ? WHERE upload_id = ? AND state = 'granted'`, grant, objectKey, uploadID) if err != nil { return fmt.Errorf("replace upload grant: %w", err) } updated, err := result.RowsAffected() if err != nil { return fmt.Errorf("inspect replaced upload grant: %w", err) } if updated == 0 { var state string if err := s.db.QueryRow(`SELECT state FROM uploads WHERE upload_id = ?`, uploadID).Scan(&state); err != nil { return err } if state == "completed" { return nil } return errors.New("upload is not in granted state") } return nil } func (s *Store) InsertUpload(record UploadRecord) error { if record.UploadID == "" || len(record.Binding) == 0 || len(record.Asset) == 0 || len(record.Grant) == 0 || record.ObjectKey == "" { return errors.New("complete upload record is required") } if record.CreatedAt.IsZero() { record.CreatedAt = s.now().UTC() } s.mu.Lock() defer s.mu.Unlock() _, err := s.db.Exec(`INSERT INTO uploads(upload_id, binding, asset, grant, object_key, state, oss_id, created_at, completed_at) VALUES(?, ?, ?, ?, ?, ?, NULL, ?, NULL)`, record.UploadID, record.Binding, record.Asset, record.Grant, record.ObjectKey, record.State, record.CreatedAt.UTC().Format(time.RFC3339Nano)) if err != nil { return fmt.Errorf("insert upload: %w", err) } return nil } func (s *Store) CompleteUpload(uploadID, ossID string, completedAt time.Time) error { return s.completeUpload(uploadID, ossID, completedAt, nil, "", "", "", "") } // CompleteUploadAndOutbox atomically records verified OSS completion and the // recording.ready event. A successful RPC therefore cannot lose the MQ handoff // between the upload state update and outbox persistence. func (s *Store) CompleteUploadAndOutbox(uploadID, ossID string, completedAt time.Time, eventID, tenantKey, exchange, routingKey string, body []byte) error { if eventID == "" || tenantKey == "" || exchange == "" || routingKey == "" || len(body) == 0 { return errors.New("verified upload outbox event is required") } return s.completeUpload(uploadID, ossID, completedAt, body, eventID, tenantKey, exchange, routingKey) } func (s *Store) completeUpload(uploadID, ossID string, completedAt time.Time, body []byte, eventID, tenantKey, exchange, routingKey string) error { if uploadID == "" || ossID == "" { return errors.New("upload ID and OSS ID are required") } if completedAt.IsZero() { completedAt = s.now().UTC() } if eventID != "" && (exchange == "" || routingKey == "") { return errors.New("exchange and routing key are required") } s.mu.Lock() defer s.mu.Unlock() tx, err := s.db.Begin() if err != nil { return fmt.Errorf("begin complete upload: %w", err) } defer func() { _ = tx.Rollback() }() result, err := tx.Exec(`UPDATE uploads SET state = 'completed', oss_id = ?, completed_at = ? WHERE upload_id = ? AND state = 'granted'`, ossID, completedAt.UTC().Format(time.RFC3339Nano), uploadID) if err != nil { return fmt.Errorf("complete upload: %w", err) } updated, err := result.RowsAffected() if err != nil { return fmt.Errorf("inspect completed upload: %w", err) } if updated == 0 { var state string var existingOSSID sql.NullString lookupErr := tx.QueryRow(`SELECT state, oss_id FROM uploads WHERE upload_id = ?`, uploadID).Scan(&state, &existingOSSID) if lookupErr != nil { if errors.Is(lookupErr, sql.ErrNoRows) { return lookupErr } return fmt.Errorf("inspect upload state: %w", lookupErr) } if state == "completed" && existingOSSID.Valid && existingOSSID.String == ossID { return tx.Commit() } return errors.New("upload is not in granted state") } if eventID != "" { if _, err := tx.Exec(`INSERT INTO outbox(event_id, tenant_key, exchange, routing_key, body, status, created_at) VALUES(?, ?, ?, ?, ?, 'pending', ?)`, eventID, tenantKey, exchange, routingKey, body, completedAt.UTC().Format(time.RFC3339Nano)); err != nil { return fmt.Errorf("persist verified upload outbox: %w", err) } } if err := tx.Commit(); err != nil { return fmt.Errorf("commit completed upload: %w", err) } return nil }