Fix #28121
I did some tests and found that the `missing signature key` error is
caused by an incorrect `Content-Type` header. Gitea correctly sets the
`Content-Type` header when serving files.
https://github.com/go-gitea/gitea/blob/
348d1d0f322ca57c459acd902f54821d687ca804/routers/api/packages/container/container.go#L712-L717
However, when `SERVE_DIRECT` is enabled, the `Content-Type` header may
be set to an incorrect value by the storage service. To fix this issue,
we can use query parameters to override response header values.
https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html
<img width="600px"
src="https://github.com/user-attachments/assets/
f2ff90f0-f1df-46f9-9680-
b8120222c555"
/>
In this PR, I introduced a new parameter to the `URL` method to support
additional parameters.
```
URL(path, name string, reqParams url.Values) (*url.URL, error)
```
---
Most S3-like services support specifying the content type when storing
objects. However, Gitea always use `application/octet-stream`.
Therefore, I believe we also need to improve the `Save` method to
support storing objects with the correct content type.
https://github.com/go-gitea/gitea/blob/
b7fb20e73e63b8edc9b90c52073e248bef428fcc/modules/storage/minio.go#L214-L221
return setting.Packages.Storage.ServeDirect()
}
-func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename string) (*url.URL, error) {
- return s.store.URL(KeyToRelativePath(key), filename)
+func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename string, reqParams url.Values) (*url.URL, error) {
+ return s.store.URL(KeyToRelativePath(key), filename, reqParams)
}
// FIXME: Workaround to be removed in v1.20
}
// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
-func (a *AzureBlobStorage) URL(path, name string) (*url.URL, error) {
+func (a *AzureBlobStorage) URL(path, name string, reqParams url.Values) (*url.URL, error) {
blobClient := a.getBlobClient(path)
startTime := time.Now()
return fmt.Errorf("%s", s)
}
-func (s discardStorage) URL(_, _ string) (*url.URL, error) {
+func (s discardStorage) URL(_, _ string, _ url.Values) (*url.URL, error) {
return nil, fmt.Errorf("%s", s)
}
assert.Error(t, err, string(tt))
}
{
- got, err := tt.URL("path", "name")
+ got, err := tt.URL("path", "name", nil)
assert.Nil(t, got)
assert.Errorf(t, err, string(tt))
}
}
// URL gets the redirect URL to a file
-func (l *LocalStorage) URL(path, name string) (*url.URL, error) {
+func (l *LocalStorage) URL(path, name string, reqParams url.Values) (*url.URL, error) {
return nil, ErrURLNotSupported
}
}
// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
-func (m *MinioStorage) URL(path, name string) (*url.URL, error) {
- reqParams := make(url.Values)
+func (m *MinioStorage) URL(path, name string, serveDirectReqParams url.Values) (*url.URL, error) {
+ // copy serveDirectReqParams
+ reqParams, err := url.ParseQuery(serveDirectReqParams.Encode())
+ 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)+"\"")
u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), 5*time.Minute, reqParams)
Save(path string, r io.Reader, size int64) (int64, error)
Stat(path string) (os.FileInfo, error)
Delete(path string) error
- URL(path, name string) (*url.URL, error)
+ URL(path, name string, reqParams url.Values) (*url.URL, error)
IterateObjects(path string, iterator func(path string, obj Object) error) error
}
for _, artifact := range artifacts {
var downloadURL string
if setting.Actions.ArtifactStorage.ServeDirect() {
- u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName)
+ u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName, nil)
if err != nil && !errors.Is(err, storage.ErrURLNotSupported) {
log.Error("Error getting serve direct url: %v", err)
}
respData := GetSignedArtifactURLResponse{}
if setting.Actions.ArtifactStorage.ServeDirect() {
- u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath)
+ u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath, nil)
if u != nil && err == nil {
respData.SignedUrl = u.String()
}
}
func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) {
- s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob)
+ serveDirectReqParams := make(url.Values)
+ serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType))
+ s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob, serveDirectReqParams)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
return
}
- s, u, _, err := packages_service.GetPackageBlobStream(ctx, pf, pb)
+ s, u, _, err := packages_service.GetPackageBlobStream(ctx, pf, pb, nil)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
if setting.LFS.Storage.ServeDirect() {
// If we have a signed url (S3, object storage), redirect to this directly.
- u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name())
+ u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name(), nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
return
rPath := archiver.RelativePath()
if setting.RepoArchive.Storage.ServeDirect() {
// If we have a signed url (S3, object storage), redirect to this directly.
- u, err := storage.RepoArchives.URL(rPath, downloadName)
+ u, err := storage.RepoArchives.URL(rPath, downloadName, nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
return
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
rPath = util.PathJoinRelX(rPath)
- u, err := objStore.URL(rPath, path.Base(rPath))
+ u, err := objStore.URL(rPath, path.Base(rPath), nil)
if err != nil {
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
log.Warn("Unable to find %s %s", prefix, rPath)
if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" {
art := artifacts[0]
if setting.Actions.ArtifactStorage.ServeDirect() {
- u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath)
+ u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath, nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
return
if setting.Attachment.Storage.ServeDirect() {
// If we have a signed url (S3, object storage), redirect to this directly.
- u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name)
+ u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name, nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
if setting.LFS.Storage.ServeDirect() {
// If we have a signed url (S3, object storage, blob storage), redirect to this directly.
- u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name())
+ u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name(), nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
return nil
rPath := archiver.RelativePath()
if setting.RepoArchive.Storage.ServeDirect() {
// If we have a signed url (S3, object storage), redirect to this directly.
- u, err := storage.RepoArchives.URL(rPath, downloadName)
+ u, err := storage.RepoArchives.URL(rPath, downloadName, nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
return
var link *lfs_module.Link
if setting.LFS.Storage.ServeDirect() {
// If we have a signed url (S3, object storage), redirect to this directly.
- u, err := storage.LFS.URL(pointer.RelativePath(), pointer.Oid)
+ u, err := storage.LFS.URL(pointer.RelativePath(), pointer.Oid, nil)
if u != nil && err == nil {
// Presigned url does not need the Authorization header
// https://github.com/go-gitea/gitea/issues/21525
return nil, nil, nil, err
}
- return GetPackageBlobStream(ctx, pf, pb)
+ return GetPackageBlobStream(ctx, pf, pb, nil)
}
// GetPackageBlobStream returns the content of the specific package blob
// If the storage supports direct serving and it's enabled, only the direct serving url is returned.
-func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) {
+func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob, serveDirectReqParams url.Values) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) {
key := packages_module.BlobHash256Key(pb.HashSHA256)
cs := packages_module.NewContentStore()
var err error
if cs.ShouldServeDirect() {
- u, err = cs.GetServeDirectURL(key, pf.Name)
+ u, err = cs.GetServeDirectURL(key, pf.Name, serveDirectReqParams)
if err != nil && !errors.Is(err, storage.ErrURLNotSupported) {
log.Error("Error getting serve direct url: %v", err)
}