From 5b78da2afe190706d6c80151e355f4dd60808873 Mon Sep 17 00:00:00 2001 From: series2 Date: Tue, 22 Oct 2019 16:04:05 +0900 Subject: [PATCH 01/63] icon to userpage --- client/src/components/shared/Icon.vue | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/components/shared/Icon.vue b/client/src/components/shared/Icon.vue index 66c47e0b..d99455ac 100644 --- a/client/src/components/shared/Icon.vue +++ b/client/src/components/shared/Icon.vue @@ -1,7 +1,13 @@ diff --git a/model/items.go b/model/items.go index 5e4a6ca1..ff5e2f13 100644 --- a/model/items.go +++ b/model/items.go @@ -144,14 +144,27 @@ func RegisterOwner(owner Owner, item Item) (Item, error) { if err != nil { return Item{}, err } + log := Log{ + ItemID: latestLog.ItemID, + UserID: owner.UserID, + OwnerID: owner.UserID, + Type: 2, + Count: latestLog.Count + owner.Count, + } + if owner.Count < 0 { + log.Type = 3 + } if latestLog.ItemID != 0 { - _, err = CreateLog(Log{ItemID: latestLog.ItemID, UserID: owner.UserID, OwnerID: owner.UserID, Type: 2, Count: latestLog.Count + owner.Count}) + _, err = CreateLog(log) if err != nil { return Item{}, err } } } if !existed { + if owner.Count < 1 { + return Item{}, errors.New("該当の物品を所有していないため数を減らせません") + } db.Create(&owner) db.Model(&item).Association("Owners").Append(&owner) _, err := CreateLog(Log{ItemID: item.ID, UserID: owner.UserID, OwnerID: owner.UserID, Type: 2, Count: owner.Count}) diff --git a/model/items_test.go b/model/items_test.go index 4d9734ed..a011d7c3 100644 --- a/model/items_test.go +++ b/model/items_test.go @@ -72,6 +72,19 @@ func TestRegisterOwner(t *testing.T) { assert.NoError(err) assert.NotEmpty(item) }) + + t.Run("delete success", func(t *testing.T) { + assert := assert.New(t) + + owner.Count = -2 + item, err := RegisterOwner(owner, item) + + assert.Equal(4, item.Owners[0].Count) + assert.Equal(item.Owners[0].User.Name, user.Name) + + assert.NoError(err) + assert.NotEmpty(item) + }) } func TestGetItems(t *testing.T) { diff --git a/router/items.go b/router/items.go index 1de6e504..d089e3a1 100644 --- a/router/items.go +++ b/router/items.go @@ -152,9 +152,6 @@ func PostOwners(c echo.Context) error { Rentalable: body.Rentalable, Count: body.Count, } - if owner.Count < 1 { - return c.NoContent(http.StatusBadRequest) - } res, err := model.RegisterOwner(owner, item) if err != nil { return c.JSON(http.StatusBadRequest, err) From 69230b6e36282f934cbf6c46927a05d72fda93e2 Mon Sep 17 00:00:00 2001 From: ryoha000 <48485650+ryoha000@users.noreply.github.com> Date: Sat, 26 Oct 2019 03:56:12 +0900 Subject: [PATCH 06/63] fix client lint --- client/src/components/shared/EditOwner.vue | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/src/components/shared/EditOwner.vue b/client/src/components/shared/EditOwner.vue index a1d515b5..95d4ad86 100644 --- a/client/src/components/shared/EditOwner.vue +++ b/client/src/components/shared/EditOwner.vue @@ -63,11 +63,12 @@ export default { this.error = e }) if (this.count - this.propOwner.count > 0) { - this.message = this.count - this.propOwner.count + "個追加しました" - }if (this.count - this.propOwner.count < 0) { - this.message = this.propOwner.count - this.count + "個減らしました" + this.message = this.count - this.propOwner.count + '個追加しました' } - if (this.error) { alert("何らかの原因で処理が完了しませんでした") } + if (this.count - this.propOwner.count < 0) { + this.message = this.propOwner.count - this.count + '個減らしました' + } + if (this.error) { alert('何らかの原因で処理が完了しませんでした') } if (!this.error) { alert(this.message) } this.isOpenEditOwner = !this.isOpenEditOwner this.$emit('reload') From b144f87d79882e6a37d576580335e7343bbfe261 Mon Sep 17 00:00:00 2001 From: takashi Date: Sun, 27 Oct 2019 02:03:36 +0900 Subject: [PATCH 07/63] =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=AC=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=83=91=E3=83=83=E3=82=B1=E3=83=BC=E3=82=B8=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 1 + go.sum | 4 +++ storage/local.go | 58 +++++++++++++++++++++++++++++++++++++++++ storage/storage.go | 35 +++++++++++++++++++++++++ storage/swift.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 163 insertions(+) create mode 100644 storage/local.go create mode 100644 storage/storage.go create mode 100644 storage/swift.go diff --git a/go.mod b/go.mod index aab750e4..c437a02f 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/jinzhu/gorm v1.9.10 github.com/labstack/echo v3.3.10+incompatible github.com/labstack/gommon v0.2.9 // indirect + github.com/ncw/swift v1.0.49 github.com/stretchr/testify v1.3.0 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect ) diff --git a/go.sum b/go.sum index fc437b55..3d1fe665 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,7 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -69,6 +70,8 @@ github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK86 github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/ncw/swift v1.0.49 h1:eQaKIjSt/PXLKfYgzg01nevmO+CMXfXGRhB1gOhDs7E= +github.com/ncw/swift v1.0.49/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -144,6 +147,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/storage/local.go b/storage/local.go new file mode 100644 index 00000000..6f03078c --- /dev/null +++ b/storage/local.go @@ -0,0 +1,58 @@ +package storage + +import ( + "errors" + "io" + "os" + "path/filepath" +) + +// Local ローカルストレージ +type Local struct { + localDir string +} + +// SetLocalStorage ローカルストレージをカレントストレージに設定します +func SetLocalStorage(dir string) error { + fi, err := os.Stat(dir) + if err != nil { + return errors.New("dir doesn't exist") + } + if !fi.IsDir() { + return errors.New("dir is not a directory") + } + + current = &Local{localDir: dir} + return nil +} + +func (l Local) Save(filename string, src io.Reader) error { + file, err := os.Create(l.getFilePath(filename)) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(file, src) + return err +} + +func (l Local) Open(filename string) (io.ReadCloser, error) { + r, err := os.Open(l.getFilePath(filename)) + if err != nil { + return nil, ErrFileNotFound + } + return r, nil +} + +func (l Local) Delete(filename string) error { + path := l.getFilePath(filename) + if _, err := os.Stat(path); err != nil { + return ErrFileNotFound + } + return os.Remove(path) +} + +func (l Local) getFilePath(filename string) string { + return filepath.Join(l.localDir, filename) +} diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 00000000..1d434e2d --- /dev/null +++ b/storage/storage.go @@ -0,0 +1,35 @@ +package storage + +import ( + "errors" + "io" +) + +var current Storage + +var ( + // ErrFileNotFound 指定されたファイル名のファイルは存在しません + ErrFileNotFound = errors.New("not found") +) + +// Storage ストレージインターフェース +type Storage interface { + Save(filename string, src io.Reader) error + Open(filename string) (io.ReadCloser, error) + Delete(filename string) error +} + +// Save 指定したファイル名で保存します。同名のファイルは上書きされます。 +func Save(filename string, src io.Reader) error { + return current.Save(filename, src) +} + +// Open 指定したファイルを開きます +func Open(filename string) (io.ReadCloser, error) { + return current.Open(filename) +} + +// Delete 指定したファイルを削除します +func Delete(filename string) error { + return current.Delete(filename) +} diff --git a/storage/swift.go b/storage/swift.go new file mode 100644 index 00000000..11e957db --- /dev/null +++ b/storage/swift.go @@ -0,0 +1,65 @@ +package storage + +import ( + "github.com/ncw/swift" + "io" +) + +// Swift Swiftオブジェクトストレージ +type Swift struct { + container string + conn swift.Connection +} + +// SetSwiftStorage Swiftオブジェクトストレージをカレントストレージに設定します +func SetSwiftStorage(container, userName, apiKey, tenant, authURL string) error { + conn := swift.Connection{ + AuthUrl: authURL, + UserName: userName, + ApiKey: apiKey, + Tenant: tenant, + } + + // 認証 + if err := conn.Authenticate(); err != nil { + return err + } + + // コンテナの存在を確認 + if _, _, err := conn.Container(container); err != nil { + return err + } + + current = &Swift{ + container: container, + conn: conn, + } + return nil +} + +func (s *Swift) Save(filename string, src io.Reader) error { + _, err := s.conn.ObjectPut(s.container, filename, src, true, "", "", swift.Headers{}) + return err +} + +func (s *Swift) Open(filename string) (io.ReadCloser, error) { + file, _, err := s.conn.ObjectOpen(s.container, filename, true, nil) + if err != nil { + if err == swift.ObjectNotFound { + return nil, ErrFileNotFound + } + return nil, err + } + return file, nil +} + +func (s *Swift) Delete(filename string) error { + err := s.conn.ObjectDelete(s.container, filename) + if err != nil { + if err == swift.ObjectNotFound { + return ErrFileNotFound + } + return err + } + return nil +} From aec13ed6a8c368fa218f1d52061f465f7f74ae03 Mon Sep 17 00:00:00 2001 From: takashi Date: Sun, 27 Oct 2019 03:19:05 +0900 Subject: [PATCH 08/63] =?UTF-8?q?=E3=82=A2=E3=83=83=E3=83=97=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E6=A7=8B?= =?UTF-8?q?=E9=80=A0=E4=BD=93=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model/db.go | 1 + model/file.go | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 model/file.go diff --git a/model/db.go b/model/db.go index 625775e7..74813526 100644 --- a/model/db.go +++ b/model/db.go @@ -16,6 +16,7 @@ var allTables = []interface{}{ Owner{}, Comment{}, RentalUser{}, + File{}, } // EstablishConnection DBに接続する diff --git a/model/file.go b/model/file.go new file mode 100644 index 00000000..0d434d83 --- /dev/null +++ b/model/file.go @@ -0,0 +1,41 @@ +package model + +import ( + "fmt" + "github.com/jinzhu/gorm" + "github.com/traPtitech/booQ/storage" + "io" +) + +// File アップロードファイルの構造体 +type File struct { + gorm.Model + UploadUserID uint `gorm:"type:int;not null"` +} + +// TableName dbのテーブル名を指定する +func (File) TableName() string { + return "files" +} + +// CreateFile Fileを作成する +func CreateFile(uploadUserID uint, src io.Reader, ext string) (File, error) { + // トランザクション開始 + tx := db.Begin() + defer tx.RollbackUnlessCommitted() + + f := File{UploadUserID: uploadUserID} + + // DBにレコード作成 + if err := tx.Create(&f).Error; err != nil { + return File{}, err + } + + // ストレージに保存 + if err := storage.Save(fmt.Sprintf("%d.%s", f.ID, ext), src); err != nil { + return File{}, err + } + + // コミット + return f, tx.Commit().Error +} From cdcfae6ce4565ef75ffd5c36aab9e6a50c735f3c Mon Sep 17 00:00:00 2001 From: takashi Date: Sun, 27 Oct 2019 03:22:38 +0900 Subject: [PATCH 09/63] =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=ABAPI?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 1 + go.sum | 4 +++ router/files.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++++ router/router.go | 7 ++++ 4 files changed, 95 insertions(+) create mode 100644 router/files.go diff --git a/go.mod b/go.mod index c437a02f..418e082a 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.12 require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/disintegration/imaging v1.6.1 github.com/go-sql-driver/mysql v1.4.1 github.com/jinzhu/gorm v1.9.10 github.com/labstack/echo v3.3.10+incompatible diff --git a/go.sum b/go.sum index 3d1fe665..4dee4800 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgf github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/disintegration/imaging v1.6.1 h1:JnBbK6ECIZb1NsWIikP9pd8gIlTIRx7fuDNpU9fsxOE= +github.com/disintegration/imaging v1.6.1/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= @@ -106,6 +108,8 @@ golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/router/files.go b/router/files.go new file mode 100644 index 00000000..b605785d --- /dev/null +++ b/router/files.go @@ -0,0 +1,83 @@ +package router + +import ( + "bytes" + "errors" + "fmt" + "github.com/disintegration/imaging" + "github.com/labstack/echo" + "github.com/traPtitech/booQ/model" + "github.com/traPtitech/booQ/storage" + "net/http" + "strconv" +) + +// アップロードを許可するMIMEタイプ +var uploadableMimes = map[string]bool{ + "image/jpeg": true, + "image/png": true, +} + +// PostFile POST /files +func PostFile(c echo.Context) error { + user := c.Get("user").(model.User) + + // フォームデータ確認 + fileHeader, err := c.FormFile("file") + if err != nil { + return c.JSON(http.StatusBadRequest, err) + } + + // ファイルタイプ確認 + if !uploadableMimes[fileHeader.Header.Get(echo.HeaderContentType)] { + return c.JSON(http.StatusBadRequest, errors.New("アップロードできないファイル形式です")) + } + + // ファイルオープン + file, err := fileHeader.Open() + if err != nil { + return c.NoContent(http.StatusInternalServerError) + } + defer file.Close() + + // サムネイル画像化 + orig, err := imaging.Decode(file, imaging.AutoOrientation(true)) + if err != nil { + // 不正な画像 + return c.JSON(http.StatusBadRequest, errors.New("不正なファイルです")) + } + b := &bytes.Buffer{} + _ = imaging.Encode(b, imaging.Fit(orig, 360, 480, imaging.Linear), imaging.JPEG, imaging.JPEGQuality(85)) + + // 保存 + f, err := model.CreateFile(user.ID, b, "jpg") + if err != nil { + return c.NoContent(http.StatusInternalServerError) + } + + // レスポンス + return c.JSON(http.StatusOK, map[string]interface{}{"id": f.ID, "url": fmt.Sprintf("/api/files/%d", f.ID)}) +} + +// GetFile GET /files/:id +func GetFile(c echo.Context) error { + // IDチェック + ID, err := strconv.Atoi(c.Param("id")) + if err != nil { + return c.NoContent(http.StatusNotFound) + } + + // ファイルオープン + f, err := storage.Open(fmt.Sprintf("%d.jpg", ID)) + if err != nil { + if err == storage.ErrFileNotFound { + return c.NoContent(http.StatusNotFound) + } + return c.NoContent(http.StatusInternalServerError) + } + defer f.Close() + + // レスポンス + c.Response().Header().Set("Cache-Control", "private, max-age=31536000") + return c.Stream(http.StatusOK, "image/jpeg", f) +} diff --git a/router/router.go b/router/router.go index 3adb86e8..8ce6ec0c 100644 --- a/router/router.go +++ b/router/router.go @@ -1,6 +1,7 @@ package router import ( + "github.com/labstack/echo/middleware" "net/http" "github.com/labstack/echo" @@ -38,5 +39,11 @@ func SetupRouting(e *echo.Echo, client Traq) { { apiComments.GET("", GetComments) } + + apiFiles := api.Group("/files") + { + apiFiles.POST("", PostFile, middleware.BodyLimit("3MB")) + apiFiles.GET("/:id", GetFile) + } } } From 30869d6a58da4155ac6624124b3a325248d21bb6 Mon Sep 17 00:00:00 2001 From: takashi Date: Sun, 27 Oct 2019 12:25:29 +0900 Subject: [PATCH 10/63] =?UTF-8?q?=E9=96=8B=E7=99=BA=E7=92=B0=E5=A2=83?= =?UTF-8?q?=E7=94=A8=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ fresh.conf | 4 ++-- uploads/.gitkeep | 0 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 uploads/.gitkeep diff --git a/.gitignore b/.gitignore index 9f965e1a..9182d98c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ booQ /tmp/ .DS_Store /.env + +!/uploads/.gitkeep +/uploads/* diff --git a/fresh.conf b/fresh.conf index 11b3309a..2dbec4b2 100644 --- a/fresh.conf +++ b/fresh.conf @@ -4,11 +4,11 @@ build_name: runner-build build_log: runner-build-errors.log valid_ext: .go no_rebuild_ext: .tpl, .tmpl, .html -ignored: tmp, client +ignored: tmp, client, uploads build_delay: 600 colors: 1 log_color_main: cyan log_color_build: yellow log_color_runner: green log_color_watcher: magenta -log_color_app: \ No newline at end of file +log_color_app: diff --git a/uploads/.gitkeep b/uploads/.gitkeep new file mode 100644 index 00000000..e69de29b From ddb2542692ca689071b0ba69b1ff86b030b8d39d Mon Sep 17 00:00:00 2001 From: takashi Date: Sun, 27 Oct 2019 12:53:27 +0900 Subject: [PATCH 11/63] =?UTF-8?q?=E3=83=A1=E3=83=A2=E3=83=AA=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=83=AC=E3=83=BC=E3=82=B8=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- storage/memory.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++ storage/storage.go | 2 +- storage/swift.go | 3 ++- 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 storage/memory.go diff --git a/storage/memory.go b/storage/memory.go new file mode 100644 index 00000000..3f8379c7 --- /dev/null +++ b/storage/memory.go @@ -0,0 +1,54 @@ +package storage + +import ( + "bytes" + "io" + "io/ioutil" + "sync" +) + +// Memory インメモリストレージ +type Memory struct { + mu sync.RWMutex + files map[string][]byte +} + +// SetMemoryStorage メモリストレージをカレントストレージに設定します +func SetMemoryStorage() { + current = &Memory{files: map[string][]byte{}} +} + +func (m *Memory) Save(filename string, src io.Reader) error { + m.mu.Lock() + defer m.mu.Unlock() + + b, err := ioutil.ReadAll(src) + if err != nil { + return err + } + m.files[filename] = b + return nil +} + +func (m *Memory) Open(filename string) (io.ReadCloser, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + b, ok := m.files[filename] + if !ok { + return nil, ErrFileNotFound + } + return ioutil.NopCloser(bytes.NewReader(b)), nil +} + +func (m *Memory) Delete(filename string) error { + m.mu.Lock() + defer m.mu.Unlock() + + _, ok := m.files[filename] + if !ok { + return ErrFileNotFound + } + delete(m.files, filename) + return nil +} diff --git a/storage/storage.go b/storage/storage.go index 1d434e2d..0883a0de 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -5,7 +5,7 @@ import ( "io" ) -var current Storage +var current Storage = &Memory{files: map[string][]byte{}} var ( // ErrFileNotFound 指定されたファイル名のファイルは存在しません diff --git a/storage/swift.go b/storage/swift.go index 11e957db..9f8f91fc 100644 --- a/storage/swift.go +++ b/storage/swift.go @@ -12,12 +12,13 @@ type Swift struct { } // SetSwiftStorage Swiftオブジェクトストレージをカレントストレージに設定します -func SetSwiftStorage(container, userName, apiKey, tenant, authURL string) error { +func SetSwiftStorage(container, userName, apiKey, tenant, tenantID, authURL string) error { conn := swift.Connection{ AuthUrl: authURL, UserName: userName, ApiKey: apiKey, Tenant: tenant, + TenantId: tenantID, } // 認証 From 3301a6100009da2b559b00971f7066ee97dbd397 Mon Sep 17 00:00:00 2001 From: takashi Date: Sun, 27 Oct 2019 12:58:08 +0900 Subject: [PATCH 12/63] =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=AC=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E5=88=9D=E6=9C=9F=E5=8C=96=E3=82=B3=E3=83=BC=E3=83=89?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.go | 27 +++++++++++++++++++++++++++ model/{file.go => files.go} | 0 2 files changed, 27 insertions(+) rename model/{file.go => files.go} (100%) diff --git a/main.go b/main.go index c63c7a92..e69d6452 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "github.com/traPtitech/booQ/model" "github.com/traPtitech/booQ/router" + "github.com/traPtitech/booQ/storage" ) func main() { @@ -30,6 +31,32 @@ func main() { panic(err) } + // Storage + if os.Getenv("OS_CONTAINER") != "" { + // Swiftオブジェクトストレージ + err := storage.SetSwiftStorage( + os.Getenv("OS_CONTAINER"), + os.Getenv("OS_USERNAME"), + os.Getenv("OS_PASSWORD"), + os.Getenv("OS_TENANT_NAME"), + os.Getenv("OS_TENANT_ID"), + os.Getenv("OS_AUTH_URL"), + ) + if err != nil { + panic(err) + } + } else { + // ローカルストレージ + dir := os.Getenv("UPLOAD_DIR") + if dir == "" { + dir = "./uploads" + } + err := storage.SetLocalStorage(dir) + if err != nil { + panic(err) + } + } + // Echo instance e := echo.New() diff --git a/model/file.go b/model/files.go similarity index 100% rename from model/file.go rename to model/files.go From b48a02c27557417ae20b5ca105a323d89f11bb41 Mon Sep 17 00:00:00 2001 From: takashi Date: Sun, 27 Oct 2019 13:04:04 +0900 Subject: [PATCH 13/63] =?UTF-8?q?model=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model/files_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 model/files_test.go diff --git a/model/files_test.go b/model/files_test.go new file mode 100644 index 00000000..f8c296ea --- /dev/null +++ b/model/files_test.go @@ -0,0 +1,26 @@ +package model + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "strings" + "testing" +) + +func TestFile_TableName(t *testing.T) { + t.Parallel() + assert.Equal(t, "files", (&File{}).TableName()) +} + +func TestCreateFile(t *testing.T) { + t.Parallel() + + user, err := CreateUser(User{Name: "testCreateFileUser"}) + require.NoError(t, err) + + f, err := CreateFile(user.ID, strings.NewReader("test file"), "txt") + if assert.NoError(t, err) { + assert.NotEmpty(t, f.ID) + assert.EqualValues(t, user.ID, f.UploadUserID) + } +} From 643ed103d3db4774774c4f1a58436d196b050742 Mon Sep 17 00:00:00 2001 From: takashi Date: Sun, 27 Oct 2019 13:42:31 +0900 Subject: [PATCH 14/63] =?UTF-8?q?router=E3=81=AE=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- router/files_test.go | 145 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 router/files_test.go diff --git a/router/files_test.go b/router/files_test.go new file mode 100644 index 00000000..dfc5eaa9 --- /dev/null +++ b/router/files_test.go @@ -0,0 +1,145 @@ +package router + +import ( + "bytes" + "encoding/base64" + "fmt" + "github.com/labstack/echo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/traPtitech/booQ/model" + "io" + "mime/multipart" + "net/http" + "net/http/httptest" + "net/textproto" + "testing" +) + +var testJpeg = `/9j/4AAQSkZJRgABAQIAOAA4AAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////2wBDAf//////////////////////////////////////////////////////////////////////////////////////wAARCAABAAEDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwBKBH//2Q` + +func TestPostFile(t *testing.T) { + t.Parallel() + e := echoSetupWithUser() + + t.Run("no form", func(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + req := httptest.NewRequest(echo.POST, "/api/files", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(http.StatusBadRequest, rec.Code) + }) + + t.Run("invalid file type", func(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + pr, pw := io.Pipe() + writer := multipart.NewWriter(pw) + go func() { + defer writer.Close() + part, _ := writer.CreateFormFile("file", "test.txt") + _, _ = part.Write([]byte("test text file")) + }() + + req := httptest.NewRequest(echo.POST, "/api/files", pr) + req.Header.Set("Content-Type", writer.FormDataContentType()) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(http.StatusBadRequest, rec.Code) + }) + + t.Run("bad image", func(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + pr, pw := io.Pipe() + writer := multipart.NewWriter(pw) + go func() { + defer writer.Close() + h := textproto.MIMEHeader{} + h.Set("Content-Disposition", `form-data; name="file"; filename="test.jpg"`) + h.Set("Content-Type", "image/jpeg") + part, _ := writer.CreatePart(h) + _, _ = part.Write([]byte("test text file")) + }() + + req := httptest.NewRequest(echo.POST, "/api/files", pr) + req.Header.Set("Content-Type", writer.FormDataContentType()) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(http.StatusBadRequest, rec.Code) + }) + + t.Run("ok", func(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + pr, pw := io.Pipe() + writer := multipart.NewWriter(pw) + go func() { + defer writer.Close() + h := textproto.MIMEHeader{} + h.Set("Content-Disposition", `form-data; name="file"; filename="test.jpg"`) + h.Set("Content-Type", "image/jpeg") + part, _ := writer.CreatePart(h) + img, _ := base64.RawStdEncoding.DecodeString(testJpeg) + _, _ = part.Write(img) + }() + + req := httptest.NewRequest(echo.POST, "/api/files", pr) + req.Header.Set("Content-Type", writer.FormDataContentType()) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(http.StatusOK, rec.Code) + }) +} + +func TestGetFile(t *testing.T) { + t.Parallel() + e := echoSetupWithUser() + + t.Run("not found", func(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + req := httptest.NewRequest(echo.GET, "/api/files/a", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(http.StatusNotFound, rec.Code) + }) + + t.Run("not found2", func(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + req := httptest.NewRequest(echo.GET, "/api/files/999999", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(http.StatusNotFound, rec.Code) + }) + + t.Run("ok", func(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + img, _ := base64.RawStdEncoding.DecodeString(testJpeg) + f, err := model.CreateFile(1, bytes.NewReader(img), "jpg") + require.NoError(t, err) + + req := httptest.NewRequest(echo.GET, fmt.Sprintf("/api/files/%d", f.ID), nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(http.StatusOK, rec.Code) + assert.EqualValues(img, rec.Body.Bytes()) + }) +} From 7bfa65f78fda2c845096c2423762df1b77e4de38 Mon Sep 17 00:00:00 2001 From: takashi Date: Sun, 27 Oct 2019 13:45:59 +0900 Subject: [PATCH 15/63] fix lint --- .dockerignore | 4 +++- storage/swift.go | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.dockerignore b/.dockerignore index 45d96147..15e47f6f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,4 +4,6 @@ README.md Dockerfile docker-compose.yml .dockerignore -.gitignore \ No newline at end of file +.gitignore +!/uploads/.gitkeep +/uploads/* diff --git a/storage/swift.go b/storage/swift.go index 9f8f91fc..d1469e5f 100644 --- a/storage/swift.go +++ b/storage/swift.go @@ -8,12 +8,12 @@ import ( // Swift Swiftオブジェクトストレージ type Swift struct { container string - conn swift.Connection + conn *swift.Connection } // SetSwiftStorage Swiftオブジェクトストレージをカレントストレージに設定します func SetSwiftStorage(container, userName, apiKey, tenant, tenantID, authURL string) error { - conn := swift.Connection{ + conn := &swift.Connection{ AuthUrl: authURL, UserName: userName, ApiKey: apiKey, From 29c82bbf74ce7c8fe6530773d9127b0d01fe8bb2 Mon Sep 17 00:00:00 2001 From: takashi Date: Sun, 27 Oct 2019 13:52:56 +0900 Subject: [PATCH 16/63] =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=BC=E3=82=BF?= =?UTF-8?q?=E3=82=B9=E3=82=B3=E3=83=BC=E3=83=89=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- router/files.go | 2 +- router/files_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/router/files.go b/router/files.go index b605785d..1f1befb5 100644 --- a/router/files.go +++ b/router/files.go @@ -56,7 +56,7 @@ func PostFile(c echo.Context) error { } // レスポンス - return c.JSON(http.StatusOK, map[string]interface{}{"id": f.ID, "url": fmt.Sprintf("/api/files/%d", f.ID)}) + return c.JSON(http.StatusCreated, map[string]interface{}{"id": f.ID, "url": fmt.Sprintf("/api/files/%d", f.ID)}) } // GetFile GET /files/:id diff --git a/router/files_test.go b/router/files_test.go index dfc5eaa9..1f00b1e7 100644 --- a/router/files_test.go +++ b/router/files_test.go @@ -97,7 +97,7 @@ func TestPostFile(t *testing.T) { rec := httptest.NewRecorder() e.ServeHTTP(rec, req) - assert.Equal(http.StatusOK, rec.Code) + assert.Equal(http.StatusCreated, rec.Code) }) } From 562797d0db820ec726bf8a508077f6c71c891a0e Mon Sep 17 00:00:00 2001 From: takashi Date: Sun, 27 Oct 2019 13:59:09 +0900 Subject: [PATCH 17/63] =?UTF-8?q?swagger.yml=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/swagger.yml | 50 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/docs/swagger.yml b/docs/swagger.yml index 4759a50e..02baa67a 100644 --- a/docs/swagger.yml +++ b/docs/swagger.yml @@ -7,7 +7,7 @@ host: "booq-dev.tokyotech.org" basePath: /api/ schemes: - https - + tags: - name: "items" description: "物品に関するAPI" @@ -23,6 +23,8 @@ tags: description: "タグに関するAPI" - name: "users" description: "ユーザーに関するAPI" +- name: "files" + description: "画像アップロードに関するAPI" paths: /items: @@ -483,7 +485,7 @@ paths: tags: - "users" summary: "ユーザー一覧を取得します。" - parameters: + parameters: - name: "name" in: "query" description: "取得したいユーザーのIDを指定します。" @@ -524,6 +526,50 @@ paths: 403: description: "権限がありません" + /files: + post: + tags: + - "files" + summary: "画像ファイルをアップロードします。" + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - in: "formData" + name: "file" + type: "file" + description: "3MBまでのJPG, PNGファイル" + responses: + 201: + description: "OK" + schema: + type: "object" + properties: + id: + type: "integer" + example: 1 + description: "ファイルID" + url: + type: "string" + example: "/api/files/1" + description: "ファイルURL" + 400: + description: "不正なリクエストです。" + + /files/:id: + get: + tags: + - "files" + summary: "画像ファイルをダウンロードします。" + produces: + - "image/jpeg" + responses: + 200: + description: "OK" + 404: + description: "ファイルが存在しません。" + definitions: item: type: "object" From 73147ace8bce0d6efbaf9f50d6a0e7933b71af9b Mon Sep 17 00:00:00 2001 From: ryoha000 <48485650+ryoha000@users.noreply.github.com> Date: Mon, 28 Oct 2019 06:29:32 +0900 Subject: [PATCH 18/63] =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/shared/EditOwner.vue | 20 ++++++--- model/items.go | 49 ++++++++++++++++++++-- model/items_test.go | 28 ++++++++++--- router/items.go | 15 +++++-- 4 files changed, 94 insertions(+), 18 deletions(-) diff --git a/client/src/components/shared/EditOwner.vue b/client/src/components/shared/EditOwner.vue index 95d4ad86..5b849d72 100644 --- a/client/src/components/shared/EditOwner.vue +++ b/client/src/components/shared/EditOwner.vue @@ -1,8 +1,9 @@