Skip to content

Commit 5f4d0db

Browse files
committed
git-annex: start archive downloads immediately
1 parent 3e18b6c commit 5f4d0db

File tree

7 files changed

+119
-133
lines changed

7 files changed

+119
-133
lines changed

modules/annex/utils.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package annex
5+
6+
import (
7+
"errors"
8+
"io"
9+
"io/fs"
10+
"math/rand"
11+
"os"
12+
"time"
13+
)
14+
15+
// UntilFileDeletedReader is a io.Reader that reads from reader until the file at path
16+
// is deleted and reader does not return any new bytes.
17+
type UntilFileDeletedReader struct {
18+
path string
19+
reader io.Reader
20+
}
21+
22+
func NewUntilFileDeletedReader(path string, reader io.Reader) UntilFileDeletedReader {
23+
return UntilFileDeletedReader{path, reader}
24+
}
25+
26+
func (r UntilFileDeletedReader) Read(buf []byte) (n int, err error) {
27+
n, err = r.reader.Read(buf)
28+
if err != io.EOF {
29+
return n, err
30+
}
31+
if n != 0 {
32+
return n, nil
33+
}
34+
_, err = os.Stat(r.path)
35+
if errors.Is(err, fs.ErrNotExist) {
36+
return 0, io.EOF
37+
}
38+
// Avoid hammering Read when there is no data, sleep for 0 to 1 seconds
39+
time.Sleep(time.Duration(rand.Intn(int(time.Second))))
40+
return 0, nil
41+
}

routers/api/v1/repo/file.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@ import (
1010
"errors"
1111
"fmt"
1212
"io"
13+
"math/rand"
1314
"net/http"
15+
"os"
1416
"path"
17+
"strconv"
1518
"strings"
1619
"time"
1720

1821
"code.gitea.io/gitea/models"
1922
git_model "code.gitea.io/gitea/models/git"
2023
repo_model "code.gitea.io/gitea/models/repo"
2124
"code.gitea.io/gitea/models/unit"
25+
"code.gitea.io/gitea/modules/annex"
2226
"code.gitea.io/gitea/modules/context"
2327
"code.gitea.io/gitea/modules/git"
2428
"code.gitea.io/gitea/modules/httpcache"
@@ -307,13 +311,39 @@ func archiveDownload(ctx *context.APIContext) {
307311
return
308312
}
309313

310-
archiver, err := aReq.Await(ctx)
314+
err = archiver_service.StartArchive(aReq)
311315
if err != nil {
312-
ctx.ServerError("archiver.Await", err)
316+
ctx.ServerError("archiver_service.StartArchive", err)
313317
return
314318
}
315319

316-
download(ctx, aReq.GetArchiveName(), archiver)
320+
for {
321+
archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
322+
if err != nil {
323+
ctx.ServerError("repo_model.GetRepoArchiver", err)
324+
return
325+
}
326+
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
327+
download(ctx, aReq.GetArchiveName(), archiver)
328+
return
329+
}
330+
if archiver != nil && archiver.Status == repo_model.ArchiverGenerating {
331+
filePath := os.TempDir() + "/" + strconv.FormatInt(archiver.RepoID, 10) + "-" + archiver.CommitID + "." + archiver.Type.String()
332+
f, err := os.Open(filePath)
333+
if err != nil {
334+
// The archiver might have finished in-between repo_model.GetRepoArchiver and os.Open, retry
335+
continue
336+
}
337+
defer f.Close()
338+
r := annex.NewUntilFileDeletedReader(filePath, f)
339+
downloadName := ctx.Repo.Repository.Name + "-" + aReq.GetArchiveName()
340+
// TODO: figure out how to serve the file with an approximate size of the resulting archive
341+
common.ServeContentByReader(ctx.Base, downloadName, -1, r)
342+
return
343+
}
344+
// archiver was nil, retry after 0 to 1 seconds
345+
time.Sleep(time.Duration(rand.Intn(int(time.Second))))
346+
}
317347
}
318348

319349
func download(ctx *context.APIContext, archiveName string, archiver *repo_model.RepoArchiver) {

routers/web/repo/repo.go

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ package repo
77
import (
88
"errors"
99
"fmt"
10+
"math/rand"
1011
"net/http"
12+
"os"
1113
"slices"
14+
"strconv"
1215
"strings"
16+
"time"
1317

1418
"code.gitea.io/gitea/models"
1519
"code.gitea.io/gitea/models/db"
@@ -19,6 +23,7 @@ import (
1923
repo_model "code.gitea.io/gitea/models/repo"
2024
"code.gitea.io/gitea/models/unit"
2125
user_model "code.gitea.io/gitea/models/user"
26+
"code.gitea.io/gitea/modules/annex"
2227
"code.gitea.io/gitea/modules/base"
2328
"code.gitea.io/gitea/modules/cache"
2429
"code.gitea.io/gitea/modules/context"
@@ -30,6 +35,7 @@ import (
3035
api "code.gitea.io/gitea/modules/structs"
3136
"code.gitea.io/gitea/modules/util"
3237
"code.gitea.io/gitea/modules/web"
38+
"code.gitea.io/gitea/routers/common"
3339
"code.gitea.io/gitea/services/convert"
3440
"code.gitea.io/gitea/services/forms"
3541
repo_service "code.gitea.io/gitea/services/repository"
@@ -429,13 +435,39 @@ func Download(ctx *context.Context) {
429435
return
430436
}
431437

432-
archiver, err := aReq.Await(ctx)
438+
err = archiver_service.StartArchive(aReq)
433439
if err != nil {
434-
ctx.ServerError("archiver.Await", err)
440+
ctx.ServerError("archiver_service.StartArchive", err)
435441
return
436442
}
437443

438-
download(ctx, aReq.GetArchiveName(), archiver)
444+
for {
445+
archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
446+
if err != nil {
447+
ctx.ServerError("repo_model.GetRepoArchiver", err)
448+
return
449+
}
450+
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
451+
download(ctx, aReq.GetArchiveName(), archiver)
452+
return
453+
}
454+
if archiver != nil && archiver.Status == repo_model.ArchiverGenerating {
455+
filePath := os.TempDir() + "/" + strconv.FormatInt(archiver.RepoID, 10) + "-" + archiver.CommitID + "." + archiver.Type.String()
456+
f, err := os.Open(filePath)
457+
if err != nil {
458+
// The archiver might have finished in-between repo_model.GetRepoArchiver and os.Open, retry
459+
continue
460+
}
461+
defer f.Close()
462+
r := annex.NewUntilFileDeletedReader(filePath, f)
463+
downloadName := ctx.Repo.Repository.Name + "-" + aReq.GetArchiveName()
464+
// TODO: figure out how to serve the file with an approximate size of the resulting archive
465+
common.ServeContentByReader(ctx.Base, downloadName, -1, r)
466+
return
467+
}
468+
// archiver was nil, retry after 0 to 1 seconds
469+
time.Sleep(time.Duration(rand.Intn(int(time.Second))))
470+
}
439471
}
440472

441473
func download(ctx *context.Context, archiveName string, archiver *repo_model.RepoArchiver) {
@@ -465,43 +497,6 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep
465497
})
466498
}
467499

468-
// InitiateDownload will enqueue an archival request, as needed. It may submit
469-
// a request that's already in-progress, but the archiver service will just
470-
// kind of drop it on the floor if this is the case.
471-
func InitiateDownload(ctx *context.Context) {
472-
uri := ctx.Params("*")
473-
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
474-
if err != nil {
475-
ctx.ServerError("archiver_service.NewRequest", err)
476-
return
477-
}
478-
if aReq == nil {
479-
ctx.Error(http.StatusNotFound)
480-
return
481-
}
482-
483-
archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
484-
if err != nil {
485-
ctx.ServerError("archiver_service.StartArchive", err)
486-
return
487-
}
488-
if archiver == nil || archiver.Status != repo_model.ArchiverReady {
489-
if err := archiver_service.StartArchive(aReq); err != nil {
490-
ctx.ServerError("archiver_service.StartArchive", err)
491-
return
492-
}
493-
}
494-
495-
var completed bool
496-
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
497-
completed = true
498-
}
499-
500-
ctx.JSON(http.StatusOK, map[string]any{
501-
"complete": completed,
502-
})
503-
}
504-
505500
// SearchRepo repositories via options
506501
func SearchRepo(ctx *context.Context) {
507502
opts := &repo_model.SearchRepoOptions{

routers/web/web.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1368,7 +1368,6 @@ func registerRoutes(m *web.Route) {
13681368

13691369
m.Group("/archive", func() {
13701370
m.Get("/*", repo.Download)
1371-
m.Post("/*", repo.InitiateDownload)
13721371
}, repo.MustBeNotEmpty, dlSourceEnabled, reqRepoCodeReader)
13731372

13741373
m.Group("/branches", func() {

services/repository/archiver/archiver.go

Lines changed: 10 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"io"
1111
"os"
1212
"regexp"
13+
"strconv"
1314
"strings"
1415
"time"
1516

@@ -130,49 +131,6 @@ func (aReq *ArchiveRequest) GetArchiveName() string {
130131
return strings.ReplaceAll(aReq.refName, "/", "-") + "." + aReq.Type.String()
131132
}
132133

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: %w", 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: %w", 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: %w", err)
168-
}
169-
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
170-
return archiver, nil
171-
}
172-
}
173-
}
174-
}
175-
176134
func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver, error) {
177135
txCtx, committer, err := db.TxContext(ctx)
178136
if err != nil {
@@ -285,7 +243,15 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver
285243
}
286244
committer.Close()
287245

288-
if _, err := storage.RepoArchives.Save(rPath, rd, -1); err != nil {
246+
f, err := os.Create(os.TempDir() + "/" + strconv.FormatInt(archiver.RepoID, 10) + "-" + archiver.CommitID + "." + archiver.Type.String())
247+
if err != nil {
248+
return nil, err
249+
}
250+
defer f.Close()
251+
defer os.Remove(f.Name())
252+
253+
trd := io.TeeReader(rd, f)
254+
if _, err := storage.RepoArchives.Save(rPath, trd, -1); err != nil {
289255
return nil, fmt.Errorf("unable to write archive: %w", err)
290256
}
291257

web_src/js/features/repo-common.js

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,6 @@
11
import $ from 'jquery';
22
import {hideElem, showElem} from '../utils/dom.js';
33

4-
const {csrfToken} = window.config;
5-
6-
function getArchive($target, url, first) {
7-
$.ajax({
8-
url,
9-
type: 'POST',
10-
data: {
11-
_csrf: csrfToken,
12-
},
13-
complete(xhr) {
14-
if (xhr.status === 200) {
15-
if (!xhr.responseJSON) {
16-
// XXX Shouldn't happen?
17-
$target.closest('.dropdown').children('i').removeClass('loading');
18-
return;
19-
}
20-
21-
if (!xhr.responseJSON.complete) {
22-
$target.closest('.dropdown').children('i').addClass('loading');
23-
// Wait for only three quarters of a second initially, in case it's
24-
// quickly archived.
25-
setTimeout(() => {
26-
getArchive($target, url, false);
27-
}, first ? 750 : 2000);
28-
} else {
29-
// We don't need to continue checking.
30-
$target.closest('.dropdown').children('i').removeClass('loading');
31-
window.location.href = url;
32-
}
33-
}
34-
},
35-
});
36-
}
37-
38-
export function initRepoArchiveLinks() {
39-
$('.archive-link').on('click', function (event) {
40-
event.preventDefault();
41-
const url = $(this).attr('href');
42-
if (!url) return;
43-
getArchive($(event.target), url, true);
44-
});
45-
}
46-
474
export function initRepoCloneLink() {
485
const $repoCloneSsh = $('#repo-clone-ssh');
496
const $repoCloneHttps = $('#repo-clone-https');

web_src/js/index.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ import {initRepoTemplateSearch} from './features/repo-template.js';
5656
import {initRepoCodeView} from './features/repo-code.js';
5757
import {initSshKeyFormParser} from './features/sshkey-helper.js';
5858
import {initUserSettings} from './features/user-settings.js';
59-
import {initRepoArchiveLinks} from './features/repo-common.js';
6059
import {initRepoMigrationStatusChecker} from './features/repo-migrate.js';
6160
import {
6261
initRepoSettingGitHook,
@@ -139,7 +138,6 @@ onDomReady(() => {
139138
initOrgTeamSettings();
140139

141140
initRepoActivityTopAuthorsChart();
142-
initRepoArchiveLinks();
143141
initRepoBranchButton();
144142
initRepoCodeView();
145143
initRepoCommentForm();

0 commit comments

Comments
 (0)