From a583f460f2bcb4f45390dcd8bacfe7f37f465511 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 2 Mar 2026 12:49:03 +0100 Subject: [PATCH 1/6] Refactor storage content-type handling of SignedURL --- modules/packages/content_store.go | 2 +- modules/public/mime_types.go | 29 +++++++++++- modules/storage/azureblob.go | 48 +++++++++++++++---- modules/storage/azureblob_test.go | 17 +++++++ modules/storage/helper.go | 2 +- modules/storage/local.go | 2 +- modules/storage/minio.go | 35 ++------------ modules/storage/minio_test.go | 16 +++++++ modules/storage/storage.go | 36 +++++++++++++- modules/storage/storage_test.go | 52 +++++++++++++++++++++ routers/api/packages/container/container.go | 7 +-- services/packages/packages.go | 2 +- 12 files changed, 201 insertions(+), 47 deletions(-) diff --git a/modules/packages/content_store.go b/modules/packages/content_store.go index 57974515e2cab..21e91c8dfcea2 100644 --- a/modules/packages/content_store.go +++ b/modules/packages/content_store.go @@ -36,7 +36,7 @@ func (s *ContentStore) ShouldServeDirect() bool { return setting.Packages.Storage.ServeDirect() } -func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename, method string, reqParams url.Values) (*url.URL, error) { +func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename, method string, reqParams *storage.SignedURLParam) (*url.URL, error) { return s.store.URL(KeyToRelativePath(key), filename, method, reqParams) } diff --git a/modules/public/mime_types.go b/modules/public/mime_types.go index 32bdf3bfa2595..b291e2b89a9de 100644 --- a/modules/public/mime_types.go +++ b/modules/public/mime_types.go @@ -3,7 +3,10 @@ package public -import "strings" +import ( + "slices" + "strings" +) // wellKnownMimeTypesLower comes from Golang's builtin mime package: `builtinTypesLower`, see the comment of detectWellKnownMimeType var wellKnownMimeTypesLower = map[string]string{ @@ -28,6 +31,19 @@ var wellKnownMimeTypesLower = map[string]string{ ".txt": "text/plain; charset=utf-8", } +var wellKnownSafeMimeTypes = []string{ + "text/plain", + "text/plain; charset=utf-8", + "image/png", + "image/jpeg", + "image/gif", + "image/webp", + "image/avif", + // ATTENTION! Don't support unsafe types like HTML/SVG due to security concerns: they can contain JS code, and maybe they need proper Content-Security-Policy + // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context, it seems fine to render it inline + "application/pdf", +} + // detectWellKnownMimeType will return the mime-type for a well-known file ext name // The purpose of this function is to bypass the unstable behavior of Golang's mime.TypeByExtension // mime.TypeByExtension would use OS's mime-type config to overwrite the well-known types (see its document). @@ -38,3 +54,14 @@ func detectWellKnownMimeType(ext string) string { ext = strings.ToLower(ext) return wellKnownMimeTypesLower[ext] } + +func IsWellKnownSafeInlineMimeType(mimeType string) bool { + mimeType = strings.ToLower(mimeType) + return slices.Contains(wellKnownSafeMimeTypes, mimeType) +} + +func DetectWellKnownSafeInlineMimeType(ext string) (mimeType string, safe bool) { + mimeType = detectWellKnownMimeType(ext) + safe = IsWellKnownSafeInlineMimeType(mimeType) + return +} diff --git a/modules/storage/azureblob.go b/modules/storage/azureblob.go index e7297cec77a0f..25abf2f3738cf 100644 --- a/modules/storage/azureblob.go +++ b/modules/storage/azureblob.go @@ -246,16 +246,48 @@ func (a *AzureBlobStorage) Delete(path string) error { return convertAzureBlobErr(err) } +func (a *AzureBlobStorage) GetSasURL(b *blob.Client, template sas.BlobSignatureValues) (string, error) { + urlParts, err := blob.ParseURL(b.URL()) + if err != nil { + return "", err + } + + t, err := time.Parse(blob.SnapshotTimeFormat, urlParts.Snapshot) + if err != nil { + t = time.Time{} + } + + template.ContainerName = urlParts.ContainerName + template.BlobName = urlParts.BlobName + template.SnapshotTime = t + template.Version = sas.Version + + qps, err := template.SignWithSharedKey(a.credential) + if err != nil { + return "", err + } + + endpoint := b.URL() + "?" + qps.Encode() + + return endpoint, nil +} + // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes. -func (a *AzureBlobStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) { - blobClient := a.getBlobClient(path) +func (a *AzureBlobStorage) URL(storePath, name, _ string, reqParams *SignedURLParam) (*url.URL, error) { + blobClient := a.getBlobClient(storePath) + + startTime := time.Now().UTC() + + param := reqParams.WithDefaults(name) - // TODO: OBJECT-STORAGE-CONTENT-TYPE: "browser inline rendering images/PDF" needs proper Content-Type header from storage - startTime := time.Now() - u, err := blobClient.GetSASURL(sas.BlobPermissions{ - Read: true, - }, time.Now().Add(5*time.Minute), &blob.GetSASURLOptions{ - StartTime: &startTime, + u, err := a.GetSasURL(blobClient, sas.BlobSignatureValues{ + Permissions: (&sas.BlobPermissions{ + Read: true, + }).String(), + StartTime: startTime, + ExpiryTime: startTime.Add(5 * time.Minute), + ContentDisposition: param.ContentDisposition, + ContentType: param.ContentType, }) if err != nil { return nil, convertAzureBlobErr(err) diff --git a/modules/storage/azureblob_test.go b/modules/storage/azureblob_test.go index b3791b491641c..33bf7f48049ae 100644 --- a/modules/storage/azureblob_test.go +++ b/modules/storage/azureblob_test.go @@ -31,6 +31,23 @@ func TestAzureBlobStorageIterator(t *testing.T) { }) } +func TestAzureBlobStorageURLContentTypeAndDisposition(t *testing.T) { + if os.Getenv("CI") == "" { + t.Skip("azureBlobStorage not present outside of CI") + return + } + testBlobStorageURLContentTypeAndDisposition(t, setting.AzureBlobStorageType, &setting.Storage{ + AzureBlobConfig: setting.AzureBlobStorageConfig{ + // https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#ip-style-url + Endpoint: "http://devstoreaccount1.azurite.local:10000", + // https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#well-known-storage-account-and-key + AccountName: "devstoreaccount1", + AccountKey: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==", + Container: "test", + }, + }) +} + func TestAzureBlobStoragePath(t *testing.T) { m := &AzureBlobStorage{cfg: &setting.AzureBlobStorageConfig{BasePath: ""}} assert.Empty(t, m.buildAzureBlobPath("/")) diff --git a/modules/storage/helper.go b/modules/storage/helper.go index f6c3d5eebbc43..2244a3c082370 100644 --- a/modules/storage/helper.go +++ b/modules/storage/helper.go @@ -30,7 +30,7 @@ func (s discardStorage) Delete(_ string) error { return fmt.Errorf("%s", s) } -func (s discardStorage) URL(_, _, _ string, _ url.Values) (*url.URL, error) { +func (s discardStorage) URL(_, _, _ string, _ *SignedURLParam) (*url.URL, error) { return nil, fmt.Errorf("%s", s) } diff --git a/modules/storage/local.go b/modules/storage/local.go index 8a1776f606db1..4c0b3f39093bf 100644 --- a/modules/storage/local.go +++ b/modules/storage/local.go @@ -114,7 +114,7 @@ func (l *LocalStorage) Delete(path string) error { } // URL gets the redirect URL to a file -func (l *LocalStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) { +func (l *LocalStorage) URL(path, name, _ string, reqParams *SignedURLParam) (*url.URL, error) { return nil, ErrURLNotSupported } diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 6993ac2d922b1..bae50b6459a64 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -279,37 +279,12 @@ func (m *MinioStorage) Delete(path string) error { } // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes. -func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams url.Values) (*url.URL, error) { - // copy serveDirectReqParams - reqParams, err := url.ParseQuery(serveDirectReqParams.Encode()) - if err != nil { - return nil, err - } +func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams *SignedURLParam) (*url.URL, error) { + reqParams := url.Values{} - // Here we might not know the real filename, and it's quite inefficient to detect the mine type by pre-fetching the object head. - // So we just do a quick detection by extension name, at least if works for the "View Raw File" for an LFS file on the Web UI. - // Detect content type by extension name, only support the well-known safe types for inline rendering. - // TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future - ext := path.Ext(name) - inlineExtMimeTypes := map[string]string{ - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".webp": "image/webp", - ".avif": "image/avif", - // ATTENTION! Don't support unsafe types like HTML/SVG due to security concerns: they can contain JS code, and maybe they need proper Content-Security-Policy - // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context, it seems fine to render it inline - ".pdf": "application/pdf", - - // TODO: refactor with "modules/public/mime_types.go", for example: "DetectWellKnownSafeInlineMimeType" - } - if mimeType, ok := inlineExtMimeTypes[ext]; ok { - reqParams.Set("response-content-type", mimeType) - reqParams.Set("response-content-disposition", "inline") - } else { - reqParams.Set("response-content-disposition", fmt.Sprintf(`attachment; filename="%s"`, quoteEscaper.Replace(name))) - } + param := serveDirectReqParams.WithDefaults(name) + reqParams.Set("response-content-type", param.ContentType) + reqParams.Set("response-content-disposition", param.ContentDisposition) expires := 5 * time.Minute if method == http.MethodHead { diff --git a/modules/storage/minio_test.go b/modules/storage/minio_test.go index 2726d765ddf30..7175432e29ec7 100644 --- a/modules/storage/minio_test.go +++ b/modules/storage/minio_test.go @@ -32,6 +32,22 @@ func TestMinioStorageIterator(t *testing.T) { }) } +func TestMinioStorageURLContentTypeAndDisposition(t *testing.T) { + if os.Getenv("CI") == "" { + t.Skip("minioStorage not present outside of CI") + return + } + testBlobStorageURLContentTypeAndDisposition(t, setting.MinioStorageType, &setting.Storage{ + MinioConfig: setting.MinioStorageConfig{ + Endpoint: "localhost:9000", + AccessKeyID: "123456", + SecretAccessKey: "12345678", + Bucket: "gitea", + Location: "us-east-1", + }, + }) +} + func TestMinioStoragePath(t *testing.T) { m := &MinioStorage{basePath: ""} assert.Empty(t, m.buildMinioPath("/")) diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 1868817c057cf..225306716b86f 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -10,9 +10,12 @@ import ( "io" "net/url" "os" + "path" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) // ErrURLNotSupported represents url is not supported @@ -56,6 +59,37 @@ type Object interface { Stat() (os.FileInfo, error) } +type SignedURLParam struct { + ContentType string + ContentDisposition string +} + +func (s *SignedURLParam) WithDefaults(name string) *SignedURLParam { + // Here we might not know the real filename, and it's quite inefficient to detect the mine type by pre-fetching the object head. + // So we just do a quick detection by extension name, at least it works for the "View Raw File" for an LFS file on the Web UI. + // Detect content type by extension name, only support the well-known safe types for inline rendering. + // TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future + + ext := path.Ext(name) + param := util.Iif(s == nil, &SignedURLParam{}, s) + + isSafe := false + if param.ContentType != "" { + isSafe = public.IsWellKnownSafeInlineMimeType(s.ContentType) + } else if mimeType, safe := public.DetectWellKnownSafeInlineMimeType(ext); safe { + param.ContentType = mimeType + isSafe = true + } + if param.ContentDisposition == "" { + if isSafe { + param.ContentDisposition = "inline" + } else { + param.ContentDisposition = fmt.Sprintf(`attachment; filename="%s"`, quoteEscaper.Replace(name)) + } + } + return param +} + // ObjectStorage represents an object storage to handle a bucket and files type ObjectStorage interface { Open(path string) (Object, error) @@ -67,7 +101,7 @@ type ObjectStorage interface { Stat(path string) (os.FileInfo, error) Delete(path string) error - URL(path, name, method string, reqParams url.Values) (*url.URL, error) + URL(path, name, method string, reqParams *SignedURLParam) (*url.URL, error) IterateObjects(path string, iterator func(path string, obj Object) error) error } diff --git a/modules/storage/storage_test.go b/modules/storage/storage_test.go index 08f274e74b42d..17f496a80c782 100644 --- a/modules/storage/storage_test.go +++ b/modules/storage/storage_test.go @@ -4,12 +4,14 @@ package storage import ( + "net/http" "strings" "testing" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func testStorageIterator(t *testing.T, typStr Type, cfg *setting.Storage) { @@ -50,3 +52,53 @@ func testStorageIterator(t *testing.T, typStr Type, cfg *setting.Storage) { assert.Len(t, expected, count) } } + +func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectStorage, path string, name string, expected SignedURLParam, reqParams *SignedURLParam) { + u, err := s.URL(path, name, http.MethodGet, reqParams) + require.NoError(t, err) + resp, err := http.Get(u.String()) + require.NoError(t, err) + assert.Equal(t, expected.ContentType, resp.Header.Get("Content-Type")) + if expected.ContentDisposition != "" { + assert.Equal(t, expected.ContentDisposition, resp.Header.Get("Content-Disposition")) + } +} + +func testBlobStorageURLContentTypeAndDisposition(t *testing.T, typStr Type, cfg *setting.Storage) { + s, err := NewStorage(typStr, cfg) + assert.NoError(t, err) + + data := "Q2xTckt6Y1hDOWh0" + _, err = s.Save("test.txt", strings.NewReader(data), int64(len(data))) + assert.NoError(t, err) + + testSingleBlobStorageURLContentTypeAndDisposition(t, s, "test.txt", "test.txt", SignedURLParam{ + ContentType: "text/plain; charset=utf-8", + ContentDisposition: "inline", + }, nil) + + testSingleBlobStorageURLContentTypeAndDisposition(t, s, "test.txt", "test.pdf", SignedURLParam{ + ContentType: "application/pdf", + ContentDisposition: "inline", + }, nil) + + testSingleBlobStorageURLContentTypeAndDisposition(t, s, "test.txt", "test.wasm", SignedURLParam{ + ContentType: "application/octet-stream", + ContentDisposition: `attachment; filename="test.wasm"`, + }, nil) + + testSingleBlobStorageURLContentTypeAndDisposition(t, s, "test.txt", "test.wasm", SignedURLParam{ + ContentType: "application/octet-stream", + ContentDisposition: `attachment; filename="test.wasm"`, + }, &SignedURLParam{}) + + testSingleBlobStorageURLContentTypeAndDisposition(t, s, "test.txt", "test.txt", SignedURLParam{ + ContentType: "application/octet-stream", + ContentDisposition: `inline; filename="test.xml"`, + }, &SignedURLParam{ + ContentType: "application/octet-stream", + ContentDisposition: `inline; filename="test.xml"`, + }) + + assert.NoError(t, s.Delete("test.txt")) +} diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index 7cf1c363754ef..8f4bc1235629d 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -26,6 +26,7 @@ import ( packages_module "code.gitea.io/gitea/modules/packages" container_module "code.gitea.io/gitea/modules/packages/container" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" auth_service "code.gitea.io/gitea/services/auth" @@ -704,9 +705,9 @@ func DeleteManifest(ctx *context.Context) { } func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) { - serveDirectReqParams := make(url.Values) - serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType)) - s, u, _, err := packages_service.OpenBlobForDownload(ctx, pfd.File, pfd.Blob, ctx.Req.Method, serveDirectReqParams) + s, u, _, err := packages_service.OpenBlobForDownload(ctx, pfd.File, pfd.Blob, ctx.Req.Method, &storage.SignedURLParam{ + ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType), + }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return diff --git a/services/packages/packages.go b/services/packages/packages.go index 22b26b65637ab..de1a4b4999eb9 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -599,7 +599,7 @@ func OpenBlobStream(pb *packages_model.PackageBlob) (io.ReadSeekCloser, error) { // OpenBlobForDownload returns the content of the specific package blob and increases the download counter. // If the storage supports direct serving and it's enabled, only the direct serving url is returned. -func OpenBlobForDownload(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob, method string, serveDirectReqParams url.Values) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { +func OpenBlobForDownload(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob, method string, serveDirectReqParams *storage.SignedURLParam) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { key := packages_module.BlobHash256Key(pb.HashSHA256) cs := packages_module.NewContentStore() From 6e87c19225e54e4b7ddf206e151f84fbc982c9ba Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 2 Mar 2026 12:55:54 +0100 Subject: [PATCH 2/6] . --- modules/public/mime_types.go | 2 +- modules/storage/storage_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/public/mime_types.go b/modules/public/mime_types.go index b291e2b89a9de..38b049967906f 100644 --- a/modules/public/mime_types.go +++ b/modules/public/mime_types.go @@ -63,5 +63,5 @@ func IsWellKnownSafeInlineMimeType(mimeType string) bool { func DetectWellKnownSafeInlineMimeType(ext string) (mimeType string, safe bool) { mimeType = detectWellKnownMimeType(ext) safe = IsWellKnownSafeInlineMimeType(mimeType) - return + return mimeType, safe } diff --git a/modules/storage/storage_test.go b/modules/storage/storage_test.go index 17f496a80c782..69c01c81d4aaa 100644 --- a/modules/storage/storage_test.go +++ b/modules/storage/storage_test.go @@ -53,7 +53,7 @@ func testStorageIterator(t *testing.T, typStr Type, cfg *setting.Storage) { } } -func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectStorage, path string, name string, expected SignedURLParam, reqParams *SignedURLParam) { +func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectStorage, path, name string, expected SignedURLParam, reqParams *SignedURLParam) { u, err := s.URL(path, name, http.MethodGet, reqParams) require.NoError(t, err) resp, err := http.Get(u.String()) From 74a0020ffbe98190503b7bccf1fc012f03975e9b Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 2 Mar 2026 13:05:36 +0100 Subject: [PATCH 3/6] close body --- modules/storage/storage_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/storage/storage_test.go b/modules/storage/storage_test.go index 69c01c81d4aaa..b7e1c42691327 100644 --- a/modules/storage/storage_test.go +++ b/modules/storage/storage_test.go @@ -58,6 +58,7 @@ func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectSto require.NoError(t, err) resp, err := http.Get(u.String()) require.NoError(t, err) + defer resp.Body.Close() assert.Equal(t, expected.ContentType, resp.Header.Get("Content-Type")) if expected.ContentDisposition != "" { assert.Equal(t, expected.ContentDisposition, resp.Header.Get("Content-Disposition")) From f32f9fbf7a58d61668df77af72688d9def5d6499 Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Mon, 2 Mar 2026 13:32:31 +0100 Subject: [PATCH 4/6] Avoid setting empty response parameters in Minio URL Add checks to avoid setting empty response parameters for Minio. --- modules/storage/minio.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/storage/minio.go b/modules/storage/minio.go index bae50b6459a64..4d0873cb1ad5e 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -283,8 +283,13 @@ func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams reqParams := url.Values{} param := serveDirectReqParams.WithDefaults(name) - reqParams.Set("response-content-type", param.ContentType) - reqParams.Set("response-content-disposition", param.ContentDisposition) + // minio does not ignore empty params + if param.ContentType != "" { + reqParams.Set("response-content-type", param.ContentType) + } + if param.ContentDisposition != "" { + reqParams.Set("response-content-disposition", param.ContentDisposition) + } expires := 5 * time.Minute if method == http.MethodHead { From 7481389309f9998774da8169589c9b3ed3a591d7 Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Mon, 2 Mar 2026 16:10:53 +0100 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: ChristopherHX --- modules/storage/storage.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 225306716b86f..fd27d188ee463 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -65,7 +65,7 @@ type SignedURLParam struct { } func (s *SignedURLParam) WithDefaults(name string) *SignedURLParam { - // Here we might not know the real filename, and it's quite inefficient to detect the mine type by pre-fetching the object head. + // Here we might not know the real filename, and it's quite inefficient to detect the MIME type by pre-fetching the object head. // So we just do a quick detection by extension name, at least it works for the "View Raw File" for an LFS file on the Web UI. // Detect content type by extension name, only support the well-known safe types for inline rendering. // TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future @@ -75,7 +75,7 @@ func (s *SignedURLParam) WithDefaults(name string) *SignedURLParam { isSafe := false if param.ContentType != "" { - isSafe = public.IsWellKnownSafeInlineMimeType(s.ContentType) + isSafe = public.IsWellKnownSafeInlineMimeType(param.ContentType) } else if mimeType, safe := public.DetectWellKnownSafeInlineMimeType(ext); safe { param.ContentType = mimeType isSafe = true From a9efd952caec62238878f9f0643d5eb409f42402 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 2 Mar 2026 16:14:21 +0100 Subject: [PATCH 6/6] apply suggestions --- modules/storage/azureblob.go | 6 +++--- modules/storage/minio.go | 2 +- modules/storage/storage.go | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/storage/azureblob.go b/modules/storage/azureblob.go index 25abf2f3738cf..c3b389432440b 100644 --- a/modules/storage/azureblob.go +++ b/modules/storage/azureblob.go @@ -246,7 +246,7 @@ func (a *AzureBlobStorage) Delete(path string) error { return convertAzureBlobErr(err) } -func (a *AzureBlobStorage) GetSasURL(b *blob.Client, template sas.BlobSignatureValues) (string, error) { +func (a *AzureBlobStorage) getSasURL(b *blob.Client, template sas.BlobSignatureValues) (string, error) { urlParts, err := blob.ParseURL(b.URL()) if err != nil { return "", err @@ -278,9 +278,9 @@ func (a *AzureBlobStorage) URL(storePath, name, _ string, reqParams *SignedURLPa startTime := time.Now().UTC() - param := reqParams.WithDefaults(name) + param := reqParams.withDefaults(name) - u, err := a.GetSasURL(blobClient, sas.BlobSignatureValues{ + u, err := a.getSasURL(blobClient, sas.BlobSignatureValues{ Permissions: (&sas.BlobPermissions{ Read: true, }).String(), diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 4d0873cb1ad5e..408ee1f2ced79 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -282,7 +282,7 @@ func (m *MinioStorage) Delete(path string) error { func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams *SignedURLParam) (*url.URL, error) { reqParams := url.Values{} - param := serveDirectReqParams.WithDefaults(name) + param := serveDirectReqParams.withDefaults(name) // minio does not ignore empty params if param.ContentType != "" { reqParams.Set("response-content-type", param.ContentType) diff --git a/modules/storage/storage.go b/modules/storage/storage.go index fd27d188ee463..a2838ffaa95a2 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -64,14 +64,14 @@ type SignedURLParam struct { ContentDisposition string } -func (s *SignedURLParam) WithDefaults(name string) *SignedURLParam { +func (s *SignedURLParam) withDefaults(name string) *SignedURLParam { // Here we might not know the real filename, and it's quite inefficient to detect the MIME type by pre-fetching the object head. // So we just do a quick detection by extension name, at least it works for the "View Raw File" for an LFS file on the Web UI. // Detect content type by extension name, only support the well-known safe types for inline rendering. // TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future ext := path.Ext(name) - param := util.Iif(s == nil, &SignedURLParam{}, s) + param := *util.Iif(s == nil, &SignedURLParam{}, s) isSafe := false if param.ContentType != "" { @@ -87,7 +87,7 @@ func (s *SignedURLParam) WithDefaults(name string) *SignedURLParam { param.ContentDisposition = fmt.Sprintf(`attachment; filename="%s"`, quoteEscaper.Replace(name)) } } - return param + return ¶m } // ObjectStorage represents an object storage to handle a bucket and files