Files

91 lines
3.6 KiB
Go
Raw Permalink Normal View History

2026-08-26 06:41:07 +08:00
package media
import (
"context"
"errors"
"strings"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/database"
"data-terminal-cloud/internal/deviceauth"
"github.com/jackc/pgx/v5"
)
// AcknowledgeLocalDelete records only deletion of the device-side encrypted copy.
// The verified cloud object and its media_segment lifecycle remain unchanged.
func (s *Service) AcknowledgeLocalDelete(ctx context.Context, identity deviceauth.Identity, segmentPublicID, status, errorCode, requestID string) (Segment, error) {
status = strings.ToUpper(strings.TrimSpace(status))
errorCode = strings.ToUpper(strings.TrimSpace(errorCode))
if status != "DELETED" && status != "FAILED" {
return Segment{}, ErrInvalidDeclaration
}
if status == "DELETED" && errorCode != "" {
return Segment{}, ErrInvalidDeclaration
}
if status == "FAILED" && (errorCode == "" || len(errorCode) > 80) {
return Segment{}, ErrInvalidDeclaration
}
var result Segment
err := database.Transact(ctx, s.pool, func(tx pgx.Tx) error {
var cloudStatus, storedStatus string
var deleteAfter *time.Time
err := tx.QueryRow(ctx, `
SELECT status,COALESCE(local_delete_status,''),local_delete_after
FROM media_segment
WHERE public_id=$1::uuid AND tenant_id=$2::uuid AND device_id=$3::uuid
FOR UPDATE`, segmentPublicID, identity.TenantID, identity.ID,
).Scan(&cloudStatus, &storedStatus, &deleteAfter)
if errors.Is(err, pgx.ErrNoRows) {
return ErrNotFound
}
if err != nil {
return err
}
if cloudStatus != "VERIFIED" || deleteAfter == nil {
return ErrConflict
}
if storedStatus == "DELETED" {
return scanDeviceSegment(ctx, tx, identity, segmentPublicID, &result)
}
if status == "DELETED" && time.Now().UTC().Before(*deleteAfter) {
return ErrLocalDeleteTooEarly
}
if _, err := tx.Exec(ctx, `
UPDATE media_segment
SET local_delete_status=$4,local_delete_reported_at=now(),local_delete_error_code=NULLIF($5,''),updated_at=now()
WHERE public_id=$1::uuid AND tenant_id=$2::uuid AND device_id=$3::uuid`,
segmentPublicID, identity.TenantID, identity.ID, status, errorCode,
); err != nil {
return err
}
auditResult := "success"
if status == "FAILED" {
auditResult = "failure"
}
if err := s.audit.Record(ctx, tx, audit.Event{
TenantID: &identity.TenantID, ActorType: "device", ActorPublicID: &identity.PublicID,
Action: "media.local_delete.ack", ResourceType: "media_segment", ResourceID: &segmentPublicID,
RequestID: requestID, Result: auditResult, Detail: map[string]any{"status": status, "error_code": errorCode},
}); err != nil {
return err
}
return scanDeviceSegment(ctx, tx, identity, segmentPublicID, &result)
})
return result, err
}
func scanDeviceSegment(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity, segmentPublicID string, result *Segment) error {
return tx.QueryRow(ctx, `
SELECT public_id::text,track,sequence_no,codec,content_type,duration_ms,expected_size,expected_digest,status,
COALESCE(last_error_code,''),verified_at,local_delete_after,COALESCE(local_delete_status,''),
local_delete_reported_at,COALESCE(local_delete_error_code,'')
FROM media_segment WHERE public_id=$1::uuid AND tenant_id=$2::uuid AND device_id=$3::uuid`,
segmentPublicID, identity.TenantID, identity.ID,
).Scan(&result.ID, &result.Track, &result.SequenceNo, &result.Codec, &result.ContentType, &result.DurationMS,
&result.ExpectedSize, &result.ExpectedDigest, &result.Status, &result.LastErrorCode, &result.VerifiedAt,
&result.LocalDeleteAfter, &result.LocalDeleteStatus, &result.LocalDeleteReportedAt, &result.LocalDeleteErrorCode)
}