Skip to content

Count downloads for tag archives #25606

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,8 @@ var migrations = []Migration{
NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable),
// v291 -> v292
NewMigration("Add Index to attachment.comment_id", v1_22.AddCommentIDIndexofAttachment),
// v292 -> v293
NewMigration("Add repo_archive_download_count table", v1_22.AddRepoArchiveDownloadCount),
}

// GetCurrentDBVersion returns the current db version
Expand Down
20 changes: 20 additions & 0 deletions models/migrations/v1_22/v292.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_22 //nolint

import (
"xorm.io/xorm"
)

func AddRepoArchiveDownloadCount(x *xorm.Engine) error {
type RepoArchiveDownloadCount struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"index unique(s)"`
ReleaseID int64 `xorm:"index unique(s)"`
Type int `xorm:"unique(s)"`
Count int64
}

return x.Sync(&RepoArchiveDownloadCount{})
}
87 changes: 87 additions & 0 deletions models/repo/archive_download_count.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package repo

import (
"context"

"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
)

// RepoArchiveDownloadCount counts all archive downloads for a tag
type RepoArchiveDownloadCount struct { //nolint:revive
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"index unique(s)"`
ReleaseID int64 `xorm:"index unique(s)"`
Type git.ArchiveType `xorm:"unique(s)"`
Count int64
}

func init() {
db.RegisterModel(new(RepoArchiveDownloadCount))
}

// CountArchiveDownload adds one download the the given archive
func CountArchiveDownload(ctx context.Context, repoID, releaseID int64, tp git.ArchiveType) error {
updateCount, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).And("release_id = ?", releaseID).And("`type` = ?", tp).Incr("count").Update(new(RepoArchiveDownloadCount))
if err != nil {
return err
}

if updateCount != 0 {
// The count was updated, so we can exit
return nil
}

// The archive does not esxists in the databse, so let's add it
newCounter := &RepoArchiveDownloadCount{
RepoID: repoID,
ReleaseID: releaseID,
Type: tp,
Count: 1,
}

_, err = db.GetEngine(ctx).Insert(newCounter)
return err
}

// GetArchiveDownloadCount returns the download count of a tag
func GetArchiveDownloadCount(ctx context.Context, repoID, releaseID int64) (*api.TagArchiveDownloadCount, error) {
downloadCountList := make([]RepoArchiveDownloadCount, 0)
err := db.GetEngine(ctx).Where("repo_id = ?", repoID).And("release_id = ?", releaseID).Find(&downloadCountList)
if err != nil {
return nil, err
}

tagCounter := new(api.TagArchiveDownloadCount)

for _, singleCount := range downloadCountList {
switch singleCount.Type {
case git.ZIP:
tagCounter.Zip = singleCount.Count
case git.TARGZ:
tagCounter.TarGz = singleCount.Count
}
}

return tagCounter, nil
}

// GetDownloadCountForTagName returns the download count of a tag with the given name
func GetArchiveDownloadCountForTagName(ctx context.Context, repoID int64, tagName string) (*api.TagArchiveDownloadCount, error) {
release, err := GetRelease(ctx, repoID, tagName)
if err != nil {
return nil, err
}

return GetArchiveDownloadCount(ctx, repoID, release.ID)
}

// DeleteArchiveDownloadCountForRelease deletes the release from the repo_archive_download_count table
func DeleteArchiveDownloadCountForRelease(ctx context.Context, releaseID int64) error {
_, err := db.GetEngine(ctx).Delete(&RepoArchiveDownloadCount{ReleaseID: releaseID})
return err
}
65 changes: 65 additions & 0 deletions models/repo/archive_download_count_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package repo_test

import (
"testing"

"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestRepoArchiveDownloadCount(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

release, err := repo_model.GetReleaseByID(db.DefaultContext, 1)
require.NoError(t, err)

// We have no count, so it should return 0
downloadCount, err := repo_model.GetArchiveDownloadCount(db.DefaultContext, release.RepoID, release.ID)
require.NoError(t, err)
assert.Equal(t, int64(0), downloadCount.Zip)
assert.Equal(t, int64(0), downloadCount.TarGz)

// Set the TarGz counter to 1
err = repo_model.CountArchiveDownload(db.DefaultContext, release.RepoID, release.ID, git.TARGZ)
require.NoError(t, err)

downloadCount, err = repo_model.GetArchiveDownloadCountForTagName(db.DefaultContext, release.RepoID, release.TagName)
require.NoError(t, err)
assert.Equal(t, int64(0), downloadCount.Zip)
assert.Equal(t, int64(1), downloadCount.TarGz)

// Set the TarGz counter to 2
err = repo_model.CountArchiveDownload(db.DefaultContext, release.RepoID, release.ID, git.TARGZ)
require.NoError(t, err)

downloadCount, err = repo_model.GetArchiveDownloadCountForTagName(db.DefaultContext, release.RepoID, release.TagName)
require.NoError(t, err)
assert.Equal(t, int64(0), downloadCount.Zip)
assert.Equal(t, int64(2), downloadCount.TarGz)

// Set the Zip counter to 1
err = repo_model.CountArchiveDownload(db.DefaultContext, release.RepoID, release.ID, git.ZIP)
require.NoError(t, err)

downloadCount, err = repo_model.GetArchiveDownloadCountForTagName(db.DefaultContext, release.RepoID, release.TagName)
require.NoError(t, err)
assert.Equal(t, int64(1), downloadCount.Zip)
assert.Equal(t, int64(2), downloadCount.TarGz)

// Delete the count
err = repo_model.DeleteArchiveDownloadCountForRelease(db.DefaultContext, release.ID)
require.NoError(t, err)

downloadCount, err = repo_model.GetArchiveDownloadCountForTagName(db.DefaultContext, release.RepoID, release.TagName)
require.NoError(t, err)
assert.Equal(t, int64(0), downloadCount.Zip)
assert.Equal(t, int64(0), downloadCount.TarGz)
}
1 change: 1 addition & 0 deletions models/repo/archiver.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type RepoArchiver struct { //revive:disable-line:exported
Status ArchiverStatus
CommitID string `xorm:"VARCHAR(64) unique(s)"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"`
ReleaseID int64 `xorm:"-"`
}

func init() {
Expand Down
70 changes: 48 additions & 22 deletions models/repo/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,28 +65,29 @@ func (err ErrReleaseNotExist) Unwrap() error {

// Release represents a release of repository.
type Release struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX UNIQUE(n)"`
Repo *Repository `xorm:"-"`
PublisherID int64 `xorm:"INDEX"`
Publisher *user_model.User `xorm:"-"`
TagName string `xorm:"INDEX UNIQUE(n)"`
OriginalAuthor string
OriginalAuthorID int64 `xorm:"index"`
LowerTagName string
Target string
TargetBehind string `xorm:"-"` // to handle non-existing or empty target
Title string
Sha1 string `xorm:"VARCHAR(64)"`
NumCommits int64
NumCommitsBehind int64 `xorm:"-"`
Note string `xorm:"TEXT"`
RenderedNote template.HTML `xorm:"-"`
IsDraft bool `xorm:"NOT NULL DEFAULT false"`
IsPrerelease bool `xorm:"NOT NULL DEFAULT false"`
IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
Attachments []*Attachment `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX"`
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX UNIQUE(n)"`
Repo *Repository `xorm:"-"`
PublisherID int64 `xorm:"INDEX"`
Publisher *user_model.User `xorm:"-"`
TagName string `xorm:"INDEX UNIQUE(n)"`
OriginalAuthor string
OriginalAuthorID int64 `xorm:"index"`
LowerTagName string
Target string
TargetBehind string `xorm:"-"` // to handle non-existing or empty target
Title string
Sha1 string `xorm:"VARCHAR(64)"`
NumCommits int64
NumCommitsBehind int64 `xorm:"-"`
Note string `xorm:"TEXT"`
RenderedNote template.HTML `xorm:"-"`
IsDraft bool `xorm:"NOT NULL DEFAULT false"`
IsPrerelease bool `xorm:"NOT NULL DEFAULT false"`
IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
Attachments []*Attachment `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX"`
ArchiveDownloadCount *structs.TagArchiveDownloadCount `xorm:"-"`
}

func init() {
Expand All @@ -112,9 +113,22 @@ func (r *Release) LoadAttributes(ctx context.Context) error {
}
}
}

err = r.LoadArchiveDownloadCount(ctx)
if err != nil {
return err
}

return GetReleaseAttachments(ctx, r)
}

// LoadArchiveDownloadCount loads the download count for the source archives
func (r *Release) LoadArchiveDownloadCount(ctx context.Context) error {
var err error
r.ArchiveDownloadCount, err = GetArchiveDownloadCount(ctx, r.RepoID, r.ID)
return err
}

// APIURL the api url for a release. release must have attributes loaded
func (r *Release) APIURL() string {
return r.Repo.APIURL() + "/releases/" + strconv.FormatInt(r.ID, 10)
Expand Down Expand Up @@ -447,6 +461,18 @@ func PushUpdateDeleteTagsContext(ctx context.Context, repo *Repository, tags []s
lowerTags = append(lowerTags, strings.ToLower(tag))
}

for _, tag := range tags {
release, err := GetRelease(ctx, repo.ID, tag)
if err != nil {
return fmt.Errorf("GetRelease: %w", err)
}

err = DeleteArchiveDownloadCountForRelease(ctx, release.ID)
if err != nil {
return fmt.Errorf("DeleteTagArchiveDownloadCount: %w", err)
}
}

if _, err := db.GetEngine(ctx).
Where("repo_id = ? AND is_tag = ?", repo.ID, true).
In("lower_tag_name", lowerTags).
Expand Down
16 changes: 9 additions & 7 deletions modules/git/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sort"
"strings"

api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
)

Expand All @@ -18,13 +19,14 @@ const (

// Tag represents a Git tag.
type Tag struct {
Name string
ID ObjectID
Object ObjectID // The id of this commit object
Type string
Tagger *Signature
Message string
Signature *CommitGPGSignature
Name string
ID ObjectID
Object ObjectID // The id of this commit object
Type string
Tagger *Signature
Message string
Signature *CommitGPGSignature
ArchiveDownloadCount *api.TagArchiveDownloadCount
}

// Commit return the commit of the tag reference
Expand Down
7 changes: 4 additions & 3 deletions modules/structs/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ type Release struct {
// swagger:strfmt date-time
CreatedAt time.Time `json:"created_at"`
// swagger:strfmt date-time
PublishedAt time.Time `json:"published_at"`
Publisher *User `json:"author"`
Attachments []*Attachment `json:"assets"`
PublishedAt time.Time `json:"published_at"`
Publisher *User `json:"author"`
Attachments []*Attachment `json:"assets"`
ArchiveDownloadCount *TagArchiveDownloadCount `json:"archive_download_count"`
}

// CreateReleaseOption options when creating a release
Expand Down
34 changes: 21 additions & 13 deletions modules/structs/repo_tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,25 @@ package structs

// Tag represents a repository tag
type Tag struct {
Name string `json:"name"`
Message string `json:"message"`
ID string `json:"id"`
Commit *CommitMeta `json:"commit"`
ZipballURL string `json:"zipball_url"`
TarballURL string `json:"tarball_url"`
Name string `json:"name"`
Message string `json:"message"`
ID string `json:"id"`
Commit *CommitMeta `json:"commit"`
ZipballURL string `json:"zipball_url"`
TarballURL string `json:"tarball_url"`
ArchiveDownloadCount *TagArchiveDownloadCount `json:"archive_download_count"`
}

// AnnotatedTag represents an annotated tag
type AnnotatedTag struct {
Tag string `json:"tag"`
SHA string `json:"sha"`
URL string `json:"url"`
Message string `json:"message"`
Tagger *CommitUser `json:"tagger"`
Object *AnnotatedTagObject `json:"object"`
Verification *PayloadCommitVerification `json:"verification"`
Tag string `json:"tag"`
SHA string `json:"sha"`
URL string `json:"url"`
Message string `json:"message"`
Tagger *CommitUser `json:"tagger"`
Object *AnnotatedTagObject `json:"object"`
Verification *PayloadCommitVerification `json:"verification"`
ArchiveDownloadCount *TagArchiveDownloadCount `json:"archive_download_count"`
}

// AnnotatedTagObject contains meta information of the tag object
Expand All @@ -38,3 +40,9 @@ type CreateTagOption struct {
Message string `json:"message"`
Target string `json:"target"`
}

// TagArchiveDownloadCount counts how many times a archive was downloaded
type TagArchiveDownloadCount struct {
Zip int64 `json:"zip"`
TarGz int64 `json:"tar_gz"`
}
2 changes: 1 addition & 1 deletion routers/api/v1/repo/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ func GetArchive(ctx *context.APIContext) {

func archiveDownload(ctx *context.APIContext) {
uri := ctx.Params("*")
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
aReq, err := archiver_service.NewRequest(ctx, 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)
Expand Down
Loading