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
This commit is contained in:
		
							parent
							
								
									dce03c19cb
								
							
						
					
					
						commit
						64375d875b
					
				
					 11 changed files with 144 additions and 14 deletions
				
			
		| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								conf/app.ini
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								conf/app.ini
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue