Move archive function to repo_model and gitrepo (#35514)

This commit is contained in:
Lunny Xiao
2025-10-06 15:01:26 -07:00
committed by GitHub
parent cdc0733047
commit ad2ff67343
9 changed files with 172 additions and 182 deletions

View File

@@ -1,7 +1,7 @@
// Copyright 2025 The Gitea Authors. All rights reserved. // Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package git package repo
import ( import (
"testing" "testing"

View File

@@ -11,7 +11,6 @@ import (
"time" "time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@@ -27,11 +26,46 @@ const (
ArchiverReady // it's ready ArchiverReady // it's ready
) )
// ArchiveType archive types
type ArchiveType int
const (
ArchiveUnknown ArchiveType = iota
ArchiveZip // 1
ArchiveTarGz // 2
ArchiveBundle // 3
)
// String converts an ArchiveType to string: the extension of the archive file without prefix dot
func (a ArchiveType) String() string {
switch a {
case ArchiveZip:
return "zip"
case ArchiveTarGz:
return "tar.gz"
case ArchiveBundle:
return "bundle"
}
return "unknown"
}
func SplitArchiveNameType(s string) (string, ArchiveType) {
switch {
case strings.HasSuffix(s, ".zip"):
return strings.TrimSuffix(s, ".zip"), ArchiveZip
case strings.HasSuffix(s, ".tar.gz"):
return strings.TrimSuffix(s, ".tar.gz"), ArchiveTarGz
case strings.HasSuffix(s, ".bundle"):
return strings.TrimSuffix(s, ".bundle"), ArchiveBundle
}
return s, ArchiveUnknown
}
// RepoArchiver represents all archivers // RepoArchiver represents all archivers
type RepoArchiver struct { //revive:disable-line:exported type RepoArchiver struct { //revive:disable-line:exported
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"index unique(s)"` RepoID int64 `xorm:"index unique(s)"`
Type git.ArchiveType `xorm:"unique(s)"` Type ArchiveType `xorm:"unique(s)"`
Status ArchiverStatus Status ArchiverStatus
CommitID string `xorm:"VARCHAR(64) unique(s)"` CommitID string `xorm:"VARCHAR(64) unique(s)"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"`
@@ -56,15 +90,15 @@ func repoArchiverForRelativePath(relativePath string) (*RepoArchiver, error) {
if err != nil { if err != nil {
return nil, util.NewInvalidArgumentErrorf("invalid storage path: invalid repo id") return nil, util.NewInvalidArgumentErrorf("invalid storage path: invalid repo id")
} }
commitID, archiveType := git.SplitArchiveNameType(parts[2]) commitID, archiveType := SplitArchiveNameType(parts[2])
if archiveType == git.ArchiveUnknown { if archiveType == ArchiveUnknown {
return nil, util.NewInvalidArgumentErrorf("invalid storage path: invalid archive type") return nil, util.NewInvalidArgumentErrorf("invalid storage path: invalid archive type")
} }
return &RepoArchiver{RepoID: repoID, CommitID: commitID, Type: archiveType}, nil return &RepoArchiver{RepoID: repoID, CommitID: commitID, Type: archiveType}, nil
} }
// GetRepoArchiver get an archiver // GetRepoArchiver get an archiver
func GetRepoArchiver(ctx context.Context, repoID int64, tp git.ArchiveType, commitID string) (*RepoArchiver, error) { func GetRepoArchiver(ctx context.Context, repoID int64, tp ArchiveType, commitID string) (*RepoArchiver, error) {
var archiver RepoArchiver var archiver RepoArchiver
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("`type`=?", tp).And("commit_id=?", commitID).Get(&archiver) has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("`type`=?", tp).And("commit_id=?", commitID).Get(&archiver)
if err != nil { if err != nil {

View File

@@ -12,14 +12,12 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/proxy" "code.gitea.io/gitea/modules/proxy"
"code.gitea.io/gitea/modules/setting"
) )
// GPGSettings represents the default GPG settings for this repository // GPGSettings represents the default GPG settings for this repository
@@ -242,43 +240,3 @@ func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error
commitTime := strings.TrimSpace(stdout) commitTime := strings.TrimSpace(stdout)
return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime) return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime)
} }
// CreateBundle create bundle content to the target path
func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error {
tmp, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-bundle")
if err != nil {
return err
}
defer cleanup()
env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects"))
_, _, err = gitcmd.NewCommand("init", "--bare").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env})
if err != nil {
return err
}
_, _, err = gitcmd.NewCommand("reset", "--soft").AddDynamicArguments(commit).RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env})
if err != nil {
return err
}
_, _, err = gitcmd.NewCommand("branch", "-m", "bundle").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env})
if err != nil {
return err
}
tmpFile := filepath.Join(tmp, "bundle")
_, _, err = gitcmd.NewCommand("bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env})
if err != nil {
return err
}
fi, err := os.Open(tmpFile)
if err != nil {
return err
}
defer fi.Close()
_, err = io.Copy(out, fi)
return err
}

View File

@@ -1,75 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"fmt"
"io"
"path/filepath"
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
)
// ArchiveType archive types
type ArchiveType int
const (
ArchiveUnknown ArchiveType = iota
ArchiveZip // 1
ArchiveTarGz // 2
ArchiveBundle // 3
)
// String converts an ArchiveType to string: the extension of the archive file without prefix dot
func (a ArchiveType) String() string {
switch a {
case ArchiveZip:
return "zip"
case ArchiveTarGz:
return "tar.gz"
case ArchiveBundle:
return "bundle"
}
return "unknown"
}
func SplitArchiveNameType(s string) (string, ArchiveType) {
switch {
case strings.HasSuffix(s, ".zip"):
return strings.TrimSuffix(s, ".zip"), ArchiveZip
case strings.HasSuffix(s, ".tar.gz"):
return strings.TrimSuffix(s, ".tar.gz"), ArchiveTarGz
case strings.HasSuffix(s, ".bundle"):
return strings.TrimSuffix(s, ".bundle"), ArchiveBundle
}
return s, ArchiveUnknown
}
// CreateArchive create archive content to the target path
func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, target io.Writer, usePrefix bool, commitID string) error {
if format.String() == "unknown" {
return fmt.Errorf("unknown format: %v", format)
}
cmd := gitcmd.NewCommand("archive")
if usePrefix {
cmd.AddOptionFormat("--prefix=%s", filepath.Base(strings.TrimSuffix(repo.Path, ".git"))+"/")
}
cmd.AddOptionFormat("--format=%s", format.String())
cmd.AddDynamicArguments(commitID)
var stderr strings.Builder
err := cmd.Run(ctx, &gitcmd.RunOpts{
Dir: repo.Path,
Stdout: target,
Stderr: &stderr,
})
if err != nil {
return gitcmd.ConcatenateError(err, stderr.String())
}
return nil
}

View File

@@ -0,0 +1,81 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/setting"
)
// CreateArchive create archive content to the target path
func CreateArchive(ctx context.Context, repo Repository, format string, target io.Writer, usePrefix bool, commitID string) error {
if format == "unknown" {
return fmt.Errorf("unknown format: %v", format)
}
cmd := gitcmd.NewCommand("archive")
if usePrefix {
cmd.AddOptionFormat("--prefix=%s", filepath.Base(strings.TrimSuffix(repo.RelativePath(), ".git"))+"/")
}
cmd.AddOptionFormat("--format=%s", format)
cmd.AddDynamicArguments(commitID)
var stderr strings.Builder
err := cmd.Run(ctx, &gitcmd.RunOpts{
Dir: repoPath(repo),
Stdout: target,
Stderr: &stderr,
})
if err != nil {
return gitcmd.ConcatenateError(err, stderr.String())
}
return nil
}
// CreateBundle create bundle content to the target path
func CreateBundle(ctx context.Context, repo Repository, commit string, out io.Writer) error {
tmp, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-bundle")
if err != nil {
return err
}
defer cleanup()
env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repoPath(repo), "objects"))
_, _, err = gitcmd.NewCommand("init", "--bare").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env})
if err != nil {
return err
}
_, _, err = gitcmd.NewCommand("reset", "--soft").AddDynamicArguments(commit).RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env})
if err != nil {
return err
}
_, _, err = gitcmd.NewCommand("branch", "-m", "bundle").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env})
if err != nil {
return err
}
tmpFile := filepath.Join(tmp, "bundle")
_, _, err = gitcmd.NewCommand("bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env})
if err != nil {
return err
}
fi, err := os.Open(tmpFile)
if err != nil {
return err
}
defer fi.Close()
_, err = io.Copy(out, fi)
return err
}

View File

@@ -7,13 +7,13 @@ import (
"errors" "errors"
"net/http" "net/http"
"code.gitea.io/gitea/modules/git" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
archiver_service "code.gitea.io/gitea/services/repository/archiver" archiver_service "code.gitea.io/gitea/services/repository/archiver"
) )
func serveRepoArchive(ctx *context.APIContext, reqFileName string) { func serveRepoArchive(ctx *context.APIContext, reqFileName string) {
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, reqFileName) aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, reqFileName)
if err != nil { if err != nil {
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
ctx.APIError(http.StatusBadRequest, err) ctx.APIError(http.StatusBadRequest, err)
@@ -24,18 +24,18 @@ func serveRepoArchive(ctx *context.APIContext, reqFileName string) {
} }
return return
} }
archiver_service.ServeRepoArchive(ctx.Base, ctx.Repo.Repository, ctx.Repo.GitRepo, aReq) archiver_service.ServeRepoArchive(ctx.Base, aReq)
} }
func DownloadArchive(ctx *context.APIContext) { func DownloadArchive(ctx *context.APIContext) {
var tp git.ArchiveType var tp repo_model.ArchiveType
switch ballType := ctx.PathParam("ball_type"); ballType { switch ballType := ctx.PathParam("ball_type"); ballType {
case "tarball": case "tarball":
tp = git.ArchiveTarGz tp = repo_model.ArchiveTarGz
case "zipball": case "zipball":
tp = git.ArchiveZip tp = repo_model.ArchiveZip
case "bundle": case "bundle":
tp = git.ArchiveBundle tp = repo_model.ArchiveBundle
default: default:
ctx.APIError(http.StatusBadRequest, "Unknown archive type: "+ballType) ctx.APIError(http.StatusBadRequest, "Unknown archive type: "+ballType)
return return

View File

@@ -364,7 +364,7 @@ func RedirectDownload(ctx *context.Context) {
// Download an archive of a repository // Download an archive of a repository
func Download(ctx *context.Context) { func Download(ctx *context.Context) {
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*")) aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("*"))
if err != nil { if err != nil {
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
ctx.HTTPError(http.StatusBadRequest, err.Error()) ctx.HTTPError(http.StatusBadRequest, err.Error())
@@ -375,7 +375,7 @@ func Download(ctx *context.Context) {
} }
return return
} }
archiver_service.ServeRepoArchive(ctx.Base, ctx.Repo.Repository, ctx.Repo.GitRepo, aReq) archiver_service.ServeRepoArchive(ctx.Base, aReq)
} }
// InitiateDownload will enqueue an archival request, as needed. It may submit // InitiateDownload will enqueue an archival request, as needed. It may submit
@@ -388,7 +388,7 @@ func InitiateDownload(ctx *context.Context) {
}) })
return return
} }
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*")) aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("*"))
if err != nil { if err != nil {
ctx.HTTPError(http.StatusBadRequest, "invalid archive request") ctx.HTTPError(http.StatusBadRequest, "invalid archive request")
return return
@@ -398,7 +398,7 @@ func InitiateDownload(ctx *context.Context) {
return return
} }
archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID) archiver, err := repo_model.GetRepoArchiver(ctx, aReq.Repo.ID, aReq.Type, aReq.CommitID)
if err != nil { if err != nil {
ctx.ServerError("archiver_service.StartArchive", err) ctx.ServerError("archiver_service.StartArchive", err)
return return

View File

@@ -33,8 +33,8 @@ import (
// This is entirely opaque to external entities, though, and mostly used as a // This is entirely opaque to external entities, though, and mostly used as a
// handle elsewhere. // handle elsewhere.
type ArchiveRequest struct { type ArchiveRequest struct {
RepoID int64 Repo *repo_model.Repository
Type git.ArchiveType Type repo_model.ArchiveType
CommitID string CommitID string
archiveRefShortName string // the ref short name to download the archive, for example: "master", "v1.0.0", "commit id" archiveRefShortName string // the ref short name to download the archive, for example: "master", "v1.0.0", "commit id"
@@ -74,20 +74,20 @@ func (e RepoRefNotFoundError) Is(err error) bool {
// NewRequest creates an archival request, based on the URI. The // NewRequest creates an archival request, based on the URI. The
// resulting ArchiveRequest is suitable for being passed to Await() // resulting ArchiveRequest is suitable for being passed to Await()
// if it's determined that the request still needs to be satisfied. // if it's determined that the request still needs to be satisfied.
func NewRequest(repoID int64, repo *git.Repository, archiveRefExt string) (*ArchiveRequest, error) { func NewRequest(repo *repo_model.Repository, gitRepo *git.Repository, archiveRefExt string) (*ArchiveRequest, error) {
// here the archiveRefShortName is not a clear ref, it could be a tag, branch or commit id // here the archiveRefShortName is not a clear ref, it could be a tag, branch or commit id
archiveRefShortName, archiveType := git.SplitArchiveNameType(archiveRefExt) archiveRefShortName, archiveType := repo_model.SplitArchiveNameType(archiveRefExt)
if archiveType == git.ArchiveUnknown { if archiveType == repo_model.ArchiveUnknown {
return nil, ErrUnknownArchiveFormat{archiveRefExt} return nil, ErrUnknownArchiveFormat{archiveRefExt}
} }
// Get corresponding commit. // Get corresponding commit.
commitID, err := repo.ConvertToGitID(archiveRefShortName) commitID, err := gitRepo.ConvertToGitID(archiveRefShortName)
if err != nil { if err != nil {
return nil, RepoRefNotFoundError{RefShortName: archiveRefShortName} return nil, RepoRefNotFoundError{RefShortName: archiveRefShortName}
} }
r := &ArchiveRequest{RepoID: repoID, archiveRefShortName: archiveRefShortName, Type: archiveType} r := &ArchiveRequest{Repo: repo, archiveRefShortName: archiveRefShortName, Type: archiveType}
r.CommitID = commitID.String() r.CommitID = commitID.String()
return r, nil return r, nil
} }
@@ -105,7 +105,7 @@ func (aReq *ArchiveRequest) GetArchiveName() string {
// context is cancelled/times out a started archiver will still continue to run // context is cancelled/times out a started archiver will still continue to run
// in the background. // in the background.
func (aReq *ArchiveRequest) Await(ctx context.Context) (*repo_model.RepoArchiver, error) { func (aReq *ArchiveRequest) Await(ctx context.Context) (*repo_model.RepoArchiver, error) {
archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID) archiver, err := repo_model.GetRepoArchiver(ctx, aReq.Repo.ID, aReq.Type, aReq.CommitID)
if err != nil { if err != nil {
return nil, fmt.Errorf("models.GetRepoArchiver: %w", err) return nil, fmt.Errorf("models.GetRepoArchiver: %w", err)
} }
@@ -130,7 +130,7 @@ func (aReq *ArchiveRequest) Await(ctx context.Context) (*repo_model.RepoArchiver
case <-ctx.Done(): case <-ctx.Done():
return nil, ctx.Err() return nil, ctx.Err()
case <-poll.C: case <-poll.C:
archiver, err = repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID) archiver, err = repo_model.GetRepoArchiver(ctx, aReq.Repo.ID, aReq.Type, aReq.CommitID)
if err != nil { if err != nil {
return nil, fmt.Errorf("repo_model.GetRepoArchiver: %w", err) return nil, fmt.Errorf("repo_model.GetRepoArchiver: %w", err)
} }
@@ -143,17 +143,19 @@ func (aReq *ArchiveRequest) Await(ctx context.Context) (*repo_model.RepoArchiver
// Stream satisfies the ArchiveRequest being passed in. Processing // Stream satisfies the ArchiveRequest being passed in. Processing
// will occur directly in this routine. // will occur directly in this routine.
func (aReq *ArchiveRequest) Stream(ctx context.Context, gitRepo *git.Repository, w io.Writer) error { func (aReq *ArchiveRequest) Stream(ctx context.Context, w io.Writer) error {
if aReq.Type == git.ArchiveBundle { if aReq.Type == repo_model.ArchiveBundle {
return gitRepo.CreateBundle( return gitrepo.CreateBundle(
ctx, ctx,
aReq.Repo,
aReq.CommitID, aReq.CommitID,
w, w,
) )
} }
return gitRepo.CreateArchive( return gitrepo.CreateArchive(
ctx, ctx,
aReq.Type, aReq.Repo,
aReq.Type.String(),
w, w,
setting.Repository.PrefixArchiveFiles, setting.Repository.PrefixArchiveFiles,
aReq.CommitID, aReq.CommitID,
@@ -167,10 +169,10 @@ func (aReq *ArchiveRequest) Stream(ctx context.Context, gitRepo *git.Repository,
// being returned for completion, as it may be different than the one they passed // being returned for completion, as it may be different than the one they passed
// in. // in.
func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver, error) { func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver, error) {
ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("ArchiveRequest[%d]: %s", r.RepoID, r.GetArchiveName())) ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("ArchiveRequest[%s]: %s", r.Repo.FullName(), r.GetArchiveName()))
defer finished() defer finished()
archiver, err := repo_model.GetRepoArchiver(ctx, r.RepoID, r.Type, r.CommitID) archiver, err := repo_model.GetRepoArchiver(ctx, r.Repo.ID, r.Type, r.CommitID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -183,7 +185,7 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver
} }
} else { } else {
archiver = &repo_model.RepoArchiver{ archiver = &repo_model.RepoArchiver{
RepoID: r.RepoID, RepoID: r.Repo.ID,
Type: r.Type, Type: r.Type,
CommitID: r.CommitID, CommitID: r.CommitID,
Status: repo_model.ArchiverGenerating, Status: repo_model.ArchiverGenerating,
@@ -215,28 +217,18 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver
_ = rd.Close() _ = rd.Close()
}() }()
done := make(chan error, 1) // Ensure that there is some capacity which will ensure that the goroutine below can always finish done := make(chan error, 1) // Ensure that there is some capacity which will ensure that the goroutine below can always finish
repo, err := repo_model.GetRepositoryByID(ctx, archiver.RepoID)
if err != nil {
return nil, fmt.Errorf("archiver.LoadRepo failed: %w", err)
}
gitRepo, err := gitrepo.OpenRepository(ctx, repo) go func(done chan error, w *io.PipeWriter, archiveReq *ArchiveRequest) {
if err != nil {
return nil, err
}
defer gitRepo.Close()
go func(done chan error, w *io.PipeWriter, archiveReq *ArchiveRequest, gitRepo *git.Repository) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
done <- fmt.Errorf("%v", r) done <- fmt.Errorf("%v", r)
} }
}() }()
err := archiveReq.Stream(ctx, gitRepo, w) err := archiveReq.Stream(ctx, w)
_ = w.CloseWithError(err) _ = w.CloseWithError(err)
done <- err done <- err
}(done, w, r, gitRepo) }(done, w, r)
// TODO: add lfs data to zip // TODO: add lfs data to zip
// TODO: add submodule data to zip // TODO: add submodule data to zip
@@ -347,20 +339,20 @@ func DeleteRepositoryArchives(ctx context.Context) error {
return storage.Clean(storage.RepoArchives) return storage.Clean(storage.RepoArchives)
} }
func ServeRepoArchive(ctx *gitea_context.Base, repo *repo_model.Repository, gitRepo *git.Repository, archiveReq *ArchiveRequest) { func ServeRepoArchive(ctx *gitea_context.Base, archiveReq *ArchiveRequest) {
// Add nix format link header so tarballs lock correctly: // Add nix format link header so tarballs lock correctly:
// https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md // 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.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.%s?rev=%s>; rel="immutable"`,
repo.APIURL(), archiveReq.Repo.APIURL(),
archiveReq.CommitID, archiveReq.CommitID,
archiveReq.Type.String(), archiveReq.Type.String(),
archiveReq.CommitID, archiveReq.CommitID,
)) ))
downloadName := repo.Name + "-" + archiveReq.GetArchiveName() downloadName := archiveReq.Repo.Name + "-" + archiveReq.GetArchiveName()
if setting.Repository.StreamArchives { if setting.Repository.StreamArchives {
httplib.ServeSetHeaders(ctx.Resp, &httplib.ServeHeaderOptions{Filename: downloadName}) httplib.ServeSetHeaders(ctx.Resp, &httplib.ServeHeaderOptions{Filename: downloadName})
if err := archiveReq.Stream(ctx, gitRepo, ctx.Resp); err != nil && !ctx.Written() { if err := archiveReq.Stream(ctx, ctx.Resp); err != nil && !ctx.Written() {
log.Error("Archive %v streaming failed: %v", archiveReq, err) log.Error("Archive %v streaming failed: %v", archiveReq, err)
ctx.HTTPError(http.StatusInternalServerError) ctx.HTTPError(http.StatusInternalServerError)
} }

View File

@@ -29,47 +29,47 @@ func TestArchive_Basic(t *testing.T) {
contexttest.LoadGitRepo(t, ctx) contexttest.LoadGitRepo(t, ctx)
defer ctx.Repo.GitRepo.Close() defer ctx.Repo.GitRepo.Close()
bogusReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") bogusReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip")
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, bogusReq) assert.NotNil(t, bogusReq)
assert.Equal(t, firstCommit+".zip", bogusReq.GetArchiveName()) assert.Equal(t, firstCommit+".zip", bogusReq.GetArchiveName())
// Check a series of bogus requests. // Check a series of bogus requests.
// Step 1, valid commit with a bad extension. // Step 1, valid commit with a bad extension.
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".unknown") bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".unknown")
assert.Error(t, err) assert.Error(t, err)
assert.Nil(t, bogusReq) assert.Nil(t, bogusReq)
// Step 2, missing commit. // Step 2, missing commit.
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "dbffff.zip") bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "dbffff.zip")
assert.Error(t, err) assert.Error(t, err)
assert.Nil(t, bogusReq) assert.Nil(t, bogusReq)
// Step 3, doesn't look like branch/tag/commit. // Step 3, doesn't look like branch/tag/commit.
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "db.zip") bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "db.zip")
assert.Error(t, err) assert.Error(t, err)
assert.Nil(t, bogusReq) assert.Nil(t, bogusReq)
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master.zip") bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "master.zip")
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, bogusReq) assert.NotNil(t, bogusReq)
assert.Equal(t, "master.zip", bogusReq.GetArchiveName()) assert.Equal(t, "master.zip", bogusReq.GetArchiveName())
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive.zip") bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "test/archive.zip")
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, bogusReq) assert.NotNil(t, bogusReq)
assert.Equal(t, "test-archive.zip", bogusReq.GetArchiveName()) assert.Equal(t, "test-archive.zip", bogusReq.GetArchiveName())
// Now two valid requests, firstCommit with valid extensions. // Now two valid requests, firstCommit with valid extensions.
zipReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") zipReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip")
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, zipReq) assert.NotNil(t, zipReq)
tgzReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".tar.gz") tgzReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".tar.gz")
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, tgzReq) assert.NotNil(t, tgzReq)
secondReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".bundle") secondReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, secondCommit+".bundle")
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, secondReq) assert.NotNil(t, secondReq)
@@ -89,7 +89,7 @@ func TestArchive_Basic(t *testing.T) {
// Sleep two seconds to make sure the queue doesn't change. // Sleep two seconds to make sure the queue doesn't change.
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
zipReq2, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") zipReq2, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip")
assert.NoError(t, err) assert.NoError(t, err)
// This zipReq should match what's sitting in the queue, as we haven't // This zipReq should match what's sitting in the queue, as we haven't
// let it release yet. From the consumer's point of view, this looks like // let it release yet. From the consumer's point of view, this looks like
@@ -104,12 +104,12 @@ func TestArchive_Basic(t *testing.T) {
// Now we'll submit a request and TimedWaitForCompletion twice, before and // Now we'll submit a request and TimedWaitForCompletion twice, before and
// after we release it. We should trigger both the timeout and non-timeout // after we release it. We should trigger both the timeout and non-timeout
// cases. // cases.
timedReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".tar.gz") timedReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, secondCommit+".tar.gz")
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, timedReq) assert.NotNil(t, timedReq)
doArchive(t.Context(), timedReq) doArchive(t.Context(), timedReq)
zipReq2, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") zipReq2, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip")
assert.NoError(t, err) assert.NoError(t, err)
// Now, we're guaranteed to have released the original zipReq from the queue. // Now, we're guaranteed to have released the original zipReq from the queue.
// Ensure that we don't get handed back the released entry somehow, but they // Ensure that we don't get handed back the released entry somehow, but they