diff --git a/models/attachment.go b/models/attachment.go index acb1f0716ce7c..83abbacaf4aa3 100644 --- a/models/attachment.go +++ b/models/attachment.go @@ -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" ) @@ -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 { @@ -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) @@ -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) diff --git a/models/release.go b/models/release.go index 1e1d339a7cbcd..64185fa4a7397 100644 --- a/models/release.go +++ b/models/release.go @@ -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 } @@ -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)) + for i := range r.Attachments { + apiAttachments[i] = r.Attachments[i].APIFormat() + } return &api.Release{ ID: r.ID, TagName: r.TagName, @@ -92,6 +104,7 @@ func (r *Release) APIFormat() *api.Release { CreatedAt: r.CreatedUnix.AsTime(), PublishedAt: r.CreatedUnix.AsTime(), Publisher: r.Publisher.APIFormat(), + Attachments: apiAttachments, } } @@ -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 { @@ -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 diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 8a300f9958438..c31877e94da7d 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -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) diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index f5b529ac6aa1e..2d5dff52fafc1 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -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 @@ -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 diff --git a/routers/api/v1/swagger/misc.go b/routers/api/v1/swagger/misc.go index 8ec0d53a7954e..afc91932d9220 100644 --- a/routers/api/v1/swagger/misc.go +++ b/routers/api/v1/swagger/misc.go @@ -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"` +}