Skip to content

Commit 06f968d

Browse files
zeripathpetergardfjalllunny
authored
Fix hard-coded timeout and error panic in API archive download endpoint (go-gitea#20925) (go-gitea#21051)
Backport go-gitea#20925 This commit updates the `GET /api/v1/repos/{owner}/{repo}/archive/{archive}` endpoint which prior to this PR had a couple of issues. 1. The endpoint had a hard-coded 20s timeout for the archiver to complete after which a 500 (Internal Server Error) was returned to client. For a scripted API client there was no clear way of telling that the operation timed out and that it should retry. 2. Whenever the timeout _did occur_, the code used to panic. This was caused by the API endpoint "delegating" to the same call path as the web, which uses a slightly different way of reporting errors (HTML rather than JSON for example). More specifically, `api/v1/repo/file.go#GetArchive` just called through to `web/repo/repo.go#Download`, which expects the `Context` to have a `Render` field set, but which is `nil` for API calls. Hence, a `nil` pointer error. The code addresses (1) by dropping the hard-coded timeout. Instead, any timeout/cancelation on the incoming `Context` is used. The code addresses (2) by updating the API endpoint to use a separate call path for the API-triggered archive download. This avoids producing HTML-errors on errors (it now produces JSON errors). Signed-off-by: Peter Gardfjäll <[email protected]> Signed-off-by: Peter Gardfjäll <[email protected]> Signed-off-by: Andrew Thornton <[email protected]> Co-authored-by: Peter Gardfjäll <[email protected]> Co-authored-by: Lunny Xiao <[email protected]>
1 parent 084797b commit 06f968d

File tree

7 files changed

+127
-71
lines changed

7 files changed

+127
-71
lines changed

cmd/migrate_storage.go

+2-5
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,8 @@ func migrateRepoAvatars(ctx context.Context, dstStorage storage.ObjectStorage) e
112112

113113
func migrateRepoArchivers(ctx context.Context, dstStorage storage.ObjectStorage) error {
114114
return db.IterateObjects(ctx, func(archiver *repo_model.RepoArchiver) error {
115-
p, err := archiver.RelativePath()
116-
if err != nil {
117-
return err
118-
}
119-
_, err = storage.Copy(dstStorage, p, storage.RepoArchives, p)
115+
p := archiver.RelativePath()
116+
_, err := storage.Copy(dstStorage, p, storage.RepoArchives, p)
120117
return err
121118
})
122119
}

models/repo.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
385385

386386
archivePaths := make([]string, 0, len(archives))
387387
for _, v := range archives {
388-
p, _ := v.RelativePath()
389-
archivePaths = append(archivePaths, p)
388+
archivePaths = append(archivePaths, v.RelativePath())
390389
}
391390

392391
if _, err := db.DeleteByBean(ctx, &repo_model.RepoArchiver{RepoID: repoID}); err != nil {

models/repo/archiver.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ func init() {
3939
db.RegisterModel(new(RepoArchiver))
4040
}
4141

42-
// RelativePath returns relative path
43-
func (archiver *RepoArchiver) RelativePath() (string, error) {
44-
return fmt.Sprintf("%d/%s/%s.%s", archiver.RepoID, archiver.CommitID[:2], archiver.CommitID, archiver.Type.String()), nil
42+
// RelativePath returns the archive path relative to the archive storage root.
43+
func (archiver *RepoArchiver) RelativePath() string {
44+
return fmt.Sprintf("%d/%s/%s.%s", archiver.RepoID, archiver.CommitID[:2], archiver.CommitID, archiver.Type.String())
4545
}
4646

4747
var delRepoArchiver = new(RepoArchiver)

modules/timeutil/timestamp.go

+5
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ func (ts TimeStamp) AsTime() (tm time.Time) {
5454
return ts.AsTimeInLocation(setting.DefaultUILocation)
5555
}
5656

57+
// AsLocalTime convert timestamp as time.Time in local location
58+
func (ts TimeStamp) AsLocalTime() time.Time {
59+
return time.Unix(int64(ts), 0)
60+
}
61+
5762
// AsTimeInLocation convert timestamp as time.Time in Local locale
5863
func (ts TimeStamp) AsTimeInLocation(loc *time.Location) (tm time.Time) {
5964
tm = time.Unix(int64(ts), 0).In(loc)

routers/api/v1/repo/file.go

+49-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package repo
88
import (
99
"bytes"
1010
"encoding/base64"
11+
"errors"
1112
"fmt"
1213
"io"
1314
"net/http"
@@ -29,7 +30,7 @@ import (
2930
api "code.gitea.io/gitea/modules/structs"
3031
"code.gitea.io/gitea/modules/web"
3132
"code.gitea.io/gitea/routers/common"
32-
"code.gitea.io/gitea/routers/web/repo"
33+
archiver_service "code.gitea.io/gitea/services/repository/archiver"
3334
files_service "code.gitea.io/gitea/services/repository/files"
3435
)
3536

@@ -294,7 +295,53 @@ func GetArchive(ctx *context.APIContext) {
294295
defer gitRepo.Close()
295296
}
296297

297-
repo.Download(ctx.Context)
298+
archiveDownload(ctx)
299+
}
300+
301+
func archiveDownload(ctx *context.APIContext) {
302+
uri := ctx.Params("*")
303+
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
304+
if err != nil {
305+
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
306+
ctx.Error(http.StatusBadRequest, "unknown archive format", err)
307+
} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
308+
ctx.Error(http.StatusNotFound, "unrecognized reference", err)
309+
} else {
310+
ctx.ServerError("archiver_service.NewRequest", err)
311+
}
312+
return
313+
}
314+
315+
archiver, err := aReq.Await(ctx)
316+
if err != nil {
317+
ctx.ServerError("archiver.Await", err)
318+
return
319+
}
320+
321+
download(ctx, aReq.GetArchiveName(), archiver)
322+
}
323+
324+
func download(ctx *context.APIContext, archiveName string, archiver *repo_model.RepoArchiver) {
325+
downloadName := ctx.Repo.Repository.Name + "-" + archiveName
326+
327+
rPath := archiver.RelativePath()
328+
if setting.RepoArchive.ServeDirect {
329+
// If we have a signed url (S3, object storage), redirect to this directly.
330+
u, err := storage.RepoArchives.URL(rPath, downloadName)
331+
if u != nil && err == nil {
332+
ctx.Redirect(u.String())
333+
return
334+
}
335+
}
336+
337+
// If we have matched and access to release or issue
338+
fr, err := storage.RepoArchives.Open(rPath)
339+
if err != nil {
340+
ctx.ServerError("Open", err)
341+
return
342+
}
343+
defer fr.Close()
344+
ctx.ServeContent(downloadName, fr, archiver.CreatedUnix.AsLocalTime())
298345
}
299346

300347
// GetEditorconfig get editor config of a repository

routers/web/repo/repo.go

+6-49
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"fmt"
1111
"net/http"
1212
"strings"
13-
"time"
1413

1514
"code.gitea.io/gitea/models"
1615
"code.gitea.io/gitea/models/db"
@@ -21,7 +20,6 @@ import (
2120
"code.gitea.io/gitea/modules/base"
2221
"code.gitea.io/gitea/modules/context"
2322
"code.gitea.io/gitea/modules/convert"
24-
"code.gitea.io/gitea/modules/graceful"
2523
"code.gitea.io/gitea/modules/log"
2624
repo_module "code.gitea.io/gitea/modules/repository"
2725
"code.gitea.io/gitea/modules/setting"
@@ -389,68 +387,27 @@ func Download(ctx *context.Context) {
389387
if err != nil {
390388
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
391389
ctx.Error(http.StatusBadRequest, err.Error())
390+
} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
391+
ctx.Error(http.StatusNotFound, err.Error())
392392
} else {
393393
ctx.ServerError("archiver_service.NewRequest", err)
394394
}
395395
return
396396
}
397-
if aReq == nil {
398-
ctx.Error(http.StatusNotFound)
399-
return
400-
}
401397

402-
archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
398+
archiver, err := aReq.Await(ctx)
403399
if err != nil {
404-
ctx.ServerError("models.GetRepoArchiver", err)
400+
ctx.ServerError("archiver.Await", err)
405401
return
406402
}
407-
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
408-
download(ctx, aReq.GetArchiveName(), archiver)
409-
return
410-
}
411-
412-
if err := archiver_service.StartArchive(aReq); err != nil {
413-
ctx.ServerError("archiver_service.StartArchive", err)
414-
return
415-
}
416-
417-
var times int
418-
t := time.NewTicker(time.Second * 1)
419-
defer t.Stop()
420403

421-
for {
422-
select {
423-
case <-graceful.GetManager().HammerContext().Done():
424-
log.Warn("exit archive download because system stop")
425-
return
426-
case <-t.C:
427-
if times > 20 {
428-
ctx.ServerError("wait download timeout", nil)
429-
return
430-
}
431-
times++
432-
archiver, err = repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
433-
if err != nil {
434-
ctx.ServerError("archiver_service.StartArchive", err)
435-
return
436-
}
437-
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
438-
download(ctx, aReq.GetArchiveName(), archiver)
439-
return
440-
}
441-
}
442-
}
404+
download(ctx, aReq.GetArchiveName(), archiver)
443405
}
444406

445407
func download(ctx *context.Context, archiveName string, archiver *repo_model.RepoArchiver) {
446408
downloadName := ctx.Repo.Repository.Name + "-" + archiveName
447409

448-
rPath, err := archiver.RelativePath()
449-
if err != nil {
450-
ctx.ServerError("archiver.RelativePath", err)
451-
return
452-
}
453-
410+
rPath := archiver.RelativePath()
454411
if setting.RepoArchive.ServeDirect {
455412
// If we have a signed url (S3, object storage), redirect to this directly.
456413
u, err := storage.RepoArchives.URL(rPath, downloadName)

services/repository/archiver/archiver.go

+61-10
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,21 @@ func (ErrUnknownArchiveFormat) Is(err error) bool {
5757
return ok
5858
}
5959

60+
// RepoRefNotFoundError is returned when a requested reference (commit, tag) was not found.
61+
type RepoRefNotFoundError struct {
62+
RefName string
63+
}
64+
65+
// Error implements error.
66+
func (e RepoRefNotFoundError) Error() string {
67+
return fmt.Sprintf("unrecognized repository reference: %s", e.RefName)
68+
}
69+
70+
func (e RepoRefNotFoundError) Is(err error) bool {
71+
_, ok := err.(RepoRefNotFoundError)
72+
return ok
73+
}
74+
6075
// NewRequest creates an archival request, based on the URI. The
6176
// resulting ArchiveRequest is suitable for being passed to ArchiveRepository()
6277
// if it's determined that the request still needs to be satisfied.
@@ -103,7 +118,7 @@ func NewRequest(repoID int64, repo *git.Repository, uri string) (*ArchiveRequest
103118
}
104119
}
105120
} else {
106-
return nil, fmt.Errorf("Unknow ref %s type", r.refName)
121+
return nil, RepoRefNotFoundError{RefName: r.refName}
107122
}
108123

109124
return r, nil
@@ -115,6 +130,49 @@ func (aReq *ArchiveRequest) GetArchiveName() string {
115130
return strings.ReplaceAll(aReq.refName, "/", "-") + "." + aReq.Type.String()
116131
}
117132

133+
// Await awaits the completion of an ArchiveRequest. If the archive has
134+
// already been prepared the method returns immediately. Otherwise an archiver
135+
// process will be started and its completion awaited. On success the returned
136+
// RepoArchiver may be used to download the archive. Note that even if the
137+
// context is cancelled/times out a started archiver will still continue to run
138+
// in the background.
139+
func (aReq *ArchiveRequest) Await(ctx context.Context) (*repo_model.RepoArchiver, error) {
140+
archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
141+
if err != nil {
142+
return nil, fmt.Errorf("models.GetRepoArchiver: %v", err)
143+
}
144+
145+
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
146+
// Archive already generated, we're done.
147+
return archiver, nil
148+
}
149+
150+
if err := StartArchive(aReq); err != nil {
151+
return nil, fmt.Errorf("archiver.StartArchive: %v", err)
152+
}
153+
154+
poll := time.NewTicker(time.Second * 1)
155+
defer poll.Stop()
156+
157+
for {
158+
select {
159+
case <-graceful.GetManager().HammerContext().Done():
160+
// System stopped.
161+
return nil, graceful.GetManager().HammerContext().Err()
162+
case <-ctx.Done():
163+
return nil, ctx.Err()
164+
case <-poll.C:
165+
archiver, err = repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
166+
if err != nil {
167+
return nil, fmt.Errorf("repo_model.GetRepoArchiver: %v", err)
168+
}
169+
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
170+
return archiver, nil
171+
}
172+
}
173+
}
174+
}
175+
118176
func doArchive(r *ArchiveRequest) (*repo_model.RepoArchiver, error) {
119177
txCtx, committer, err := db.TxContext()
120178
if err != nil {
@@ -147,11 +205,7 @@ func doArchive(r *ArchiveRequest) (*repo_model.RepoArchiver, error) {
147205
}
148206
}
149207

150-
rPath, err := archiver.RelativePath()
151-
if err != nil {
152-
return nil, err
153-
}
154-
208+
rPath := archiver.RelativePath()
155209
_, err = storage.RepoArchives.Stat(rPath)
156210
if err == nil {
157211
if archiver.Status == repo_model.ArchiverGenerating {
@@ -284,13 +338,10 @@ func StartArchive(request *ArchiveRequest) error {
284338
}
285339

286340
func deleteOldRepoArchiver(ctx context.Context, archiver *repo_model.RepoArchiver) error {
287-
p, err := archiver.RelativePath()
288-
if err != nil {
289-
return err
290-
}
291341
if err := repo_model.DeleteRepoArchiver(ctx, archiver); err != nil {
292342
return err
293343
}
344+
p := archiver.RelativePath()
294345
if err := storage.RepoArchives.Delete(p); err != nil {
295346
log.Error("delete repo archive file failed: %v", err)
296347
}

0 commit comments

Comments
 (0)