Feature non-zipped actions artifacts (action v7) (#36786)

- content_encoding contains a slash => v4 artifact
- updated proto files to support mime_type and no longer return errors for upload-artifact v7
- json and txt files are now previewed in browser
- normalized content-disposition header creation
- azure blob storage uploads directly in servedirect mode (no proxying data)
- normalize content-disposition headers based on go mime package
  - getting both filename and filename* encoding is done via custom code

Closes #36829

-----

Signed-off-by: ChristopherHX <christopher.homberger@web.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
ChristopherHX
2026-03-25 17:37:48 +01:00
committed by GitHub
parent 435123fe65
commit bc5c554072
29 changed files with 1003 additions and 826 deletions

View File

@@ -23,11 +23,7 @@ import (
"github.com/minio/minio-go/v7/pkg/credentials"
)
var (
_ ObjectStorage = &MinioStorage{}
quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
)
var _ ObjectStorage = &MinioStorage{}
type minioObject struct {
*minio.Object

View File

@@ -12,6 +12,7 @@ import (
"os"
"path"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
@@ -62,31 +63,30 @@ type Object interface {
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) {
func prepareServeDirectOptions(optsOptional *ServeDirectOptions, name string) (ret struct {
ContentType string
ContentDisposition string
},
) {
// 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
ret.ContentType = optsOptional.ContentType
}
// TODO: UNIFY-CONTENT-DISPOSITION-FROM-STORAGE
name = path.Base(name)
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))
}
// 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 = httplib.EncodeContentDispositionInline(name)
return ret
}

View File

@@ -53,7 +53,12 @@ func testStorageIterator(t *testing.T, typStr Type, cfg *setting.Storage) {
}
}
func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectStorage, path, name string, expected ServeDirectOptions, reqParams *ServeDirectOptions) {
type expectedServeDirectHeaders struct {
ContentType string
ContentDisposition string
}
func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectStorage, path, name string, expected expectedServeDirectHeaders, reqParams *ServeDirectOptions) {
u, err := s.ServeDirectURL(path, name, http.MethodGet, reqParams)
require.NoError(t, err)
resp, err := http.Get(u.String())
@@ -71,36 +76,29 @@ func testBlobStorageURLContentTypeAndDisposition(t *testing.T, typStr Type, cfg
s, err := NewStorage(typStr, cfg)
assert.NoError(t, err)
data := "Q2xTckt6Y1hDOWh0" // arbitrary test content; specific value is irrelevant to this test
testfilename := "test.txt" // arbitrary file name; specific value is irrelevant to this test
_, err = s.Save(testfilename, strings.NewReader(data), int64(len(data)))
testFilename := "test.txt"
_, err = s.Save(testFilename, strings.NewReader("dummy-content"), -1)
assert.NoError(t, err)
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.txt", ServeDirectOptions{
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.txt", expectedServeDirectHeaders{
ContentType: "text/plain; charset=utf-8",
ContentDisposition: `inline; filename="test.txt"`,
ContentDisposition: `inline; filename=test.txt`,
}, nil)
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.pdf", ServeDirectOptions{
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.pdf", expectedServeDirectHeaders{
ContentType: "application/pdf",
ContentDisposition: `inline; filename="test.pdf"`,
ContentDisposition: `inline; filename=test.pdf`,
}, nil)
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.wasm", ServeDirectOptions{
ContentDisposition: `inline; filename="test.wasm"`,
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.wasm", expectedServeDirectHeaders{
ContentDisposition: `inline; filename=test.wasm`,
}, nil)
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.wasm", ServeDirectOptions{
ContentDisposition: `inline; filename="test.wasm"`,
}, &ServeDirectOptions{})
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.txt", ServeDirectOptions{
ContentType: "application/octet-stream",
ContentDisposition: `inline; filename="test.xml"`,
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.wasm", expectedServeDirectHeaders{
ContentType: "application/wasm",
ContentDisposition: `inline; filename=test.wasm`,
}, &ServeDirectOptions{
ContentType: "application/octet-stream",
ContentDisposition: `inline; filename="test.xml"`,
ContentType: "application/wasm",
})
assert.NoError(t, s.Delete(testfilename))
assert.NoError(t, s.Delete(testFilename))
}