finish attachments when create issue
This commit is contained in:
parent
89c2bd4a0d
commit
34f6cbfc2a
18
cmd/web.go
18
cmd/web.go
|
@ -327,7 +327,22 @@ func runWeb(ctx *cli.Context) {
|
||||||
|
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Get("/:username", user.Profile)
|
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)
|
}, ignSignIn)
|
||||||
|
|
||||||
if macaron.Env == macaron.DEV {
|
if macaron.Env == macaron.DEV {
|
||||||
|
@ -428,7 +443,6 @@ func runWeb(ctx *cli.Context) {
|
||||||
m.Post("/:index/label", repo.UpdateIssueLabel)
|
m.Post("/:index/label", repo.UpdateIssueLabel)
|
||||||
m.Post("/:index/milestone", repo.UpdateIssueMilestone)
|
m.Post("/:index/milestone", repo.UpdateIssueMilestone)
|
||||||
m.Post("/:index/assignee", repo.UpdateAssignee)
|
m.Post("/:index/assignee", repo.UpdateAssignee)
|
||||||
m.Get("/:index/attachment/:id", repo.IssueGetAttachment)
|
|
||||||
})
|
})
|
||||||
m.Group("/labels", func() {
|
m.Group("/labels", func() {
|
||||||
m.Post("/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel)
|
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 of each file. Defaults to 32MB
|
||||||
MAX_SIZE = 32
|
MAX_SIZE = 32
|
||||||
; Max number of files per upload. Defaults to 10
|
; Max number of files per upload. Defaults to 10
|
||||||
MAX_FILES = 10
|
MAX_FILES = 5
|
||||||
|
|
||||||
[time]
|
[time]
|
||||||
; Specifies the format for fully outputed dates. Defaults to RFC1123
|
; Specifies the format for fully outputed dates. Defaults to RFC1123
|
||||||
|
|
2
gogs.go
2
gogs.go
|
@ -17,7 +17,7 @@ import (
|
||||||
"github.com/gogits/gogs/modules/setting"
|
"github.com/gogits/gogs/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
const APP_VER = "0.6.4.0810 Beta"
|
const APP_VER = "0.6.4.0811 Beta"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
|
|
|
@ -279,3 +279,24 @@ func IsErrMilestoneNotExist(err error) bool {
|
||||||
func (err ErrMilestoneNotExist) Error() string {
|
func (err ErrMilestoneNotExist) Error() string {
|
||||||
return fmt.Sprintf("milestone does not exist [id: %d, repo_id: %d]", err.ID, err.RepoID)
|
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)
|
||||||
|
}
|
||||||
|
|
106
models/issue.go
106
models/issue.go
|
@ -9,7 +9,10 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -20,12 +23,12 @@ import (
|
||||||
"github.com/gogits/gogs/modules/base"
|
"github.com/gogits/gogs/modules/base"
|
||||||
"github.com/gogits/gogs/modules/log"
|
"github.com/gogits/gogs/modules/log"
|
||||||
"github.com/gogits/gogs/modules/setting"
|
"github.com/gogits/gogs/modules/setting"
|
||||||
|
gouuid "github.com/gogits/gogs/modules/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrIssueNotExist = errors.New("Issue does not exist")
|
ErrIssueNotExist = errors.New("Issue does not exist")
|
||||||
ErrWrongIssueCounter = errors.New("Invalid number of issues for this milestone")
|
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")
|
ErrAttachmentNotLinked = errors.New("Attachment does not belong to this issue")
|
||||||
ErrMissingIssueNumber = errors.New("No issue number specified")
|
ErrMissingIssueNumber = errors.New("No issue number specified")
|
||||||
)
|
)
|
||||||
|
@ -159,7 +162,20 @@ func (i *Issue) AfterDelete() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateIssue creates new issue with labels for repository.
|
// 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()
|
sess := x.NewSession()
|
||||||
defer sessionRelease(sess)
|
defer sessionRelease(sess)
|
||||||
if err = sess.Begin(); err != nil {
|
if err = sess.Begin(); err != nil {
|
||||||
|
@ -188,6 +204,14 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64) (err error) {
|
||||||
return err
|
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.
|
// Notify watchers.
|
||||||
act := &Action{
|
act := &Action{
|
||||||
ActUserID: issue.Poster.Id,
|
ActUserID: issue.Poster.Id,
|
||||||
|
@ -1210,49 +1234,73 @@ func (c *Comment) AfterDelete() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attachment represent a attachment of issue/comment/release.
|
||||||
type Attachment struct {
|
type Attachment struct {
|
||||||
Id int64
|
ID int64 `xorm:"pk autoincr"`
|
||||||
IssueId int64
|
UUID string `xorm:"uuid UNIQUE"`
|
||||||
CommentId int64
|
IssueID int64 `xorm:"INDEX"`
|
||||||
|
CommentID int64
|
||||||
|
ReleaseID int64 `xorm:"INDEX"`
|
||||||
Name string
|
Name string
|
||||||
Path string `xorm:"TEXT"`
|
|
||||||
Created time.Time `xorm:"CREATED"`
|
Created time.Time `xorm:"CREATED"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAttachment creates a new attachment inside the database and
|
// AttachmentLocalPath returns where attachment is stored in local file system based on given UUID.
|
||||||
func CreateAttachment(issueId, commentId int64, name, path string) (*Attachment, error) {
|
func AttachmentLocalPath(uuid string) string {
|
||||||
sess := x.NewSession()
|
return path.Join(setting.AttachmentPath, uuid[0:1], uuid[1:2], uuid)
|
||||||
defer sess.Close()
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if err := sess.Begin(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
a := &Attachment{IssueId: issueId, CommentId: commentId, Name: name, Path: path}
|
if _, err := sess.Insert(attach); err != nil {
|
||||||
|
|
||||||
if _, err := sess.Insert(a); err != nil {
|
|
||||||
sess.Rollback()
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return a, sess.Commit()
|
return attach, sess.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attachment returns the attachment by given ID.
|
// GetAttachmentByUUID returns attachment by given UUID.
|
||||||
func GetAttachmentById(id int64) (*Attachment, error) {
|
func GetAttachmentByUUID(uuid string) (*Attachment, error) {
|
||||||
m := &Attachment{Id: id}
|
attach := &Attachment{UUID: uuid}
|
||||||
|
has, err := x.Get(attach)
|
||||||
has, err := x.Get(m)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, ErrAttachmentNotExist{0, uuid}
|
||||||
}
|
}
|
||||||
|
return attach, nil
|
||||||
if !has {
|
|
||||||
return nil, ErrAttachmentNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAttachmentsForIssue(issueId int64) ([]*Attachment, error) {
|
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) {
|
func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) {
|
||||||
for i, a := range attachments {
|
for i, a := range attachments {
|
||||||
if remove {
|
if remove {
|
||||||
if err := os.Remove(a.Path); err != nil {
|
if err := os.Remove(a.LocalPath()); err != nil {
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := x.Delete(a.Id); err != nil {
|
if _, err := x.Delete(a.ID); err != nil {
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,12 @@
|
||||||
package migrations
|
package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -16,6 +20,7 @@ import (
|
||||||
|
|
||||||
"github.com/gogits/gogs/modules/log"
|
"github.com/gogits/gogs/modules/log"
|
||||||
"github.com/gogits/gogs/modules/setting"
|
"github.com/gogits/gogs/modules/setting"
|
||||||
|
gouuid "github.com/gogits/gogs/modules/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const _MIN_DB_VER = 0
|
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("fix locale file load panic", fixLocaleFileLoadPanic), // V4 -> V5:v0.6.0
|
||||||
NewMigration("trim action compare URL prefix", trimCommitActionAppUrlPrefix), // V5 -> V6:v0.6.3
|
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("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
|
// Migrate database to current version
|
||||||
|
@ -97,8 +103,11 @@ func Migrate(x *xorm.Engine) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
v := currentVersion.Version
|
v := currentVersion.Version
|
||||||
if int(v) > len(migrations) {
|
if int(v-_MIN_DB_VER) > len(migrations) {
|
||||||
return nil
|
// 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:] {
|
for i, m := range migrations[v-_MIN_DB_VER:] {
|
||||||
log.Info("Migration: %s", m.Description())
|
log.Info("Migration: %s", m.Description())
|
||||||
|
@ -515,3 +524,85 @@ func issueToIssueLabel(x *xorm.Engine) error {
|
||||||
|
|
||||||
return sess.Commit()
|
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
|
MilestoneID int64
|
||||||
AssigneeID int64
|
AssigneeID int64
|
||||||
Content string
|
Content string
|
||||||
|
Attachments []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *CreateIssueForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
func (f *CreateIssueForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -285,9 +285,9 @@ func NewConfigContext() {
|
||||||
if !filepath.IsAbs(AttachmentPath) {
|
if !filepath.IsAbs(AttachmentPath) {
|
||||||
AttachmentPath = path.Join(workDir, 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)
|
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)
|
AttachmentEnabled = sec.Key("ENABLE").MustBool(true)
|
||||||
|
|
||||||
TimeFormat = map[string]string{
|
TimeFormat = map[string]string{
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -254,7 +254,7 @@ $(document).ready(function () {
|
||||||
$dropz.dropzone({
|
$dropz.dropzone({
|
||||||
url: $dropz.data('upload-url'),
|
url: $dropz.data('upload-url'),
|
||||||
headers: {"X-Csrf-Token": csrf},
|
headers: {"X-Csrf-Token": csrf},
|
||||||
maxFiles: 5,
|
maxFiles: $dropz.data('max-file'),
|
||||||
maxFilesize: $dropz.data('max-size'),
|
maxFilesize: $dropz.data('max-size'),
|
||||||
acceptedFiles: $dropz.data('accepts'),
|
acceptedFiles: $dropz.data('accepts'),
|
||||||
addRemoveLinks: true,
|
addRemoveLinks: true,
|
||||||
|
@ -265,10 +265,12 @@ $(document).ready(function () {
|
||||||
init: function () {
|
init: function () {
|
||||||
this.on("success", function (file, data) {
|
this.on("success", function (file, data) {
|
||||||
filenameDict[file.name] = data.uuid;
|
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) {
|
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;
|
margin-bottom: 10px;
|
||||||
border: 2px dashed #0087F7;
|
border: 2px dashed #0087F7;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
.dz-error-message {
|
||||||
|
top: 140px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,8 @@ package repo
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -181,6 +178,7 @@ func NewIssue(ctx *middleware.Context) {
|
||||||
ctx.Data["RequireDropzone"] = true
|
ctx.Data["RequireDropzone"] = true
|
||||||
ctx.Data["IsAttachmentEnabled"] = setting.AttachmentEnabled
|
ctx.Data["IsAttachmentEnabled"] = setting.AttachmentEnabled
|
||||||
ctx.Data["AttachmentAllowedTypes"] = setting.AttachmentAllowedTypes
|
ctx.Data["AttachmentAllowedTypes"] = setting.AttachmentAllowedTypes
|
||||||
|
ctx.Data["AttachmentMaxFiles"] = setting.AttachmentMaxFiles
|
||||||
|
|
||||||
if ctx.User.IsAdmin {
|
if ctx.User.IsAdmin {
|
||||||
var (
|
var (
|
||||||
|
@ -215,18 +213,19 @@ func NewIssue(ctx *middleware.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) {
|
func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) {
|
||||||
fmt.Println(ctx.QueryStrings("uuids"))
|
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
|
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
|
||||||
ctx.Data["PageIsIssueList"] = true
|
ctx.Data["PageIsIssueList"] = true
|
||||||
ctx.Data["RequireDropzone"] = true
|
ctx.Data["RequireDropzone"] = true
|
||||||
ctx.Data["IsAttachmentEnabled"] = setting.AttachmentEnabled
|
ctx.Data["IsAttachmentEnabled"] = setting.AttachmentEnabled
|
||||||
ctx.Data["AttachmentAllowedTypes"] = setting.AttachmentAllowedTypes
|
ctx.Data["AttachmentAllowedTypes"] = setting.AttachmentAllowedTypes
|
||||||
|
ctx.Data["AttachmentMaxFiles"] = setting.AttachmentMaxFiles
|
||||||
|
|
||||||
var (
|
var (
|
||||||
repo = ctx.Repo.Repository
|
repo = ctx.Repo.Repository
|
||||||
labelIDs []int64
|
labelIDs []int64
|
||||||
milestoneID int64
|
milestoneID int64
|
||||||
assigneeID int64
|
assigneeID int64
|
||||||
|
attachments []string
|
||||||
)
|
)
|
||||||
if ctx.User.IsAdmin {
|
if ctx.User.IsAdmin {
|
||||||
// Check labels.
|
// Check labels.
|
||||||
|
@ -286,6 +285,10 @@ func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if setting.AttachmentEnabled {
|
||||||
|
attachments = ctx.QueryStrings("attachments")
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.HasError() {
|
if ctx.HasError() {
|
||||||
ctx.HTML(200, ISSUE_NEW)
|
ctx.HTML(200, ISSUE_NEW)
|
||||||
return
|
return
|
||||||
|
@ -301,7 +304,7 @@ func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) {
|
||||||
AssigneeID: assigneeID,
|
AssigneeID: assigneeID,
|
||||||
Content: form.Content,
|
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)
|
ctx.Handle(500, "NewIssue", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -347,9 +350,50 @@ func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) {
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
|
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{
|
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) {
|
func Comment(ctx *middleware.Context) {
|
||||||
send := func(status int, data interface{}, err error) {
|
send := func(status int, data interface{}, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -884,7 +856,7 @@ func Comment(ctx *middleware.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if comment != nil {
|
if comment != nil {
|
||||||
uploadFiles(ctx, issue.ID, comment.Id)
|
// uploadFiles(ctx, issue.ID, comment.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify watchers.
|
// 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) {
|
func PullRequest2(ctx *middleware.Context) {
|
||||||
ctx.HTML(200, "repo/pr2/list")
|
ctx.HTML(200, "repo/pr2/list")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
0.6.4.0810 Beta
|
0.6.4.0811 Beta
|
|
@ -17,18 +17,19 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui top attached tabular menu">
|
<div class="ui top attached tabular menu">
|
||||||
<a class="active item" data-tab="write">{{.i18n.Tr "repo.release.write"}}</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>
|
<a class="item" data-tab="preview" data-url="/api/v1/markdown" data-context="{{.RepoLink}}">{{.i18n.Tr "repo.release.preview"}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui bottom attached active tab segment" data-tab="write">
|
<div class="ui bottom attached active tab segment" data-tab="write">
|
||||||
<textarea name="content"></textarea>
|
<textarea name="content"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui bottom attached tab segment markdown" data-tab="preview">
|
<div class="ui bottom attached tab segment markdown" data-tab="preview">
|
||||||
{{.i18n.Tr "repo.release.loading"}}
|
{{.i18n.Tr "repo.release.loading"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{if .IsAttachmentEnabled}}
|
{{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}}
|
{{end}}
|
||||||
<div class="text right">
|
<div class="text right">
|
||||||
<button class="ui green button">
|
<button class="ui green button">
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
<span class="attachment-label label label-info">Attachments:</span>
|
<span class="attachment-label label label-info">Attachments:</span>
|
||||||
|
|
||||||
{{range $attachments}}
|
{{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}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -145,17 +145,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{if .AttachmentsEnabled}}
|
{{if .AttachmentsEnabled}}
|
||||||
<div id="attached">
|
<!-- <div id="attached">
|
||||||
<div id="attached-list">
|
<div id="attached-list">
|
||||||
<b>Attachments:</b>
|
<b>Attachments:</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{if .AttachmentsEnabled}}
|
{{if .AttachmentsEnabled}}
|
||||||
<input type="file" accept="{{.AllowedTypes}}" style="display: none;" id="attachments-input" name="attachments" multiple />
|
<!-- <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>
|
<button class="btn-default btn attachment-add" id="attachments-button">Select Attachments...</button> -->
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .IsIssueOwner}}{{if .Issue.IsClosed}}
|
{{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-open" id="issue-open-btn" data-origin="Reopen" data-text="Reopen & Comment" name="change_status" value="Reopen"/>{{else}}
|
||||||
|
|
Loading…
Reference in New Issue