From 7450424b126e44384b2edbb655ffe6eb3c023c92 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Thu, 6 Nov 2025 19:24:33 +0800 Subject: [PATCH 1/5] Allow to display embed images/pdfs when SERVE_DIRECT was enabled on MinIO storage --- modules/storage/minio.go | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 01f2c16267971..aff0bf458b392 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -12,6 +12,7 @@ import ( "net/url" "os" "path" + "path/filepath" "strings" "time" @@ -285,8 +286,24 @@ func (m *MinioStorage) URL(path, name, method string, serveDirectReqParams url.V if err != nil { return nil, err } - // TODO it may be good to embed images with 'inline' like ServeData does, but we don't want to have to read the file, do we? - reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"") + // Detect content type by extension name + contentDisposition := "attachment; filename=\"" + quoteEscaper.Replace(name) + "\"" + inlineExtMime := map[string]string{ + "png": "image/png", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "pdf": "application/pdf", + "gif": "image/gif", + "webp": "iamge/webp", + } + ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(name), ".")) + mime, ok := inlineExtMime[ext] + if ok { + reqParams.Set("response-content-type", mime) + contentDisposition = "inline" + } + + reqParams.Set("response-content-disposition", contentDisposition) expires := 5 * time.Minute if method == http.MethodHead { u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams) From e7f111ba35ebd1906f2e32cfebfabfb66bfcd148 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sat, 8 Nov 2025 09:56:19 +0800 Subject: [PATCH 2/5] Use mime.TypeByExtension --- modules/storage/minio.go | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/modules/storage/minio.go b/modules/storage/minio.go index aff0bf458b392..9ce3ec637276d 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -8,6 +8,7 @@ import ( "crypto/tls" "fmt" "io" + "mime" "net/http" "net/url" "os" @@ -288,18 +289,10 @@ func (m *MinioStorage) URL(path, name, method string, serveDirectReqParams url.V } // Detect content type by extension name contentDisposition := "attachment; filename=\"" + quoteEscaper.Replace(name) + "\"" - inlineExtMime := map[string]string{ - "png": "image/png", - "jpg": "image/jpeg", - "jpeg": "image/jpeg", - "pdf": "application/pdf", - "gif": "image/gif", - "webp": "iamge/webp", - } - ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(name), ".")) - mime, ok := inlineExtMime[ext] - if ok { - reqParams.Set("response-content-type", mime) + ext := filepath.Ext(name) + mimetype := mime.TypeByExtension(ext) + reqParams.Set("response-content-type", mimetype) + if mimetype == "application/pdf" || strings.HasPrefix(mimetype, "image/") { contentDisposition = "inline" } From 64f89290f0fc5b89e14331dca8c9a4ce005285f2 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 8 Nov 2025 22:01:14 +0800 Subject: [PATCH 3/5] fix --- modules/storage/minio.go | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 9ce3ec637276d..be2604f1c6cf0 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -8,12 +8,10 @@ import ( "crypto/tls" "fmt" "io" - "mime" "net/http" "net/url" "os" "path" - "path/filepath" "strings" "time" @@ -281,28 +279,41 @@ 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(path, name, method string, serveDirectReqParams url.Values) (*url.URL, error) { +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 } + + // 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 a LFS file on the Web UI. // Detect content type by extension name - contentDisposition := "attachment; filename=\"" + quoteEscaper.Replace(name) + "\"" - ext := filepath.Ext(name) - mimetype := mime.TypeByExtension(ext) - reqParams.Set("response-content-type", mimetype) - if mimetype == "application/pdf" || strings.HasPrefix(mimetype, "image/") { - contentDisposition = "inline" + ext := path.Ext(name) + inlineExtMimeTypes := map[string]string{ + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "iamge/webp", + ".avif": "image/avif", + // don't support SVG because of security concerns: it can contain JS code, and maybe it needs 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", + } + 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))) } - reqParams.Set("response-content-disposition", contentDisposition) expires := 5 * time.Minute if method == http.MethodHead { - u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams) + u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams) return u, convertMinioErr(err) } - u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams) + u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams) return u, convertMinioErr(err) } From 93d963beb1124c507f3531ac84dd4071b20c6bdc Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 8 Nov 2025 22:11:47 +0800 Subject: [PATCH 4/5] fix --- modules/storage/minio.go | 6 +++--- routers/web/repo/view.go | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/storage/minio.go b/modules/storage/minio.go index be2604f1c6cf0..13a12f3e6aece 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -287,15 +287,15 @@ func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams } // 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 a LFS file on the Web UI. - // Detect content type by extension name + // 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. ext := path.Ext(name) inlineExtMimeTypes := map[string]string{ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", - ".webp": "iamge/webp", + ".webp": "image/webp", ".avif": "image/avif", // don't support SVG because of security concerns: it can contain JS code, and maybe it needs proper Content-Security-Policy // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context, it seems fine to render it inline diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 79357bfd76498..09ac33cff43f0 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -95,6 +95,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []b meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid) if err != nil { // fallback to a plain file + fi.lfsMeta = &pointer log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err) return buf, dataRc, fi, nil } From eaa98075de1a70832c4a4567b3a9339fe3da135a Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 9 Nov 2025 15:03:30 +0800 Subject: [PATCH 5/5] add comments --- modules/storage/azureblob.go | 1 + modules/storage/minio.go | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/storage/azureblob.go b/modules/storage/azureblob.go index 6860d81131b65..e7297cec77a0f 100644 --- a/modules/storage/azureblob.go +++ b/modules/storage/azureblob.go @@ -250,6 +250,7 @@ func (a *AzureBlobStorage) Delete(path string) error { func (a *AzureBlobStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) { blobClient := a.getBlobClient(path) + // 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, diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 13a12f3e6aece..6993ac2d922b1 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -289,6 +289,7 @@ func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams // 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", @@ -297,9 +298,11 @@ func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams ".gif": "image/gif", ".webp": "image/webp", ".avif": "image/avif", - // don't support SVG because of security concerns: it can contain JS code, and maybe it needs proper Content-Security-Policy + // 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)