fix(media): 资源交付 - 按内容识别上传类型并统一重试与附件交付 (#678)
Co-authored-by: ddcat666 <26564566+ddcat666@users.noreply.github.com>
This commit is contained in:
@@ -4,6 +4,7 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.45.5
|
||||
github.com/gabriel-vasile/mimetype v1.4.13
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/qiniu/go-sdk/v7 v7.27.0
|
||||
@@ -32,7 +33,6 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/cloudwego/base64x v0.1.7 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -19,7 +18,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"gorm.io/gorm"
|
||||
"infinite-canvas/backend/internal/assets"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
@@ -43,9 +44,6 @@ func (s *Service) uploadResource(userID string, header *multipart.FileHeader, ki
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing != nil && existing.Status == model.ResourceStatusReady {
|
||||
return existing, nil
|
||||
}
|
||||
if existing != nil && existing.Status == model.ResourceStatusPending {
|
||||
return nil, resourceUploadInProgress()
|
||||
}
|
||||
@@ -55,8 +53,13 @@ func (s *Service) uploadResource(userID string, header *multipart.FileHeader, ki
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
mimeType := strings.TrimSpace(header.Header.Get("Content-Type"))
|
||||
mimeType = detectUploadedMimeType(file, header.Filename, mimeType)
|
||||
mimeType, err := detectUploadedMimeType(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !(forceLocal && kind == "live2d") {
|
||||
kind = uploadedResourceKind(mimeType)
|
||||
}
|
||||
if existing != nil {
|
||||
return s.retryStoredResource(userID, existing, kind, mimeType, header.Size, file)
|
||||
}
|
||||
@@ -87,13 +90,14 @@ func (s *Service) UploadResourceFile(userID string, fileName string, size int64,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing != nil && existing.Status == model.ResourceStatusReady {
|
||||
return existing, nil
|
||||
}
|
||||
if existing != nil && existing.Status == model.ResourceStatusPending {
|
||||
return nil, resourceUploadInProgress()
|
||||
}
|
||||
mimeType := detectUploadedMimeType(file, fileName, "")
|
||||
mimeType, err := detectUploadedMimeType(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kind = uploadedResourceKind(mimeType)
|
||||
if existing != nil {
|
||||
return s.retryStoredResource(userID, existing, kind, mimeType, size, file)
|
||||
}
|
||||
@@ -112,21 +116,23 @@ func (s *Service) UploadResourceFile(userID string, fileName string, size int64,
|
||||
return resource, err
|
||||
}
|
||||
|
||||
func detectUploadedMimeType(file io.ReadSeeker, fileName string, declared string) string {
|
||||
declared = strings.TrimSpace(strings.Split(declared, ";")[0])
|
||||
if declared != "" && declared != "application/octet-stream" {
|
||||
return declared
|
||||
func detectUploadedMimeType(file io.ReadSeeker) (string, error) {
|
||||
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||
return "", err
|
||||
}
|
||||
buffer := make([]byte, 512)
|
||||
read, _ := file.Read(buffer)
|
||||
_, _ = file.Seek(0, io.SeekStart)
|
||||
if detected := http.DetectContentType(buffer[:read]); detected != "" && detected != "application/octet-stream" {
|
||||
return strings.TrimSpace(strings.Split(detected, ";")[0])
|
||||
detected, err := mimetype.DetectReader(file)
|
||||
_, seekErr := file.Seek(0, io.SeekStart)
|
||||
if err != nil || seekErr != nil {
|
||||
return "", errors.Join(err, seekErr)
|
||||
}
|
||||
if fromExtension := mime.TypeByExtension(filepath.Ext(fileName)); fromExtension != "" {
|
||||
return strings.TrimSpace(strings.Split(fromExtension, ";")[0])
|
||||
return strings.TrimSpace(strings.Split(detected.String(), ";")[0]), nil
|
||||
}
|
||||
|
||||
func uploadedResourceKind(mimeType string) string {
|
||||
if !assets.InlineMediaType(mimeType) {
|
||||
return "file"
|
||||
}
|
||||
return "application/octet-stream"
|
||||
return normalizeResourceKind("", mimeType)
|
||||
}
|
||||
|
||||
func (s *Service) ImportResourceURL(userID string, rawURL string, kind string, width int, height int, durationMs int64, uploadIdentity ...string) (*model.Resource, error) {
|
||||
@@ -135,9 +141,6 @@ func (s *Service) ImportResourceURL(userID string, rawURL string, kind string, w
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing != nil && existing.Status == model.ResourceStatusReady {
|
||||
return existing, nil
|
||||
}
|
||||
if existing != nil && existing.Status == model.ResourceStatusPending {
|
||||
return nil, resourceUploadInProgress()
|
||||
}
|
||||
@@ -149,7 +152,11 @@ func (s *Service) ImportResourceURL(userID string, rawURL string, kind string, w
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kind = normalizeResourceKind(kind, payload.mimeType)
|
||||
payload.mimeType, err = detectUploadedMimeType(bytes.NewReader(payload.data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kind = uploadedResourceKind(payload.mimeType)
|
||||
if kind == "image" && (width <= 0 || height <= 0) {
|
||||
if decodedWidth, decodedHeight := imageDimensions(payload.data); decodedWidth > 0 && decodedHeight > 0 {
|
||||
width = decodedWidth
|
||||
@@ -251,19 +258,22 @@ func (s *Service) storeResource(userID string, kind string, fileName string, mim
|
||||
}
|
||||
|
||||
func (s *Service) storeResourceWithWriter(userID string, kind string, fileName string, mimeType string, size int64, width int, height int, durationMs int64, body io.Reader, uploadKey *string, forceLocal bool, writeObject func(*model.Resource, string, io.Reader) (string, error)) (*model.Resource, bool, error) {
|
||||
// Live2D is reserved for the administrator's validated local import path.
|
||||
if !(forceLocal && kind == "live2d") {
|
||||
kind = normalizeResourceKind(kind, mimeType)
|
||||
}
|
||||
if existing, err := s.resourceForUploadKey(userID, uploadKey); err != nil {
|
||||
return nil, false, err
|
||||
} else if existing != nil {
|
||||
if existing.Status == model.ResourceStatusReady {
|
||||
if err := validateResourceUploadIdentity(existing, kind, mimeType, size); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return existing, false, nil
|
||||
}
|
||||
return nil, false, resourceUploadInProgress()
|
||||
}
|
||||
now := time.Now()
|
||||
// Live2D is reserved for the administrator's validated local import path.
|
||||
if !(forceLocal && kind == "live2d") {
|
||||
kind = normalizeResourceKind(kind, mimeType)
|
||||
}
|
||||
var setting ossSettingValue
|
||||
var storageSettingID string
|
||||
var useOSS bool
|
||||
@@ -289,6 +299,9 @@ func (s *Service) storeResourceWithWriter(userID string, kind string, fileName s
|
||||
if err := s.repo.CreateResource(&resource); err != nil {
|
||||
if existing, lookupErr := s.resourceForUploadKey(userID, uploadKey); lookupErr == nil && existing != nil {
|
||||
if existing.Status == model.ResourceStatusReady {
|
||||
if err := validateResourceUploadIdentity(existing, kind, mimeType, size); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return existing, false, nil
|
||||
}
|
||||
return nil, false, resourceUploadInProgress()
|
||||
@@ -371,15 +384,15 @@ func (s *Service) retryStoredResource(userID string, resource *model.Resource, k
|
||||
if resource == nil {
|
||||
return nil, errors.New("资源不存在")
|
||||
}
|
||||
if resource.Status == model.ResourceStatusReady {
|
||||
return resource, nil
|
||||
}
|
||||
if resource.Status != model.ResourceStatusFailed {
|
||||
if resource.Status != model.ResourceStatusFailed && resource.Status != model.ResourceStatusReady {
|
||||
return nil, resourceUploadInProgress()
|
||||
}
|
||||
kind = normalizeResourceKind(kind, mimeType)
|
||||
if resource.Size != size || resource.Kind != kind || (resource.MimeType != "" && mimeType != "" && resource.MimeType != mimeType) {
|
||||
return nil, NewAppError(http.StatusConflict, "上传幂等标识已用于其他文件")
|
||||
if err := validateResourceUploadIdentity(resource, kind, mimeType, size); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resource.Status == model.ResourceStatusReady {
|
||||
return resource, nil
|
||||
}
|
||||
claimed, err := s.repo.ClaimFailedResourceUpload(userID, resource.ID)
|
||||
if err != nil {
|
||||
@@ -388,6 +401,9 @@ func (s *Service) retryStoredResource(userID string, resource *model.Resource, k
|
||||
if !claimed {
|
||||
latest, latestErr := s.repo.ResourceForUser(userID, resource.ID)
|
||||
if latestErr == nil && latest.Status == model.ResourceStatusReady {
|
||||
if err := validateResourceUploadIdentity(latest, kind, mimeType, size); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return latest, nil
|
||||
}
|
||||
return nil, resourceUploadInProgress()
|
||||
@@ -431,3 +447,10 @@ func (s *Service) retryStoredResource(userID string, resource *model.Resource, k
|
||||
s.recordActivity(userID, "resource", 1)
|
||||
return resource, nil
|
||||
}
|
||||
|
||||
func validateResourceUploadIdentity(resource *model.Resource, kind, mimeType string, size int64) error {
|
||||
if resource.Size != size || resource.Kind != kind || resource.MimeType != mimeType {
|
||||
return NewAppError(http.StatusConflict, "上传幂等标识已用于其他文件")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
161
backend/internal/app/resource_upload_content_test.go
Normal file
161
backend/internal/app/resource_upload_content_test.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
func contentUploadHeader(t *testing.T, data []byte) *multipart.FileHeader {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
w := multipart.NewWriter(&buf)
|
||||
h := make(textproto.MIMEHeader)
|
||||
h.Set("Content-Disposition", `form-data; name="file"; filename="picture.png"`)
|
||||
h.Set("Content-Type", "image/png")
|
||||
p, err := w.CreatePart(h)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.Write(data)
|
||||
w.Close()
|
||||
form, err := multipart.NewReader(&buf, w.Boundary()).ReadForm(1 << 20)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { form.RemoveAll() })
|
||||
return form.File["file"][0]
|
||||
}
|
||||
|
||||
func TestUploadContentImportSocket(t *testing.T) {
|
||||
t.Setenv("CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS", "127.0.0.1")
|
||||
png := mediaTestPNG(t)
|
||||
var payload atomic.Value
|
||||
payload.Store(png)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
_, _ = w.Write(payload.Load().([]byte))
|
||||
}))
|
||||
defer server.Close()
|
||||
for _, status := range []model.ResourceStatus{model.ResourceStatusReady, model.ResourceStatusFailed} {
|
||||
t.Run(string(status), func(t *testing.T) {
|
||||
s := newResourceTestService(t)
|
||||
payload.Store(png)
|
||||
r, err := s.ImportResourceURL("user", server.URL+"/picture.png", "image", 0, 0, 0, "identity")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Status = status
|
||||
if err := s.repo.SaveResource(r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload.Store([]byte("<html>" + strings.Repeat(" ", len(png)-6)))
|
||||
if _, err := s.ImportResourceURL("user", server.URL+"/picture.png", "image", 0, 0, 0, "identity"); err == nil {
|
||||
t.Fatal("changed import content accepted")
|
||||
}
|
||||
fresh, err := s.ImportResourceURL("user", server.URL+"/picture.png", "image", 0, 0, 0, "fresh-identity")
|
||||
if err != nil || fresh.MimeType != "text/html" || fresh.Kind != "file" {
|
||||
t.Fatalf("spoofed import: %+v %v", fresh, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadContentDetection(t *testing.T) {
|
||||
data := []byte("<html><script>alert(1)</script></html>")
|
||||
s := newResourceTestService(t)
|
||||
r, err := s.UploadResource("user", contentUploadHeader(t, data), "image", 0, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.MimeType != "text/html" || r.Kind != "file" {
|
||||
t.Fatalf("spoofed content stored as MIME=%s kind=%s", r.MimeType, r.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadContentRetryConsistency(t *testing.T) {
|
||||
for _, status := range []model.ResourceStatus{model.ResourceStatusReady, model.ResourceStatusFailed} {
|
||||
for _, chunked := range []bool{false, true} {
|
||||
name := string(status)
|
||||
if chunked {
|
||||
name += "/chunked"
|
||||
} else {
|
||||
name += "/multipart"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
s := newResourceTestService(t)
|
||||
png := mediaTestPNG(t)
|
||||
r, err := s.UploadResource("user", contentUploadHeader(t, png), "image", 0, 0, 0, "identity")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Status = status
|
||||
if err := s.repo.SaveResource(r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data := []byte("<html>" + strings.Repeat(" ", len(png)-6))
|
||||
if chunked {
|
||||
_, err = s.UploadResourceFile("user", "picture.png", int64(len(data)), "image", 0, 0, 0, bytes.NewReader(data), "identity")
|
||||
} else {
|
||||
_, err = s.UploadResource("user", contentUploadHeader(t, data), "image", 0, 0, 0, "identity")
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("changed upload content accepted under existing identity")
|
||||
}
|
||||
stored, err := s.repo.ResourceForUser("user", r.ID)
|
||||
if err != nil || stored.Status != status || stored.MimeType != "image/png" {
|
||||
t.Fatalf("rejected retry changed resource: %+v %v", stored, err)
|
||||
}
|
||||
var same *model.Resource
|
||||
if chunked {
|
||||
same, err = s.UploadResourceFile("user", "picture.png", int64(len(png)), "image", 0, 0, 0, bytes.NewReader(png), "identity")
|
||||
} else {
|
||||
same, err = s.UploadResource("user", contentUploadHeader(t, png), "image", 0, 0, 0, "identity")
|
||||
}
|
||||
if err != nil || same.ID != r.ID || same.Status != model.ResourceStatusReady {
|
||||
t.Fatalf("valid retry failed: %+v %v", same, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadContentAcceptsAttachments(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
data []byte
|
||||
mimeType string
|
||||
}{
|
||||
{"pdf", []byte("%PDF-1.7\nexample"), "application/pdf"},
|
||||
{"svg", []byte(`<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>`), "image/svg+xml"},
|
||||
{"unknown", []byte{0, 1, 0, 2, 0, 3}, "application/octet-stream"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := newResourceTestService(t)
|
||||
r, err := s.UploadResourceFile("user", "picture.png", int64(len(tc.data)), "image", 0, 0, 0, bytes.NewReader(tc.data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.MimeType != tc.mimeType || r.Kind != "file" {
|
||||
t.Fatalf("attachment detection: %+v", r)
|
||||
}
|
||||
_, body, err := s.OpenResource("user", r.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer body.Close()
|
||||
got, err := io.ReadAll(body)
|
||||
if err != nil || !bytes.Equal(got, tc.data) {
|
||||
t.Fatalf("stored bytes changed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -140,6 +140,10 @@ func ResolveAccess(resource *model.Resource, setting storage.Settings, options A
|
||||
}
|
||||
if resource.Provider == "local" {
|
||||
access.Delivery = DeliveryLocal
|
||||
} else if !InlineMediaType(resource.MimeType) {
|
||||
// A redirect cannot enforce our attachment and sandbox response headers.
|
||||
access.Delivery = DeliveryProxy
|
||||
access.FallbackReason = "attachment_required"
|
||||
} else if options.Purpose == PurposeDownload {
|
||||
// Cross-origin `a[download]` is only advisory. A real browser download must
|
||||
// therefore be enforced by the object store response itself. Downloads use
|
||||
|
||||
24
backend/internal/assets/content_delivery_test.go
Normal file
24
backend/internal/assets/content_delivery_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/storage"
|
||||
)
|
||||
|
||||
func TestNonMediaDeliveryUsesPlatform(t *testing.T) {
|
||||
for _, mimeType := range []string{"text/html", "image/svg+xml", "application/pdf", "application/octet-stream"} {
|
||||
for _, purpose := range []AccessPurpose{PurposeDisplay, PurposeCopy, PurposeProcess, PurposeDownload, PurposeProvider} {
|
||||
t.Run(mimeType+"/"+string(purpose), func(t *testing.T) {
|
||||
r := testReadyResource("aliyun")
|
||||
r.MimeType = mimeType
|
||||
var variants []ResourceVariant
|
||||
got, err := ResolveAccess(r, storage.Settings{Provider: "aliyun", CDNBaseURL: "https://cdn.example", Delivery: storage.DeliverySettings{CDNAuthMode: "public"}}, AccessOptions{Purpose: purpose}, time.Now(), testPlatformURL(&variants))
|
||||
if err != nil || got.Delivery != DeliveryProxy || len(variants) != 1 {
|
||||
t.Fatalf("non-media escaped controlled delivery: access=%+v err=%v", got, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
16
backend/internal/assets/content_type.go
Normal file
16
backend/internal/assets/content_type.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InlineMediaType excludes active XML images such as SVG. Other documents and
|
||||
// unknown formats remain uploadable, but must use controlled attachment delivery.
|
||||
func InlineMediaType(value string) bool {
|
||||
mediaType, _, err := mime.ParseMediaType(value)
|
||||
if err != nil || strings.HasSuffix(mediaType, "+xml") || mediaType == "image/svg" {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(mediaType, "image/") || strings.HasPrefix(mediaType, "audio/") || strings.HasPrefix(mediaType, "video/")
|
||||
}
|
||||
39
backend/internal/handler/resource_content_delivery_test.go
Normal file
39
backend/internal/handler/resource_content_delivery_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"infinite-canvas/backend/internal/assets"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
"infinite-canvas/backend/internal/service"
|
||||
)
|
||||
|
||||
func TestNonMediaDeliveryAttachment(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
for _, mimeType := range []string{"text/html", "image/svg+xml", "application/pdf", "application/octet-stream", "image/png"} {
|
||||
t.Run(mimeType, func(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.GET("/file", func(c *gin.Context) {
|
||||
r := &model.Resource{ID: "file", MimeType: mimeType}
|
||||
serveResourceDelivery(c, &service.ResourceDelivery{Resource: r, Access: &assets.ResourceAccess{Delivery: assets.DeliveryLocal}, Stream: &service.ResourceStream{Resource: r, Body: io.NopCloser(strings.NewReader("content")), StatusCode: 200, ContentLength: 7}}, "", "inline")
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/file", nil))
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("status=%d", w.Code)
|
||||
}
|
||||
if mimeType == "image/png" {
|
||||
if w.Header().Get("Content-Disposition") != "inline" {
|
||||
t.Fatal("media no longer inline")
|
||||
}
|
||||
} else if !strings.HasPrefix(w.Header().Get("Content-Disposition"), "attachment") || !strings.Contains(w.Header().Get("Content-Security-Policy"), "sandbox") {
|
||||
t.Fatalf("unsafe non-media headers: %v", w.Header())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -69,6 +70,12 @@ func serveResourceDelivery(c *gin.Context, delivery *service.ResourceDelivery, c
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user