* 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 UploadAttachmentmaster
@@ -309,7 +309,7 @@ func runWeb(ctx *cli.Context) error { | |||
return | |||
} | |||
}) | |||
m.Post("/issues/attachments", repo.UploadIssueAttachment) | |||
m.Post("/attachments", repo.UploadAttachment) | |||
}, ignSignIn) | |||
m.Group("/:username", func() { | |||
@@ -463,13 +463,11 @@ func runWeb(ctx *cli.Context) error { | |||
m.Get("/:id/:action", repo.ChangeMilestonStatus) | |||
m.Post("/delete", repo.DeleteMilestone) | |||
}, reqRepoWriter, context.RepoRef()) | |||
m.Group("/releases", func() { | |||
m.Get("/new", repo.NewRelease) | |||
m.Post("/new", bindIgnErr(auth.NewReleaseForm{}), repo.NewReleasePost) | |||
m.Post("/delete", repo.DeleteRelease) | |||
}, reqRepoWriter, context.RepoRef()) | |||
m.Group("/releases", func() { | |||
m.Get("/edit/*", repo.EditRelease) | |||
m.Post("/edit/*", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost) | |||
@@ -289,7 +289,7 @@ ENABLE = true | |||
; Path for attachments. Defaults to `data/attachments` | |||
PATH = data/attachments | |||
; 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 = 4 | |||
; Max number of files per upload. Defaults to 10 | |||
@@ -38,6 +38,8 @@ type Release struct { | |||
IsDraft bool `xorm:"NOT NULL DEFAULT false"` | |||
IsPrerelease bool | |||
Attachments []*Attachment `xorm:"-"` | |||
Created time.Time `xorm:"-"` | |||
CreatedUnix int64 `xorm:"INDEX"` | |||
} | |||
@@ -155,8 +157,33 @@ func createTag(gitRepo *git.Repository, rel *Release) error { | |||
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. | |||
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) | |||
if err != nil { | |||
return err | |||
@@ -168,7 +195,14 @@ func CreateRelease(gitRepo *git.Repository, rel *Release) error { | |||
return err | |||
} | |||
rel.LowerTagName = strings.ToLower(rel.TagName) | |||
_, err = x.InsertOne(rel) | |||
if err != nil { | |||
return err | |||
} | |||
err = addReleaseAttachments(rel.ID, attachmentUUIDs) | |||
return err | |||
} | |||
@@ -222,6 +256,64 @@ func GetReleasesByRepoIDAndNames(repoID int64, tagNames []string) (rels []*Relea | |||
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 { | |||
rels []*Release | |||
} | |||
@@ -249,11 +341,17 @@ func SortReleases(rels []*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 { | |||
return err | |||
} | |||
_, err = x.Id(rel.ID).AllCols().Update(rel) | |||
if err != nil { | |||
return err | |||
} | |||
err = addReleaseAttachments(rel.ID, attachmentUUIDs) | |||
return err | |||
} | |||
@@ -267,6 +267,7 @@ type NewReleaseForm struct { | |||
Content string | |||
Draft string | |||
Prerelease bool | |||
Files []string | |||
} | |||
// Validate valideates the fields | |||
@@ -280,6 +281,7 @@ type EditReleaseForm struct { | |||
Content string `form:"content"` | |||
Draft string `form:"draft"` | |||
Prerelease bool `form:"prerelease"` | |||
Files []string | |||
} | |||
// Validate valideates the fields | |||
@@ -718,7 +718,7 @@ please consider changing to GITEA_CUSTOM`) | |||
if !filepath.IsAbs(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) | |||
AttachmentMaxFiles = sec.Key("MAX_FILES").MustInt(5) | |||
AttachmentEnabled = sec.Key("ENABLE").MustBool(true) | |||
@@ -99,7 +99,7 @@ func CreateRelease(ctx *context.APIContext, form api.CreateReleaseOption) { | |||
IsPrerelease: form.IsPrerelease, | |||
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) { | |||
ctx.Status(409) | |||
} else { | |||
@@ -145,7 +145,7 @@ func EditRelease(ctx *context.APIContext, form api.EditReleaseOption) { | |||
if form.IsPrerelease != nil { | |||
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) | |||
return | |||
} | |||
@@ -477,8 +477,8 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) { | |||
ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index)) | |||
} | |||
// UploadIssueAttachment response for uploading issue's attachment | |||
func UploadIssueAttachment(ctx *context.Context) { | |||
// UploadAttachment response for uploading issue's attachment | |||
func UploadAttachment(ctx *context.Context) { | |||
if !setting.AttachmentEnabled { | |||
ctx.Error(404, "attachment is not enabled") | |||
return | |||
@@ -15,6 +15,7 @@ import ( | |||
"code.gitea.io/gitea/modules/context" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/markdown" | |||
"code.gitea.io/gitea/modules/setting" | |||
"github.com/Unknwon/paginater" | |||
) | |||
@@ -99,6 +100,12 @@ func Releases(ctx *context.Context) { | |||
return | |||
} | |||
err = models.GetReleaseAttachments(releases...) | |||
if err != nil { | |||
ctx.Handle(500, "GetReleaseAttachments", err) | |||
return | |||
} | |||
// Temproray cache commits count of used branches to speed up. | |||
countCache := make(map[string]int64) | |||
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["PageIsReleaseList"] = true | |||
ctx.Data["tag_target"] = ctx.Repo.Repository.DefaultBranch | |||
renderAttachmentSettings(ctx); | |||
ctx.HTML(200, tplReleaseNew) | |||
} | |||
@@ -215,7 +223,12 @@ func NewReleasePost(ctx *context.Context, form auth.NewReleaseForm) { | |||
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 | |||
switch { | |||
case models.IsErrReleaseAlreadyExist(err): | |||
@@ -237,6 +250,7 @@ func EditRelease(ctx *context.Context) { | |||
ctx.Data["Title"] = ctx.Tr("repo.release.edit_release") | |||
ctx.Data["PageIsReleaseList"] = true | |||
ctx.Data["PageIsEditRelease"] = true | |||
renderAttachmentSettings(ctx); | |||
tagName := ctx.Params("*") | |||
rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName) | |||
@@ -286,11 +300,16 @@ func EditReleasePost(ctx *context.Context, form auth.EditReleaseForm) { | |||
return | |||
} | |||
var attachmentUUIDs []string | |||
if setting.AttachmentEnabled { | |||
attachmentUUIDs = form.Files | |||
} | |||
rel.Title = form.Title | |||
rel.Note = form.Content | |||
rel.IsDraft = len(form.Draft) > 0 | |||
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) | |||
return | |||
} | |||
@@ -13,5 +13,5 @@ | |||
</div> | |||
{{if .IsAttachmentEnabled}} | |||
<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}} |
@@ -59,6 +59,15 @@ | |||
<li> | |||
<a href="{{$.RepoLink}}/archive/{{.TagName}}.tar.gz"><i class="octicon octicon-file-zip"></i> {{$.i18n.Tr "repo.release.source_code"}} (TAR.GZ)</a> | |||
</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> | |||
</div> | |||
{{else}} | |||
@@ -48,6 +48,10 @@ | |||
<label>{{.i18n.Tr "repo.release.content"}}</label> | |||
<textarea name="content">{{.content}}</textarea> | |||
</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 class="ui container"> | |||
<div class="ui divider"></div> | |||