Files
ttstd-infinite-canvas/backend/internal/app/resource_access.go

160 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"errors"
"net/url"
"strconv"
"time"
"gorm.io/gorm"
"yingce/backend/internal/assets"
"yingce/backend/internal/model"
)
type ResourceAccessOptions = assets.AccessOptions
type ResourceAccessRequest = assets.AccessRequest
type ResourceAccess = assets.ResourceAccess
type ResourceAccessFailure struct {
Code int `json:"code"`
Reason string `json:"reason"`
Message string `json:"msg"`
}
type ResourceAccessResult struct {
ResourceID string `json:"resourceId"`
Access *ResourceAccess `json:"access,omitempty"`
Error *ResourceAccessFailure `json:"error,omitempty"`
}
func (s *Service) ResourceAccessBatch(userID string, requests []ResourceAccessRequest) ([]ResourceAccessResult, error) {
if userID == "" {
return nil, Unauthorized("请先登录")
}
if len(requests) == 0 || len(requests) > 100 {
return nil, BadAuthRequest("每批资源访问请求须为 1–100 项")
}
results := make([]ResourceAccessResult, 0, len(requests))
for _, request := range requests {
item := ResourceAccessResult{ResourceID: request.ResourceID}
options, err := assets.NormalizeAccessOptions(request.AccessOptions, false)
if err == nil {
var resource *model.Resource
resource, err = s.repo.ResourceForUser(userID, request.ResourceID)
if errors.Is(err, gorm.ErrRecordNotFound) {
err = NotFound("资源不存在或不可访问")
}
if err == nil {
item.Access, err = s.resolveResourceAccess(resource, options)
}
}
if err != nil {
item.Error = &ResourceAccessFailure{Code: 500, Reason: "resource_access_failed", Message: "资源访问失败"}
var appErr *AppError
if errors.As(err, &appErr) {
item.Error = &ResourceAccessFailure{Code: appErr.Code, Reason: string(appErr.Reason), Message: appErr.Message}
}
}
results = append(results, item)
}
return results, nil
}
func (s *Service) resolveResourceAccess(resource *model.Resource, options ResourceAccessOptions) (*ResourceAccess, error) {
if resource == nil {
return nil, NotFound("资源不存在")
}
var setting ossSettingValue
if resource.Provider != "local" {
var err error
setting, err = s.ossSettingForResource(resource.UserID, resource)
if err != nil {
return nil, WrapAppError(503, "无法解析资源实际存储位置", err)
}
setting = s.storageSettingWithRuntimePolicy(setting)
} else {
// 本地资源无需存储配置,只注入运行时策略,让平台签名地址同样遵循后台配置的有效期。
setting.Runtime = s.storageSettingWithRuntimePolicy(ossSettingValue{}).Runtime
}
return assets.ResolveAccess(resource, setting, options, time.Now().UTC(), func(variant assets.ResourceVariant, expires time.Time) (string, error) {
return s.signedResourceAccessURL(resource, variant, expires, options.Purpose == assets.PurposeProvider)
})
}
func (s *Service) PrepareResourceDelivery(userID, id string, options ResourceAccessOptions, rangeHeader string) (*ResourceDelivery, error) {
options, err := assets.NormalizeAccessOptions(options, false)
if err != nil {
return nil, err
}
resource, err := s.repo.ResourceForUser(userID, id)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, NotFound("资源不存在或不可访问")
}
if err != nil {
return nil, err
}
return s.prepareResourceDelivery(userID, resource, options, rangeHeader)
}
func (s *Service) prepareResourceDelivery(userID string, resource *model.Resource, options ResourceAccessOptions, rangeHeader string) (*ResourceDelivery, error) {
if resource == nil || resource.UserID != userID {
return nil, Forbidden("资源不可访问")
}
access, err := s.resolveResourceAccess(resource, options)
if err != nil {
return nil, err
}
delivery := &ResourceDelivery{Resource: resource, Access: access}
if options.DescribeOnly || access.Delivery == assets.DeliveryCDN || access.Delivery == assets.DeliveryOrigin {
return delivery, nil
}
if access.ActualVariant == assets.VariantPlayback {
delivery.Stream, err = s.OpenResourcePlaybackRange(userID, resource.ID)
} else {
delivery.Stream, err = s.openResourceRange(userID, resource, rangeHeader)
}
if err != nil {
return nil, err
}
delivery.Resource = delivery.Stream.Resource
return delivery, nil
}
func (s *Service) signedResourceAccessURL(resource *model.Resource, variant assets.ResourceVariant, expires time.Time, public bool) (string, error) {
expiry := strconv.FormatInt(expires.Unix(), 10)
signature, err := s.signPublicResource(resource.ID+"\n"+string(variant), expiry)
if err != nil {
return "", err
}
u := &url.URL{Path: "/api/public/resources/" + url.PathEscape(resource.ID) + "/file"}
q := u.Query()
q.Set("expires", expiry)
q.Set("signature", signature)
q.Set("variant", string(variant))
u.RawQuery = q.Encode()
if !public {
return u.String(), nil
}
base, err := s.publicResourceBaseURL()
if err != nil {
return "", err
}
return base.ResolveReference(u).String(), nil
}
func (s *Service) PreparePublicResourceDelivery(id, expires, signature string, options ResourceAccessOptions, rangeHeader string) (*ResourceDelivery, error) {
options, err := assets.NormalizeAccessOptions(options, false)
if err != nil {
return nil, err
}
if err := s.verifyPublicResourceSignature(id+"\n"+string(options.Variant), expires, signature); err != nil {
return nil, err
}
resource, err := s.repo.Resource(id)
if err != nil {
return nil, Forbidden("匿名下载链接无效")
}
seconds, _ := strconv.ParseInt(expires, 10, 64)
options.ExpiresAt = time.Unix(seconds, 0)
return s.prepareResourceDelivery(resource.UserID, resource, options, rangeHeader)
}