Refactor storage content-type handling of ServeDirectURL (#36804)

* replace raw url.Values by *storage.ServeDirectOptions
* implement content-type in azblob
* implement content-disposition in azblob
* add tests for content types in response
* http.MethodPut for azure now allows implementing servedirect uploads

---------

Signed-off-by: ChristopherHX <christopher.homberger@web.de>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
ChristopherHX
2026-03-22 05:26:13 +01:00
committed by GitHub
parent c8545033cc
commit 0ab612f5ab
24 changed files with 234 additions and 81 deletions

View File

@@ -10,8 +10,10 @@ import (
"io"
"net/url"
"os"
"path"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
)
@@ -56,6 +58,38 @@ type Object interface {
Stat() (os.FileInfo, error)
}
// ServeDirectOptions customizes HTTP headers for a generated signed URL.
type ServeDirectOptions struct {
// Overrides the automatically detected MIME type.
ContentType string
// Overrides the default Content-Disposition header, which is `inline; filename="name"`.
ContentDisposition string
}
// Safe defaults are applied only when not explicitly overridden by the caller.
func prepareServeDirectOptions(optsOptional *ServeDirectOptions, name string) (ret ServeDirectOptions) {
// 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.
// TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future
if optsOptional != nil {
ret = *optsOptional
}
// TODO: UNIFY-CONTENT-DISPOSITION-FROM-STORAGE
if ret.ContentType == "" {
ext := path.Ext(name)
ret.ContentType = public.DetectWellKnownMimeType(ext)
}
if ret.ContentDisposition == "" {
// When using ServeDirect, the URL is from the object storage's web server,
// it is not the same origin as Gitea server, so it should be safe enough to use "inline" to render the content directly.
// If a browser doesn't support the content type to be displayed inline, browser will download with the filename.
ret.ContentDisposition = fmt.Sprintf(`inline; filename="%s"`, quoteEscaper.Replace(name))
}
return ret
}
// ObjectStorage represents an object storage to handle a bucket and files
type ObjectStorage interface {
Open(path string) (Object, error)
@@ -67,7 +101,15 @@ type ObjectStorage interface {
Stat(path string) (os.FileInfo, error)
Delete(path string) error
URL(path, name, method string, reqParams url.Values) (*url.URL, error)
// ServeDirectURL generates a "serve-direct" URL for the specified blob storage file,
// end user (browser) will use this URL to access the file directly from the object storage, bypassing Gitea server.
// Usually the link is time-limited (a few minutes) and contains a signature to ensure security.
// The generated URL must NOT use the same origin as Gitea server, otherwise it will cause security issues.
// * method defines which HTTP method is permitted for certain storage providers (e.g., MinIO).
// * opt allows customizing the Content-Type and Content-Disposition headers.
// TODO: need to merge "ServeDirect()" check into this function, avoid duplicate code and potential inconsistency.
ServeDirectURL(path, name, method string, opt *ServeDirectOptions) (*url.URL, error)
// IterateObjects calls the iterator function for each object in the storage with the given path as prefix
// The "fullPath" argument in callback is the full path in this storage.
@@ -136,7 +178,7 @@ var (
// Actions represents actions storage
Actions ObjectStorage = uninitializedStorage
// Actions Artifacts represents actions artifacts storage
// ActionsArtifacts Artifacts represents actions artifacts storage
ActionsArtifacts ObjectStorage = uninitializedStorage
)