170 lines
5.8 KiB
Go
170 lines
5.8 KiB
Go
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
|
|
}
|