loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request '[Feat]Count downloads for tag archives' (#2976) from JakobDev/forgejo:archivecount into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2976
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>

+496 -96
+2
models/forgejo_migrations/migrate.go
··· 58 58 NewMigration("Add the `apply_to_admins` column to the `protected_branch` table", forgejo_v1_22.AddApplyToAdminsSetting), 59 59 // v9 -> v10 60 60 NewMigration("Add pronouns to user", forgejo_v1_22.AddPronounsToUser), 61 + // v11 -> v12 62 + NewMigration("Add repo_archive_download_count table", forgejo_v1_22.AddRepoArchiveDownloadCount), 61 63 } 62 64 63 65 // GetCurrentDBVersion returns the current Forgejo database version.
+18
models/forgejo_migrations/v1_22/v11.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package v1_22 //nolint 5 + 6 + import "xorm.io/xorm" 7 + 8 + func AddRepoArchiveDownloadCount(x *xorm.Engine) error { 9 + type RepoArchiveDownloadCount struct { 10 + ID int64 `xorm:"pk autoincr"` 11 + RepoID int64 `xorm:"index unique(s)"` 12 + ReleaseID int64 `xorm:"index unique(s)"` 13 + Type int `xorm:"unique(s)"` 14 + Count int64 15 + } 16 + 17 + return x.Sync(&RepoArchiveDownloadCount{}) 18 + }
+90
models/repo/archive_download_count.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package repo 5 + 6 + import ( 7 + "context" 8 + 9 + "code.gitea.io/gitea/models/db" 10 + "code.gitea.io/gitea/modules/git" 11 + api "code.gitea.io/gitea/modules/structs" 12 + ) 13 + 14 + // RepoArchiveDownloadCount counts all archive downloads for a tag 15 + type RepoArchiveDownloadCount struct { //nolint:revive 16 + ID int64 `xorm:"pk autoincr"` 17 + RepoID int64 `xorm:"index unique(s)"` 18 + ReleaseID int64 `xorm:"index unique(s)"` 19 + Type git.ArchiveType `xorm:"unique(s)"` 20 + Count int64 21 + } 22 + 23 + func init() { 24 + db.RegisterModel(new(RepoArchiveDownloadCount)) 25 + } 26 + 27 + // CountArchiveDownload adds one download the the given archive 28 + func CountArchiveDownload(ctx context.Context, repoID, releaseID int64, tp git.ArchiveType) error { 29 + updateCount, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).And("release_id = ?", releaseID).And("`type` = ?", tp).Incr("count").Update(new(RepoArchiveDownloadCount)) 30 + if err != nil { 31 + return err 32 + } 33 + 34 + if updateCount != 0 { 35 + // The count was updated, so we can exit 36 + return nil 37 + } 38 + 39 + // The archive does not esxists in the databse, so let's add it 40 + newCounter := &RepoArchiveDownloadCount{ 41 + RepoID: repoID, 42 + ReleaseID: releaseID, 43 + Type: tp, 44 + Count: 1, 45 + } 46 + 47 + _, err = db.GetEngine(ctx).Insert(newCounter) 48 + return err 49 + } 50 + 51 + // GetArchiveDownloadCount returns the download count of a tag 52 + func GetArchiveDownloadCount(ctx context.Context, repoID, releaseID int64) (*api.TagArchiveDownloadCount, error) { 53 + downloadCountList := make([]RepoArchiveDownloadCount, 0) 54 + err := db.GetEngine(ctx).Where("repo_id = ?", repoID).And("release_id = ?", releaseID).Find(&downloadCountList) 55 + if err != nil { 56 + return nil, err 57 + } 58 + 59 + tagCounter := new(api.TagArchiveDownloadCount) 60 + 61 + for _, singleCount := range downloadCountList { 62 + switch singleCount.Type { 63 + case git.ZIP: 64 + tagCounter.Zip = singleCount.Count 65 + case git.TARGZ: 66 + tagCounter.TarGz = singleCount.Count 67 + } 68 + } 69 + 70 + return tagCounter, nil 71 + } 72 + 73 + // GetDownloadCountForTagName returns the download count of a tag with the given name 74 + func GetArchiveDownloadCountForTagName(ctx context.Context, repoID int64, tagName string) (*api.TagArchiveDownloadCount, error) { 75 + release, err := GetRelease(ctx, repoID, tagName) 76 + if err != nil { 77 + if IsErrReleaseNotExist(err) { 78 + return new(api.TagArchiveDownloadCount), nil 79 + } 80 + return nil, err 81 + } 82 + 83 + return GetArchiveDownloadCount(ctx, repoID, release.ID) 84 + } 85 + 86 + // DeleteArchiveDownloadCountForRelease deletes the release from the repo_archive_download_count table 87 + func DeleteArchiveDownloadCountForRelease(ctx context.Context, releaseID int64) error { 88 + _, err := db.GetEngine(ctx).Delete(&RepoArchiveDownloadCount{ReleaseID: releaseID}) 89 + return err 90 + }
+65
models/repo/archive_download_count_test.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package repo_test 5 + 6 + import ( 7 + "testing" 8 + 9 + "code.gitea.io/gitea/models/db" 10 + repo_model "code.gitea.io/gitea/models/repo" 11 + "code.gitea.io/gitea/models/unittest" 12 + "code.gitea.io/gitea/modules/git" 13 + 14 + "github.com/stretchr/testify/assert" 15 + "github.com/stretchr/testify/require" 16 + ) 17 + 18 + func TestRepoArchiveDownloadCount(t *testing.T) { 19 + assert.NoError(t, unittest.PrepareTestDatabase()) 20 + 21 + release, err := repo_model.GetReleaseByID(db.DefaultContext, 1) 22 + require.NoError(t, err) 23 + 24 + // We have no count, so it should return 0 25 + downloadCount, err := repo_model.GetArchiveDownloadCount(db.DefaultContext, release.RepoID, release.ID) 26 + require.NoError(t, err) 27 + assert.Equal(t, int64(0), downloadCount.Zip) 28 + assert.Equal(t, int64(0), downloadCount.TarGz) 29 + 30 + // Set the TarGz counter to 1 31 + err = repo_model.CountArchiveDownload(db.DefaultContext, release.RepoID, release.ID, git.TARGZ) 32 + require.NoError(t, err) 33 + 34 + downloadCount, err = repo_model.GetArchiveDownloadCountForTagName(db.DefaultContext, release.RepoID, release.TagName) 35 + require.NoError(t, err) 36 + assert.Equal(t, int64(0), downloadCount.Zip) 37 + assert.Equal(t, int64(1), downloadCount.TarGz) 38 + 39 + // Set the TarGz counter to 2 40 + err = repo_model.CountArchiveDownload(db.DefaultContext, release.RepoID, release.ID, git.TARGZ) 41 + require.NoError(t, err) 42 + 43 + downloadCount, err = repo_model.GetArchiveDownloadCountForTagName(db.DefaultContext, release.RepoID, release.TagName) 44 + require.NoError(t, err) 45 + assert.Equal(t, int64(0), downloadCount.Zip) 46 + assert.Equal(t, int64(2), downloadCount.TarGz) 47 + 48 + // Set the Zip counter to 1 49 + err = repo_model.CountArchiveDownload(db.DefaultContext, release.RepoID, release.ID, git.ZIP) 50 + require.NoError(t, err) 51 + 52 + downloadCount, err = repo_model.GetArchiveDownloadCountForTagName(db.DefaultContext, release.RepoID, release.TagName) 53 + require.NoError(t, err) 54 + assert.Equal(t, int64(1), downloadCount.Zip) 55 + assert.Equal(t, int64(2), downloadCount.TarGz) 56 + 57 + // Delete the count 58 + err = repo_model.DeleteArchiveDownloadCountForRelease(db.DefaultContext, release.ID) 59 + require.NoError(t, err) 60 + 61 + downloadCount, err = repo_model.GetArchiveDownloadCountForTagName(db.DefaultContext, release.RepoID, release.TagName) 62 + require.NoError(t, err) 63 + assert.Equal(t, int64(0), downloadCount.Zip) 64 + assert.Equal(t, int64(0), downloadCount.TarGz) 65 + }
+1
models/repo/archiver.go
··· 35 35 Status ArchiverStatus 36 36 CommitID string `xorm:"VARCHAR(64) unique(s)"` 37 37 CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"` 38 + ReleaseID int64 `xorm:"-"` 38 39 } 39 40 40 41 func init() {
+48 -22
models/repo/release.go
··· 65 65 66 66 // Release represents a release of repository. 67 67 type Release struct { 68 - ID int64 `xorm:"pk autoincr"` 69 - RepoID int64 `xorm:"INDEX UNIQUE(n)"` 70 - Repo *Repository `xorm:"-"` 71 - PublisherID int64 `xorm:"INDEX"` 72 - Publisher *user_model.User `xorm:"-"` 73 - TagName string `xorm:"INDEX UNIQUE(n)"` 74 - OriginalAuthor string 75 - OriginalAuthorID int64 `xorm:"index"` 76 - LowerTagName string 77 - Target string 78 - TargetBehind string `xorm:"-"` // to handle non-existing or empty target 79 - Title string 80 - Sha1 string `xorm:"VARCHAR(64)"` 81 - NumCommits int64 82 - NumCommitsBehind int64 `xorm:"-"` 83 - Note string `xorm:"TEXT"` 84 - RenderedNote template.HTML `xorm:"-"` 85 - IsDraft bool `xorm:"NOT NULL DEFAULT false"` 86 - IsPrerelease bool `xorm:"NOT NULL DEFAULT false"` 87 - IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases 88 - Attachments []*Attachment `xorm:"-"` 89 - CreatedUnix timeutil.TimeStamp `xorm:"INDEX"` 68 + ID int64 `xorm:"pk autoincr"` 69 + RepoID int64 `xorm:"INDEX UNIQUE(n)"` 70 + Repo *Repository `xorm:"-"` 71 + PublisherID int64 `xorm:"INDEX"` 72 + Publisher *user_model.User `xorm:"-"` 73 + TagName string `xorm:"INDEX UNIQUE(n)"` 74 + OriginalAuthor string 75 + OriginalAuthorID int64 `xorm:"index"` 76 + LowerTagName string 77 + Target string 78 + TargetBehind string `xorm:"-"` // to handle non-existing or empty target 79 + Title string 80 + Sha1 string `xorm:"VARCHAR(64)"` 81 + NumCommits int64 82 + NumCommitsBehind int64 `xorm:"-"` 83 + Note string `xorm:"TEXT"` 84 + RenderedNote template.HTML `xorm:"-"` 85 + IsDraft bool `xorm:"NOT NULL DEFAULT false"` 86 + IsPrerelease bool `xorm:"NOT NULL DEFAULT false"` 87 + IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases 88 + Attachments []*Attachment `xorm:"-"` 89 + CreatedUnix timeutil.TimeStamp `xorm:"INDEX"` 90 + ArchiveDownloadCount *structs.TagArchiveDownloadCount `xorm:"-"` 90 91 } 91 92 92 93 func init() { ··· 112 113 } 113 114 } 114 115 } 116 + 117 + err = r.LoadArchiveDownloadCount(ctx) 118 + if err != nil { 119 + return err 120 + } 121 + 115 122 return GetReleaseAttachments(ctx, r) 123 + } 124 + 125 + // LoadArchiveDownloadCount loads the download count for the source archives 126 + func (r *Release) LoadArchiveDownloadCount(ctx context.Context) error { 127 + var err error 128 + r.ArchiveDownloadCount, err = GetArchiveDownloadCount(ctx, r.RepoID, r.ID) 129 + return err 116 130 } 117 131 118 132 // APIURL the api url for a release. release must have attributes loaded ··· 445 459 lowerTags := make([]string, 0, len(tags)) 446 460 for _, tag := range tags { 447 461 lowerTags = append(lowerTags, strings.ToLower(tag)) 462 + } 463 + 464 + for _, tag := range tags { 465 + release, err := GetRelease(ctx, repo.ID, tag) 466 + if err != nil { 467 + return fmt.Errorf("GetRelease: %w", err) 468 + } 469 + 470 + err = DeleteArchiveDownloadCountForRelease(ctx, release.ID) 471 + if err != nil { 472 + return fmt.Errorf("DeleteTagArchiveDownloadCount: %w", err) 473 + } 448 474 } 449 475 450 476 if _, err := db.GetEngine(ctx).
+9 -7
modules/git/tag.go
··· 8 8 "sort" 9 9 "strings" 10 10 11 + api "code.gitea.io/gitea/modules/structs" 11 12 "code.gitea.io/gitea/modules/util" 12 13 ) 13 14 ··· 20 21 21 22 // Tag represents a Git tag. 22 23 type Tag struct { 23 - Name string 24 - ID ObjectID 25 - Object ObjectID // The id of this commit object 26 - Type string 27 - Tagger *Signature 28 - Message string 29 - Signature *ObjectSignature 24 + Name string 25 + ID ObjectID 26 + Object ObjectID // The id of this commit object 27 + Type string 28 + Tagger *Signature 29 + Message string 30 + Signature *ObjectSignature 31 + ArchiveDownloadCount *api.TagArchiveDownloadCount 30 32 } 31 33 32 34 // Commit return the commit of the tag reference
+4 -3
modules/structs/release.go
··· 24 24 // swagger:strfmt date-time 25 25 CreatedAt time.Time `json:"created_at"` 26 26 // swagger:strfmt date-time 27 - PublishedAt time.Time `json:"published_at"` 28 - Publisher *User `json:"author"` 29 - Attachments []*Attachment `json:"assets"` 27 + PublishedAt time.Time `json:"published_at"` 28 + Publisher *User `json:"author"` 29 + Attachments []*Attachment `json:"assets"` 30 + ArchiveDownloadCount *TagArchiveDownloadCount `json:"archive_download_count"` 30 31 } 31 32 32 33 // CreateReleaseOption options when creating a release
+21 -13
modules/structs/repo_tag.go
··· 5 5 6 6 // Tag represents a repository tag 7 7 type Tag struct { 8 - Name string `json:"name"` 9 - Message string `json:"message"` 10 - ID string `json:"id"` 11 - Commit *CommitMeta `json:"commit"` 12 - ZipballURL string `json:"zipball_url"` 13 - TarballURL string `json:"tarball_url"` 8 + Name string `json:"name"` 9 + Message string `json:"message"` 10 + ID string `json:"id"` 11 + Commit *CommitMeta `json:"commit"` 12 + ZipballURL string `json:"zipball_url"` 13 + TarballURL string `json:"tarball_url"` 14 + ArchiveDownloadCount *TagArchiveDownloadCount `json:"archive_download_count"` 14 15 } 15 16 16 17 // AnnotatedTag represents an annotated tag 17 18 type AnnotatedTag struct { 18 - Tag string `json:"tag"` 19 - SHA string `json:"sha"` 20 - URL string `json:"url"` 21 - Message string `json:"message"` 22 - Tagger *CommitUser `json:"tagger"` 23 - Object *AnnotatedTagObject `json:"object"` 24 - Verification *PayloadCommitVerification `json:"verification"` 19 + Tag string `json:"tag"` 20 + SHA string `json:"sha"` 21 + URL string `json:"url"` 22 + Message string `json:"message"` 23 + Tagger *CommitUser `json:"tagger"` 24 + Object *AnnotatedTagObject `json:"object"` 25 + Verification *PayloadCommitVerification `json:"verification"` 26 + ArchiveDownloadCount *TagArchiveDownloadCount `json:"archive_download_count"` 25 27 } 26 28 27 29 // AnnotatedTagObject contains meta information of the tag object ··· 38 40 Message string `json:"message"` 39 41 Target string `json:"target"` 40 42 } 43 + 44 + // TagArchiveDownloadCount counts how many times a archive was downloaded 45 + type TagArchiveDownloadCount struct { 46 + Zip int64 `json:"zip"` 47 + TarGz int64 `json:"tar_gz"` 48 + }
+1 -1
routers/api/v1/repo/file.go
··· 302 302 303 303 func archiveDownload(ctx *context.APIContext) { 304 304 uri := ctx.Params("*") 305 - aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) 305 + aReq, err := archiver_service.NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) 306 306 if err != nil { 307 307 if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { 308 308 ctx.Error(http.StatusBadRequest, "unknown archive format", err)
+27
routers/api/v1/repo/tag.go
··· 60 60 61 61 apiTags := make([]*api.Tag, len(tags)) 62 62 for i := range tags { 63 + tags[i].ArchiveDownloadCount, err = repo_model.GetArchiveDownloadCountForTagName(ctx, ctx.Repo.Repository.ID, tags[i].Name) 64 + if err != nil { 65 + ctx.Error(http.StatusInternalServerError, "GetTagArchiveDownloadCountForName", err) 66 + return 67 + } 68 + 63 69 apiTags[i] = convert.ToTag(ctx.Repo.Repository, tags[i]) 64 70 } 65 71 ··· 111 117 if err != nil { 112 118 ctx.Error(http.StatusBadRequest, "GetAnnotatedTag", err) 113 119 } 120 + 121 + tag.ArchiveDownloadCount, err = repo_model.GetArchiveDownloadCountForTagName(ctx, ctx.Repo.Repository.ID, tag.Name) 122 + if err != nil { 123 + ctx.Error(http.StatusInternalServerError, "GetTagArchiveDownloadCountForName", err) 124 + return 125 + } 126 + 114 127 ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit)) 115 128 } 116 129 } ··· 150 163 ctx.NotFound(tagName) 151 164 return 152 165 } 166 + 167 + tag.ArchiveDownloadCount, err = repo_model.GetArchiveDownloadCountForTagName(ctx, ctx.Repo.Repository.ID, tag.Name) 168 + if err != nil { 169 + ctx.Error(http.StatusInternalServerError, "GetTagArchiveDownloadCountForName", err) 170 + return 171 + } 172 + 153 173 ctx.JSON(http.StatusOK, convert.ToTag(ctx.Repo.Repository, tag)) 154 174 } 155 175 ··· 218 238 ctx.InternalServerError(err) 219 239 return 220 240 } 241 + 242 + tag.ArchiveDownloadCount, err = repo_model.GetArchiveDownloadCountForTagName(ctx, ctx.Repo.Repository.ID, tag.Name) 243 + if err != nil { 244 + ctx.Error(http.StatusInternalServerError, "GetTagArchiveDownloadCountForName", err) 245 + return 246 + } 247 + 221 248 ctx.JSON(http.StatusCreated, convert.ToTag(ctx.Repo.Repository, tag)) 222 249 } 223 250
+11
routers/web/repo/release.go
··· 127 127 return nil, err 128 128 } 129 129 130 + err = r.LoadArchiveDownloadCount(ctx) 131 + if err != nil { 132 + return nil, err 133 + } 134 + 130 135 if !r.IsDraft { 131 136 if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil { 132 137 return nil, err ··· 353 358 ctx.Data["Title"] = release.TagName 354 359 } else { 355 360 ctx.Data["Title"] = release.Title 361 + } 362 + 363 + err = release.LoadArchiveDownloadCount(ctx) 364 + if err != nil { 365 + ctx.ServerError("LoadArchiveDownloadCount", err) 366 + return 356 367 } 357 368 358 369 ctx.Data["Releases"] = releases
+18 -2
routers/web/repo/repo.go
··· 456 456 // Download an archive of a repository 457 457 func Download(ctx *context.Context) { 458 458 uri := ctx.Params("*") 459 - aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) 459 + aReq, err := archiver_service.NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) 460 460 if err != nil { 461 461 if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { 462 462 ctx.Error(http.StatusBadRequest, err.Error()) ··· 485 485 // If we have a signed url (S3, object storage), redirect to this directly. 486 486 u, err := storage.RepoArchives.URL(rPath, downloadName) 487 487 if u != nil && err == nil { 488 + if archiver.ReleaseID != 0 { 489 + err = repo_model.CountArchiveDownload(ctx, ctx.Repo.Repository.ID, archiver.ReleaseID, archiver.Type) 490 + if err != nil { 491 + ctx.ServerError("CountArchiveDownload", err) 492 + return 493 + } 494 + } 495 + 488 496 ctx.Redirect(u.String()) 489 497 return 490 498 } ··· 498 506 } 499 507 defer fr.Close() 500 508 509 + if archiver.ReleaseID != 0 { 510 + err = repo_model.CountArchiveDownload(ctx, ctx.Repo.Repository.ID, archiver.ReleaseID, archiver.Type) 511 + if err != nil { 512 + ctx.ServerError("CountArchiveDownload", err) 513 + return 514 + } 515 + } 516 + 501 517 ctx.ServeContent(fr, &context.ServeHeaderOptions{ 502 518 Filename: downloadName, 503 519 LastModified: archiver.CreatedUnix.AsLocalTime(), ··· 509 525 // kind of drop it on the floor if this is the case. 510 526 func InitiateDownload(ctx *context.Context) { 511 527 uri := ctx.Params("*") 512 - aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) 528 + aReq, err := archiver_service.NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) 513 529 if err != nil { 514 530 ctx.ServerError("archiver_service.NewRequest", err) 515 531 return
+15 -13
services/convert/convert.go
··· 171 171 // ToTag convert a git.Tag to an api.Tag 172 172 func ToTag(repo *repo_model.Repository, t *git.Tag) *api.Tag { 173 173 return &api.Tag{ 174 - Name: t.Name, 175 - Message: strings.TrimSpace(t.Message), 176 - ID: t.ID.String(), 177 - Commit: ToCommitMeta(repo, t), 178 - ZipballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".zip"), 179 - TarballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".tar.gz"), 174 + Name: t.Name, 175 + Message: strings.TrimSpace(t.Message), 176 + ID: t.ID.String(), 177 + Commit: ToCommitMeta(repo, t), 178 + ZipballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".zip"), 179 + TarballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".tar.gz"), 180 + ArchiveDownloadCount: t.ArchiveDownloadCount, 180 181 } 181 182 } 182 183 ··· 349 350 // ToAnnotatedTag convert git.Tag to api.AnnotatedTag 350 351 func ToAnnotatedTag(ctx context.Context, repo *repo_model.Repository, t *git.Tag, c *git.Commit) *api.AnnotatedTag { 351 352 return &api.AnnotatedTag{ 352 - Tag: t.Name, 353 - SHA: t.ID.String(), 354 - Object: ToAnnotatedTagObject(repo, c), 355 - Message: t.Message, 356 - URL: util.URLJoin(repo.APIURL(), "git/tags", t.ID.String()), 357 - Tagger: ToCommitUser(t.Tagger), 358 - Verification: ToVerification(ctx, c), 353 + Tag: t.Name, 354 + SHA: t.ID.String(), 355 + Object: ToAnnotatedTagObject(repo, c), 356 + Message: t.Message, 357 + URL: util.URLJoin(repo.APIURL(), "git/tags", t.ID.String()), 358 + Tagger: ToCommitUser(t.Tagger), 359 + Verification: ToVerification(ctx, c), 360 + ArchiveDownloadCount: t.ArchiveDownloadCount, 359 361 } 360 362 } 361 363
+17 -16
services/convert/release.go
··· 13 13 // ToAPIRelease convert a repo_model.Release to api.Release 14 14 func ToAPIRelease(ctx context.Context, repo *repo_model.Repository, r *repo_model.Release) *api.Release { 15 15 return &api.Release{ 16 - ID: r.ID, 17 - TagName: r.TagName, 18 - Target: r.Target, 19 - Title: r.Title, 20 - Note: r.Note, 21 - URL: r.APIURL(), 22 - HTMLURL: r.HTMLURL(), 23 - TarURL: r.TarURL(), 24 - ZipURL: r.ZipURL(), 25 - UploadURL: r.APIUploadURL(), 26 - IsDraft: r.IsDraft, 27 - IsPrerelease: r.IsPrerelease, 28 - CreatedAt: r.CreatedUnix.AsTime(), 29 - PublishedAt: r.CreatedUnix.AsTime(), 30 - Publisher: ToUser(ctx, r.Publisher, nil), 31 - Attachments: ToAPIAttachments(repo, r.Attachments), 16 + ID: r.ID, 17 + TagName: r.TagName, 18 + Target: r.Target, 19 + Title: r.Title, 20 + Note: r.Note, 21 + URL: r.APIURL(), 22 + HTMLURL: r.HTMLURL(), 23 + TarURL: r.TarURL(), 24 + ZipURL: r.ZipURL(), 25 + UploadURL: r.APIUploadURL(), 26 + IsDraft: r.IsDraft, 27 + IsPrerelease: r.IsPrerelease, 28 + CreatedAt: r.CreatedUnix.AsTime(), 29 + PublishedAt: r.CreatedUnix.AsTime(), 30 + Publisher: ToUser(ctx, r.Publisher, nil), 31 + Attachments: ToAPIAttachments(repo, r.Attachments), 32 + ArchiveDownloadCount: r.ArchiveDownloadCount, 32 33 } 33 34 }
+3
services/doctor/dbconsistency.go
··· 227 227 // find redirects without existing user. 228 228 genericOrphanCheck("Orphaned Redirects without existing redirect user", 229 229 "user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"), 230 + // find archive download count without existing release 231 + genericOrphanCheck("Archive download count without existing Release", 232 + "repo_archive_download_count", "release", "repo_archive_download_count.release_id=release.id"), 230 233 ) 231 234 232 235 for _, c := range consistencyChecks {
+5
services/release/release.go
··· 318 318 } 319 319 } 320 320 321 + err = repo_model.DeleteArchiveDownloadCountForRelease(ctx, rel.ID) 322 + if err != nil { 323 + return err 324 + } 325 + 321 326 if stdout, _, err := git.NewCommand(ctx, "tag", "-d").AddDashesAndList(rel.TagName). 322 327 SetDescription(fmt.Sprintf("DeleteReleaseByID (git tag -d): %d", rel.ID)). 323 328 RunStdString(&git.RunOpts{Dir: repo.RepoPath()}); err != nil && !strings.Contains(err.Error(), "not found") {
+22 -5
services/repository/archiver/archiver.go
··· 30 30 // This is entirely opaque to external entities, though, and mostly used as a 31 31 // handle elsewhere. 32 32 type ArchiveRequest struct { 33 - RepoID int64 34 - refName string 35 - Type git.ArchiveType 36 - CommitID string 33 + RepoID int64 34 + refName string 35 + Type git.ArchiveType 36 + CommitID string 37 + ReleaseID int64 37 38 } 38 39 39 40 // ErrUnknownArchiveFormat request archive format is not supported ··· 70 71 // NewRequest creates an archival request, based on the URI. The 71 72 // resulting ArchiveRequest is suitable for being passed to ArchiveRepository() 72 73 // 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 + func NewRequest(ctx context.Context, repoID int64, repo *git.Repository, uri string) (*ArchiveRequest, error) { 74 75 r := &ArchiveRequest{ 75 76 RepoID: repoID, 76 77 } ··· 99 100 } 100 101 101 102 r.CommitID = commitID.String() 103 + 104 + release, err := repo_model.GetRelease(ctx, repoID, r.refName) 105 + if err != nil { 106 + if !repo_model.IsErrReleaseNotExist(err) { 107 + return nil, err 108 + } 109 + } 110 + if release != nil { 111 + r.ReleaseID = release.ID 112 + } 113 + 102 114 return r, nil 103 115 } 104 116 ··· 118 130 archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID) 119 131 if err != nil { 120 132 return nil, fmt.Errorf("models.GetRepoArchiver: %w", err) 133 + } 134 + 135 + if archiver != nil { 136 + archiver.ReleaseID = aReq.ReleaseID 121 137 } 122 138 123 139 if archiver != nil && archiver.Status == repo_model.ArchiverReady { ··· 145 161 return nil, fmt.Errorf("repo_model.GetRepoArchiver: %w", err) 146 162 } 147 163 if archiver != nil && archiver.Status == repo_model.ArchiverReady { 164 + archiver.ReleaseID = aReq.ReleaseID 148 165 return archiver, nil 149 166 } 150 167 }
+12 -12
services/repository/archiver/archiver_test.go
··· 31 31 contexttest.LoadGitRepo(t, ctx) 32 32 defer ctx.Repo.GitRepo.Close() 33 33 34 - bogusReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") 34 + bogusReq, err := NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") 35 35 assert.NoError(t, err) 36 36 assert.NotNil(t, bogusReq) 37 37 assert.EqualValues(t, firstCommit+".zip", bogusReq.GetArchiveName()) 38 38 39 39 // Check a series of bogus requests. 40 40 // Step 1, valid commit with a bad extension. 41 - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".dilbert") 41 + bogusReq, err = NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".dilbert") 42 42 assert.Error(t, err) 43 43 assert.Nil(t, bogusReq) 44 44 45 45 // Step 2, missing commit. 46 - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "dbffff.zip") 46 + bogusReq, err = NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "dbffff.zip") 47 47 assert.Error(t, err) 48 48 assert.Nil(t, bogusReq) 49 49 50 50 // Step 3, doesn't look like branch/tag/commit. 51 - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "db.zip") 51 + bogusReq, err = NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "db.zip") 52 52 assert.Error(t, err) 53 53 assert.Nil(t, bogusReq) 54 54 55 - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master.zip") 55 + bogusReq, err = NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master.zip") 56 56 assert.NoError(t, err) 57 57 assert.NotNil(t, bogusReq) 58 58 assert.EqualValues(t, "master.zip", bogusReq.GetArchiveName()) 59 59 60 - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive.zip") 60 + bogusReq, err = NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive.zip") 61 61 assert.NoError(t, err) 62 62 assert.NotNil(t, bogusReq) 63 63 assert.EqualValues(t, "test-archive.zip", bogusReq.GetArchiveName()) 64 64 65 65 // Now two valid requests, firstCommit with valid extensions. 66 - zipReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") 66 + zipReq, err := NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") 67 67 assert.NoError(t, err) 68 68 assert.NotNil(t, zipReq) 69 69 70 - tgzReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".tar.gz") 70 + tgzReq, err := NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".tar.gz") 71 71 assert.NoError(t, err) 72 72 assert.NotNil(t, tgzReq) 73 73 74 - secondReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".zip") 74 + secondReq, err := NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".zip") 75 75 assert.NoError(t, err) 76 76 assert.NotNil(t, secondReq) 77 77 ··· 91 91 // Sleep two seconds to make sure the queue doesn't change. 92 92 time.Sleep(2 * time.Second) 93 93 94 - zipReq2, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") 94 + zipReq2, err := NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") 95 95 assert.NoError(t, err) 96 96 // This zipReq should match what's sitting in the queue, as we haven't 97 97 // let it release yet. From the consumer's point of view, this looks like ··· 106 106 // Now we'll submit a request and TimedWaitForCompletion twice, before and 107 107 // after we release it. We should trigger both the timeout and non-timeout 108 108 // cases. 109 - timedReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".tar.gz") 109 + timedReq, err := NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".tar.gz") 110 110 assert.NoError(t, err) 111 111 assert.NotNil(t, timedReq) 112 112 ArchiveRepository(db.DefaultContext, timedReq) 113 113 114 - zipReq2, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") 114 + zipReq2, err = NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") 115 115 assert.NoError(t, err) 116 116 // Now, we're guaranteed to have released the original zipReq from the queue. 117 117 // Ensure that we don't get handed back the released entry somehow, but they
+1
services/repository/delete.go
··· 162 162 &actions_model.ActionScheduleSpec{RepoID: repoID}, 163 163 &actions_model.ActionSchedule{RepoID: repoID}, 164 164 &actions_model.ActionArtifact{RepoID: repoID}, 165 + &repo_model.RepoArchiveDownloadCount{RepoID: repoID}, 165 166 ); err != nil { 166 167 return fmt.Errorf("deleteBeans: %w", err) 167 168 }
+8 -2
templates/repo/release/list.tmpl
··· 70 70 {{$hasReleaseAttachment := gt (len $release.Attachments) 0}} 71 71 {{if and (not $.DisableDownloadSourceArchives) (not $release.IsDraft) ($.Permission.CanRead $.UnitTypeCode)}} 72 72 <li> 73 - <a class="archive-link" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong></a> 73 + <a class="archive-link tw-flex-1" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong></a> 74 + <div class="tw-mr-1"> 75 + <span class="text grey">{{ctx.Locale.TrN .Release.ArchiveDownloadCount.Zip "repo.release.download_count_one" "repo.release.download_count_few" (ctx.Locale.PrettyNumber .Release.ArchiveDownloadCount.Zip)}}</span> 76 + </div> 74 77 <span data-tooltip-content="{{ctx.Locale.Tr "repo.release.system_generated"}}"> 75 78 {{svg "octicon-info"}} 76 79 </span> 77 80 </li> 78 81 <li class="{{if $hasReleaseAttachment}}start-gap{{end}}"> 79 - <a class="archive-link" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong></a> 82 + <a class="archive-link tw-flex-1" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong></a> 83 + <div class="tw-mr-1"> 84 + <span class="text grey">{{ctx.Locale.TrN .Release.ArchiveDownloadCount.TarGz "repo.release.download_count_one" "repo.release.download_count_few" (ctx.Locale.PrettyNumber .Release.ArchiveDownloadCount.TarGz)}}</span> 85 + </div> 80 86 <span data-tooltip-content="{{ctx.Locale.Tr "repo.release.system_generated"}}"> 81 87 {{svg "octicon-info"}} 82 88 </span>
+26
templates/swagger/v1_json.tmpl
··· 17606 17606 "description": "AnnotatedTag represents an annotated tag", 17607 17607 "type": "object", 17608 17608 "properties": { 17609 + "archive_download_count": { 17610 + "$ref": "#/definitions/TagArchiveDownloadCount" 17611 + }, 17609 17612 "message": { 17610 17613 "type": "string", 17611 17614 "x-go-name": "Message" ··· 22755 22758 "description": "Release represents a repository release", 22756 22759 "type": "object", 22757 22760 "properties": { 22761 + "archive_download_count": { 22762 + "$ref": "#/definitions/TagArchiveDownloadCount" 22763 + }, 22758 22764 "assets": { 22759 22765 "type": "array", 22760 22766 "items": { ··· 23330 23336 "description": "Tag represents a repository tag", 23331 23337 "type": "object", 23332 23338 "properties": { 23339 + "archive_download_count": { 23340 + "$ref": "#/definitions/TagArchiveDownloadCount" 23341 + }, 23333 23342 "commit": { 23334 23343 "$ref": "#/definitions/CommitMeta" 23335 23344 }, ··· 23352 23361 "zipball_url": { 23353 23362 "type": "string", 23354 23363 "x-go-name": "ZipballURL" 23364 + } 23365 + }, 23366 + "x-go-package": "code.gitea.io/gitea/modules/structs" 23367 + }, 23368 + "TagArchiveDownloadCount": { 23369 + "description": "TagArchiveDownloadCount counts how many times a archive was downloaded", 23370 + "type": "object", 23371 + "properties": { 23372 + "tar_gz": { 23373 + "type": "integer", 23374 + "format": "int64", 23375 + "x-go-name": "TarGz" 23376 + }, 23377 + "zip": { 23378 + "type": "integer", 23379 + "format": "int64", 23380 + "x-go-name": "Zip" 23355 23381 } 23356 23382 }, 23357 23383 "x-go-package": "code.gitea.io/gitea/modules/structs"
+36
tests/integration/api_releases_test.go
··· 319 319 assert.EqualValues(t, 104, attachment.Size) 320 320 }) 321 321 } 322 + 323 + func TestAPIGetReleaseArchiveDownloadCount(t *testing.T) { 324 + defer tests.PrepareTestEnv(t)() 325 + 326 + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) 327 + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) 328 + session := loginUser(t, owner.LowerName) 329 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) 330 + 331 + name := "ReleaseDownloadCount" 332 + 333 + createNewReleaseUsingAPI(t, session, token, owner, repo, name, "", name, "test") 334 + 335 + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, name) 336 + 337 + req := NewRequest(t, "GET", urlStr) 338 + resp := MakeRequest(t, req, http.StatusOK) 339 + 340 + var release *api.Release 341 + DecodeJSON(t, resp, &release) 342 + 343 + // Check if everything defaults to 0 344 + assert.Equal(t, int64(0), release.ArchiveDownloadCount.TarGz) 345 + assert.Equal(t, int64(0), release.ArchiveDownloadCount.Zip) 346 + 347 + // Download the tarball to increase the count 348 + MakeRequest(t, NewRequest(t, "GET", release.TarURL), http.StatusOK) 349 + 350 + // Check if the count has increased 351 + resp = MakeRequest(t, req, http.StatusOK) 352 + 353 + DecodeJSON(t, resp, &release) 354 + 355 + assert.Equal(t, int64(1), release.ArchiveDownloadCount.TarGz) 356 + assert.Equal(t, int64(0), release.ArchiveDownloadCount.Zip) 357 + }
+36
tests/integration/api_repo_tags_test.go
··· 85 85 DecodeJSON(t, resp, &respObj) 86 86 return &respObj 87 87 } 88 + 89 + func TestAPIGetTagArchiveDownloadCount(t *testing.T) { 90 + defer tests.PrepareTestEnv(t)() 91 + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 92 + // Login as User2. 93 + session := loginUser(t, user.Name) 94 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) 95 + 96 + repoName := "repo1" 97 + tagName := "TagDownloadCount" 98 + 99 + createNewTagUsingAPI(t, session, token, user.Name, repoName, tagName, "", "") 100 + 101 + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/tags/%s?token=%s", user.Name, repoName, tagName, token) 102 + 103 + req := NewRequest(t, "GET", urlStr) 104 + resp := MakeRequest(t, req, http.StatusOK) 105 + 106 + var tagInfo *api.Tag 107 + DecodeJSON(t, resp, &tagInfo) 108 + 109 + // Check if everything defaults to 0 110 + assert.Equal(t, int64(0), tagInfo.ArchiveDownloadCount.TarGz) 111 + assert.Equal(t, int64(0), tagInfo.ArchiveDownloadCount.Zip) 112 + 113 + // Download the tarball to increase the count 114 + MakeRequest(t, NewRequest(t, "GET", tagInfo.TarballURL), http.StatusOK) 115 + 116 + // Check if the count has increased 117 + resp = MakeRequest(t, req, http.StatusOK) 118 + 119 + DecodeJSON(t, resp, &tagInfo) 120 + 121 + assert.Equal(t, int64(1), tagInfo.ArchiveDownloadCount.TarGz) 122 + assert.Equal(t, int64(0), tagInfo.ArchiveDownloadCount.Zip) 123 + }