Skip to content

Added attachments to releases API #3075

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

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 52 additions & 0 deletions models/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import (
"os"
"path"

api "code.gitea.io/sdk/gitea"
gouuid "github.com/satori/go.uuid"

"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
Expand All @@ -39,6 +41,24 @@ func (a *Attachment) IncreaseDownloadCount() error {
return nil
}

// APIFormat converts a Attachment to an api.Attachment
func (a *Attachment) APIFormat() *api.Attachment {
apiAttachment := &api.Attachment{
ID: a.ID,
Created: a.Created,
Name: a.Name,
UUID: a.UUID,
DownloadURL: setting.AppURL + "attachments/" + a.UUID,
DownloadCount: a.DownloadCount,
}
fileSize, err := a.getSize()
log.Warn("Error getting the file size for attachment %s. ", a.UUID, err)
if err == nil {
apiAttachment.Size = fileSize
}
return apiAttachment
}

// AttachmentLocalPath returns where attachment is stored in local file
// system based on given UUID.
func AttachmentLocalPath(uuid string) string {
Expand Down Expand Up @@ -102,11 +122,32 @@ func getAttachmentsByUUIDs(e Engine, uuids []string) ([]*Attachment, error) {
return attachments, e.In("uuid", uuids).Find(&attachments)
}

// getSize gets the size of the attachment in bytes
func (a *Attachment) getSize() (int64, error) {
info, err := os.Stat(a.LocalPath())
if err != nil {
return 0, err
}
return info.Size(), nil
}

// GetAttachmentByUUID returns attachment by given UUID.
func GetAttachmentByUUID(uuid string) (*Attachment, error) {
return getAttachmentByUUID(x, uuid)
}

// GetAttachmentByID returns attachment by given ID.
func GetAttachmentByID(id int64) (*Attachment, error) {
attach := &Attachment{ID: id}
has, err := x.Get(attach)
if err != nil {
return nil, err
} else if !has {
return nil, ErrAttachmentNotExist{id, ""}
}
return attach, nil
}

func getAttachmentsByIssueID(e Engine, issueID int64) ([]*Attachment, error) {
attachments := make([]*Attachment, 0, 10)
return attachments, e.Where("issue_id = ? AND comment_id = 0", issueID).Find(&attachments)
Expand All @@ -127,6 +168,17 @@ func getAttachmentsByCommentID(e Engine, commentID int64) ([]*Attachment, error)
return attachments, x.Where("comment_id=?", commentID).Find(&attachments)
}

// GetAttachmentsByReleaseID returns all attachments of a release
func GetAttachmentsByReleaseID(releaseID int64) ([]*Attachment, error) {
return getAttachmentsByReleaseID(x, releaseID)
}

// getAttachmentsByReleaseID returns all attachments of a release
func getAttachmentsByReleaseID(e Engine, releaseID int64) ([]*Attachment, error) {
attachments := make([]*Attachment, 0, 10)
return attachments, e.Where("release_id=?", releaseID).Find(&attachments)
}

// DeleteAttachment deletes the given attachment and optionally the associated file.
func DeleteAttachment(a *Attachment, remove bool) error {
_, err := DeleteAttachments([]*Attachment{a}, remove)
Expand Down
25 changes: 21 additions & 4 deletions models/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ func (r *Release) loadAttributes(e Engine) error {
return err
}
}
// load the attachments of this release
if r.Attachments == nil {
attachments, err := GetAttachmentsByReleaseID(r.ID)
if err != nil {
return err
}
r.Attachments = attachments
}
return nil
}

Expand All @@ -79,6 +87,10 @@ func (r *Release) TarURL() string {

// APIFormat convert a Release to api.Release
func (r *Release) APIFormat() *api.Release {
apiAttachments := make([]*api.Attachment, len(r.Attachments))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will only work if r.Attachments has been populated before calling APIFormat(). This means we will need to update each endpoint that uses Release.APIFormat() accordingly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am populating the list in loadAttributes on line 71->78. Is that ok?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I missed that. Never mind then

for i := range r.Attachments {
apiAttachments[i] = r.Attachments[i].APIFormat()
}
return &api.Release{
ID: r.ID,
TagName: r.TagName,
Expand All @@ -92,6 +104,7 @@ func (r *Release) APIFormat() *api.Release {
CreatedAt: r.CreatedUnix.AsTime(),
PublishedAt: r.CreatedUnix.AsTime(),
Publisher: r.Publisher.APIFormat(),
Attachments: apiAttachments,
}
}

Expand Down Expand Up @@ -218,9 +231,10 @@ func GetReleaseByID(id int64) (*Release, error) {

// FindReleasesOptions describes the conditions to Find releases
type FindReleasesOptions struct {
IncludeDrafts bool
IncludeTags bool
TagNames []string
IncludeDrafts bool
IncludeTags bool
ExcludePrereleases bool
TagNames []string
}

func (opts *FindReleasesOptions) toConds(repoID int64) builder.Cond {
Expand All @@ -233,13 +247,16 @@ func (opts *FindReleasesOptions) toConds(repoID int64) builder.Cond {
if !opts.IncludeTags {
cond = cond.And(builder.Eq{"is_tag": false})
}
if opts.ExcludePrereleases {
cond = cond.And(builder.Eq{"is_prerelease": false})
}
if len(opts.TagNames) > 0 {
cond = cond.And(builder.In("tag_name", opts.TagNames))
}
return cond
}

// GetReleasesByRepoID returns a list of releases of repository.
// GetReleasesByRepoID returns a list of releases of repository. The results are sorted by created date and id descending
func GetReleasesByRepoID(repoID int64, opts FindReleasesOptions, page, pageSize int) (rels []*Release, err error) {
if page <= 0 {
page = 1
Expand Down
13 changes: 10 additions & 3 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,9 +464,16 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/releases", func() {
m.Combo("").Get(repo.ListReleases).
Post(reqToken(), reqRepoWriter(), bind(api.CreateReleaseOption{}), repo.CreateRelease)
m.Combo("/:id").Get(repo.GetRelease).
Patch(reqToken(), reqRepoWriter(), bind(api.EditReleaseOption{}), repo.EditRelease).
Delete(reqToken(), reqRepoWriter(), repo.DeleteRelease)
m.Combo("/latest").Get(repo.GetLatestRelease)
m.Group("/:id", func() {
m.Combo("").Get(repo.GetRelease).
Patch(reqToken(), reqRepoWriter(), bind(api.EditReleaseOption{}), repo.EditRelease).
Delete(reqToken(), reqRepoWriter(), repo.DeleteRelease)
m.Group("/assets", func() {
m.Combo("").Get(repo.ListReleaseAttachments)
m.Combo("/:assetId").Get(repo.GetReleaseAttachment)
})
})
})
m.Post("/mirror-sync", reqToken(), reqRepoWriter(), repo.MirrorSync)
m.Get("/editorconfig/:filename", context.RepoRef(), repo.GetEditorconfig)
Expand Down
146 changes: 146 additions & 0 deletions routers/api/v1/repo/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,109 @@ func GetRelease(ctx *context.APIContext) {
ctx.JSON(200, release.APIFormat())
}

// ListReleaseAttachments get all the attachments of a release
func ListReleaseAttachments(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/releases/{id}/assets repository getReleaseAttachments
// ---
// summary: List the assets (attachments in a release)
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the release in the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/AttachmentList"
id := ctx.ParamsInt64(":id")
release, err := models.GetReleaseByID(id)
if err != nil {
ctx.Error(500, "GetReleaseByID", err)
return
}
if release.RepoID != ctx.Repo.Repository.ID {
ctx.Status(404)
return
}
// load the attachments of this release
attachments, err := models.GetAttachmentsByReleaseID(id)
if err != nil {
ctx.Error(500, "GetAttachmentsByReleaseID", err)
return
}
// build the attachment list
apiAttachments := make([]*api.Attachment, len(attachments))
for i := range attachments {
apiAttachments[i] = attachments[i].APIFormat()
}
ctx.JSON(200, apiAttachments)
}

// GetReleaseAttachment get a single attachment of a release
func GetReleaseAttachment(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/releases/{id}/assets/{assetId} repository getReleaseAttachment
// ---
// summary: Get a specific asset (attachment) from a release of a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the release in the repo
// type: string
// required: true
// - name: assetId
// in: path
// description: assetId of the asset (attachment) in the release of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Attachment"
id := ctx.ParamsInt64(":id")
attachmentID := ctx.ParamsInt64(":assetId")
release, err := models.GetReleaseByID(id)
if err != nil {
ctx.Error(500, "GetReleaseByID", err)
return
}
if release.RepoID != ctx.Repo.Repository.ID {
ctx.Status(404)
return
}
// load the attachments of this release
attachment, err := models.GetAttachmentByID(attachmentID)
// if the attachment was not found, or it was found but is not associated with this release, then throw 404
if err != nil || id != attachment.ReleaseID {
ctx.Status(404)
return
}

ctx.JSON(200, attachment.APIFormat())
}

// ListReleases list a repository's releases
func ListReleases(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/releases repository repoListReleases
Expand Down Expand Up @@ -94,6 +197,49 @@ func ListReleases(ctx *context.APIContext) {
ctx.JSON(200, rels)
}

// GetLatestRelease gets the latest release in a repository. Draft releases and prereleases are excluded
func GetLatestRelease(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/releases/latest repository repoGetLatestRelease
// ---
// summary: Gets the latest release in a repository. Draft releases and prereleases are excluded
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Release"

// we set the pageSize to 1 to get back only one release
releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, models.FindReleasesOptions{
IncludeDrafts: false,
ExcludePrereleases: true,
}, 1, 1)
if err != nil {
ctx.Error(500, "GetReleasesByRepoID", err)
return
}
if len(releases) <= 0 {
// no releases found, just return 404
ctx.Status(404)
return
}
if err := releases[0].LoadAttributes(); err != nil {
ctx.Error(500, "LoadAttributes", err)
return
}
ctx.JSON(200, releases[0].APIFormat())
}

// CreateRelease create a release
func CreateRelease(ctx *context.APIContext, form api.CreateReleaseOption) {
// swagger:operation GET /repos/{owner}/{repo}/releases repository repoCreateRelease
Expand Down
12 changes: 12 additions & 0 deletions routers/api/v1/swagger/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,15 @@ type swaggerResponseServerVersion struct {
// in:body
Body api.ServerVersion `json:"body"`
}

// swagger:response Attachment
type swaggerResponseAttachment struct {
// in:body
Body api.Attachment `json:"body"`
}

// swagger:response AttachmentList
type swaggerResponseAttachmentList struct {
// in:body
Body []api.Attachment `json:"body"`
}