Stream repo zip/tar.gz/bundle achives by default (#35487)

Initial implementation of linked proposal.

* Closes #29942
* Fix #34003
* Fix #30443

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
ChristopherHX
2025-09-19 05:51:21 +02:00
committed by GitHub
parent 90cb5f9a1f
commit 9a0ec53ee3
6 changed files with 110 additions and 156 deletions

View File

@@ -1247,7 +1247,7 @@ func Routes() *web.Router {
}, reqToken())
m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile)
m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS)
m.Methods("HEAD,GET", "/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
m.Methods("HEAD,GET", "/archive/*", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true), repo.GetArchive)
m.Combo("/forks").Get(repo.ListForks).
Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
m.Post("/merge-upstream", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeCode), bind(api.MergeUpstreamRequest{}), repo.MergeUpstream)
@@ -1466,7 +1466,7 @@ func Routes() *web.Router {
m.Delete("", repo.DeleteAvatar)
}, reqAdmin(), reqToken())
m.Methods("HEAD,GET", "/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive)
m.Methods("HEAD,GET", "/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true), repo.DownloadArchive)
}, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))

View File

@@ -4,14 +4,29 @@
package repo
import (
"errors"
"net/http"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/services/context"
archiver_service "code.gitea.io/gitea/services/repository/archiver"
)
func serveRepoArchive(ctx *context.APIContext, reqFileName string) {
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, reqFileName)
if err != nil {
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
ctx.APIError(http.StatusBadRequest, err)
} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
archiver_service.ServeRepoArchive(ctx.Base, ctx.Repo.Repository, ctx.Repo.GitRepo, aReq)
}
func DownloadArchive(ctx *context.APIContext) {
var tp git.ArchiveType
switch ballType := ctx.PathParam("ball_type"); ballType {
@@ -25,27 +40,5 @@ func DownloadArchive(ctx *context.APIContext) {
ctx.APIError(http.StatusBadRequest, "Unknown archive type: "+ballType)
return
}
if ctx.Repo.GitRepo == nil {
var err error
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
r, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*")+"."+tp.String())
if err != nil {
ctx.APIErrorInternal(err)
return
}
archive, err := r.Await(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
}
download(ctx, r.GetArchiveName(), archive)
serveRepoArchive(ctx, ctx.PathParam("*")+"."+tp.String())
}

View File

@@ -15,9 +15,7 @@ import (
"time"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/lfs"
@@ -31,7 +29,6 @@ import (
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
pull_service "code.gitea.io/gitea/services/pull"
archiver_service "code.gitea.io/gitea/services/repository/archiver"
files_service "code.gitea.io/gitea/services/repository/files"
)
@@ -282,74 +279,7 @@ func GetArchive(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
if ctx.Repo.GitRepo == nil {
var err error
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
archiveDownload(ctx)
}
func archiveDownload(ctx *context.APIContext) {
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"))
if err != nil {
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
ctx.APIError(http.StatusBadRequest, err)
} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
archiver, err := aReq.Await(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
}
download(ctx, aReq.GetArchiveName(), archiver)
}
func download(ctx *context.APIContext, archiveName string, archiver *repo_model.RepoArchiver) {
downloadName := ctx.Repo.Repository.Name + "-" + archiveName
// Add nix format link header so tarballs lock correctly:
// https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md
ctx.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.%s?rev=%s>; rel="immutable"`,
ctx.Repo.Repository.APIURL(),
archiver.CommitID,
archiver.Type.String(),
archiver.CommitID,
))
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, ctx.Req.Method, nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
return
}
}
// If we have matched and access to release or issue
fr, err := storage.RepoArchives.Open(rPath)
if err != nil {
ctx.APIErrorInternal(err)
return
}
defer fr.Close()
ctx.ServeContent(fr, &context.ServeHeaderOptions{
Filename: downloadName,
LastModified: archiver.CreatedUnix.AsLocalTime(),
})
serveRepoArchive(ctx, ctx.PathParam("*"))
}
// GetEditorconfig get editor config of a repository