Files
story/internal/httpapi/media_handlers.go
T

217 lines
7.6 KiB
Go

package httpapi
import (
"errors"
"net/http"
"strings"
"time"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/media"
)
func GrantRecordingAccessHandler(service *media.Service) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
principal, ok := resourcePrincipal(w, r, "id")
if !ok {
return
}
var input struct {
GranteeUserID string `json:"grantee_user_id"`
Permission string `json:"permission"`
Purpose string `json:"purpose"`
ExpiresAt time.Time `json:"expires_at"`
}
if !decodeWebJSON(w, r, &input, "录音访问授权参数不正确") {
return
}
if !validUUID(input.GranteeUserID) || strings.TrimSpace(input.Purpose) == "" || input.ExpiresAt.IsZero() {
writeValidation(w, r, "录音访问授权参数不正确", FieldError{Field: "grant", Message: "被授权用户、权限、用途和到期时间均为必填"})
return
}
item, err := service.GrantRecordingAccess(r.Context(), principal, r.PathValue("id"), input.GranteeUserID, input.Permission, input.Purpose, RequestID(r.Context()), input.ExpiresAt)
if err != nil {
writeMediaError(w, r, err)
return
}
WriteJSON(w, r, http.StatusCreated, item)
})
}
func ListRecordingAccessGrantsHandler(service *media.Service) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
principal, ok := resourcePrincipal(w, r, "id")
if !ok {
return
}
items, err := service.ListRecordingAccessGrants(r.Context(), principal, r.PathValue("id"))
if err != nil {
writeMediaError(w, r, err)
return
}
w.Header().Set("Cache-Control", "no-store")
WriteJSON(w, r, http.StatusOK, map[string]any{"items": items})
})
}
func RevokeRecordingAccessHandler(service *media.Service) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
principal, ok := resourcePrincipal(w, r, "id")
if !ok {
return
}
item, err := service.RevokeRecordingAccess(r.Context(), principal, r.PathValue("id"), RequestID(r.Context()))
if err != nil {
writeMediaError(w, r, err)
return
}
WriteJSON(w, r, http.StatusOK, item)
})
}
func GetConversationMediaHandler(service *media.Service) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
principal, ok := resourcePrincipal(w, r, "id")
if !ok {
return
}
item, err := service.GetConversationMedia(r.Context(), principal, r.PathValue("id"))
if err != nil {
writeMediaError(w, r, err)
return
}
w.Header().Set("Cache-Control", "no-store")
WriteJSON(w, r, http.StatusOK, item)
})
}
func CreateMediaAccessHandler(service *media.Service) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
principal, ok := resourcePrincipal(w, r, "id")
if !ok {
return
}
var input struct {
Action string `json:"action"`
Purpose string `json:"purpose"`
}
if !decodeWebJSON(w, r, &input, "录音访问参数不正确") {
return
}
item, err := service.CreateMediaAccess(r.Context(), principal, r.PathValue("id"), input.Action, input.Purpose, RequestID(r.Context()))
if err != nil {
writeMediaError(w, r, err)
return
}
w.Header().Set("Cache-Control", "no-store")
WriteJSON(w, r, http.StatusOK, item)
})
}
func PrepareMediaUploadBatchHandler(service *media.Service) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
identity, ok := deviceauth.FromContext(r.Context())
if !ok {
WriteError(w, r, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
return
}
idempotencyKey := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
if len(idempotencyKey) < 16 || len(idempotencyKey) > 128 {
writeValidation(w, r, "录音上传批次参数不正确", FieldError{Field: "Idempotency-Key", Message: "长度必须为16至128个字符"})
return
}
var input struct {
Segments []media.SegmentDeclaration `json:"segments"`
}
if !decodeWebJSON(w, r, &input, "录音上传批次参数不正确") {
return
}
item, err := service.PrepareUploadBatch(r.Context(), identity, r.PathValue("id"), idempotencyKey, RequestID(r.Context()), input.Segments)
if err != nil {
writeMediaError(w, r, err)
return
}
w.Header().Set("Cache-Control", "no-store")
WriteJSON(w, r, http.StatusCreated, item)
})
}
func CompleteMediaUploadBatchHandler(service *media.Service) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
identity, ok := deviceauth.FromContext(r.Context())
if !ok {
WriteError(w, r, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
return
}
item, err := service.CompleteUploadBatch(r.Context(), identity, r.PathValue("id"), RequestID(r.Context()))
if err != nil {
writeMediaError(w, r, err)
return
}
w.Header().Set("Cache-Control", "no-store")
WriteJSON(w, r, http.StatusAccepted, item)
})
}
func GetDeviceMediaUploadBatchHandler(service *media.Service) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
identity, ok := deviceauth.FromContext(r.Context())
if !ok {
WriteError(w, r, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
return
}
item, err := service.GetUploadBatch(r.Context(), identity, r.PathValue("id"), false)
if err != nil {
writeMediaError(w, r, err)
return
}
w.Header().Set("Cache-Control", "no-store")
WriteJSON(w, r, http.StatusOK, item)
})
}
func AcknowledgeLocalMediaDeleteHandler(service *media.Service) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
identity, ok := deviceauth.FromContext(r.Context())
if !ok {
WriteError(w, r, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
return
}
var input struct {
Status string `json:"status"`
ErrorCode string `json:"error_code"`
}
if !decodeWebJSON(w, r, &input, "本地录音删除回执参数不正确") {
return
}
item, err := service.AcknowledgeLocalDelete(r.Context(), identity, r.PathValue("id"), input.Status, input.ErrorCode, RequestID(r.Context()))
if err != nil {
writeMediaError(w, r, err)
return
}
w.Header().Set("Cache-Control", "no-store")
WriteJSON(w, r, http.StatusOK, item)
})
}
func writeMediaError(w http.ResponseWriter, r *http.Request, err error) {
switch {
case errors.Is(err, media.ErrRecordingAccessDenied):
WriteError(w, r, http.StatusForbidden, "RECORDING_ACCESS_DENIED", "当前账号没有该录音的播放或下载授权")
case errors.Is(err, media.ErrNotFound):
WriteError(w, r, http.StatusNotFound, "RESOURCE_NOT_FOUND", "录音上传批次不存在")
case errors.Is(err, media.ErrAccessDenied):
WriteError(w, r, http.StatusForbidden, "UPLOAD_NOT_ALLOWED", "当前授权、通话状态或设备归属不允许上传录音")
case errors.Is(err, media.ErrInvalidDeclaration):
writeValidation(w, r, "录音或授权参数不正确", FieldError{Field: "request", Message: "轨道、序号、编码、时长、大小、SHA-256、权限或用途不符合要求"})
case errors.Is(err, media.ErrExpired):
WriteError(w, r, http.StatusGone, "UPLOAD_BATCH_EXPIRED", "录音上传批次已经过期")
case errors.Is(err, media.ErrConflict):
WriteError(w, r, http.StatusConflict, "RESOURCE_STATE_CONFLICT", "录音分片清单与已有数据冲突")
case errors.Is(err, media.ErrLocalDeleteTooEarly):
WriteError(w, r, http.StatusConflict, "LOCAL_DELETE_NOT_DUE", "尚未到达设备本地录音的允许删除时间")
default:
WriteError(w, r, http.StatusInternalServerError, "INTERNAL_ERROR", "录音上传处理失败")
}
}