Files
go-sip/cmd/sip-go-agent/upload.go
T

167 lines
6.1 KiB
Go

package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net/url"
"path/filepath"
"strings"
"time"
agentv1 "git.ipao.vip/rogee/go-sip/gen/agent/v1"
"git.ipao.vip/rogee/go-sip/internal/agent"
"git.ipao.vip/rogee/go-sip/internal/callruntime"
"git.ipao.vip/rogee/go-sip/internal/config"
"git.ipao.vip/rogee/go-sip/internal/rpc"
)
func uploadCallRecordings(ctx context.Context, cfg config.Config, result callruntime.Result) ([]map[string]any, error) {
if strings.TrimSpace(cfg.DispatcherGRPCEndpoint) == "" {
return nil, nil
}
for name, value := range map[string]string{
"AGENT_CALL_TENANT_ID": cfg.CallTenantID,
"AGENT_CALL_TENANT_KEY": cfg.CallTenantKey,
"AGENT_CALL_TASK_ID": cfg.CallTaskID,
"AGENT_CALL_TASK_ITEM_ID": cfg.CallTaskItemID,
} {
if strings.TrimSpace(value) == "" {
return nil, fmt.Errorf("%s is required when Dispatcher OSS upload is enabled", name)
}
}
if result.ChannelID == "" {
return nil, errors.New("call result has no channel ID for upload binding")
}
executionID := cfg.CallExecutionID
if executionID == "" {
executionID = result.ChannelID
}
binding := &agentv1.ExecutionBinding{
TenantId: cfg.CallTenantID,
TenantKey: cfg.CallTenantKey,
ExecutionId: executionID,
TaskId: cfg.CallTaskID,
TaskItemId: cfg.CallTaskItemID,
TaskRevision: 1,
CallId: result.ChannelID,
AttemptId: result.ChannelID,
AgentVersionId: cfg.Version,
}
client, err := rpc.DialFromFiles(cfg.DispatcherGRPCEndpoint, cfg.MTLSCAFile, cfg.MTLSCertFile, cfg.MTLSKeyFile, cfg.DispatcherGRPCServerName)
if err != nil {
return nil, fmt.Errorf("dial Dispatcher gRPC service: %w", err)
}
defer client.Close()
uploader := agent.UploadClient{Now: time.Now}
facts := append(append([]callruntime.RecordingFact(nil), result.InboundRecordings...), result.OutboundRecordings...)
if len(facts) == 0 {
return nil, errors.New("call produced no recordings for OSS upload")
}
uploaded := make([]map[string]any, 0, len(facts))
for index, recording := range facts {
if recording.Path == "" || recording.Bytes <= 0 || recording.SHA256 == "" {
return nil, fmt.Errorf("recording %d is missing path, size or checksum", index+1)
}
assetID := recordingAssetID(recording.Segment, recording.Path, index+1)
asset := &agentv1.AssetDescriptor{
Kind: agentv1.AssetKind_ASSET_KIND_RECORDING,
AssetId: assetID,
CallId: result.ChannelID,
ExecutionId: executionID,
Format: "wav",
SizeBytes: int64(recording.Bytes),
ChecksumSha256: recording.SHA256,
Channels: 1,
SampleRateHz: 16000,
}
uploadID := stableUploadID(binding, asset)
meta := uploadMeta(cfg, "request", uploadID)
grantResponse, err := client.RequestUpload(ctx, &agentv1.RequestUploadRequest{Meta: meta, Binding: binding, Asset: asset, UploadId: uploadID})
if err != nil {
return nil, fmt.Errorf("request upload %s: %w", uploadID, err)
}
if grantResponse == nil || grantResponse.Grant == nil || grantResponse.Receipt == nil || grantResponse.Receipt.Result != agentv1.ResultCode_RESULT_CODE_ACCEPTED {
return nil, fmt.Errorf("request upload %s was rejected", uploadID)
}
parsed, err := url.Parse(grantResponse.Grant.TargetUrl)
if err != nil || parsed.Host == "" {
return nil, fmt.Errorf("upload %s returned invalid target URL", uploadID)
}
uploader.AllowedHosts = map[string]struct{}{strings.ToLower(parsed.Host): {}}
uploadResult, err := uploader.UploadFile(ctx, grantResponse.Grant, recording.Path)
if err != nil {
return nil, fmt.Errorf("upload %s data plane: %w", uploadID, err)
}
if uploadResult.SizeBytes != asset.SizeBytes || !strings.EqualFold(uploadResult.SHA256, asset.ChecksumSha256) {
return nil, fmt.Errorf("upload %s local result does not match asset", uploadID)
}
completeResponse, err := client.CompleteUpload(ctx, &agentv1.CompleteUploadRequest{
Meta: uploadMeta(cfg, "complete", uploadID),
Binding: binding,
Asset: asset,
UploadId: uploadID,
UploadedSizeBytes: uploadResult.SizeBytes,
UploadedChecksumSha256: uploadResult.SHA256,
})
if err != nil {
return nil, fmt.Errorf("complete upload %s: %w", uploadID, err)
}
if completeResponse == nil || completeResponse.Receipt == nil || completeResponse.Receipt.Result != agentv1.ResultCode_RESULT_CODE_ACCEPTED || completeResponse.OssId == "" {
return nil, fmt.Errorf("complete upload %s was not verified", uploadID)
}
uploaded = append(uploaded, map[string]any{
"upload_id": uploadID, "asset_id": asset.AssetId, "object_key": grantResponse.Grant.ObjectKey,
"oss_id": completeResponse.OssId, "bytes": uploadResult.SizeBytes, "sha256": uploadResult.SHA256,
"status_code": uploadResult.StatusCode,
})
}
return uploaded, nil
}
func recordingAssetID(segment, path string, index int) string {
raw := segment
if raw == "" {
raw = fmt.Sprintf("segment-%d", index)
}
raw += "-" + filepath.Base(path)
var builder strings.Builder
for _, r := range raw {
if r == '/' || r == '\\' || r == ' ' || r == '\t' || r == '\n' || r == '\r' {
builder.WriteByte('-')
continue
}
builder.WriteRune(r)
}
assetID := strings.Trim(builder.String(), "-")
if assetID == "" {
assetID = fmt.Sprintf("segment-%d", index)
}
if len([]byte(assetID)) > 120 {
digest := sha256.Sum256([]byte(assetID))
assetID = "recording-" + hex.EncodeToString(digest[:16])
}
return assetID
}
func stableUploadID(binding *agentv1.ExecutionBinding, asset *agentv1.AssetDescriptor) string {
value := binding.ExecutionId + "\x00" + asset.AssetId + "\x00" + asset.ChecksumSha256
digest := sha256.Sum256([]byte(value))
return "upload-" + hex.EncodeToString(digest[:16])
}
func uploadMeta(cfg config.Config, phase, uploadID string) *agentv1.RequestMeta {
operationID := "recording-" + phase + "-" + uploadID
return &agentv1.RequestMeta{
ProtocolVersion: "agent.v1",
RequestId: operationID,
TraceId: operationID,
OperationId: operationID,
IdempotencyKey: operationID,
AgentId: cfg.AgentID,
CellId: cfg.CellID,
}
}