From f4c414c0f62fd040ac0aeab36b528b541149109b Mon Sep 17 00:00:00 2001
From: HFO4 <912394456@qq.com>
Date: Sat, 7 Dec 2019 15:05:48 +0800
Subject: [PATCH] Fix: uint may overflow / Test: get user storage

---
 azure-pipelines.yml         | 43 --------------------------
 models/migration.go         |  3 +-
 models/migration_test.go    |  3 ++
 models/user.go              |  2 +-
 pkg/filesystem/file.go      |  1 +
 pkg/filesystem/path.go      | 18 +++++++++--
 pkg/filesystem/path_test.go |  6 ++--
 pkg/serializer/user.go      | 37 +++++++++++++++++-----
 pkg/serializer/user_test.go | 61 +++++++++++++++++++++++++++++++++++++
 routers/controllers/user.go | 12 ++++++--
 routers/router.go           |  1 +
 11 files changed, 126 insertions(+), 61 deletions(-)
 delete mode 100644 azure-pipelines.yml
 create mode 100644 pkg/serializer/user_test.go

diff --git a/azure-pipelines.yml b/azure-pipelines.yml
deleted file mode 100644
index 8899c25..0000000
--- a/azure-pipelines.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-# Go
-# Build your Go project.
-# Add steps that test, save build artifacts, deploy, and more:
-# https://docs.microsoft.com/azure/devops/pipelines/languages/go
-
-trigger:
-- master
-
-pool:
-  vmImage: 'ubuntu-latest'
-
-variables:
-  GOBIN:  '$(GOPATH)/bin' # Go binaries path
-  GOROOT: '/usr/local/go1.13' # Go installation path
-  GOPATH: '$(system.defaultWorkingDirectory)/gopath' # Go workspace path
-  modulePath: '$(GOPATH)/src/github.com/$(build.repository.name)' # Path to the module's code
-
-steps:
-- script: |
-    mkdir -p '$(GOBIN)'
-    mkdir -p '$(GOPATH)/pkg'
-    mkdir -p '$(modulePath)'
-    shopt -s extglob
-    shopt -s dotglob
-    mv !(gopath) '$(modulePath)'
-    echo '##vso[task.prependpath]$(GOBIN)'
-    echo '##vso[task.prependpath]$(GOROOT)/bin'
-  displayName: 'Set up the Go workspace'
-
-- script: |
-    go version
-    go get -v -t -d ./...
-    if [ -f Gopkg.toml ]; then
-        curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
-        dep ensure
-    fi
-    go build -v .
-  workingDirectory: '$(modulePath)'
-  displayName: 'Get dependencies, then build'
-
-- script: go test -v ./...
-  workingDirectory: '$(modulePath)'
-  displayName: 'Run tests'
\ No newline at end of file
diff --git a/models/migration.go b/models/migration.go
index a065fa0..3c78a62 100644
--- a/models/migration.go
+++ b/models/migration.go
@@ -138,8 +138,7 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
 		{Name: "sendfile", Value: `0`, Type: "download"},
 		{Name: "defaultTheme", Value: `#3f51b5`, Type: "basic"},
 		{Name: "header", Value: `X-Sendfile`, Type: "download"},
-		{Name: "themes", Value: `{"#3f51b5":{"palette":{"common":{"black":"#000","white":"#fff"},"background":{"paper":"#fff","default":"#fafafa"},"primary":{"light":"#7986cb","main":"#3f51b5","dark":"#303f9f","contrastText":"#fff"},"secondary":{"light":"#ff4081","main":"#f50057","dark":"#c51162","contrastText":"#fff"},"error":{"light":"#e57373","main":"#f44336","dark":"#d32f2f","contrastText":"#fff"},"text":{"primary":"rgba(0, 0, 0, 0.87)","secondary":"rgba(0, 0, 0, 0.54)","disabled":"rgba(0, 0, 0, 0.38)","hint":"rgba(0, 0, 0, 0.38)"},"explorer":{"filename":"#474849","icon":"#8f8f8f","bgSelected":"#D5DAF0","emptyIcon":"#e8e8e8"}}}}
-`, Type: "basic"},
+		{Name: "themes", Value: `{"#3f51b5":{"palette":{"primary":{"light":"#7986cb","main":"#3f51b5","dark":"#303f9f","contrastText":"#fff"},"secondary":{"light":"#ff4081","main":"#f50057","dark":"#c51162","contrastText":"#fff"},"error":{"light":"#e57373","main":"#f44336","dark":"#d32f2f","contrastText":"#fff"},"explorer":{"filename":"#474849","icon":"#8f8f8f","bgSelected":"#D5DAF0","emptyIcon":"#e8e8e8"}}}}`, Type: "basic"},
 		{Name: "refererCheck", Value: `true`, Type: "share"},
 		{Name: "header", Value: `X-Sendfile`, Type: "download"},
 		{Name: "aria2_tmppath", Value: `/path/to/public/download`, Type: "aria2"},
diff --git a/models/migration_test.go b/models/migration_test.go
index f2bf414..5c1cf8c 100644
--- a/models/migration_test.go
+++ b/models/migration_test.go
@@ -1,6 +1,7 @@
 package model
 
 import (
+	"github.com/HFO4/cloudreve/pkg/conf"
 	"github.com/jinzhu/gorm"
 	"github.com/stretchr/testify/assert"
 	"testing"
@@ -8,10 +9,12 @@ import (
 
 func TestMigration(t *testing.T) {
 	asserts := assert.New(t)
+	conf.DatabaseConfig.Type = "sqlite3"
 	DB, _ = gorm.Open("sqlite3", ":memory:")
 
 	asserts.NotPanics(func() {
 		migration()
 	})
+	conf.DatabaseConfig.Type = "mysql"
 	DB = mockDB
 }
diff --git a/models/user.go b/models/user.go
index 2db0403..67e3440 100644
--- a/models/user.go
+++ b/models/user.go
@@ -65,7 +65,7 @@ func (user *User) DeductionStorage(size uint64) bool {
 		DB.Model(user).UpdateColumn("storage", gorm.Expr("storage - ?", size))
 		return true
 	}
-	// 如果要减少的容量超出以用容量,则设为零
+	// 如果要减少的容量超出已用容量,则设为零
 	user.Storage = 0
 	DB.Model(user).UpdateColumn("storage", 0)
 
diff --git a/pkg/filesystem/file.go b/pkg/filesystem/file.go
index b34a136..5b63a41 100644
--- a/pkg/filesystem/file.go
+++ b/pkg/filesystem/file.go
@@ -107,6 +107,7 @@ func (fs *FileSystem) GetContent(ctx context.Context, path string) (io.ReadSeeke
 // 返回每个分组失败的文件列表
 func (fs *FileSystem) deleteGroupedFile(ctx context.Context, files map[uint][]*model.File) map[uint][]string {
 	// 失败的文件列表
+	// TODO 并行删除
 	failed := make(map[uint][]string, len(files))
 
 	for policyID, toBeDeletedFiles := range files {
diff --git a/pkg/filesystem/path.go b/pkg/filesystem/path.go
index 661375c..d972807 100644
--- a/pkg/filesystem/path.go
+++ b/pkg/filesystem/path.go
@@ -7,6 +7,7 @@ import (
 	"github.com/HFO4/cloudreve/pkg/serializer"
 	"github.com/HFO4/cloudreve/pkg/util"
 	"path"
+	"sync"
 )
 
 /* =================
@@ -209,12 +210,25 @@ func (fs *FileSystem) List(ctx context.Context, dirPath string, pathProcessor fu
 		return []Object{}, nil
 	}
 
+	var wg sync.WaitGroup
+	var childFolders []model.Folder
+	var childFiles []model.File
+	wg.Add(2)
+
 	// 获取子目录
-	childFolders, _ := folder.GetChildFolder()
+	go func() {
+		childFolders, _ = folder.GetChildFolder()
+		wg.Done()
+	}()
+
 	// 获取子文件
-	childFiles, _ := folder.GetChildFile()
+	go func() {
+		childFiles, _ = folder.GetChildFile()
+		wg.Done()
+	}()
 
 	// 汇总处理结果
+	wg.Wait()
 	objects := make([]Object, 0, len(childFiles)+len(childFolders))
 	// 所有对象的父目录
 	var processedPath string
diff --git a/pkg/filesystem/path_test.go b/pkg/filesystem/path_test.go
index 77571ed..9da0bf9 100644
--- a/pkg/filesystem/path_test.go
+++ b/pkg/filesystem/path_test.go
@@ -299,17 +299,17 @@ func TestFileSystem_Delete(t *testing.T) {
 		mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "type"}).AddRow(1, "local"))
 		// 删除文件记录
 		mock.ExpectBegin()
-		mock.ExpectExec("DELETE(.+)").
+		mock.ExpectExec("DELETE(.+)files").
 			WillReturnResult(sqlmock.NewResult(0, 3))
 		mock.ExpectCommit()
 		// 归还容量
 		mock.ExpectBegin()
-		mock.ExpectExec("UPDATE(.+)").
+		mock.ExpectExec("UPDATE(.+)users").
 			WillReturnResult(sqlmock.NewResult(0, 3))
 		mock.ExpectCommit()
 		// 删除目录
 		mock.ExpectBegin()
-		mock.ExpectExec("DELETE(.+)").
+		mock.ExpectExec("DELETE(.+)folders").
 			WillReturnResult(sqlmock.NewResult(0, 3))
 		mock.ExpectCommit()
 
diff --git a/pkg/serializer/user.go b/pkg/serializer/user.go
index b61dfa3..a5cd139 100644
--- a/pkg/serializer/user.go
+++ b/pkg/serializer/user.go
@@ -22,11 +22,11 @@ type User struct {
 	Avatar         string `json:"avatar"`
 	CreatedAt      int64  `json:"created_at"`
 	PreferredTheme string `json:"preferred_theme"`
-	Policy         Policy `json:"policy"`
-	Group          Group  `json:"group"`
+	Policy         policy `json:"policy"`
+	Group          group  `json:"group"`
 }
 
-type Policy struct {
+type policy struct {
 	SaveType       string   `json:"saveType"`
 	MaxSize        string   `json:"maxSize"`
 	AllowedType    []string `json:"allowedType"`
@@ -34,12 +34,18 @@ type Policy struct {
 	AllowGetSource bool     `json:"allowSource"`
 }
 
-type Group struct {
+type group struct {
 	AllowShare           bool `json:"allowShare"`
 	AllowRemoteDownload  bool `json:"allowRemoteDownload"`
 	AllowTorrentDownload bool `json:"allowTorrentDownload"`
 }
 
+type storage struct {
+	Used  uint64 `json:"used"`
+	Free  uint64 `json:"free"`
+	Total uint64 `json:"total"`
+}
+
 // BuildUser 序列化用户
 func BuildUser(user model.User) User {
 	aria2Option := user.Group.GetAria2Option()
@@ -51,14 +57,14 @@ func BuildUser(user model.User) User {
 		Avatar:         user.Avatar,
 		CreatedAt:      user.CreatedAt.Unix(),
 		PreferredTheme: user.OptionsSerialized.PreferredTheme,
-		Policy: Policy{
+		Policy: policy{
 			SaveType:       user.Policy.Type,
-			MaxSize:        fmt.Sprintf("%.2fmb", float64(user.Policy.MaxSize)/1024*1024),
+			MaxSize:        fmt.Sprintf("%.2fmb", float64(user.Policy.MaxSize)/(1024*1024)),
 			AllowedType:    user.Policy.OptionsSerialized.FileType,
 			UploadURL:      user.Policy.Server,
 			AllowGetSource: user.Policy.IsOriginLinkEnable,
 		},
-		Group: Group{
+		Group: group{
 			AllowShare:           user.Group.ShareEnabled,
 			AllowRemoteDownload:  aria2Option[0],
 			AllowTorrentDownload: aria2Option[2],
@@ -72,3 +78,20 @@ func BuildUserResponse(user model.User) Response {
 		Data: BuildUser(user),
 	}
 }
+
+// BuildUserStorageResponse 序列化用户存储概况响应
+func BuildUserStorageResponse(user model.User) Response {
+	storageResp := storage{
+		Used:  user.Storage,
+		Free:  user.Group.MaxStorage - user.Storage,
+		Total: user.Group.MaxStorage,
+	}
+
+	if user.Group.MaxStorage < user.Storage {
+		storageResp.Free = 0
+	}
+
+	return Response{
+		Data: storageResp,
+	}
+}
diff --git a/pkg/serializer/user_test.go b/pkg/serializer/user_test.go
new file mode 100644
index 0000000..fa3df73
--- /dev/null
+++ b/pkg/serializer/user_test.go
@@ -0,0 +1,61 @@
+package serializer
+
+import (
+	model "github.com/HFO4/cloudreve/models"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestBuildUser(t *testing.T) {
+	asserts := assert.New(t)
+	user := model.User{
+		Policy: model.Policy{MaxSize: 1024 * 1024},
+	}
+	res := BuildUser(user)
+	asserts.Equal("1.00mb", res.Policy.MaxSize)
+
+}
+
+func TestBuildUserResponse(t *testing.T) {
+	asserts := assert.New(t)
+	user := model.User{
+		Policy: model.Policy{MaxSize: 1024 * 1024},
+	}
+	res := BuildUserResponse(user)
+	asserts.Equal("1.00mb", res.Data.(User).Policy.MaxSize)
+}
+
+func TestBuildUserStorageResponse(t *testing.T) {
+	asserts := assert.New(t)
+
+	{
+		user := model.User{
+			Storage: 0,
+			Group:   model.Group{MaxStorage: 10},
+		}
+		res := BuildUserStorageResponse(user)
+		asserts.Equal(uint64(0), res.Data.(storage).Used)
+		asserts.Equal(uint64(10), res.Data.(storage).Total)
+		asserts.Equal(uint64(10), res.Data.(storage).Free)
+	}
+	{
+		user := model.User{
+			Storage: 6,
+			Group:   model.Group{MaxStorage: 10},
+		}
+		res := BuildUserStorageResponse(user)
+		asserts.Equal(uint64(6), res.Data.(storage).Used)
+		asserts.Equal(uint64(10), res.Data.(storage).Total)
+		asserts.Equal(uint64(4), res.Data.(storage).Free)
+	}
+	{
+		user := model.User{
+			Storage: 20,
+			Group:   model.Group{MaxStorage: 10},
+		}
+		res := BuildUserStorageResponse(user)
+		asserts.Equal(uint64(20), res.Data.(storage).Used)
+		asserts.Equal(uint64(10), res.Data.(storage).Total)
+		asserts.Equal(uint64(0), res.Data.(storage).Free)
+	}
+}
diff --git a/routers/controllers/user.go b/routers/controllers/user.go
index f58ed2b..4463cdc 100644
--- a/routers/controllers/user.go
+++ b/routers/controllers/user.go
@@ -20,8 +20,14 @@ func UserLogin(c *gin.Context) {
 
 // UserMe 获取当前登录的用户
 func UserMe(c *gin.Context) {
-	user := CurrentUser(c)
-	res := serializer.BuildUserResponse(*user)
+	currUser := CurrentUser(c)
+	res := serializer.BuildUserResponse(*currUser)
+	c.JSON(200, res)
+}
+
+// UserStorage 获取用户的存储信息
+func UserStorage(c *gin.Context) {
+	currUser := CurrentUser(c)
+	res := serializer.BuildUserStorageResponse(*currUser)
 	c.JSON(200, res)
-
 }
diff --git a/routers/router.go b/routers/router.go
index 5484064..f49ec7d 100644
--- a/routers/router.go
+++ b/routers/router.go
@@ -57,6 +57,7 @@ func InitRouter() *gin.Engine {
 			{
 				// 当前登录用户信息
 				user.GET("me", controllers.UserMe)
+				user.GET("storage", controllers.UserStorage)
 			}
 
 			// 文件