From cfbfbc3c5517b6009f19c4c7af09355df191c804 Mon Sep 17 00:00:00 2001
From: HFO4 <912394456@qq.com>
Date: Thu, 13 Feb 2020 11:53:24 +0800
Subject: [PATCH] Feat: list/search/update/delete shares

---
 middleware/share.go          | 26 +++++++++++++++-
 models/share.go              | 53 +++++++++++++++++++++++++++++++
 models/user.go               |  2 ++
 pkg/serializer/share.go      | 60 +++++++++++++++++++++++++++++++++++-
 routers/controllers/share.go | 44 ++++++++++++++++++++++++++
 routers/router.go            | 14 +++++++++
 service/callback/upload.go   |  6 +++-
 service/share/manage.go      | 56 +++++++++++++++++++++++++++++++--
 service/share/visit.go       | 34 ++++++++++++++++++++
 9 files changed, 290 insertions(+), 5 deletions(-)

diff --git a/middleware/share.go b/middleware/share.go
index 99868a5..a67a67b 100644
--- a/middleware/share.go
+++ b/middleware/share.go
@@ -8,6 +8,30 @@ import (
 	"github.com/gin-gonic/gin"
 )
 
+// ShareOwner 检查当前登录用户是否为分享所有者
+func ShareOwner() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		var user *model.User
+		if userCtx, ok := c.Get("user"); ok {
+			user = userCtx.(*model.User)
+		} else {
+			c.JSON(200, serializer.Err(serializer.CodeCheckLogin, "请先登录", nil))
+			c.Abort()
+			return
+		}
+
+		if share, ok := c.Get("share"); ok {
+			if share.(*model.Share).Creator().ID != user.ID {
+				c.JSON(200, serializer.Err(serializer.CodeNotFound, "分享不存在", nil))
+				c.Abort()
+				return
+			}
+		}
+
+		c.Next()
+	}
+}
+
 // ShareAvailable 检查分享是否可用
 func ShareAvailable() gin.HandlerFunc {
 	return func(c *gin.Context) {
@@ -21,7 +45,7 @@ func ShareAvailable() gin.HandlerFunc {
 		share := model.GetShareByHashID(c.Param("id"))
 
 		if share == nil || !share.IsAvailable() {
-			c.JSON(200, serializer.Err(serializer.CodeNotFound, "分享不存在或已被取消", nil))
+			c.JSON(200, serializer.Err(serializer.CodeNotFound, "分享不存在或已失效", nil))
 			c.Abort()
 			return
 		}
diff --git a/models/share.go b/models/share.go
index 9dcde9d..bf9e6a0 100644
--- a/models/share.go
+++ b/models/share.go
@@ -25,6 +25,7 @@ type Share struct {
 	Expires         *time.Time // 过期时间,空值表示无过期时间
 	Score           int        // 每人次下载扣除积分
 	PreviewEnabled  bool       // 是否允许直接预览
+	SourceName      string     `gorm:"index:source"` // 用于搜索的字段
 
 	// 数据库忽略字段
 	User   User   `gorm:"PRELOAD:false,association_autoupdate:false"`
@@ -79,6 +80,11 @@ func (share *Share) IsAvailable() bool {
 		return false
 	}
 
+	// 检查创建者状态
+	if share.Creator().Status != Active {
+		return false
+	}
+
 	return true
 }
 
@@ -202,3 +208,50 @@ func (share *Share) Downloaded() {
 		"remain_downloads": share.RemainDownloads,
 	})
 }
+
+// Update 更新分享属性
+func (share *Share) Update(props map[string]interface{}) error {
+	return DB.Model(share).Updates(props).Error
+}
+
+// Delete 删除分享
+func (share *Share) Delete() error {
+	return DB.Model(share).Delete(share).Error
+}
+
+// ListShares 列出UID下的分享
+func ListShares(uid uint, page, pageSize int, order string, publicOnly bool) ([]Share, int) {
+	var (
+		shares []Share
+		total  int
+	)
+	dbChain := DB
+	dbChain = dbChain.Where("user_id = ?", uid)
+	if publicOnly {
+		dbChain.Where("password = ?", "")
+	}
+
+	// 计算总数用于分页
+	dbChain.Model(&Share{}).Count(&total)
+
+	// 查询记录
+	dbChain.Limit(pageSize).Offset((page - 1) * pageSize).Order(order).Find(&shares)
+	return shares, total
+}
+
+// SearchShares 根据关键字搜索分享
+func SearchShares(page, pageSize int, order, keywords string) ([]Share, int) {
+	var (
+		shares []Share
+		total  int
+	)
+	dbChain := DB
+	dbChain = dbChain.Where("password = ? and remain_downloads <> 0 and (expires is NULL or expires > ?) and source_name like ?", "", time.Now(), "%"+keywords+"%")
+
+	// 计算总数用于分页
+	dbChain.Model(&Share{}).Count(&total)
+
+	// 查询记录
+	dbChain.Limit(pageSize).Offset((page - 1) * pageSize).Order(order).Find(&shares)
+	return shares, total
+}
diff --git a/models/user.go b/models/user.go
index aef7d08..8583711 100644
--- a/models/user.go
+++ b/models/user.go
@@ -18,6 +18,8 @@ const (
 	NotActivicated
 	// Baned 被封禁
 	Baned
+	// OveruseBaned 超额使用被封禁
+	OveruseBaned
 )
 
 // User 用户模型
diff --git a/pkg/serializer/share.go b/pkg/serializer/share.go
index ddf4621..83542b8 100644
--- a/pkg/serializer/share.go
+++ b/pkg/serializer/share.go
@@ -6,7 +6,7 @@ import (
 	"time"
 )
 
-// Share 分享序列化
+// Share 分享信息序列化
 type Share struct {
 	Key        string        `json:"key"`
 	Locked     bool          `json:"locked"`
@@ -32,6 +32,64 @@ type shareSource struct {
 	Size uint64 `json:"size"`
 }
 
+// myShareItem 我的分享列表条目
+type myShareItem struct {
+	Key             string       `json:"key"`
+	IsDir           bool         `json:"is_dir"`
+	Score           int          `json:"score"`
+	Password        string       `json:"password"`
+	CreateDate      string       `json:"create_date,omitempty"`
+	Downloads       int          `json:"downloads"`
+	RemainDownloads int          `json:"remain_downloads"`
+	Views           int          `json:"views"`
+	Expire          int64        `json:"expire"`
+	Preview         bool         `json:"preview"`
+	Source          *shareSource `json:"source,omitempty"`
+}
+
+// BuildShareList 构建我的分享列表响应
+func BuildShareList(shares []model.Share, total int) Response {
+	res := make([]myShareItem, 0, total)
+	now := time.Now().Unix()
+	for i := 0; i < len(shares); i++ {
+		item := myShareItem{
+			Key:             hashid.HashID(shares[i].ID, hashid.ShareID),
+			IsDir:           shares[i].IsDir,
+			Score:           shares[i].Score,
+			Password:        shares[i].Password,
+			CreateDate:      shares[i].CreatedAt.Format("2006-01-02 15:04:05"),
+			Downloads:       shares[i].Downloads,
+			Views:           shares[i].Views,
+			Preview:         shares[i].PreviewEnabled,
+			Expire:          -1,
+			RemainDownloads: shares[i].RemainDownloads,
+		}
+		if shares[i].Expires != nil {
+			item.Expire = shares[i].Expires.Unix() - now
+			if item.Expire == 0 {
+				item.Expire = 0
+			}
+		}
+		if shares[i].File.ID != 0 {
+			item.Source = &shareSource{
+				Name: shares[i].File.Name,
+				Size: shares[i].File.Size,
+			}
+		} else if shares[i].Folder.ID != 0 {
+			item.Source = &shareSource{
+				Name: shares[i].Folder.Name,
+			}
+		}
+
+		res = append(res, item)
+	}
+
+	return Response{Data: map[string]interface{}{
+		"total": total,
+		"items": res,
+	}}
+}
+
 // BuildShareResponse 构建获取分享信息响应
 func BuildShareResponse(share *model.Share, unlocked bool) Share {
 	creator := share.Creator()
diff --git a/routers/controllers/share.go b/routers/controllers/share.go
index 09612f1..49ad750 100644
--- a/routers/controllers/share.go
+++ b/routers/controllers/share.go
@@ -33,6 +33,50 @@ func GetShare(c *gin.Context) {
 	}
 }
 
+// ListShare 列出分享
+func ListShare(c *gin.Context) {
+	var service share.ShareListService
+	if err := c.ShouldBindQuery(&service); err == nil {
+		res := service.List(c, CurrentUser(c))
+		c.JSON(200, res)
+	} else {
+		c.JSON(200, ErrorResponse(err))
+	}
+}
+
+// SearchShare 搜索分享
+func SearchShare(c *gin.Context) {
+	var service share.ShareListService
+	if err := c.ShouldBindQuery(&service); err == nil {
+		res := service.Search(c)
+		c.JSON(200, res)
+	} else {
+		c.JSON(200, ErrorResponse(err))
+	}
+}
+
+// UpdateShare 更新分享属性
+func UpdateShare(c *gin.Context) {
+	var service share.ShareUpdateService
+	if err := c.ShouldBindJSON(&service); err == nil {
+		res := service.Update(c)
+		c.JSON(200, res)
+	} else {
+		c.JSON(200, ErrorResponse(err))
+	}
+}
+
+// DeleteShare 删除分享
+func DeleteShare(c *gin.Context) {
+	var service share.Service
+	if err := c.ShouldBindUri(&service); err == nil {
+		res := service.Delete(c, CurrentUser(c))
+		c.JSON(200, res)
+	} else {
+		c.JSON(200, ErrorResponse(err))
+	}
+}
+
 // GetShareDownload 创建分享下载会话
 func GetShareDownload(c *gin.Context) {
 	var service share.Service
diff --git a/routers/router.go b/routers/router.go
index 9cd34ef..88ebef5 100644
--- a/routers/router.go
+++ b/routers/router.go
@@ -320,6 +320,10 @@ func InitMasterRouter() *gin.Engine {
 			{
 				// 创建新分享
 				share.POST("", controllers.CreateShare)
+				// 列出我的分享
+				share.GET("", controllers.ListShare)
+				// 搜索公共分享
+				share.GET("search", controllers.SearchShare)
 				// 转存他人分享
 				share.POST("save/:id",
 					middleware.ShareAvailable(),
@@ -327,6 +331,16 @@ func InitMasterRouter() *gin.Engine {
 					middleware.BeforeShareDownload(),
 					controllers.SaveShare,
 				)
+				// 更新分享属性
+				share.PATCH(":id",
+					middleware.ShareAvailable(),
+					middleware.ShareOwner(),
+					controllers.UpdateShare,
+				)
+				// 删除分享
+				share.DELETE(":id",
+					controllers.DeleteShare,
+				)
 			}
 
 			// 用户标签
diff --git a/service/callback/upload.go b/service/callback/upload.go
index b261474..9cc6722 100644
--- a/service/callback/upload.go
+++ b/service/callback/upload.go
@@ -124,7 +124,11 @@ func ProcessCallback(service CallbackProcessService, c *gin.Context) serializer.
 	// 获取父目录
 	exist, parentFolder := fs.IsPathExist(callbackSession.VirtualPath)
 	if !exist {
-		return serializer.Err(serializer.CodeParamErr, "指定目录不存在", nil)
+		newFolder, err := fs.CreateDirectory(context.Background(), callbackSession.VirtualPath)
+		if err != nil {
+			return serializer.Err(serializer.CodeParamErr, "指定目录不存在", err)
+		}
+		parentFolder = newFolder
 	}
 
 	// 创建文件头
diff --git a/service/share/manage.go b/service/share/manage.go
index 48f4452..bc6b7e6 100644
--- a/service/share/manage.go
+++ b/service/share/manage.go
@@ -20,6 +20,52 @@ type ShareCreateService struct {
 	Preview         bool   `json:"preview"`
 }
 
+// ShareUpdateService 分享更新服务
+type ShareUpdateService struct {
+	Prop  string `json:"prop" binding:"required,eq=password|eq=preview_enabled"`
+	Value string `json:"value" binding:"max=255"`
+}
+
+// Delete 删除分享
+func (service *Service) Delete(c *gin.Context, user *model.User) serializer.Response {
+	share := model.GetShareByHashID(c.Param("id"))
+	if share == nil || share.Creator().ID != user.ID {
+		return serializer.Err(serializer.CodeNotFound, "分享不存在", nil)
+	}
+
+	if err := share.Delete(); err != nil {
+		return serializer.Err(serializer.CodeDBError, "分享删除失败", err)
+	}
+
+	return serializer.Response{}
+}
+
+// Update 更新分享属性
+func (service *ShareUpdateService) Update(c *gin.Context) serializer.Response {
+	shareCtx, _ := c.Get("share")
+	share := shareCtx.(*model.Share)
+
+	switch service.Prop {
+	case "password":
+		err := share.Update(map[string]interface{}{"password": service.Value})
+		if err != nil {
+			return serializer.Err(serializer.CodeDBError, "无法更新分享密码", err)
+		}
+	case "preview_enabled":
+		value := service.Value == "true"
+		err := share.Update(map[string]interface{}{"preview_enabled": value})
+		if err != nil {
+			return serializer.Err(serializer.CodeDBError, "无法更新分享属性", err)
+		}
+		return serializer.Response{
+			Data: value,
+		}
+	}
+	return serializer.Response{
+		Data: service.Value,
+	}
+}
+
 // Create 创建新分享
 func (service *ShareCreateService) Create(c *gin.Context) serializer.Response {
 	userCtx, _ := c.Get("user")
@@ -32,8 +78,9 @@ func (service *ShareCreateService) Create(c *gin.Context) serializer.Response {
 
 	// 源对象真实ID
 	var (
-		sourceID uint
-		err      error
+		sourceID   uint
+		sourceName string
+		err        error
 	)
 	if service.IsDir {
 		sourceID, err = hashid.DecodeHashID(service.SourceID, hashid.FolderID)
@@ -50,11 +97,15 @@ func (service *ShareCreateService) Create(c *gin.Context) serializer.Response {
 		folder, err := model.GetFoldersByIDs([]uint{sourceID}, user.ID)
 		if err != nil || len(folder) == 0 {
 			exist = false
+		} else {
+			sourceName = folder[0].Name
 		}
 	} else {
 		file, err := model.GetFilesByIDs([]uint{sourceID}, user.ID)
 		if err != nil || len(file) == 0 {
 			exist = false
+		} else {
+			sourceName = file[0].Name
 		}
 	}
 	if !exist {
@@ -69,6 +120,7 @@ func (service *ShareCreateService) Create(c *gin.Context) serializer.Response {
 		Score:           service.Score,
 		RemainDownloads: -1,
 		PreviewEnabled:  service.Preview,
+		SourceName:      sourceName,
 	}
 
 	// 如果开启了自动过期
diff --git a/service/share/visit.go b/service/share/visit.go
index 546d8cb..b0077d1 100644
--- a/service/share/visit.go
+++ b/service/share/visit.go
@@ -33,6 +33,40 @@ type ArchiveService struct {
 	Dirs  []string `json:"dirs" binding:"exists"`
 }
 
+// ShareListService 列出分享
+type ShareListService struct {
+	Page     uint   `form:"page" binding:"required,min=1"`
+	OrderBy  string `form:"order_by" binding:"required,eq=created_at|eq=downloads|eq=views"`
+	Order    string `form:"order" binding:"required,eq=DESC|eq=ASC"`
+	Keywords string `form:"keywords"`
+}
+
+// Search 搜索公共分享
+func (service *ShareListService) Search(c *gin.Context) serializer.Response {
+	// 列出分享
+	shares, total := model.SearchShares(int(service.Page), 18, service.OrderBy+" "+
+		service.Order, service.Keywords)
+	// 列出分享对应的文件
+	for i := 0; i < len(shares); i++ {
+		shares[i].Source()
+	}
+
+	return serializer.BuildShareList(shares, total)
+}
+
+// List 列出用户分享
+func (service *ShareListService) List(c *gin.Context, user *model.User) serializer.Response {
+	// 列出分享
+	shares, total := model.ListShares(user.ID, int(service.Page), 18, service.OrderBy+" "+
+		service.Order, false)
+	// 列出分享对应的文件
+	for i := 0; i < len(shares); i++ {
+		shares[i].Source()
+	}
+
+	return serializer.BuildShareList(shares, total)
+}
+
 // Get 获取分享内容
 func (service *ShareGetService) Get(c *gin.Context) serializer.Response {
 	shareCtx, _ := c.Get("share")