diff --git a/models/attachment.go b/models/attachment.go index 3b3521bf60364..f30025687fcc4 100644 --- a/models/attachment.go +++ b/models/attachment.go @@ -12,9 +12,11 @@ import ( "path" "time" + api "code.gitea.io/sdk/gitea" "github.com/go-xorm/xorm" gouuid "github.com/satori/go.uuid" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) @@ -58,6 +60,15 @@ func (a *Attachment) IncreaseDownloadCount() error { return nil } +// 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 +} + // AttachmentLocalPath returns where attachment is stored in local file // system based on given UUID. func AttachmentLocalPath(uuid string) string { @@ -69,6 +80,24 @@ func (a *Attachment) LocalPath() string { return AttachmentLocalPath(a.UUID) } +// 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 +} + // NewAttachment creates a new attachment object. func NewAttachment(name string, buf []byte, file multipart.File) (_ *Attachment, err error) { attach := &Attachment{ @@ -126,6 +155,18 @@ 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) @@ -142,6 +183,12 @@ func GetAttachmentsByCommentID(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) { + attachments := make([]*Attachment, 0, 10) + return attachments, x.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/pull.go b/models/pull.go index 94bd7930ff684..1db0a89741432 100644 --- a/models/pull.go +++ b/models/pull.go @@ -495,7 +495,7 @@ func (pr *PullRequest) getMergeCommit() (*git.Commit, error) { if err != nil { // Errors are signaled by a non-zero status that is not 1 - if err.Error() == "exit status 1" { + if strings.Contains(err.Error(), "exit status 1") { return nil, nil } return nil, fmt.Errorf("git merge-base --is-ancestor: %v %v", stderr, err) diff --git a/models/release.go b/models/release.go index f12fc06840583..6d7352ad3c16c 100644 --- a/models/release.go +++ b/models/release.go @@ -71,6 +71,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 } @@ -97,6 +105,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, @@ -110,6 +122,7 @@ func (r *Release) APIFormat() *api.Release { CreatedAt: r.Created, PublishedAt: r.Created, Publisher: r.Publisher.APIFormat(), + Attachments: apiAttachments, } } @@ -235,8 +248,9 @@ func GetReleaseByID(id int64) (*Release, error) { // FindReleasesOptions describes the conditions to Find releases type FindReleasesOptions struct { - IncludeDrafts bool - TagNames []string + IncludeDrafts bool + IncludePrereleases bool + TagNames []string } func (opts *FindReleasesOptions) toConds(repoID int64) builder.Cond { @@ -246,13 +260,16 @@ func (opts *FindReleasesOptions) toConds(repoID int64) builder.Cond { if !opts.IncludeDrafts { cond = cond.And(builder.Eq{"is_draft": false}) } + if !opts.IncludePrereleases { + 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/models/user.go b/models/user.go index 6484c8cf696f8..2a4fb557db76c 100644 --- a/models/user.go +++ b/models/user.go @@ -333,15 +333,14 @@ func (u *User) generateRandomAvatar(e Engine) error { // which includes app sub-url as prefix. However, it is possible // to return full URL if user enables Gravatar-like service. func (u *User) RelAvatarLink() string { - defaultImgURL := setting.AppSubURL + "/img/avatar_default.png" if u.ID == -1 { - return defaultImgURL + return base.DefaultAvatarLink() } switch { case u.UseCustomAvatar: if !com.IsFile(u.CustomAvatarPath()) { - return defaultImgURL + return base.DefaultAvatarLink() } return setting.AppSubURL + "/avatars/" + u.Avatar case setting.DisableGravatar, setting.OfflineMode: diff --git a/modules/base/tool.go b/modules/base/tool.go index 8952e7a8c959a..543775e0dfdc7 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -192,13 +192,21 @@ func HashEmail(email string) string { return EncodeMD5(strings.ToLower(strings.TrimSpace(email))) } +// DefaultAvatarLink the default avatar link +func DefaultAvatarLink() string { + return setting.AppSubURL + "/img/avatar_default.png" +} + // AvatarLink returns relative avatar link to the site domain by given email, // which includes app sub-url as prefix. However, it is possible // to return full URL if user enables Gravatar-like service. func AvatarLink(email string) string { if setting.EnableFederatedAvatar && setting.LibravatarService != nil { - // TODO: This doesn't check any error. AvatarLink should return (string, error) - url, _ := setting.LibravatarService.FromEmail(email) + url, err := setting.LibravatarService.FromEmail(email) + if err != nil { + log.Error(4, "LibravatarService.FromEmail(email=%s): error %v", email, err) + return DefaultAvatarLink() + } return url } @@ -206,7 +214,7 @@ func AvatarLink(email string) string { return setting.GravatarSource + HashEmail(email) } - return setting.AppSubURL + "/img/avatar_default.png" + return DefaultAvatarLink() } // Seconds-based time units diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0e356b1f907ac..c6fd8e31e9ee0 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -420,9 +420,16 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/releases", func() { m.Combo("").Get(repo.ListReleases). Post(bind(api.CreateReleaseOption{}), repo.CreateRelease) - m.Combo("/:id").Get(repo.GetRelease). - Patch(bind(api.EditReleaseOption{}), repo.EditRelease). - Delete(repo.DeleteRelease) + m.Combo("/latest").Get(repo.GetLatestRelease) + m.Group("/:id", func() { + m.Combo("").Get(repo.GetRelease). + Patch(bind(api.EditReleaseOption{}), repo.EditRelease). + Delete(repo.DeleteRelease) + m.Group("/assets", func() { + m.Combo("").Get(repo.ListReleaseAttachments) + m.Combo("/:assetId").Get(repo.GetReleaseAttachment) + }) + }) }) m.Post("/mirror-sync", 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 ed5b8f4f788ba..a1aa928291def 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -32,6 +32,56 @@ func GetRelease(ctx *context.APIContext) { ctx.JSON(200, release.APIFormat()) } +// ListReleaseAttachments get all the attachments of a release +func ListReleaseAttachments(ctx *context.APIContext) { + 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) { + 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) { access, err := models.AccessLevel(ctx.User.ID, ctx.Repo.Repository) @@ -58,6 +108,29 @@ func ListReleases(ctx *context.APIContext) { ctx.JSON(200, rels) } +// GetLatestRelease Gets the latest release in a repository. Draft releases and prereleases are not returned +func GetLatestRelease(ctx *context.APIContext) { + // we set the pageSize to 1 to get back only one release + releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, models.FindReleasesOptions{ + IncludeDrafts: false, + IncludePrereleases: false, + }, 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) { if ctx.Repo.AccessMode < models.AccessModeWrite {