mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-27 06:03:51 +03:00
Count downloads for tag archives
This commit is contained in:
parent
f8a5d6872c
commit
613e5387c5
22 changed files with 469 additions and 95 deletions
87
models/repo/archive_download_count.go
Normal file
87
models/repo/archive_download_count.go
Normal file
|
@ -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
models/repo/archive_download_count_test.go
Normal file
65
models/repo/archive_download_count_test.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
@ -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)
|
||||
|
@ -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).
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"sort"
|
||||
"strings"
|
||||
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
|
@ -20,13 +21,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 *ObjectSignature
|
||||
Name string
|
||||
ID ObjectID
|
||||
Object ObjectID // The id of this commit object
|
||||
Type string
|
||||
Tagger *Signature
|
||||
Message string
|
||||
Signature *ObjectSignature
|
||||
ArchiveDownloadCount *api.TagArchiveDownloadCount
|
||||
}
|
||||
|
||||
// Commit return the commit of the tag reference
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -302,7 +302,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)
|
||||
|
|
|
@ -60,6 +60,12 @@ func ListTags(ctx *context.APIContext) {
|
|||
|
||||
apiTags := make([]*api.Tag, len(tags))
|
||||
for i := range tags {
|
||||
tags[i].ArchiveDownloadCount, err = repo_model.GetArchiveDownloadCountForTagName(ctx, ctx.Repo.Repository.ID, tags[i].Name)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetTagArchiveDownloadCountForName", err)
|
||||
return
|
||||
}
|
||||
|
||||
apiTags[i] = convert.ToTag(ctx.Repo.Repository, tags[i])
|
||||
}
|
||||
|
||||
|
@ -111,6 +117,13 @@ func GetAnnotatedTag(ctx *context.APIContext) {
|
|||
if err != nil {
|
||||
ctx.Error(http.StatusBadRequest, "GetAnnotatedTag", err)
|
||||
}
|
||||
|
||||
tag.ArchiveDownloadCount, err = repo_model.GetArchiveDownloadCountForTagName(ctx, ctx.Repo.Repository.ID, tag.Name)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetTagArchiveDownloadCountForName", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit))
|
||||
}
|
||||
}
|
||||
|
@ -150,6 +163,13 @@ func GetTag(ctx *context.APIContext) {
|
|||
ctx.NotFound(tagName)
|
||||
return
|
||||
}
|
||||
|
||||
tag.ArchiveDownloadCount, err = repo_model.GetArchiveDownloadCountForTagName(ctx, ctx.Repo.Repository.ID, tag.Name)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetTagArchiveDownloadCountForName", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToTag(ctx.Repo.Repository, tag))
|
||||
}
|
||||
|
||||
|
@ -218,6 +238,13 @@ func CreateTag(ctx *context.APIContext) {
|
|||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
tag.ArchiveDownloadCount, err = repo_model.GetArchiveDownloadCountForTagName(ctx, ctx.Repo.Repository.ID, tag.Name)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetTagArchiveDownloadCountForName", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToTag(ctx.Repo.Repository, tag))
|
||||
}
|
||||
|
||||
|
|
|
@ -127,6 +127,11 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
|
|||
return nil, err
|
||||
}
|
||||
|
||||
err = r.LoadArchiveDownloadCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !r.IsDraft {
|
||||
if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil {
|
||||
return nil, err
|
||||
|
@ -355,6 +360,12 @@ func SingleRelease(ctx *context.Context) {
|
|||
ctx.Data["Title"] = release.Title
|
||||
}
|
||||
|
||||
err = release.LoadArchiveDownloadCount(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadArchiveDownloadCount", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Releases"] = releases
|
||||
ctx.HTML(http.StatusOK, tplReleasesList)
|
||||
}
|
||||
|
|
|
@ -456,7 +456,7 @@ func RedirectDownload(ctx *context.Context) {
|
|||
// Download an archive of a repository
|
||||
func Download(ctx *context.Context) {
|
||||
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, err.Error())
|
||||
|
@ -485,6 +485,14 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep
|
|||
// 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 {
|
||||
if archiver.ReleaseID != 0 {
|
||||
err = repo_model.CountArchiveDownload(ctx, ctx.Repo.Repository.ID, archiver.ReleaseID, archiver.Type)
|
||||
if err != nil {
|
||||
ctx.ServerError("CountArchiveDownload", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Redirect(u.String())
|
||||
return
|
||||
}
|
||||
|
@ -498,6 +506,14 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep
|
|||
}
|
||||
defer fr.Close()
|
||||
|
||||
if archiver.ReleaseID != 0 {
|
||||
err = repo_model.CountArchiveDownload(ctx, ctx.Repo.Repository.ID, archiver.ReleaseID, archiver.Type)
|
||||
if err != nil {
|
||||
ctx.ServerError("CountArchiveDownload", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.ServeContent(fr, &context.ServeHeaderOptions{
|
||||
Filename: downloadName,
|
||||
LastModified: archiver.CreatedUnix.AsLocalTime(),
|
||||
|
@ -509,7 +525,7 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep
|
|||
// kind of drop it on the floor if this is the case.
|
||||
func InitiateDownload(ctx *context.Context) {
|
||||
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 {
|
||||
ctx.ServerError("archiver_service.NewRequest", err)
|
||||
return
|
||||
|
|
|
@ -171,12 +171,13 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch) *api
|
|||
// ToTag convert a git.Tag to an api.Tag
|
||||
func ToTag(repo *repo_model.Repository, t *git.Tag) *api.Tag {
|
||||
return &api.Tag{
|
||||
Name: t.Name,
|
||||
Message: strings.TrimSpace(t.Message),
|
||||
ID: t.ID.String(),
|
||||
Commit: ToCommitMeta(repo, t),
|
||||
ZipballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".zip"),
|
||||
TarballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".tar.gz"),
|
||||
Name: t.Name,
|
||||
Message: strings.TrimSpace(t.Message),
|
||||
ID: t.ID.String(),
|
||||
Commit: ToCommitMeta(repo, t),
|
||||
ZipballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".zip"),
|
||||
TarballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".tar.gz"),
|
||||
ArchiveDownloadCount: t.ArchiveDownloadCount,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -349,13 +350,14 @@ func ToTeams(ctx context.Context, teams []*organization.Team, loadOrgs bool) ([]
|
|||
// ToAnnotatedTag convert git.Tag to api.AnnotatedTag
|
||||
func ToAnnotatedTag(ctx context.Context, repo *repo_model.Repository, t *git.Tag, c *git.Commit) *api.AnnotatedTag {
|
||||
return &api.AnnotatedTag{
|
||||
Tag: t.Name,
|
||||
SHA: t.ID.String(),
|
||||
Object: ToAnnotatedTagObject(repo, c),
|
||||
Message: t.Message,
|
||||
URL: util.URLJoin(repo.APIURL(), "git/tags", t.ID.String()),
|
||||
Tagger: ToCommitUser(t.Tagger),
|
||||
Verification: ToVerification(ctx, c),
|
||||
Tag: t.Name,
|
||||
SHA: t.ID.String(),
|
||||
Object: ToAnnotatedTagObject(repo, c),
|
||||
Message: t.Message,
|
||||
URL: util.URLJoin(repo.APIURL(), "git/tags", t.ID.String()),
|
||||
Tagger: ToCommitUser(t.Tagger),
|
||||
Verification: ToVerification(ctx, c),
|
||||
ArchiveDownloadCount: t.ArchiveDownloadCount,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,21 +13,22 @@ import (
|
|||
// ToAPIRelease convert a repo_model.Release to api.Release
|
||||
func ToAPIRelease(ctx context.Context, repo *repo_model.Repository, r *repo_model.Release) *api.Release {
|
||||
return &api.Release{
|
||||
ID: r.ID,
|
||||
TagName: r.TagName,
|
||||
Target: r.Target,
|
||||
Title: r.Title,
|
||||
Note: r.Note,
|
||||
URL: r.APIURL(),
|
||||
HTMLURL: r.HTMLURL(),
|
||||
TarURL: r.TarURL(),
|
||||
ZipURL: r.ZipURL(),
|
||||
UploadURL: r.APIUploadURL(),
|
||||
IsDraft: r.IsDraft,
|
||||
IsPrerelease: r.IsPrerelease,
|
||||
CreatedAt: r.CreatedUnix.AsTime(),
|
||||
PublishedAt: r.CreatedUnix.AsTime(),
|
||||
Publisher: ToUser(ctx, r.Publisher, nil),
|
||||
Attachments: ToAPIAttachments(repo, r.Attachments),
|
||||
ID: r.ID,
|
||||
TagName: r.TagName,
|
||||
Target: r.Target,
|
||||
Title: r.Title,
|
||||
Note: r.Note,
|
||||
URL: r.APIURL(),
|
||||
HTMLURL: r.HTMLURL(),
|
||||
TarURL: r.TarURL(),
|
||||
ZipURL: r.ZipURL(),
|
||||
UploadURL: r.APIUploadURL(),
|
||||
IsDraft: r.IsDraft,
|
||||
IsPrerelease: r.IsPrerelease,
|
||||
CreatedAt: r.CreatedUnix.AsTime(),
|
||||
PublishedAt: r.CreatedUnix.AsTime(),
|
||||
Publisher: ToUser(ctx, r.Publisher, nil),
|
||||
Attachments: ToAPIAttachments(repo, r.Attachments),
|
||||
ArchiveDownloadCount: r.ArchiveDownloadCount,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -227,6 +227,9 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
|
|||
// find redirects without existing user.
|
||||
genericOrphanCheck("Orphaned Redirects without existing redirect user",
|
||||
"user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"),
|
||||
// find archive download count without existing release
|
||||
genericOrphanCheck("Archive download count without existing Release",
|
||||
"repo_archive_download_count", "release", "repo_archive_download_count.release_id=release.id"),
|
||||
)
|
||||
|
||||
for _, c := range consistencyChecks {
|
||||
|
|
|
@ -318,6 +318,11 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
|
|||
}
|
||||
}
|
||||
|
||||
err = repo_model.DeleteArchiveDownloadCountForRelease(ctx, rel.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if stdout, _, err := git.NewCommand(ctx, "tag", "-d").AddDashesAndList(rel.TagName).
|
||||
SetDescription(fmt.Sprintf("DeleteReleaseByID (git tag -d): %d", rel.ID)).
|
||||
RunStdString(&git.RunOpts{Dir: repo.RepoPath()}); err != nil && !strings.Contains(err.Error(), "not found") {
|
||||
|
|
|
@ -30,10 +30,11 @@ import (
|
|||
// This is entirely opaque to external entities, though, and mostly used as a
|
||||
// handle elsewhere.
|
||||
type ArchiveRequest struct {
|
||||
RepoID int64
|
||||
refName string
|
||||
Type git.ArchiveType
|
||||
CommitID string
|
||||
RepoID int64
|
||||
refName string
|
||||
Type git.ArchiveType
|
||||
CommitID string
|
||||
ReleaseID int64
|
||||
}
|
||||
|
||||
// ErrUnknownArchiveFormat request archive format is not supported
|
||||
|
@ -70,7 +71,7 @@ func (e RepoRefNotFoundError) Is(err error) bool {
|
|||
// NewRequest creates an archival request, based on the URI. The
|
||||
// resulting ArchiveRequest is suitable for being passed to ArchiveRepository()
|
||||
// if it's determined that the request still needs to be satisfied.
|
||||
func NewRequest(repoID int64, repo *git.Repository, uri string) (*ArchiveRequest, error) {
|
||||
func NewRequest(ctx context.Context, repoID int64, repo *git.Repository, uri string) (*ArchiveRequest, error) {
|
||||
r := &ArchiveRequest{
|
||||
RepoID: repoID,
|
||||
}
|
||||
|
@ -99,6 +100,17 @@ func NewRequest(repoID int64, repo *git.Repository, uri string) (*ArchiveRequest
|
|||
}
|
||||
|
||||
r.CommitID = commitID.String()
|
||||
|
||||
release, err := repo_model.GetRelease(ctx, repoID, r.refName)
|
||||
if err != nil {
|
||||
if !repo_model.IsErrReleaseNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if release != nil {
|
||||
r.ReleaseID = release.ID
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
|
@ -120,6 +132,10 @@ func (aReq *ArchiveRequest) Await(ctx context.Context) (*repo_model.RepoArchiver
|
|||
return nil, fmt.Errorf("models.GetRepoArchiver: %w", err)
|
||||
}
|
||||
|
||||
if archiver != nil {
|
||||
archiver.ReleaseID = aReq.ReleaseID
|
||||
}
|
||||
|
||||
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
|
||||
// Archive already generated, we're done.
|
||||
return archiver, nil
|
||||
|
@ -145,6 +161,7 @@ func (aReq *ArchiveRequest) Await(ctx context.Context) (*repo_model.RepoArchiver
|
|||
return nil, fmt.Errorf("repo_model.GetRepoArchiver: %w", err)
|
||||
}
|
||||
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
|
||||
archiver.ReleaseID = aReq.ReleaseID
|
||||
return archiver, nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,47 +31,47 @@ func TestArchive_Basic(t *testing.T) {
|
|||
contexttest.LoadGitRepo(t, ctx)
|
||||
defer ctx.Repo.GitRepo.Close()
|
||||
|
||||
bogusReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
bogusReq, err := NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, bogusReq)
|
||||
assert.EqualValues(t, firstCommit+".zip", bogusReq.GetArchiveName())
|
||||
|
||||
// Check a series of bogus requests.
|
||||
// Step 1, valid commit with a bad extension.
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".dilbert")
|
||||
bogusReq, err = NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".dilbert")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, bogusReq)
|
||||
|
||||
// Step 2, missing commit.
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "dbffff.zip")
|
||||
bogusReq, err = NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "dbffff.zip")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, bogusReq)
|
||||
|
||||
// Step 3, doesn't look like branch/tag/commit.
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "db.zip")
|
||||
bogusReq, err = NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "db.zip")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, bogusReq)
|
||||
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master.zip")
|
||||
bogusReq, err = NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master.zip")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, bogusReq)
|
||||
assert.EqualValues(t, "master.zip", bogusReq.GetArchiveName())
|
||||
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive.zip")
|
||||
bogusReq, err = NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive.zip")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, bogusReq)
|
||||
assert.EqualValues(t, "test-archive.zip", bogusReq.GetArchiveName())
|
||||
|
||||
// Now two valid requests, firstCommit with valid extensions.
|
||||
zipReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
zipReq, err := NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, zipReq)
|
||||
|
||||
tgzReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".tar.gz")
|
||||
tgzReq, err := NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".tar.gz")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, tgzReq)
|
||||
|
||||
secondReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".zip")
|
||||
secondReq, err := NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".zip")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, secondReq)
|
||||
|
||||
|
@ -91,7 +91,7 @@ func TestArchive_Basic(t *testing.T) {
|
|||
// Sleep two seconds to make sure the queue doesn't change.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
zipReq2, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
zipReq2, err := NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
assert.NoError(t, err)
|
||||
// This zipReq should match what's sitting in the queue, as we haven't
|
||||
// let it release yet. From the consumer's point of view, this looks like
|
||||
|
@ -106,12 +106,12 @@ func TestArchive_Basic(t *testing.T) {
|
|||
// Now we'll submit a request and TimedWaitForCompletion twice, before and
|
||||
// after we release it. We should trigger both the timeout and non-timeout
|
||||
// cases.
|
||||
timedReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".tar.gz")
|
||||
timedReq, err := NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".tar.gz")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, timedReq)
|
||||
ArchiveRepository(db.DefaultContext, timedReq)
|
||||
|
||||
zipReq2, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
zipReq2, err = NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
assert.NoError(t, err)
|
||||
// Now, we're guaranteed to have released the original zipReq from the queue.
|
||||
// Ensure that we don't get handed back the released entry somehow, but they
|
||||
|
|
|
@ -162,6 +162,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
|
|||
&actions_model.ActionScheduleSpec{RepoID: repoID},
|
||||
&actions_model.ActionSchedule{RepoID: repoID},
|
||||
&actions_model.ActionArtifact{RepoID: repoID},
|
||||
&repo_model.RepoArchiveDownloadCount{RepoID: repoID},
|
||||
); err != nil {
|
||||
return fmt.Errorf("deleteBeans: %w", err)
|
||||
}
|
||||
|
|
|
@ -71,13 +71,16 @@
|
|||
{{if and (not $.DisableDownloadSourceArchives) (not $release.IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
|
||||
<li>
|
||||
<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>
|
||||
<span class="ui middle aligned right" data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber .Release.ArchiveDownloadCount.Zip)}}">
|
||||
{{svg "octicon-info"}}
|
||||
</span>
|
||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.release.system_generated"}}">
|
||||
{{svg "octicon-info"}}
|
||||
</span>
|
||||
</li>
|
||||
<li class="{{if $hasReleaseAttachment}}start-gap{{end}}">
|
||||
<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>
|
||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.release.system_generated"}}">
|
||||
<span class="ui middle aligned right" data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber .Release.ArchiveDownloadCount.TarGz)}}">
|
||||
{{svg "octicon-info"}}
|
||||
</span>
|
||||
</li>
|
||||
|
|
26
templates/swagger/v1_json.tmpl
generated
26
templates/swagger/v1_json.tmpl
generated
|
@ -17606,6 +17606,9 @@
|
|||
"description": "AnnotatedTag represents an annotated tag",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archive_download_count": {
|
||||
"$ref": "#/definitions/TagArchiveDownloadCount"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"x-go-name": "Message"
|
||||
|
@ -22755,6 +22758,9 @@
|
|||
"description": "Release represents a repository release",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archive_download_count": {
|
||||
"$ref": "#/definitions/TagArchiveDownloadCount"
|
||||
},
|
||||
"assets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -23330,6 +23336,9 @@
|
|||
"description": "Tag represents a repository tag",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archive_download_count": {
|
||||
"$ref": "#/definitions/TagArchiveDownloadCount"
|
||||
},
|
||||
"commit": {
|
||||
"$ref": "#/definitions/CommitMeta"
|
||||
},
|
||||
|
@ -23356,6 +23365,23 @@
|
|||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"TagArchiveDownloadCount": {
|
||||
"description": "TagArchiveDownloadCount counts how many times a archive was downloaded",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tar_gz": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "TarGz"
|
||||
},
|
||||
"zip": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "Zip"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"Team": {
|
||||
"description": "Team represents a team in an organization",
|
||||
"type": "object",
|
||||
|
|
|
@ -319,3 +319,39 @@ func TestAPIUploadAssetRelease(t *testing.T) {
|
|||
assert.EqualValues(t, 104, attachment.Size)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIGetReleaseArchiveDownloadCount(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, owner.LowerName)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
name := "ReleaseDownloadCount"
|
||||
|
||||
createNewReleaseUsingAPI(t, session, token, owner, repo, name, "", name, "test")
|
||||
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, name)
|
||||
|
||||
req := NewRequest(t, "GET", urlStr)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var release *api.Release
|
||||
DecodeJSON(t, resp, &release)
|
||||
|
||||
// Check if everything defaults to 0
|
||||
assert.Equal(t, int64(0), release.ArchiveDownloadCount.TarGz)
|
||||
assert.Equal(t, int64(0), release.ArchiveDownloadCount.Zip)
|
||||
|
||||
// Download the tarball to increase the count
|
||||
MakeRequest(t, NewRequest(t, "GET", release.TarURL), http.StatusOK)
|
||||
|
||||
// Check if the count has increased
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
DecodeJSON(t, resp, &release)
|
||||
|
||||
assert.Equal(t, int64(1), release.ArchiveDownloadCount.TarGz)
|
||||
assert.Equal(t, int64(0), release.ArchiveDownloadCount.Zip)
|
||||
}
|
||||
|
|
|
@ -85,3 +85,39 @@ func createNewTagUsingAPI(t *testing.T, session *TestSession, token, ownerName,
|
|||
DecodeJSON(t, resp, &respObj)
|
||||
return &respObj
|
||||
}
|
||||
|
||||
func TestAPIGetTagArchiveDownloadCount(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
// Login as User2.
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
repoName := "repo1"
|
||||
tagName := "TagDownloadCount"
|
||||
|
||||
createNewTagUsingAPI(t, session, token, user.Name, repoName, tagName, "", "")
|
||||
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/tags/%s?token=%s", user.Name, repoName, tagName, token)
|
||||
|
||||
req := NewRequest(t, "GET", urlStr)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var tagInfo *api.Tag
|
||||
DecodeJSON(t, resp, &tagInfo)
|
||||
|
||||
// Check if everything defaults to 0
|
||||
assert.Equal(t, int64(0), tagInfo.ArchiveDownloadCount.TarGz)
|
||||
assert.Equal(t, int64(0), tagInfo.ArchiveDownloadCount.Zip)
|
||||
|
||||
// Download the tarball to increase the count
|
||||
MakeRequest(t, NewRequest(t, "GET", tagInfo.TarballURL), http.StatusOK)
|
||||
|
||||
// Check if the count has increased
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
DecodeJSON(t, resp, &tagInfo)
|
||||
|
||||
assert.Equal(t, int64(1), tagInfo.ArchiveDownloadCount.TarGz)
|
||||
assert.Equal(t, int64(0), tagInfo.ArchiveDownloadCount.Zip)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue