| @@ -327,7 +327,22 @@ func runWeb(ctx *cli.Context) { | |||
| m.Group("", func() { | |||
| m.Get("/:username", user.Profile) | |||
| m.Post("/attachments", repo.UploadAttachment) | |||
| m.Get("/attachments/:uuid", func(ctx *middleware.Context) { | |||
| attach, err := models.GetAttachmentByUUID(ctx.Params(":uuid")) | |||
| if err != nil { | |||
| if models.IsErrAttachmentNotExist(err) { | |||
| ctx.Error(404) | |||
| } else { | |||
| ctx.Handle(500, "GetAttachmentByUUID", err) | |||
| } | |||
| return | |||
| } | |||
| // Fix #312. Attachments with , in their name are not handled correctly by Google Chrome. | |||
| // We must put the name in " manually. | |||
| ctx.ServeFileContent(attach.LocalPath(), "\""+attach.Name+"\"") | |||
| }) | |||
| m.Post("/issues/attachments", repo.UploadIssueAttachment) | |||
| }, ignSignIn) | |||
| if macaron.Env == macaron.DEV { | |||
| @@ -428,7 +443,6 @@ func runWeb(ctx *cli.Context) { | |||
| m.Post("/:index/label", repo.UpdateIssueLabel) | |||
| m.Post("/:index/milestone", repo.UpdateIssueMilestone) | |||
| m.Post("/:index/assignee", repo.UpdateAssignee) | |||
| m.Get("/:index/attachment/:id", repo.IssueGetAttachment) | |||
| }) | |||
| m.Group("/labels", func() { | |||
| m.Post("/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel) | |||
| @@ -212,7 +212,7 @@ ALLOWED_TYPES = image/jpeg|image/png | |||
| ; Max size of each file. Defaults to 32MB | |||
| MAX_SIZE = 32 | |||
| ; Max number of files per upload. Defaults to 10 | |||
| MAX_FILES = 10 | |||
| MAX_FILES = 5 | |||
| [time] | |||
| ; Specifies the format for fully outputed dates. Defaults to RFC1123 | |||
| @@ -17,7 +17,7 @@ import ( | |||
| "github.com/gogits/gogs/modules/setting" | |||
| ) | |||
| const APP_VER = "0.6.4.0810 Beta" | |||
| const APP_VER = "0.6.4.0811 Beta" | |||
| func init() { | |||
| runtime.GOMAXPROCS(runtime.NumCPU()) | |||
| @@ -279,3 +279,24 @@ func IsErrMilestoneNotExist(err error) bool { | |||
| func (err ErrMilestoneNotExist) Error() string { | |||
| return fmt.Sprintf("milestone does not exist [id: %d, repo_id: %d]", err.ID, err.RepoID) | |||
| } | |||
| // _____ __ __ .__ __ | |||
| // / _ \_/ |__/ |______ ____ | |__ _____ ____ _____/ |_ | |||
| // / /_\ \ __\ __\__ \ _/ ___\| | \ / \_/ __ \ / \ __\ | |||
| // / | \ | | | / __ \\ \___| Y \ Y Y \ ___/| | \ | | |||
| // \____|__ /__| |__| (____ /\___ >___| /__|_| /\___ >___| /__| | |||
| // \/ \/ \/ \/ \/ \/ \/ | |||
| type ErrAttachmentNotExist struct { | |||
| ID int64 | |||
| UUID string | |||
| } | |||
| func IsErrAttachmentNotExist(err error) bool { | |||
| _, ok := err.(ErrAttachmentNotExist) | |||
| return ok | |||
| } | |||
| func (err ErrAttachmentNotExist) Error() string { | |||
| return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID) | |||
| } | |||
| @@ -9,7 +9,10 @@ import ( | |||
| "errors" | |||
| "fmt" | |||
| "html/template" | |||
| "io" | |||
| "mime/multipart" | |||
| "os" | |||
| "path" | |||
| "strconv" | |||
| "strings" | |||
| "time" | |||
| @@ -20,12 +23,12 @@ import ( | |||
| "github.com/gogits/gogs/modules/base" | |||
| "github.com/gogits/gogs/modules/log" | |||
| "github.com/gogits/gogs/modules/setting" | |||
| gouuid "github.com/gogits/gogs/modules/uuid" | |||
| ) | |||
| var ( | |||
| ErrIssueNotExist = errors.New("Issue does not exist") | |||
| ErrWrongIssueCounter = errors.New("Invalid number of issues for this milestone") | |||
| ErrAttachmentNotExist = errors.New("Attachment does not exist") | |||
| ErrAttachmentNotLinked = errors.New("Attachment does not belong to this issue") | |||
| ErrMissingIssueNumber = errors.New("No issue number specified") | |||
| ) | |||
| @@ -159,7 +162,20 @@ func (i *Issue) AfterDelete() { | |||
| } | |||
| // CreateIssue creates new issue with labels for repository. | |||
| func NewIssue(repo *Repository, issue *Issue, labelIDs []int64) (err error) { | |||
| func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { | |||
| // Check attachments. | |||
| attachments := make([]*Attachment, 0, len(uuids)) | |||
| for _, uuid := range uuids { | |||
| attach, err := GetAttachmentByUUID(uuid) | |||
| if err != nil { | |||
| if IsErrAttachmentNotExist(err) { | |||
| continue | |||
| } | |||
| return fmt.Errorf("GetAttachmentByUUID[%s]: %v", uuid, err) | |||
| } | |||
| attachments = append(attachments, attach) | |||
| } | |||
| sess := x.NewSession() | |||
| defer sessionRelease(sess) | |||
| if err = sess.Begin(); err != nil { | |||
| @@ -188,6 +204,14 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64) (err error) { | |||
| return err | |||
| } | |||
| for i := range attachments { | |||
| attachments[i].IssueID = issue.ID | |||
| // No assign value could be 0, so ignore AllCols(). | |||
| if _, err = sess.Id(attachments[i].ID).Update(attachments[i]); err != nil { | |||
| return fmt.Errorf("update attachment[%d]: %v", attachments[i].ID, err) | |||
| } | |||
| } | |||
| // Notify watchers. | |||
| act := &Action{ | |||
| ActUserID: issue.Poster.Id, | |||
| @@ -1210,49 +1234,73 @@ func (c *Comment) AfterDelete() { | |||
| } | |||
| } | |||
| // Attachment represent a attachment of issue/comment/release. | |||
| type Attachment struct { | |||
| Id int64 | |||
| IssueId int64 | |||
| CommentId int64 | |||
| ID int64 `xorm:"pk autoincr"` | |||
| UUID string `xorm:"uuid UNIQUE"` | |||
| IssueID int64 `xorm:"INDEX"` | |||
| CommentID int64 | |||
| ReleaseID int64 `xorm:"INDEX"` | |||
| Name string | |||
| Path string `xorm:"TEXT"` | |||
| Created time.Time `xorm:"CREATED"` | |||
| } | |||
| // CreateAttachment creates a new attachment inside the database and | |||
| func CreateAttachment(issueId, commentId int64, name, path string) (*Attachment, error) { | |||
| sess := x.NewSession() | |||
| defer sess.Close() | |||
| // AttachmentLocalPath returns where attachment is stored in local file system based on given UUID. | |||
| func AttachmentLocalPath(uuid string) string { | |||
| return path.Join(setting.AttachmentPath, uuid[0:1], uuid[1:2], uuid) | |||
| } | |||
| // LocalPath returns where attachment is stored in local file system. | |||
| func (attach *Attachment) LocalPath() string { | |||
| return AttachmentLocalPath(attach.UUID) | |||
| } | |||
| // NewAttachment creates a new attachment object. | |||
| func NewAttachment(name string, buf []byte, file multipart.File) (_ *Attachment, err error) { | |||
| attach := &Attachment{ | |||
| UUID: gouuid.NewV4().String(), | |||
| Name: name, | |||
| } | |||
| if err = os.MkdirAll(path.Dir(attach.LocalPath()), os.ModePerm); err != nil { | |||
| return nil, fmt.Errorf("MkdirAll: %v", err) | |||
| } | |||
| fw, err := os.Create(attach.LocalPath()) | |||
| if err != nil { | |||
| return nil, fmt.Errorf("Create: %v", err) | |||
| } | |||
| defer fw.Close() | |||
| if _, err = fw.Write(buf); err != nil { | |||
| return nil, fmt.Errorf("Write: %v", err) | |||
| } else if _, err = io.Copy(fw, file); err != nil { | |||
| return nil, fmt.Errorf("Copy: %v", err) | |||
| } | |||
| sess := x.NewSession() | |||
| defer sessionRelease(sess) | |||
| if err := sess.Begin(); err != nil { | |||
| return nil, err | |||
| } | |||
| a := &Attachment{IssueId: issueId, CommentId: commentId, Name: name, Path: path} | |||
| if _, err := sess.Insert(a); err != nil { | |||
| sess.Rollback() | |||
| if _, err := sess.Insert(attach); err != nil { | |||
| return nil, err | |||
| } | |||
| return a, sess.Commit() | |||
| return attach, sess.Commit() | |||
| } | |||
| // Attachment returns the attachment by given ID. | |||
| func GetAttachmentById(id int64) (*Attachment, error) { | |||
| m := &Attachment{Id: id} | |||
| has, err := x.Get(m) | |||
| // GetAttachmentByUUID returns attachment by given UUID. | |||
| func GetAttachmentByUUID(uuid string) (*Attachment, error) { | |||
| attach := &Attachment{UUID: uuid} | |||
| has, err := x.Get(attach) | |||
| if err != nil { | |||
| return nil, err | |||
| } else if !has { | |||
| return nil, ErrAttachmentNotExist{0, uuid} | |||
| } | |||
| if !has { | |||
| return nil, ErrAttachmentNotExist | |||
| } | |||
| return m, nil | |||
| return attach, nil | |||
| } | |||
| func GetAttachmentsForIssue(issueId int64) ([]*Attachment, error) { | |||
| @@ -1285,12 +1333,12 @@ func DeleteAttachment(a *Attachment, remove bool) error { | |||
| func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) { | |||
| for i, a := range attachments { | |||
| if remove { | |||
| if err := os.Remove(a.Path); err != nil { | |||
| if err := os.Remove(a.LocalPath()); err != nil { | |||
| return i, err | |||
| } | |||
| } | |||
| if _, err := x.Delete(a.Id); err != nil { | |||
| if _, err := x.Delete(a.ID); err != nil { | |||
| return i, err | |||
| } | |||
| } | |||
| @@ -5,8 +5,12 @@ | |||
| package migrations | |||
| import ( | |||
| "bytes" | |||
| "encoding/json" | |||
| "fmt" | |||
| "io/ioutil" | |||
| "os" | |||
| "path" | |||
| "strings" | |||
| "time" | |||
| @@ -16,6 +20,7 @@ import ( | |||
| "github.com/gogits/gogs/modules/log" | |||
| "github.com/gogits/gogs/modules/setting" | |||
| gouuid "github.com/gogits/gogs/modules/uuid" | |||
| ) | |||
| const _MIN_DB_VER = 0 | |||
| @@ -59,6 +64,7 @@ var migrations = []Migration{ | |||
| NewMigration("fix locale file load panic", fixLocaleFileLoadPanic), // V4 -> V5:v0.6.0 | |||
| NewMigration("trim action compare URL prefix", trimCommitActionAppUrlPrefix), // V5 -> V6:v0.6.3 | |||
| NewMigration("generate issue-label from issue", issueToIssueLabel), // V6 -> V7:v0.6.4 | |||
| NewMigration("refactor attachment table", attachmentRefactor), // V7 -> V8:v0.6.4 | |||
| } | |||
| // Migrate database to current version | |||
| @@ -97,8 +103,11 @@ func Migrate(x *xorm.Engine) error { | |||
| } | |||
| v := currentVersion.Version | |||
| if int(v) > len(migrations) { | |||
| return nil | |||
| if int(v-_MIN_DB_VER) > len(migrations) { | |||
| // User downgraded Gogs. | |||
| currentVersion.Version = int64(len(migrations) + _MIN_DB_VER) | |||
| _, err = x.Id(1).Update(currentVersion) | |||
| return err | |||
| } | |||
| for i, m := range migrations[v-_MIN_DB_VER:] { | |||
| log.Info("Migration: %s", m.Description()) | |||
| @@ -515,3 +524,85 @@ func issueToIssueLabel(x *xorm.Engine) error { | |||
| return sess.Commit() | |||
| } | |||
| func attachmentRefactor(x *xorm.Engine) error { | |||
| type Attachment struct { | |||
| ID int64 `xorm:"pk autoincr"` | |||
| UUID string `xorm:"uuid INDEX"` | |||
| // For rename purpose. | |||
| Path string `xorm:"-"` | |||
| NewPath string `xorm:"-"` | |||
| } | |||
| results, err := x.Query("SELECT * FROM `attachment`") | |||
| if err != nil { | |||
| return fmt.Errorf("select attachments: %v", err) | |||
| } | |||
| attachments := make([]*Attachment, 0, len(results)) | |||
| for _, attach := range results { | |||
| if !com.IsExist(string(attach["path"])) { | |||
| // If the attachment is already missing, there is no point to update it. | |||
| continue | |||
| } | |||
| attachments = append(attachments, &Attachment{ | |||
| ID: com.StrTo(attach["id"]).MustInt64(), | |||
| UUID: gouuid.NewV4().String(), | |||
| Path: string(attach["path"]), | |||
| }) | |||
| } | |||
| sess := x.NewSession() | |||
| defer sessionRelease(sess) | |||
| if err = sess.Begin(); err != nil { | |||
| return err | |||
| } | |||
| if err = sess.Sync2(new(Attachment)); err != nil { | |||
| return fmt.Errorf("Sync2: %v", err) | |||
| } | |||
| // Note: Roll back for rename can be a dead loop, | |||
| // so produces a backup file. | |||
| var buf bytes.Buffer | |||
| buf.WriteString("# old path -> new path\n") | |||
| // Update database first because this is where error happens the most often. | |||
| for _, attach := range attachments { | |||
| if _, err = sess.Id(attach.ID).Update(attach); err != nil { | |||
| return err | |||
| } | |||
| attach.NewPath = path.Join(setting.AttachmentPath, attach.UUID[0:1], attach.UUID[1:2], attach.UUID) | |||
| buf.WriteString(attach.Path) | |||
| buf.WriteString("\t") | |||
| buf.WriteString(attach.NewPath) | |||
| buf.WriteString("\n") | |||
| } | |||
| // Then rename attachments. | |||
| isSucceed := true | |||
| defer func() { | |||
| if isSucceed { | |||
| return | |||
| } | |||
| dumpPath := path.Join(setting.LogRootPath, "attachment_path.dump") | |||
| ioutil.WriteFile(dumpPath, buf.Bytes(), 0666) | |||
| fmt.Println("Fail to rename some attachments, old and new paths are saved into:", dumpPath) | |||
| }() | |||
| for _, attach := range attachments { | |||
| if err = os.MkdirAll(path.Dir(attach.NewPath), os.ModePerm); err != nil { | |||
| isSucceed = false | |||
| return err | |||
| } | |||
| if err = os.Rename(attach.Path, attach.NewPath); err != nil { | |||
| isSucceed = false | |||
| return err | |||
| } | |||
| } | |||
| return sess.Commit() | |||
| } | |||
| @@ -103,6 +103,7 @@ type CreateIssueForm struct { | |||
| MilestoneID int64 | |||
| AssigneeID int64 | |||
| Content string | |||
| Attachments []string | |||
| } | |||
| func (f *CreateIssueForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | |||
| @@ -285,9 +285,9 @@ func NewConfigContext() { | |||
| if !filepath.IsAbs(AttachmentPath) { | |||
| AttachmentPath = path.Join(workDir, AttachmentPath) | |||
| } | |||
| AttachmentAllowedTypes = sec.Key("ALLOWED_TYPES").MustString("image/jpeg|image/png") | |||
| AttachmentAllowedTypes = strings.Replace(sec.Key("ALLOWED_TYPES").MustString("image/jpeg,image/png"), "|", ",", -1) | |||
| AttachmentMaxSize = sec.Key("MAX_SIZE").MustInt64(32) | |||
| AttachmentMaxFiles = sec.Key("MAX_FILES").MustInt(10) | |||
| AttachmentMaxFiles = sec.Key("MAX_FILES").MustInt(5) | |||
| AttachmentEnabled = sec.Key("ENABLE").MustBool(true) | |||
| TimeFormat = map[string]string{ | |||
| @@ -254,7 +254,7 @@ $(document).ready(function () { | |||
| $dropz.dropzone({ | |||
| url: $dropz.data('upload-url'), | |||
| headers: {"X-Csrf-Token": csrf}, | |||
| maxFiles: 5, | |||
| maxFiles: $dropz.data('max-file'), | |||
| maxFilesize: $dropz.data('max-size'), | |||
| acceptedFiles: $dropz.data('accepts'), | |||
| addRemoveLinks: true, | |||
| @@ -265,10 +265,12 @@ $(document).ready(function () { | |||
| init: function () { | |||
| this.on("success", function (file, data) { | |||
| filenameDict[file.name] = data.uuid; | |||
| console.log(data) | |||
| $('.attachments').append('<input id="' + data.uuid + '" name="attachments" type="hidden" value="' + data.uuid + '">'); | |||
| }) | |||
| this.on("removedfile", function (file) { | |||
| console.log(filenameDict[file.name]); | |||
| if (file.name in filenameDict) { | |||
| $('#' + filenameDict[file.name]).remove(); | |||
| } | |||
| }) | |||
| } | |||
| }); | |||
| @@ -152,6 +152,9 @@ | |||
| margin-bottom: 10px; | |||
| border: 2px dashed #0087F7; | |||
| box-shadow: none; | |||
| .dz-error-message { | |||
| top: 140px; | |||
| } | |||
| } | |||
| } | |||
| @@ -7,11 +7,8 @@ package repo | |||
| import ( | |||
| "errors" | |||
| "fmt" | |||
| "io" | |||
| "io/ioutil" | |||
| "net/http" | |||
| "net/url" | |||
| "os" | |||
| "strings" | |||
| "time" | |||
| @@ -181,6 +178,7 @@ func NewIssue(ctx *middleware.Context) { | |||
| ctx.Data["RequireDropzone"] = true | |||
| ctx.Data["IsAttachmentEnabled"] = setting.AttachmentEnabled | |||
| ctx.Data["AttachmentAllowedTypes"] = setting.AttachmentAllowedTypes | |||
| ctx.Data["AttachmentMaxFiles"] = setting.AttachmentMaxFiles | |||
| if ctx.User.IsAdmin { | |||
| var ( | |||
| @@ -215,18 +213,19 @@ func NewIssue(ctx *middleware.Context) { | |||
| } | |||
| func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) { | |||
| fmt.Println(ctx.QueryStrings("uuids")) | |||
| ctx.Data["Title"] = ctx.Tr("repo.issues.new") | |||
| ctx.Data["PageIsIssueList"] = true | |||
| ctx.Data["RequireDropzone"] = true | |||
| ctx.Data["IsAttachmentEnabled"] = setting.AttachmentEnabled | |||
| ctx.Data["AttachmentAllowedTypes"] = setting.AttachmentAllowedTypes | |||
| ctx.Data["AttachmentMaxFiles"] = setting.AttachmentMaxFiles | |||
| var ( | |||
| repo = ctx.Repo.Repository | |||
| labelIDs []int64 | |||
| milestoneID int64 | |||
| assigneeID int64 | |||
| attachments []string | |||
| ) | |||
| if ctx.User.IsAdmin { | |||
| // Check labels. | |||
| @@ -286,6 +285,10 @@ func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) { | |||
| } | |||
| } | |||
| if setting.AttachmentEnabled { | |||
| attachments = ctx.QueryStrings("attachments") | |||
| } | |||
| if ctx.HasError() { | |||
| ctx.HTML(200, ISSUE_NEW) | |||
| return | |||
| @@ -301,7 +304,7 @@ func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) { | |||
| AssigneeID: assigneeID, | |||
| Content: form.Content, | |||
| } | |||
| if err := models.NewIssue(repo, issue, labelIDs); err != nil { | |||
| if err := models.NewIssue(repo, issue, labelIDs, attachments); err != nil { | |||
| ctx.Handle(500, "NewIssue", err) | |||
| return | |||
| } | |||
| @@ -347,9 +350,50 @@ func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) { | |||
| ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index)) | |||
| } | |||
| func UploadAttachment(ctx *middleware.Context) { | |||
| func UploadIssueAttachment(ctx *middleware.Context) { | |||
| if !setting.AttachmentEnabled { | |||
| ctx.Error(404, "attachment is not enabled") | |||
| return | |||
| } | |||
| allowedTypes := strings.Split(setting.AttachmentAllowedTypes, ",") | |||
| file, header, err := ctx.Req.FormFile("file") | |||
| if err != nil { | |||
| ctx.Error(500, fmt.Sprintf("FormFile: %v", err)) | |||
| return | |||
| } | |||
| defer file.Close() | |||
| buf := make([]byte, 1024) | |||
| n, _ := file.Read(buf) | |||
| if n > 0 { | |||
| buf = buf[:n] | |||
| } | |||
| fileType := http.DetectContentType(buf) | |||
| allowed := false | |||
| for _, t := range allowedTypes { | |||
| t := strings.Trim(t, " ") | |||
| if t == "*/*" || t == fileType { | |||
| allowed = true | |||
| break | |||
| } | |||
| } | |||
| if !allowed { | |||
| ctx.Error(400, ErrFileTypeForbidden.Error()) | |||
| return | |||
| } | |||
| attach, err := models.NewAttachment(header.Filename, buf, file) | |||
| if err != nil { | |||
| ctx.Error(500, fmt.Sprintf("NewAttachment: %v", err)) | |||
| return | |||
| } | |||
| log.Trace("New attachment uploaded: %s", attach.UUID) | |||
| ctx.JSON(200, map[string]string{ | |||
| "uuid": "fuck", | |||
| "uuid": attach.UUID, | |||
| }) | |||
| } | |||
| @@ -687,78 +731,6 @@ func UpdateAssignee(ctx *middleware.Context) { | |||
| }) | |||
| } | |||
| func uploadFiles(ctx *middleware.Context, issueId, commentId int64) { | |||
| if !setting.AttachmentEnabled { | |||
| return | |||
| } | |||
| 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() | |||
| buf := make([]byte, 1024) | |||
| n, _ := file.Read(buf) | |||
| if n > 0 { | |||
| buf = buf[:n] | |||
| } | |||
| fileType := http.DetectContentType(buf) | |||
| fmt.Println(fileType) | |||
| allowed := false | |||
| for _, t := range allowedTypes { | |||
| t := strings.Trim(t, " ") | |||
| if t == "*/*" || t == fileType { | |||
| allowed = true | |||
| break | |||
| } | |||
| } | |||
| if !allowed { | |||
| ctx.Handle(400, "issue.Comment", ErrFileTypeForbidden) | |||
| return | |||
| } | |||
| os.MkdirAll(setting.AttachmentPath, os.ModePerm) | |||
| out, err := ioutil.TempFile(setting.AttachmentPath, "attachment_") | |||
| if err != nil { | |||
| ctx.Handle(500, "ioutil.TempFile", err) | |||
| return | |||
| } | |||
| defer out.Close() | |||
| out.Write(buf) | |||
| _, err = io.Copy(out, file) | |||
| if err != nil { | |||
| ctx.Handle(500, "io.Copy", err) | |||
| return | |||
| } | |||
| _, err = models.CreateAttachment(issueId, commentId, header.Filename, out.Name()) | |||
| if err != nil { | |||
| ctx.Handle(500, "CreateAttachment", err) | |||
| return | |||
| } | |||
| } | |||
| } | |||
| func Comment(ctx *middleware.Context) { | |||
| send := func(status int, data interface{}, err error) { | |||
| if err != nil { | |||
| @@ -884,7 +856,7 @@ func Comment(ctx *middleware.Context) { | |||
| } | |||
| if comment != nil { | |||
| uploadFiles(ctx, issue.ID, comment.Id) | |||
| // uploadFiles(ctx, issue.ID, comment.Id) | |||
| } | |||
| // Notify watchers. | |||
| @@ -1194,25 +1166,6 @@ func DeleteMilestone(ctx *middleware.Context) { | |||
| }) | |||
| } | |||
| func IssueGetAttachment(ctx *middleware.Context) { | |||
| id := com.StrTo(ctx.Params(":id")).MustInt64() | |||
| if id == 0 { | |||
| ctx.Error(404) | |||
| return | |||
| } | |||
| attachment, err := models.GetAttachmentById(id) | |||
| if err != nil { | |||
| ctx.Handle(404, "models.GetAttachmentById", err) | |||
| return | |||
| } | |||
| // Fix #312. Attachments with , in their name are not handled correctly by Google Chrome. | |||
| // We must put the name in " manually. | |||
| ctx.ServeFile(attachment.Path, "\""+attachment.Name+"\"") | |||
| } | |||
| func PullRequest2(ctx *middleware.Context) { | |||
| ctx.HTML(200, "repo/pr2/list") | |||
| } | |||
| @@ -1 +1 @@ | |||
| 0.6.4.0810 Beta | |||
| 0.6.4.0811 Beta | |||
| @@ -17,18 +17,19 @@ | |||
| </div> | |||
| <div class="field"> | |||
| <div class="ui top attached tabular menu"> | |||
| <a class="active item" data-tab="write">{{.i18n.Tr "repo.release.write"}}</a> | |||
| <a class="item" data-tab="preview" data-url="/api/v1/markdown" data-context="{{.RepoLink}}">{{.i18n.Tr "repo.release.preview"}}</a> | |||
| <a class="active item" data-tab="write">{{.i18n.Tr "repo.release.write"}}</a> | |||
| <a class="item" data-tab="preview" data-url="/api/v1/markdown" data-context="{{.RepoLink}}">{{.i18n.Tr "repo.release.preview"}}</a> | |||
| </div> | |||
| <div class="ui bottom attached active tab segment" data-tab="write"> | |||
| <textarea name="content"></textarea> | |||
| </div> | |||
| <div class="ui bottom attached tab segment markdown" data-tab="preview"> | |||
| {{.i18n.Tr "repo.release.loading"}} | |||
| {{.i18n.Tr "repo.release.loading"}} | |||
| </div> | |||
| </div> | |||
| {{if .IsAttachmentEnabled}} | |||
| <div class="ui basic button dropzone" id="dropzone" data-upload-url="/attachments" data-accepts="{{.AttachmentAllowedTypes}}" data-max-size="1" 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="attachments"></div> | |||
| <div class="ui basic button dropzone" id="dropzone" data-upload-url="/issues/attachments" data-accepts="{{.AttachmentAllowedTypes}}" data-max-file="{{.AttachmentMaxFiles}}" data-max-size="2" 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 class="text right"> | |||
| <button class="ui green button"> | |||
| @@ -53,7 +53,7 @@ | |||
| <span class="attachment-label label label-info">Attachments:</span> | |||
| {{range $attachments}} | |||
| <a class="attachment label label-default" href="{{.IssueId}}/attachment/{{.Id}}">{{.Name}}</a> | |||
| <a class="attachment label label-default" href="/attachments/{{.UUID}}">{{.Name}}</a> | |||
| {{end}} | |||
| </div> | |||
| {{end}} | |||
| @@ -145,17 +145,17 @@ | |||
| </div> | |||
| </div> | |||
| {{if .AttachmentsEnabled}} | |||
| <div id="attached"> | |||
| <!-- <div id="attached"> | |||
| <div id="attached-list"> | |||
| <b>Attachments:</b> | |||
| </div> | |||
| </div> | |||
| </div> --> | |||
| {{end}} | |||
| <div class="text-right"> | |||
| <div class="form-group"> | |||
| {{if .AttachmentsEnabled}} | |||
| <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="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> --> | |||
| {{end}} | |||
| {{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}} | |||