Attach to release (#673)

* Moved attachaments POST url from /issues/attachments to /attachments

* Implemented attachment upload on release page

* Implemented downloading attachments on the release page

* Added zip and gzip files to default allowed attachments

* Implemented uploading attachments on edit release

* Renamed UploadIssueAttachment to UploadAttachment
release/v1.15
Philip Couling 2017-01-15 14:57:00 +00:00 committed by Lunny Xiao
parent dce03c19cb
commit 64375d875b
11 changed files with 144 additions and 14 deletions

View File

@ -309,7 +309,7 @@ func runWeb(ctx *cli.Context) error {
return return
} }
}) })
m.Post("/issues/attachments", repo.UploadIssueAttachment) m.Post("/attachments", repo.UploadAttachment)
}, ignSignIn) }, ignSignIn)
m.Group("/:username", func() { m.Group("/:username", func() {
@ -463,13 +463,11 @@ func runWeb(ctx *cli.Context) error {
m.Get("/:id/:action", repo.ChangeMilestonStatus) m.Get("/:id/:action", repo.ChangeMilestonStatus)
m.Post("/delete", repo.DeleteMilestone) m.Post("/delete", repo.DeleteMilestone)
}, reqRepoWriter, context.RepoRef()) }, reqRepoWriter, context.RepoRef())
m.Group("/releases", func() { m.Group("/releases", func() {
m.Get("/new", repo.NewRelease) m.Get("/new", repo.NewRelease)
m.Post("/new", bindIgnErr(auth.NewReleaseForm{}), repo.NewReleasePost) m.Post("/new", bindIgnErr(auth.NewReleaseForm{}), repo.NewReleasePost)
m.Post("/delete", repo.DeleteRelease) m.Post("/delete", repo.DeleteRelease)
}, reqRepoWriter, context.RepoRef()) }, reqRepoWriter, context.RepoRef())
m.Group("/releases", func() { m.Group("/releases", func() {
m.Get("/edit/*", repo.EditRelease) m.Get("/edit/*", repo.EditRelease)
m.Post("/edit/*", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost) m.Post("/edit/*", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost)

View File

@ -289,7 +289,7 @@ ENABLE = true
; Path for attachments. Defaults to `data/attachments` ; Path for attachments. Defaults to `data/attachments`
PATH = data/attachments PATH = data/attachments
; One or more allowed types, e.g. image/jpeg|image/png ; One or more allowed types, e.g. image/jpeg|image/png
ALLOWED_TYPES = image/jpeg|image/png ALLOWED_TYPES = image/jpeg|image/png|application/zip|application/gzip
; Max size of each file. Defaults to 32MB ; Max size of each file. Defaults to 32MB
MAX_SIZE = 4 MAX_SIZE = 4
; Max number of files per upload. Defaults to 10 ; Max number of files per upload. Defaults to 10

View File

@ -38,6 +38,8 @@ type Release struct {
IsDraft bool `xorm:"NOT NULL DEFAULT false"` IsDraft bool `xorm:"NOT NULL DEFAULT false"`
IsPrerelease bool IsPrerelease bool
Attachments []*Attachment `xorm:"-"`
Created time.Time `xorm:"-"` Created time.Time `xorm:"-"`
CreatedUnix int64 `xorm:"INDEX"` CreatedUnix int64 `xorm:"INDEX"`
} }
@ -155,8 +157,33 @@ func createTag(gitRepo *git.Repository, rel *Release) error {
return nil return nil
} }
func addReleaseAttachments(releaseID int64, attachmentUUIDs []string) (err error) {
// Check attachments
var attachments = make([]*Attachment,0)
for _, uuid := range attachmentUUIDs {
attach, err := getAttachmentByUUID(x, uuid)
if err != nil {
if IsErrAttachmentNotExist(err) {
continue
}
return fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err)
}
attachments = append(attachments, attach)
}
for i := range attachments {
attachments[i].ReleaseID = releaseID
// No assign value could be 0, so ignore AllCols().
if _, err = x.Id(attachments[i].ID).Update(attachments[i]); err != nil {
return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
}
}
return
}
// CreateRelease creates a new release of repository. // CreateRelease creates a new release of repository.
func CreateRelease(gitRepo *git.Repository, rel *Release) error { func CreateRelease(gitRepo *git.Repository, rel *Release, attachmentUUIDs []string) error {
isExist, err := IsReleaseExist(rel.RepoID, rel.TagName) isExist, err := IsReleaseExist(rel.RepoID, rel.TagName)
if err != nil { if err != nil {
return err return err
@ -168,7 +195,14 @@ func CreateRelease(gitRepo *git.Repository, rel *Release) error {
return err return err
} }
rel.LowerTagName = strings.ToLower(rel.TagName) rel.LowerTagName = strings.ToLower(rel.TagName)
_, err = x.InsertOne(rel) _, err = x.InsertOne(rel)
if err != nil {
return err
}
err = addReleaseAttachments(rel.ID, attachmentUUIDs)
return err return err
} }
@ -222,6 +256,64 @@ func GetReleasesByRepoIDAndNames(repoID int64, tagNames []string) (rels []*Relea
return rels, err return rels, err
} }
type releaseMetaSearch struct {
ID [] int64
Rel [] *Release
}
func (s releaseMetaSearch) Len() int {
return len(s.ID)
}
func (s releaseMetaSearch) Swap(i, j int) {
s.ID[i], s.ID[j] = s.ID[j], s.ID[i]
s.Rel[i], s.Rel[j] = s.Rel[j], s.Rel[i]
}
func (s releaseMetaSearch) Less(i, j int) bool {
return s.ID[i] < s.ID[j]
}
// GetReleaseAttachments retrieves the attachments for releases
func GetReleaseAttachments(rels ... *Release) (err error){
if len(rels) == 0 {
return
}
// To keep this efficient as possible sort all releases by id,
// select attachments by release id,
// then merge join them
// Sort
var sortedRels = releaseMetaSearch{ID: make([]int64, len(rels)), Rel: make([]*Release, len(rels))}
var attachments [] *Attachment
for index, element := range rels {
element.Attachments = []*Attachment{}
sortedRels.ID[index] = element.ID
sortedRels.Rel[index] = element
}
sort.Sort(sortedRels)
// Select attachments
err = x.
Asc("release_id").
In("release_id", sortedRels.ID).
Find(&attachments, Attachment{})
if err != nil {
return err
}
// merge join
var currentIndex = 0
for _, attachment := range attachments {
for sortedRels.ID[currentIndex] < attachment.ReleaseID {
currentIndex++
}
sortedRels.Rel[currentIndex].Attachments = append(sortedRels.Rel[currentIndex].Attachments, attachment)
}
return
}
type releaseSorter struct { type releaseSorter struct {
rels []*Release rels []*Release
} }
@ -249,11 +341,17 @@ func SortReleases(rels []*Release) {
} }
// UpdateRelease updates information of a release. // UpdateRelease updates information of a release.
func UpdateRelease(gitRepo *git.Repository, rel *Release) (err error) { func UpdateRelease(gitRepo *git.Repository, rel *Release, attachmentUUIDs []string) (err error) {
if err = createTag(gitRepo, rel); err != nil { if err = createTag(gitRepo, rel); err != nil {
return err return err
} }
_, err = x.Id(rel.ID).AllCols().Update(rel) _, err = x.Id(rel.ID).AllCols().Update(rel)
if err != nil {
return err
}
err = addReleaseAttachments(rel.ID, attachmentUUIDs)
return err return err
} }

View File

@ -267,6 +267,7 @@ type NewReleaseForm struct {
Content string Content string
Draft string Draft string
Prerelease bool Prerelease bool
Files []string
} }
// Validate valideates the fields // Validate valideates the fields
@ -280,6 +281,7 @@ type EditReleaseForm struct {
Content string `form:"content"` Content string `form:"content"`
Draft string `form:"draft"` Draft string `form:"draft"`
Prerelease bool `form:"prerelease"` Prerelease bool `form:"prerelease"`
Files []string
} }
// Validate valideates the fields // Validate valideates the fields

View File

@ -718,7 +718,7 @@ please consider changing to GITEA_CUSTOM`)
if !filepath.IsAbs(AttachmentPath) { if !filepath.IsAbs(AttachmentPath) {
AttachmentPath = path.Join(workDir, AttachmentPath) AttachmentPath = path.Join(workDir, AttachmentPath)
} }
AttachmentAllowedTypes = strings.Replace(sec.Key("ALLOWED_TYPES").MustString("image/jpeg,image/png"), "|", ",", -1) AttachmentAllowedTypes = strings.Replace(sec.Key("ALLOWED_TYPES").MustString("image/jpeg,image/png,application/zip,application/gzip"), "|", ",", -1)
AttachmentMaxSize = sec.Key("MAX_SIZE").MustInt64(4) AttachmentMaxSize = sec.Key("MAX_SIZE").MustInt64(4)
AttachmentMaxFiles = sec.Key("MAX_FILES").MustInt(5) AttachmentMaxFiles = sec.Key("MAX_FILES").MustInt(5)
AttachmentEnabled = sec.Key("ENABLE").MustBool(true) AttachmentEnabled = sec.Key("ENABLE").MustBool(true)

View File

@ -99,7 +99,7 @@ func CreateRelease(ctx *context.APIContext, form api.CreateReleaseOption) {
IsPrerelease: form.IsPrerelease, IsPrerelease: form.IsPrerelease,
CreatedUnix: commit.Author.When.Unix(), CreatedUnix: commit.Author.When.Unix(),
} }
if err := models.CreateRelease(ctx.Repo.GitRepo, rel); err != nil { if err := models.CreateRelease(ctx.Repo.GitRepo, rel, nil); err != nil {
if models.IsErrReleaseAlreadyExist(err) { if models.IsErrReleaseAlreadyExist(err) {
ctx.Status(409) ctx.Status(409)
} else { } else {
@ -145,7 +145,7 @@ func EditRelease(ctx *context.APIContext, form api.EditReleaseOption) {
if form.IsPrerelease != nil { if form.IsPrerelease != nil {
rel.IsPrerelease = *form.IsPrerelease rel.IsPrerelease = *form.IsPrerelease
} }
if err := models.UpdateRelease(ctx.Repo.GitRepo, rel); err != nil { if err := models.UpdateRelease(ctx.Repo.GitRepo, rel, nil); err != nil {
ctx.Error(500, "UpdateRelease", err) ctx.Error(500, "UpdateRelease", err)
return return
} }

View File

@ -477,8 +477,8 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index)) ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
} }
// UploadIssueAttachment response for uploading issue's attachment // UploadAttachment response for uploading issue's attachment
func UploadIssueAttachment(ctx *context.Context) { func UploadAttachment(ctx *context.Context) {
if !setting.AttachmentEnabled { if !setting.AttachmentEnabled {
ctx.Error(404, "attachment is not enabled") ctx.Error(404, "attachment is not enabled")
return return

View File

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markdown" "code.gitea.io/gitea/modules/markdown"
"code.gitea.io/gitea/modules/setting"
"github.com/Unknwon/paginater" "github.com/Unknwon/paginater"
) )
@ -99,6 +100,12 @@ func Releases(ctx *context.Context) {
return return
} }
err = models.GetReleaseAttachments(releases...)
if err != nil {
ctx.Handle(500, "GetReleaseAttachments", err)
return
}
// Temproray cache commits count of used branches to speed up. // Temproray cache commits count of used branches to speed up.
countCache := make(map[string]int64) countCache := make(map[string]int64)
var cacheUsers = make(map[int64]*models.User) var cacheUsers = make(map[int64]*models.User)
@ -162,6 +169,7 @@ func NewRelease(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.release.new_release") ctx.Data["Title"] = ctx.Tr("repo.release.new_release")
ctx.Data["PageIsReleaseList"] = true ctx.Data["PageIsReleaseList"] = true
ctx.Data["tag_target"] = ctx.Repo.Repository.DefaultBranch ctx.Data["tag_target"] = ctx.Repo.Repository.DefaultBranch
renderAttachmentSettings(ctx);
ctx.HTML(200, tplReleaseNew) ctx.HTML(200, tplReleaseNew)
} }
@ -215,7 +223,12 @@ func NewReleasePost(ctx *context.Context, form auth.NewReleaseForm) {
CreatedUnix: tagCreatedUnix, CreatedUnix: tagCreatedUnix,
} }
if err = models.CreateRelease(ctx.Repo.GitRepo, rel); err != nil { var attachmentUUIDs []string
if setting.AttachmentEnabled {
attachmentUUIDs = form.Files
}
if err = models.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs); err != nil {
ctx.Data["Err_TagName"] = true ctx.Data["Err_TagName"] = true
switch { switch {
case models.IsErrReleaseAlreadyExist(err): case models.IsErrReleaseAlreadyExist(err):
@ -237,6 +250,7 @@ func EditRelease(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.release.edit_release") ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
ctx.Data["PageIsReleaseList"] = true ctx.Data["PageIsReleaseList"] = true
ctx.Data["PageIsEditRelease"] = true ctx.Data["PageIsEditRelease"] = true
renderAttachmentSettings(ctx);
tagName := ctx.Params("*") tagName := ctx.Params("*")
rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName) rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName)
@ -286,11 +300,16 @@ func EditReleasePost(ctx *context.Context, form auth.EditReleaseForm) {
return return
} }
var attachmentUUIDs []string
if setting.AttachmentEnabled {
attachmentUUIDs = form.Files
}
rel.Title = form.Title rel.Title = form.Title
rel.Note = form.Content rel.Note = form.Content
rel.IsDraft = len(form.Draft) > 0 rel.IsDraft = len(form.Draft) > 0
rel.IsPrerelease = form.Prerelease rel.IsPrerelease = form.Prerelease
if err = models.UpdateRelease(ctx.Repo.GitRepo, rel); err != nil { if err = models.UpdateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs); err != nil {
ctx.Handle(500, "UpdateRelease", err) ctx.Handle(500, "UpdateRelease", err)
return return
} }

View File

@ -13,5 +13,5 @@
</div> </div>
{{if .IsAttachmentEnabled}} {{if .IsAttachmentEnabled}}
<div class="files"></div> <div class="files"></div>
<div class="ui basic button dropzone" id="dropzone" data-upload-url="{{AppSubUrl}}/issues/attachments" data-accepts="{{.AttachmentAllowedTypes}}" data-max-file="{{.AttachmentMaxFiles}}" data-max-size="{{.AttachmentMaxSize}}" data-default-message="{{.i18n.Tr "dropzone.default_message"}}" data-invalid-input-type="{{.i18n.Tr "dropzone.invalid_input_type"}}" data-file-too-big="{{.i18n.Tr "dropzone.file_too_big"}}" data-remove-file="{{.i18n.Tr "dropzone.remove_file"}}"></div> <div class="ui basic button dropzone" id="dropzone" data-upload-url="{{AppSubUrl}}/attachments" data-accepts="{{.AttachmentAllowedTypes}}" data-max-file="{{.AttachmentMaxFiles}}" data-max-size="{{.AttachmentMaxSize}}" data-default-message="{{.i18n.Tr "dropzone.default_message"}}" data-invalid-input-type="{{.i18n.Tr "dropzone.invalid_input_type"}}" data-file-too-big="{{.i18n.Tr "dropzone.file_too_big"}}" data-remove-file="{{.i18n.Tr "dropzone.remove_file"}}"></div>
{{end}} {{end}}

View File

@ -59,6 +59,15 @@
<li> <li>
<a href="{{$.RepoLink}}/archive/{{.TagName}}.tar.gz"><i class="octicon octicon-file-zip"></i> {{$.i18n.Tr "repo.release.source_code"}} (TAR.GZ)</a> <a href="{{$.RepoLink}}/archive/{{.TagName}}.tar.gz"><i class="octicon octicon-file-zip"></i> {{$.i18n.Tr "repo.release.source_code"}} (TAR.GZ)</a>
</li> </li>
{{if .Attachments}}
{{range .Attachments}}
<li>
<a target="_blank" rel="noopener" href="{{AppSubUrl}}/attachments/{{.UUID}}">
<span class="ui image octicon octicon-desktop-download" title='{{.Name}}'></span> {{.Name}}
</a>
</li>
{{end}}
{{end}}
</ul> </ul>
</div> </div>
{{else}} {{else}}

View File

@ -48,6 +48,10 @@
<label>{{.i18n.Tr "repo.release.content"}}</label> <label>{{.i18n.Tr "repo.release.content"}}</label>
<textarea name="content">{{.content}}</textarea> <textarea name="content">{{.content}}</textarea>
</div> </div>
{{if .IsAttachmentEnabled}}
<div class="files"></div>
<div class="ui basic button dropzone" id="dropzone" data-upload-url="{{AppSubUrl}}/attachments" data-accepts="{{.AttachmentAllowedTypes}}" data-max-file="{{.AttachmentMaxFiles}}" data-max-size="{{.AttachmentMaxSize}}" data-default-message="{{.i18n.Tr "dropzone.default_message"}}" data-invalid-input-type="{{.i18n.Tr "dropzone.invalid_input_type"}}" data-file-too-big="{{.i18n.Tr "dropzone.file_too_big"}}" data-remove-file="{{.i18n.Tr "dropzone.remove_file"}}"></div>
{{end}}
</div> </div>
<div class="ui container"> <div class="ui container">
<div class="ui divider"></div> <div class="ui divider"></div>