96 lines
3.1 KiB
Go
96 lines
3.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"yingce/backend/internal/assets"
|
|
"yingce/backend/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func resourceAccessOptions(c *gin.Context) service.ResourceAccessOptions {
|
|
purpose := assets.AccessPurpose(strings.TrimSpace(c.Query("purpose")))
|
|
if purpose == "" && c.Query("download") == "1" {
|
|
purpose = assets.PurposeDownload
|
|
}
|
|
variant := assets.ResourceVariant(strings.TrimSpace(c.Query("variant")))
|
|
return service.ResourceAccessOptions{
|
|
Purpose: purpose,
|
|
Variant: variant,
|
|
DescribeOnly: c.Query("access") == "1",
|
|
}
|
|
}
|
|
|
|
// serveResourceDelivery is the only HTTP byte/redirect executor. Authorization and
|
|
// delivery selection happen in app; this function only applies the already-resolved
|
|
// contract to an HTTP response.
|
|
func serveResourceDelivery(c *gin.Context, delivery *service.ResourceDelivery, cacheControl string, disposition string) {
|
|
if delivery == nil || delivery.Access == nil {
|
|
fail(c, http.StatusServiceUnavailable, fmt.Errorf("资源分发结果无效"))
|
|
return
|
|
}
|
|
access := delivery.Access
|
|
if access.URL == "" && delivery.Stream == nil {
|
|
fail(c, http.StatusServiceUnavailable, fmt.Errorf("资源分发结果无效"))
|
|
return
|
|
}
|
|
if c.Query("access") == "1" {
|
|
c.Header("Cache-Control", "private, no-store")
|
|
c.Header("Referrer-Policy", "no-referrer")
|
|
ok(c, gin.H{"access": access})
|
|
return
|
|
}
|
|
c.Header("Referrer-Policy", "no-referrer")
|
|
c.Header("X-Content-Type-Options", "nosniff")
|
|
c.Header("Accept-Ranges", "bytes")
|
|
if access.Delivery == assets.DeliveryCDN || access.Delivery == assets.DeliveryOrigin {
|
|
c.Header("Cache-Control", "private, no-store")
|
|
// Do not let Gin generate its default HTML redirect body. A media request
|
|
// should transfer only the redirect metadata; the actual bytes must be
|
|
// fetched from the CDN/origin selected by the access contract.
|
|
c.Header("Location", access.URL)
|
|
c.Status(http.StatusTemporaryRedirect)
|
|
return
|
|
}
|
|
stream := delivery.Stream
|
|
if stream == nil || stream.Body == nil {
|
|
fail(c, http.StatusServiceUnavailable, fmt.Errorf("资源流不可用"))
|
|
return
|
|
}
|
|
defer stream.Body.Close()
|
|
if cacheControl != "" {
|
|
c.Header("Cache-Control", cacheControl)
|
|
}
|
|
if disposition != "" {
|
|
c.Header("Content-Disposition", disposition)
|
|
}
|
|
resource := stream.Resource
|
|
if !assets.InlineMediaType(resource.MimeType) {
|
|
if dispositionType, _, _ := mime.ParseMediaType(disposition); dispositionType != "attachment" {
|
|
c.Header("Content-Disposition", "attachment")
|
|
}
|
|
c.Header("Content-Security-Policy", "sandbox; default-src 'none'")
|
|
}
|
|
mimeType := resource.MimeType
|
|
if mimeType == "" {
|
|
mimeType = "application/octet-stream"
|
|
}
|
|
if stream.ContentRange != "" {
|
|
c.Header("Content-Range", stream.ContentRange)
|
|
}
|
|
if stream.AcceptRanges != "" {
|
|
c.Header("Accept-Ranges", stream.AcceptRanges)
|
|
}
|
|
if seeker, ok := stream.Body.(io.ReadSeeker); ok {
|
|
c.Header("Content-Type", mimeType)
|
|
http.ServeContent(c.Writer, c.Request, resource.ID, resource.UpdatedAt, seeker)
|
|
return
|
|
}
|
|
c.DataFromReader(stream.StatusCode, stream.ContentLength, mimeType, stream.Body, nil)
|
|
}
|