mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 11:07:18 -04:00 
			
		
		
		
	fix hard-coded timeout and error panic in API archive download endpoint (#20925)
* fix hard-coded timeout and error panic in API archive download endpoint
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 <peter.gardfjall.work@gmail.com>
			
			
This commit is contained in:
		| @@ -112,11 +112,8 @@ func migrateRepoAvatars(ctx context.Context, dstStorage storage.ObjectStorage) e | |||||||
|  |  | ||||||
| func migrateRepoArchivers(ctx context.Context, dstStorage storage.ObjectStorage) error { | func migrateRepoArchivers(ctx context.Context, dstStorage storage.ObjectStorage) error { | ||||||
| 	return db.IterateObjects(ctx, func(archiver *repo_model.RepoArchiver) error { | 	return db.IterateObjects(ctx, func(archiver *repo_model.RepoArchiver) error { | ||||||
| 		p, err := archiver.RelativePath() | 		p := archiver.RelativePath() | ||||||
| 		if err != nil { | 		_, err := storage.Copy(dstStorage, p, storage.RepoArchives, p) | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		_, err = storage.Copy(dstStorage, p, storage.RepoArchives, p) |  | ||||||
| 		return err | 		return err | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -218,8 +218,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { | |||||||
|  |  | ||||||
| 	archivePaths := make([]string, 0, len(archives)) | 	archivePaths := make([]string, 0, len(archives)) | ||||||
| 	for _, v := range archives { | 	for _, v := range archives { | ||||||
| 		p, _ := v.RelativePath() | 		archivePaths = append(archivePaths, v.RelativePath()) | ||||||
| 		archivePaths = append(archivePaths, p) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if _, err := db.DeleteByBean(ctx, &repo_model.RepoArchiver{RepoID: repoID}); err != nil { | 	if _, err := db.DeleteByBean(ctx, &repo_model.RepoArchiver{RepoID: repoID}); err != nil { | ||||||
|   | |||||||
| @@ -39,9 +39,9 @@ func init() { | |||||||
| 	db.RegisterModel(new(RepoArchiver)) | 	db.RegisterModel(new(RepoArchiver)) | ||||||
| } | } | ||||||
|  |  | ||||||
| // RelativePath returns relative path | // RelativePath returns the archive path relative to the archive storage root. | ||||||
| func (archiver *RepoArchiver) RelativePath() (string, error) { | func (archiver *RepoArchiver) RelativePath() string { | ||||||
| 	return fmt.Sprintf("%d/%s/%s.%s", archiver.RepoID, archiver.CommitID[:2], archiver.CommitID, archiver.Type.String()), nil | 	return fmt.Sprintf("%d/%s/%s.%s", archiver.RepoID, archiver.CommitID[:2], archiver.CommitID, archiver.Type.String()) | ||||||
| } | } | ||||||
|  |  | ||||||
| var delRepoArchiver = new(RepoArchiver) | var delRepoArchiver = new(RepoArchiver) | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ package repo | |||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| @@ -28,7 +29,7 @@ import ( | |||||||
| 	api "code.gitea.io/gitea/modules/structs" | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
| 	"code.gitea.io/gitea/routers/common" | 	"code.gitea.io/gitea/routers/common" | ||||||
| 	"code.gitea.io/gitea/routers/web/repo" | 	archiver_service "code.gitea.io/gitea/services/repository/archiver" | ||||||
| 	files_service "code.gitea.io/gitea/services/repository/files" | 	files_service "code.gitea.io/gitea/services/repository/files" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -294,7 +295,53 @@ func GetArchive(ctx *context.APIContext) { | |||||||
| 		defer gitRepo.Close() | 		defer gitRepo.Close() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	repo.Download(ctx.Context) | 	archiveDownload(ctx) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func archiveDownload(ctx *context.APIContext) { | ||||||
|  | 	uri := ctx.Params("*") | ||||||
|  | 	aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { | ||||||
|  | 			ctx.Error(http.StatusBadRequest, "unknown archive format", err) | ||||||
|  | 		} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) { | ||||||
|  | 			ctx.Error(http.StatusNotFound, "unrecognized reference", err) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.ServerError("archiver_service.NewRequest", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	archiver, err := aReq.Await(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("archiver.Await", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	download(ctx, aReq.GetArchiveName(), archiver) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func download(ctx *context.APIContext, archiveName string, archiver *repo_model.RepoArchiver) { | ||||||
|  | 	downloadName := ctx.Repo.Repository.Name + "-" + archiveName | ||||||
|  |  | ||||||
|  | 	rPath := archiver.RelativePath() | ||||||
|  | 	if setting.RepoArchive.ServeDirect { | ||||||
|  | 		// If we have a signed url (S3, object storage), redirect to this directly. | ||||||
|  | 		u, err := storage.RepoArchives.URL(rPath, downloadName) | ||||||
|  | 		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.ServerError("Open", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	defer fr.Close() | ||||||
|  | 	ctx.ServeContent(downloadName, fr, archiver.CreatedUnix.AsLocalTime()) | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetEditorconfig get editor config of a repository | // GetEditorconfig get editor config of a repository | ||||||
|   | |||||||
| @@ -10,7 +10,6 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| @@ -22,7 +21,6 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
| 	"code.gitea.io/gitea/modules/convert" | 	"code.gitea.io/gitea/modules/convert" | ||||||
| 	"code.gitea.io/gitea/modules/graceful" |  | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	repo_module "code.gitea.io/gitea/modules/repository" | 	repo_module "code.gitea.io/gitea/modules/repository" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| @@ -390,68 +388,27 @@ func Download(ctx *context.Context) { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { | 		if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { | ||||||
| 			ctx.Error(http.StatusBadRequest, err.Error()) | 			ctx.Error(http.StatusBadRequest, err.Error()) | ||||||
|  | 		} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) { | ||||||
|  | 			ctx.Error(http.StatusNotFound, err.Error()) | ||||||
| 		} else { | 		} else { | ||||||
| 			ctx.ServerError("archiver_service.NewRequest", err) | 			ctx.ServerError("archiver_service.NewRequest", err) | ||||||
| 		} | 		} | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if aReq == nil { |  | ||||||
| 		ctx.Error(http.StatusNotFound) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID) | 	archiver, err := aReq.Await(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError("models.GetRepoArchiver", err) | 		ctx.ServerError("archiver.Await", err) | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	if archiver != nil && archiver.Status == repo_model.ArchiverReady { |  | ||||||
| 		download(ctx, aReq.GetArchiveName(), archiver) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := archiver_service.StartArchive(aReq); err != nil { | 	download(ctx, aReq.GetArchiveName(), archiver) | ||||||
| 		ctx.ServerError("archiver_service.StartArchive", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var times int |  | ||||||
| 	t := time.NewTicker(time.Second * 1) |  | ||||||
| 	defer t.Stop() |  | ||||||
|  |  | ||||||
| 	for { |  | ||||||
| 		select { |  | ||||||
| 		case <-graceful.GetManager().HammerContext().Done(): |  | ||||||
| 			log.Warn("exit archive download because system stop") |  | ||||||
| 			return |  | ||||||
| 		case <-t.C: |  | ||||||
| 			if times > 20 { |  | ||||||
| 				ctx.ServerError("wait download timeout", nil) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 			times++ |  | ||||||
| 			archiver, err = repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID) |  | ||||||
| 			if err != nil { |  | ||||||
| 				ctx.ServerError("archiver_service.StartArchive", err) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 			if archiver != nil && archiver.Status == repo_model.ArchiverReady { |  | ||||||
| 				download(ctx, aReq.GetArchiveName(), archiver) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func download(ctx *context.Context, archiveName string, archiver *repo_model.RepoArchiver) { | func download(ctx *context.Context, archiveName string, archiver *repo_model.RepoArchiver) { | ||||||
| 	downloadName := ctx.Repo.Repository.Name + "-" + archiveName | 	downloadName := ctx.Repo.Repository.Name + "-" + archiveName | ||||||
|  |  | ||||||
| 	rPath, err := archiver.RelativePath() | 	rPath := archiver.RelativePath() | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.ServerError("archiver.RelativePath", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if setting.RepoArchive.ServeDirect { | 	if setting.RepoArchive.ServeDirect { | ||||||
| 		// If we have a signed url (S3, object storage), redirect to this directly. | 		// 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) | ||||||
|   | |||||||
| @@ -57,6 +57,21 @@ func (ErrUnknownArchiveFormat) Is(err error) bool { | |||||||
| 	return ok | 	return ok | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // RepoRefNotFoundError is returned when a requested reference (commit, tag) was not found. | ||||||
|  | type RepoRefNotFoundError struct { | ||||||
|  | 	RefName string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Error implements error. | ||||||
|  | func (e RepoRefNotFoundError) Error() string { | ||||||
|  | 	return fmt.Sprintf("unrecognized repository reference: %s", e.RefName) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e RepoRefNotFoundError) Is(err error) bool { | ||||||
|  | 	_, ok := err.(RepoRefNotFoundError) | ||||||
|  | 	return ok | ||||||
|  | } | ||||||
|  |  | ||||||
| // 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 ArchiveRepository() | // resulting ArchiveRequest is suitable for being passed to ArchiveRepository() | ||||||
| // if it's determined that the request still needs to be satisfied. | // 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 | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		return nil, fmt.Errorf("Unknow ref %s type", r.refName) | 		return nil, RepoRefNotFoundError{RefName: r.refName} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return r, nil | 	return r, nil | ||||||
| @@ -115,6 +130,49 @@ func (aReq *ArchiveRequest) GetArchiveName() string { | |||||||
| 	return strings.ReplaceAll(aReq.refName, "/", "-") + "." + aReq.Type.String() | 	return strings.ReplaceAll(aReq.refName, "/", "-") + "." + aReq.Type.String() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Await awaits the completion of an ArchiveRequest. If the archive has | ||||||
|  | // already been prepared the method returns immediately. Otherwise an archiver | ||||||
|  | // process will be started and its completion awaited. On success the returned | ||||||
|  | // RepoArchiver may be used to download the archive. Note that even if the | ||||||
|  | // context is cancelled/times out a started archiver will still continue to run | ||||||
|  | // in the background. | ||||||
|  | func (aReq *ArchiveRequest) Await(ctx context.Context) (*repo_model.RepoArchiver, error) { | ||||||
|  | 	archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("models.GetRepoArchiver: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if archiver != nil && archiver.Status == repo_model.ArchiverReady { | ||||||
|  | 		// Archive already generated, we're done. | ||||||
|  | 		return archiver, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := StartArchive(aReq); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("archiver.StartArchive: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	poll := time.NewTicker(time.Second * 1) | ||||||
|  | 	defer poll.Stop() | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-graceful.GetManager().HammerContext().Done(): | ||||||
|  | 			// System stopped. | ||||||
|  | 			return nil, graceful.GetManager().HammerContext().Err() | ||||||
|  | 		case <-ctx.Done(): | ||||||
|  | 			return nil, ctx.Err() | ||||||
|  | 		case <-poll.C: | ||||||
|  | 			archiver, err = repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, fmt.Errorf("repo_model.GetRepoArchiver: %v", err) | ||||||
|  | 			} | ||||||
|  | 			if archiver != nil && archiver.Status == repo_model.ArchiverReady { | ||||||
|  | 				return archiver, nil | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func doArchive(r *ArchiveRequest) (*repo_model.RepoArchiver, error) { | func doArchive(r *ArchiveRequest) (*repo_model.RepoArchiver, error) { | ||||||
| 	txCtx, committer, err := db.TxContext() | 	txCtx, committer, err := db.TxContext() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -147,11 +205,7 @@ func doArchive(r *ArchiveRequest) (*repo_model.RepoArchiver, error) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	rPath, err := archiver.RelativePath() | 	rPath := archiver.RelativePath() | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	_, err = storage.RepoArchives.Stat(rPath) | 	_, err = storage.RepoArchives.Stat(rPath) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		if archiver.Status == repo_model.ArchiverGenerating { | 		if archiver.Status == repo_model.ArchiverGenerating { | ||||||
| @@ -284,13 +338,10 @@ func StartArchive(request *ArchiveRequest) error { | |||||||
| } | } | ||||||
|  |  | ||||||
| func deleteOldRepoArchiver(ctx context.Context, archiver *repo_model.RepoArchiver) error { | func deleteOldRepoArchiver(ctx context.Context, archiver *repo_model.RepoArchiver) error { | ||||||
| 	p, err := archiver.RelativePath() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	if err := repo_model.DeleteRepoArchiver(ctx, archiver); err != nil { | 	if err := repo_model.DeleteRepoArchiver(ctx, archiver); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | 	p := archiver.RelativePath() | ||||||
| 	if err := storage.RepoArchives.Delete(p); err != nil { | 	if err := storage.RepoArchives.Delete(p); err != nil { | ||||||
| 		log.Error("delete repo archive file failed: %v", err) | 		log.Error("delete repo archive file failed: %v", err) | ||||||
| 	} | 	} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user