* 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 UploadAttachmenttags/v1.21.12.1
| @@ -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) | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| } | } | ||||
| @@ -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 | ||||
| @@ -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) | ||||
| @@ -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 | ||||
| } | } | ||||
| @@ -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 | |||||
| func UploadIssueAttachment(ctx *context.Context) { | |||||
| // UploadAttachment response for uploading issue's attachment | |||||
| 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 | ||||
| @@ -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 | ||||
| } | } | ||||
| @@ -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}} | ||||
| @@ -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}} | ||||
| @@ -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> | ||||