平台展示与厂商读取的签名地址有效期改为读取「资源与请求策略」,默认均为 4 小时,不再硬编码 5 分钟。服务器访问地址只校验 http/https 格式,允许本机、局域网或 HTTP 地址,模型读取平台资源不再强制 HTTPS 公网地址。
160 lines
5.5 KiB
Go
160 lines
5.5 KiB
Go
package app
|
||
|
||
import (
|
||
"errors"
|
||
"net/url"
|
||
"strconv"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
"infinite-canvas/backend/internal/assets"
|
||
"infinite-canvas/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)
|
||
}
|