217 lines
7.6 KiB
Go
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", "录音上传处理失败")
|
||
|
|
}
|
||
|
|
}
|