Parcourir la source

Add file upload for attachments

Justin Nuß il y a 10 ans
Parent
commit
bfe5b86004

+ 1 - 7
cmd/web.go

@@ -238,6 +238,7 @@ func runWeb(*cli.Context) {
 			r.Post("/:index/label", repo.UpdateIssueLabel)
 			r.Post("/:index/milestone", repo.UpdateIssueMilestone)
 			r.Post("/:index/assignee", repo.UpdateAssignee)
+			r.Get("/:index/attachment/:id", repo.IssueGetAttachment)
 			r.Post("/labels/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel)
 			r.Post("/labels/edit", bindIgnErr(auth.CreateLabelForm{}), repo.UpdateLabel)
 			r.Post("/labels/delete", repo.DeleteLabel)
@@ -254,13 +255,6 @@ func runWeb(*cli.Context) {
 		r.Get("/releases/edit/:tagname", repo.EditRelease)
 	}, reqSignIn, middleware.RepoAssignment(true))
 
-	m.Group("/:username/:reponame/issues/:index/attachment", func(r martini.Router) {
-		r.Get("/:id", repo.IssueGetAttachment)
-		r.Post("/", repo.IssuePostAttachment)
-		r.Post("/:comment", repo.IssuePostAttachment)
-		r.Delete("/:comment/:id", repo.IssueDeleteAttachment)
-	}, reqSignIn, middleware.RepoAssignment(true), middleware.Toggle(&middleware.ToggleOptions{DisableCsrf: true}))
-
 	m.Group("/:username/:reponame", func(r martini.Router) {
 		r.Post("/releases/new", bindIgnErr(auth.NewReleaseForm{}), repo.NewReleasePost)
 		r.Post("/releases/edit/:tagname", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost)

+ 0 - 18
models/issue.go

@@ -1085,21 +1085,3 @@ func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) {
 
 	return DeleteAttachments(attachments, remove)
 }
-
-// AssignAttachment assigns the given attachment to the specified comment
-func AssignAttachment(issueId, commentId, attachmentId int64) error {
-	a, err := GetAttachmentById(attachmentId)
-
-	if err != nil {
-		return err
-	}
-
-	if a.IssueId != issueId {
-		return ErrAttachmentNotLinked
-	}
-
-	a.CommentId = commentId
-
-	_, err = x.Id(a.Id).Update(a)
-	return err
-}

+ 8 - 2
modules/middleware/context.go

@@ -323,7 +323,6 @@ func (f *Flash) Success(msg string) {
 // InitContext initializes a classic context for a request.
 func InitContext() martini.Handler {
 	return func(res http.ResponseWriter, r *http.Request, c martini.Context, rd *Render) {
-
 		ctx := &Context{
 			c: c,
 			// p:      p,
@@ -332,7 +331,6 @@ func InitContext() martini.Handler {
 			Cache:  setting.Cache,
 			Render: rd,
 		}
-
 		ctx.Data["PageStartTime"] = time.Now()
 
 		// start session
@@ -374,6 +372,14 @@ func InitContext() martini.Handler {
 			ctx.Data["IsAdmin"] = ctx.User.IsAdmin
 		}
 
+		// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
+		if strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") {
+			if err = ctx.Req.ParseMultipartForm(setting.AttachmentMaxSize << 20); err != nil { // 32MB max size
+				ctx.Handle(500, "issue.Comment(ctx.Req.ParseMultipartForm)", err)
+				return
+			}
+		}
+
 		// get or create csrf token
 		ctx.Data["CsrfToken"] = ctx.CsrfToken()
 		ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.csrfToken + `">`)

+ 4 - 0
modules/setting/setting.go

@@ -74,6 +74,8 @@ var (
 	// Attachment settings.
 	AttachmentPath         string
 	AttachmentAllowedTypes string
+	AttachmentMaxSize      int64
+	AttachmentMaxFiles     int
 
 	// Cache settings.
 	Cache        cache.Cache
@@ -172,6 +174,8 @@ func NewConfigContext() {
 
 	AttachmentPath = Cfg.MustValue("attachment", "PATH", "files/attachments")
 	AttachmentAllowedTypes = Cfg.MustValue("attachment", "ALLOWED_TYPES", "*/*")
+	AttachmentMaxSize = Cfg.MustInt64("attachment", "MAX_SIZE", 32)
+	AttachmentMaxFiles = Cfg.MustInt("attachment", "MAX_FILES", 10)
 
 	if err = os.MkdirAll(AttachmentPath, os.ModePerm); err != nil {
 		log.Fatal("Could not create directory %s: %s", AttachmentPath, err)

+ 17 - 0
public/css/gogs.css

@@ -1819,4 +1819,21 @@ body {
 
 .attachment-preview-img {
     border: 1px solid #d8d8d8;
+}
+
+#attachments-button {
+    float: left;
+}
+
+#attached {
+    height: 18px;
+    margin: 10px 10px 15px 10px;
+}
+
+#attached-list .label {
+    margin-right: 10px;
+}
+
+#issue-create-form #attached {
+    margin-bottom: 0;
 }

+ 21 - 6
public/js/app.js

@@ -536,7 +536,7 @@ function initIssue() {
         var over = function() {
             var $this = $(this);
 
-            if ($this.text().match(/\.(png|jpg|jpeg|gif)$/) == false) {
+            if ($this.text().match(/\.(png|jpg|jpeg|gif)$/i) == false) {
                 return;
             }
 
@@ -576,15 +576,30 @@ function initIssue() {
 
     // Upload.
     (function() {
-        var $attached = $("#attached");
-        var $attachments = $("input[name=attachments]");
+        var $attachedList = $("#attached-list");
         var $addButton = $("#attachments-button");
 
-        var commentId = $addButton.attr("data-comment-id"); // "0" == for issue, "" == for comment
-        var accepted = $addButton.attr("data-accept");
+        var fileInput = $("#attachments-input")[0];
+
+        fileInput.addEventListener("change", function(event) {
+            $attachedList.empty();
+            $attachedList.append("<b>Attachments:</b> ");
+
+            for (var index = 0; index < fileInput.files.length; index++) {
+                var file = fileInput.files[index];
+
+                var $span = $("<span></span>");
+
+                $span.addClass("label");
+                $span.addClass("label-default");
+
+                $span.append(file.name.toLowerCase());
+                $attachedList.append($span);
+            }
+        });
 
         $addButton.on("click", function() {
-            // TODO: (nuss-justin): open dialog, upload file, add id to list, add file to $attached list
+            fileInput.click();
             return false;
         });
     }());

+ 71 - 250
routers/repo/issue.go

@@ -5,6 +5,7 @@
 package repo
 
 import (
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -35,6 +36,11 @@ const (
 	MILESTONE_EDIT base.TplName = "repo/issue/milestone_edit"
 )
 
+var (
+	ErrFileTypeForbidden = errors.New("File type is not allowed")
+	ErrTooManyFiles      = errors.New("Maximum number of files to upload exceeded")
+)
+
 func Issues(ctx *middleware.Context) {
 	ctx.Data["Title"] = "Issues"
 	ctx.Data["IsRepoToolbarIssues"] = true
@@ -233,6 +239,8 @@ func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.C
 		return
 	}
 
+	uploadFiles(ctx, issue.Id, 0)
+
 	// Update mentions.
 	ms := base.MentionPattern.FindAllString(issue.Content, -1)
 	if len(ms) > 0 {
@@ -619,6 +627,67 @@ func UpdateAssignee(ctx *middleware.Context) {
 	})
 }
 
+func uploadFiles(ctx *middleware.Context, issueId, commentId int64) {
+	allowedTypes := strings.Split(setting.AttachmentAllowedTypes, "|")
+	attachments := ctx.Req.MultipartForm.File["attachments"]
+
+	if len(attachments) > setting.AttachmentMaxFiles {
+		ctx.Handle(400, "issue.Comment", ErrTooManyFiles)
+		return
+	}
+
+	for _, header := range attachments {
+		file, err := header.Open()
+
+		if err != nil {
+			ctx.Handle(500, "issue.Comment(header.Open)", err)
+			return
+		}
+
+		defer file.Close()
+
+		allowed := false
+		fileType := mime.TypeByExtension(header.Filename)
+
+		for _, t := range allowedTypes {
+			t := strings.Trim(t, " ")
+
+			if t == "*/*" || t == fileType {
+				allowed = true
+				break
+			}
+		}
+
+		if !allowed {
+			ctx.Handle(400, "issue.Comment", ErrFileTypeForbidden)
+			return
+		}
+
+		out, err := ioutil.TempFile(setting.AttachmentPath, "attachment_")
+
+		if err != nil {
+			ctx.Handle(500, "issue.Comment(ioutil.TempFile)", err)
+			return
+		}
+
+		defer out.Close()
+
+		_, err = io.Copy(out, file)
+
+		if err != nil {
+			ctx.Handle(500, "issue.Comment(io.Copy)", err)
+			return
+		}
+
+		_, err = models.CreateAttachment(issueId, commentId, header.Filename, out.Name())
+
+		if err != nil {
+			ctx.Handle(500, "issue.Comment(io.Copy)", err)
+			return
+		}
+	}
+}
+
 func Comment(ctx *middleware.Context, params martini.Params) {
 	index, err := base.StrTo(ctx.Query("issueIndex")).Int64()
 	if err != nil {
@@ -706,28 +775,8 @@ func Comment(ctx *middleware.Context, params martini.Params) {
 		}
 	}
 
-	attachments := strings.Split(params["attachments"], ",")
-
-	for _, a := range attachments {
-		a = strings.Trim(a, " ")
-
-		if len(a) == 0 {
-			continue
-		}
-
-		aId, err := base.StrTo(a).Int64()
-
-		if err != nil {
-			ctx.Handle(400, "issue.Comment(base.StrTo.Int64)", err)
-			return
-		}
-
-		err = models.AssignAttachment(issue.Id, comment.Id, aId)
-
-		if err != nil {
-			ctx.Handle(400, "issue.Comment(models.AssignAttachment)", err)
-			return
-		}
+	if comment != nil {
+		uploadFiles(ctx, issue.Id, comment.Id)
 	}
 
 	// Notify watchers.
@@ -1007,122 +1056,6 @@ func UpdateMilestonePost(ctx *middleware.Context, params martini.Params, form au
 	ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones")
 }
 
-func IssuePostAttachment(ctx *middleware.Context, params martini.Params) {
-	index, _ := base.StrTo(params["index"]).Int64()
-
-	if index == 0 {
-		ctx.JSON(400, map[string]interface{}{
-			"ok":    false,
-			"error": "invalid issue index",
-		})
-
-		return
-	}
-
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, index)
-
-	if err != nil {
-		ctx.JSON(400, map[string]interface{}{
-			"ok":    false,
-			"error": "invalid comment id",
-		})
-
-		return
-	}
-
-	commentId, err := base.StrTo(params["comment"]).Int64()
-
-	if err != nil && len(params["comment"]) > 0 {
-		ctx.JSON(400, map[string]interface{}{
-			"ok":    false,
-			"error": "invalid comment id",
-		})
-
-		return
-	}
-
-	if commentId == 0 {
-		commentId = -1
-	}
-
-	file, header, err := ctx.Req.FormFile("attachment")
-
-	if err != nil {
-		ctx.JSON(400, map[string]interface{}{
-			"ok":    false,
-			"error": "upload error",
-		})
-
-		return
-	}
-
-	defer file.Close()
-
-	// check mime type, write to file, insert attachment to db
-	allowedTypes := strings.Split(setting.AttachmentAllowedTypes, "|")
-	allowed := false
-
-	fileType := mime.TypeByExtension(header.Filename)
-
-	for _, t := range allowedTypes {
-		t := strings.Trim(t, " ")
-
-		if t == "*/*" || t == fileType {
-			allowed = true
-			break
-		}
-	}
-
-	if !allowed {
-		ctx.JSON(400, map[string]interface{}{
-			"ok":    false,
-			"error": "mime type not allowed",
-		})
-
-		return
-	}
-
-	out, err := ioutil.TempFile(setting.AttachmentPath, "attachment_")
-
-	if err != nil {
-		ctx.JSON(500, map[string]interface{}{
-			"ok":    false,
-			"error": "internal server error",
-		})
-
-		return
-	}
-
-	defer out.Close()
-
-	_, err = io.Copy(out, file)
-
-	if err != nil {
-		ctx.JSON(500, map[string]interface{}{
-			"ok":    false,
-			"error": "internal server error",
-		})
-
-		return
-	}
-
-	a, err := models.CreateAttachment(issue.Id, commentId, header.Filename, out.Name())
-
-	if err != nil {
-		ctx.JSON(500, map[string]interface{}{
-			"ok":    false,
-			"error": "internal server error",
-		})
-
-		return
-	}
-
-	ctx.JSON(500, map[string]interface{}{
-		"ok": true,
-		"id": a.Id,
-	})
-}
-
 func IssueGetAttachment(ctx *middleware.Context, params martini.Params) {
 	id, err := base.StrTo(params["id"]).Int64()
 
@@ -1138,117 +1071,5 @@ func IssueGetAttachment(ctx *middleware.Context, params martini.Params) {
 		return
 	}
 
-	log.Error("path=%s name=%s", attachment.Path, attachment.Name)
-
 	ctx.ServeFile(attachment.Path, attachment.Name)
 }
-
-func IssueDeleteAttachment(ctx *middleware.Context, params martini.Params) {
-	index, _ := base.StrTo(params["index"]).Int64()
-
-	if index == 0 {
-		ctx.JSON(400, map[string]interface{}{
-			"ok":    false,
-			"error": "invalid issue index",
-		})
-
-		return
-	}
-
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, index)
-
-	if err != nil {
-		ctx.JSON(400, map[string]interface{}{
-			"ok":    false,
-			"error": "invalid comment id",
-		})
-
-		return
-	}
-
-	commentId, err := base.StrTo(params["comment"]).Int64()
-
-	if err != nil || commentId < 0 {
-		ctx.JSON(400, map[string]interface{}{
-			"ok":    false,
-			"error": "invalid comment id",
-		})
-
-		return
-	}
-
-	comment, err := models.GetCommentById(commentId)
-
-	if err != nil {
-		ctx.JSON(400, map[string]interface{}{
-			"ok":    false,
-			"error": "invalid issue id",
-		})
-
-		return
-	}
-
-	if comment.PosterId != ctx.User.Id && !ctx.User.IsAdmin {
-		ctx.JSON(400, map[string]interface{}{
-			"ok":    false,
-			"error": "no permissions",
-		})
-
-		return
-	}
-
-	attachmentId, err := base.StrTo(params["id"]).Int64()
-
-	if err != nil {
-		ctx.JSON(400, map[string]interface{}{
-			"ok":    false,
-			"error": "invalid attachment id",
-		})
-
-		return
-	}
-
-	attachment, err := models.GetAttachmentById(attachmentId)
-
-	if err != nil {
-		ctx.JSON(400, map[string]interface{}{
-			"ok":    false,
-			"error": "wrong attachment id",
-		})
-
-		return
-	}
-
-	if attachment.IssueId != issue.Id {
-		ctx.JSON(400, map[string]interface{}{
-			"ok":    false,
-			"error": "attachment not associated with the given issue",
-		})
-
-		return
-	}
-
-	if attachment.CommentId != commentId {
-		ctx.JSON(400, map[string]interface{}{
-			"ok":    false,
-			"error": "attachment not associated with the given comment",
-		})
-
-		return
-	}
-
-	err = models.DeleteAttachment(attachment, true)
-
-	if err != nil {
-		ctx.JSON(500, map[string]interface{}{
-			"ok":    false,
-			"error": "could not delete attachment",
-		})
-
-		return
-	}
-
-	ctx.JSON(200, map[string]interface{}{
-		"ok": true,
-	})
-}

+ 5 - 10
templates/repo/issue/create.tmpl

@@ -4,7 +4,7 @@
 {{template "repo/toolbar" .}}
 <div id="body" class="container">
     <div id="issue">
-        <form class="form" action="{{.RepoLink}}/issues/new" method="post" id="issue-create-form">
+        <form class="form" action="{{.RepoLink}}/issues/new" method="post" id="issue-create-form" enctype="multipart/form-data">
             {{.CsrfTokenHtml}}
             {{template "base/alert" .}}
             <div class="col-md-1">
@@ -101,18 +101,13 @@
                         <div class="tab-pane issue-preview-content" id="issue-preview">loading...</div>
                     </div>
                 </div>
-                <!--
-                <div>
-                    <div id="attached"></div>
+                <div id="attached">
+                    <div id="attached-list"></div>
                 </div>
-                -->
                 <div class="text-right panel-body">
                     <div class="form-group">
-                        <!--
-                        <input type="hidden" name="attachments" value="" />
-                        <button data-accept="{{.AllowedTypes}}" data-comment-id="0" class="btn-default btn attachment-add" id="attachments-button">Add Attachments...</button>
-                        -->
-
+                        <input type="file" accept="{{.AllowedTypes}}" style="display: none;" id="attachments-input" name="attachments" multiple />
+                        <button class="btn-default btn attachment-add" id="attachments-button">Select Attachments...</button>
                         <input type="hidden" value="id" name="repo-id"/>
                         <button class="btn-success btn">Create new issue</button>
                     </div>

+ 5 - 10
templates/repo/issue/view.tmpl

@@ -117,7 +117,7 @@
                     <hr class="issue-line"/>
                     {{if .SignedUser}}<div class="issue-child issue-reply">
                     <a class="user pull-left" href="/user/{{.SignedUser.Name}}"><img class="avatar" src="{{.SignedUser.AvatarLink}}" alt=""/></a>
-                    <form class="panel panel-default issue-content" action="{{.RepoLink}}/comment/new" method="post">
+                    <form class="panel panel-default issue-content" action="{{.RepoLink}}/comment/new" method="post" enctype="multipart/form-data">
                         {{.CsrfTokenHtml}}
                         <div class="panel-body">
                             <div class="form-group">
@@ -137,18 +137,13 @@
                                     <div class="tab-pane issue-preview-content" id="issue-preview">Loading...</div>
                                 </div>
                             </div>
-                            <!--
-                            <div>
-                                <div id="attached"></div>
+                            <div id="attached">
+                                <div id="attached-list"></div>
                             </div>
-                            -->
                             <div class="text-right">
                                 <div class="form-group">
-                                    <!--
-                                    <input type="hidden" name="attachments" value="" />
-                                    <button data-accept="{{.AllowedTypes}}" class="btn-default btn attachment-add" id="attachments-button">Add Attachments...</button>
-                                    -->
-
+                                    <input type="file" accept="{{.AllowedTypes}}" style="display: none;" id="attachments-input" name="attachments" multiple />
+                                    <button class="btn-default btn attachment-add" id="attachments-button">Select Attachments...</button>
                                     {{if .IsIssueOwner}}{{if .Issue.IsClosed}}
                                     <input type="submit" class="btn-default btn issue-open" id="issue-open-btn" data-origin="Reopen" data-text="Reopen & Comment" name="change_status" value="Reopen"/>{{else}}
                                     <input type="submit" class="btn-default btn issue-close" id="issue-close-btn" data-origin="Close" data-text="Close & Comment" name="change_status" value="Close"/>{{end}}{{end}}&nbsp;&nbsp;