Skip to content

Commit be017b7

Browse files
committed
Add github compatible tarball download API endpoints
1 parent 0d5abd9 commit be017b7

File tree

6 files changed

+135
-19
lines changed

6 files changed

+135
-19
lines changed

routers/api/v1/api.go

+2
Original file line numberDiff line numberDiff line change
@@ -1377,6 +1377,8 @@ func Routes() *web.Router {
13771377
m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar)
13781378
m.Delete("", repo.DeleteAvatar)
13791379
}, reqAdmin(), reqToken())
1380+
1381+
m.Get("/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive)
13801382
}, repoAssignment(), checkTokenPublicOnly())
13811383
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
13821384

routers/api/v1/repo/download.go

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"fmt"
8+
"net/http"
9+
10+
"code.gitea.io/gitea/modules/git"
11+
"code.gitea.io/gitea/modules/gitrepo"
12+
"code.gitea.io/gitea/services/context"
13+
archiver_service "code.gitea.io/gitea/services/repository/archiver"
14+
)
15+
16+
func DownloadArchive(ctx *context.APIContext) {
17+
var tp git.ArchiveType
18+
switch ballType := ctx.PathParam("ball_type"); ballType {
19+
case "tarball":
20+
tp = git.TARGZ
21+
case "zipball":
22+
tp = git.ZIP
23+
case "bundle":
24+
tp = git.BUNDLE
25+
default:
26+
ctx.Error(http.StatusBadRequest, "", fmt.Sprintf("Unknown archive type: %s", ballType))
27+
return
28+
}
29+
30+
if ctx.Repo.GitRepo == nil {
31+
gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
32+
if err != nil {
33+
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
34+
return
35+
}
36+
ctx.Repo.GitRepo = gitRepo
37+
defer gitRepo.Close()
38+
}
39+
40+
r, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"), tp)
41+
if err != nil {
42+
ctx.ServerError("NewRequest", err)
43+
return
44+
}
45+
46+
archive, err := r.Await(ctx)
47+
if err != nil {
48+
ctx.ServerError("archive.Await", err)
49+
return
50+
}
51+
52+
download(ctx, r.GetArchiveName(), archive)
53+
}

routers/api/v1/repo/file.go

+12-3
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,13 @@ func GetArchive(ctx *context.APIContext) {
301301

302302
func archiveDownload(ctx *context.APIContext) {
303303
uri := ctx.PathParam("*")
304-
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
304+
ext, tp, err := archiver_service.ParseFileName(uri)
305+
if err != nil {
306+
ctx.Error(http.StatusBadRequest, "ParseFileName", err)
307+
return
308+
}
309+
310+
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp)
305311
if err != nil {
306312
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
307313
ctx.Error(http.StatusBadRequest, "unknown archive format", err)
@@ -327,9 +333,12 @@ func download(ctx *context.APIContext, archiveName string, archiver *repo_model.
327333

328334
// Add nix format link header so tarballs lock correctly:
329335
// https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md
330-
ctx.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.tar.gz?rev=%s>; rel="immutable"`,
336+
ctx.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.%s?rev=%s>; rel="immutable"`,
331337
ctx.Repo.Repository.APIURL(),
332-
archiver.CommitID, archiver.CommitID))
338+
archiver.CommitID,
339+
archiver.Type.String(),
340+
archiver.CommitID,
341+
))
333342

334343
rPath := archiver.RelativePath()
335344
if setting.RepoArchive.Storage.ServeDirect() {

routers/web/repo/repo.go

+12-2
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,12 @@ func RedirectDownload(ctx *context.Context) {
461461
// Download an archive of a repository
462462
func Download(ctx *context.Context) {
463463
uri := ctx.PathParam("*")
464-
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
464+
ext, tp, err := archiver_service.ParseFileName(uri)
465+
if err != nil {
466+
ctx.ServerError("ParseFileName", err)
467+
return
468+
}
469+
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp)
465470
if err != nil {
466471
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
467472
ctx.Error(http.StatusBadRequest, err.Error())
@@ -520,7 +525,12 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep
520525
// kind of drop it on the floor if this is the case.
521526
func InitiateDownload(ctx *context.Context) {
522527
uri := ctx.PathParam("*")
523-
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
528+
ext, tp, err := archiver_service.ParseFileName(uri)
529+
if err != nil {
530+
ctx.ServerError("ParseFileName", err)
531+
return
532+
}
533+
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp)
524534
if err != nil {
525535
ctx.ServerError("archiver_service.NewRequest", err)
526536
return

services/repository/archiver/archiver.go

+16-14
Original file line numberDiff line numberDiff line change
@@ -67,30 +67,32 @@ func (e RepoRefNotFoundError) Is(err error) bool {
6767
return ok
6868
}
6969

70-
// NewRequest creates an archival request, based on the URI. The
71-
// resulting ArchiveRequest is suitable for being passed to Await()
72-
// if it's determined that the request still needs to be satisfied.
73-
func NewRequest(repoID int64, repo *git.Repository, uri string) (*ArchiveRequest, error) {
74-
r := &ArchiveRequest{
75-
RepoID: repoID,
76-
}
77-
78-
var ext string
70+
func ParseFileName(uri string) (ext string, tp git.ArchiveType, err error) {
7971
switch {
8072
case strings.HasSuffix(uri, ".zip"):
8173
ext = ".zip"
82-
r.Type = git.ZIP
74+
tp = git.ZIP
8375
case strings.HasSuffix(uri, ".tar.gz"):
8476
ext = ".tar.gz"
85-
r.Type = git.TARGZ
77+
tp = git.TARGZ
8678
case strings.HasSuffix(uri, ".bundle"):
8779
ext = ".bundle"
88-
r.Type = git.BUNDLE
80+
tp = git.BUNDLE
8981
default:
90-
return nil, ErrUnknownArchiveFormat{RequestFormat: uri}
82+
return "", 0, ErrUnknownArchiveFormat{RequestFormat: uri}
9183
}
84+
return
85+
}
9286

93-
r.refName = strings.TrimSuffix(uri, ext)
87+
// NewRequest creates an archival request, based on the URI. The
88+
// resulting ArchiveRequest is suitable for being passed to Await()
89+
// if it's determined that the request still needs to be satisfied.
90+
func NewRequest(repoID int64, repo *git.Repository, refName string, fileType git.ArchiveType) (*ArchiveRequest, error) {
91+
r := &ArchiveRequest{
92+
RepoID: repoID,
93+
refName: refName,
94+
Type: fileType,
95+
}
9496

9597
// Get corresponding commit.
9698
commitID, err := repo.ConvertToGitID(r.refName)

tests/integration/api_repo_archive_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,43 @@ func TestAPIDownloadArchive(t *testing.T) {
5959
link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master", user2.Name, repo.Name))
6060
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusBadRequest)
6161
}
62+
63+
func TestAPIDownloadArchive2(t *testing.T) {
64+
defer tests.PrepareTestEnv(t)()
65+
66+
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
67+
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
68+
session := loginUser(t, user2.LowerName)
69+
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
70+
71+
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/zipball/master", user2.Name, repo.Name))
72+
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
73+
bs, err := io.ReadAll(resp.Body)
74+
assert.NoError(t, err)
75+
assert.Len(t, bs, 320)
76+
77+
link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/tarball/master", user2.Name, repo.Name))
78+
resp = MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
79+
bs, err = io.ReadAll(resp.Body)
80+
assert.NoError(t, err)
81+
assert.Len(t, bs, 266)
82+
83+
// Must return a link to a commit ID as the "immutable" archive link
84+
linkHeaderRe := regexp.MustCompile(`^<(https?://.*/api/v1/repos/user2/repo1/archive/[a-f0-9]+\.tar\.gz.*)>; rel="immutable"$`)
85+
m := linkHeaderRe.FindStringSubmatch(resp.Header().Get("Link"))
86+
assert.NotEmpty(t, m[1])
87+
resp = MakeRequest(t, NewRequest(t, "GET", m[1]).AddTokenAuth(token), http.StatusOK)
88+
bs2, err := io.ReadAll(resp.Body)
89+
assert.NoError(t, err)
90+
// The locked URL should give the same bytes as the non-locked one
91+
assert.EqualValues(t, bs, bs2)
92+
93+
link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/bundle/master", user2.Name, repo.Name))
94+
resp = MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
95+
bs, err = io.ReadAll(resp.Body)
96+
assert.NoError(t, err)
97+
assert.Len(t, bs, 382)
98+
99+
link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master", user2.Name, repo.Name))
100+
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusBadRequest)
101+
}

0 commit comments

Comments
 (0)