Compare commits

..
8 Commits
22 changed files with 282 additions and 52 deletions
+1 -1
Submodule assets updated: c4a7dfea2c...2d20892994
+8 -3
View File
@@ -117,8 +117,8 @@ func GetFilesByIDsFromTX(tx *gorm.DB, ids []uint, uid uint) ([]File, error) {
}
// GetFilesByKeywords 根据关键字搜索文件,
// UID为0表示忽略用户,只根据文件ID检索
func GetFilesByKeywords(uid uint, keywords ...interface{}) ([]File, error) {
// UID为0表示忽略用户,只根据文件ID检索. 如果 parents 非空, 则只限制在 parent 包含的目录下搜索
func GetFilesByKeywords(uid uint, parents []uint, keywords ...interface{}) ([]File, error) {
var (
files []File
result = DB
@@ -136,6 +136,11 @@ func GetFilesByKeywords(uid uint, keywords ...interface{}) ([]File, error) {
if uid != 0 {
result = result.Where("user_id = ?", uid)
}
if len(parents) > 0 {
result = result.Where("folder_id in (?)", parents)
}
result = result.Where("("+conditions+")", keywords...).Find(&files)
return files, result.Error
@@ -143,7 +148,7 @@ func GetFilesByKeywords(uid uint, keywords ...interface{}) ([]File, error) {
// GetChildFilesOfFolders 批量检索目录子文件
func GetChildFilesOfFolders(folders *[]Folder) ([]File, error) {
// 将所有待删除目录ID抽离,以便检索文件
// 将所有待检索目录ID抽离,以便检索文件
folderIDs := make([]uint, 0, len(*folders))
for _, value := range *folders {
folderIDs = append(folderIDs, value.ID)
+11 -2
View File
@@ -561,7 +561,7 @@ func TestGetFilesByKeywords(t *testing.T) {
// 未指定用户
{
mock.ExpectQuery("SELECT(.+)").WithArgs("k1", "k2").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
res, err := GetFilesByKeywords(0, "k1", "k2")
res, err := GetFilesByKeywords(0, nil, "k1", "k2")
asserts.NoError(mock.ExpectationsWereMet())
asserts.NoError(err)
asserts.Len(res, 1)
@@ -570,7 +570,16 @@ func TestGetFilesByKeywords(t *testing.T) {
// 指定用户
{
mock.ExpectQuery("SELECT(.+)").WithArgs(1, "k1", "k2").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
res, err := GetFilesByKeywords(1, "k1", "k2")
res, err := GetFilesByKeywords(1, nil, "k1", "k2")
asserts.NoError(mock.ExpectationsWereMet())
asserts.NoError(err)
asserts.Len(res, 1)
}
// 指定父目录
{
mock.ExpectQuery("SELECT(.+)").WithArgs(1, 12, "k1", "k2").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
res, err := GetFilesByKeywords(1, []uint{12}, "k1", "k2")
asserts.NoError(mock.ExpectationsWereMet())
asserts.NoError(err)
asserts.Len(res, 1)
+2
View File
@@ -31,6 +31,8 @@ type GroupOption struct {
ShareDownload bool `json:"share_download,omitempty"`
Aria2 bool `json:"aria2,omitempty"` // 离线下载
Aria2Options map[string]interface{} `json:"aria2_options,omitempty"` // 离线下载用户组配置
SourceBatchSize int `json:"source_batch,omitempty"`
Aria2BatchSize int `json:"aria2_batch,omitempty"`
}
// GetGroupByID 用ID获取用户组
+5 -1
View File
@@ -108,6 +108,8 @@ func addDefaultGroups() {
ArchiveTask: true,
ShareDownload: true,
Aria2: true,
SourceBatchSize: 1000,
Aria2BatchSize: 50,
},
}
if err := DB.Create(&defaultAdminGroup).Error; err != nil {
@@ -126,7 +128,9 @@ func addDefaultGroups() {
ShareEnabled: true,
WebDAVEnabled: true,
OptionsSerialized: GroupOption{
ShareDownload: true,
ShareDownload: true,
SourceBatchSize: 10,
Aria2BatchSize: 1,
},
}
if err := DB.Create(&defaultAdminGroup).Error; err != nil {
+1
View File
@@ -63,6 +63,7 @@ type PolicyOption struct {
PlaceholderWithSize bool `json:"placeholder_with_size,omitempty"`
}
// thumbSuffix 支持缩略图处理的文件扩展名
var thumbSuffix = map[string][]string{
"local": {},
"qiniu": {".psd", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"},
+2 -2
View File
@@ -1,13 +1,13 @@
package conf
// BackendVersion 当前后端版本号
var BackendVersion = "3.5.2"
var BackendVersion = "3.5.3"
// RequiredDBVersion 与当前版本匹配的数据库版本
var RequiredDBVersion = "3.5.2"
// RequiredStaticVersion 与当前版本匹配的静态资源版本
var RequiredStaticVersion = "3.5.2"
var RequiredStaticVersion = "3.5.3"
// IsPro 是否为Pro版本
var IsPro = "false"
+16 -1
View File
@@ -2,6 +2,7 @@ package filesystem
import (
"context"
"fmt"
"io"
model "github.com/cloudreve/Cloudreve/v3/models"
@@ -361,7 +362,21 @@ func (fs *FileSystem) resetPolicyToFirstFile(ctx context.Context) error {
// Search 搜索文件
func (fs *FileSystem) Search(ctx context.Context, keywords ...interface{}) ([]serializer.Object, error) {
files, _ := model.GetFilesByKeywords(fs.User.ID, keywords...)
parents := make([]uint, 0)
// 如果限定了根目录,则只在这个根目录下搜索。
if fs.Root != nil {
allFolders, err := model.GetRecursiveChildFolder([]uint{fs.Root.ID}, fs.User.ID, true)
if err != nil {
return nil, fmt.Errorf("failed to list all folders: %w", err)
}
for _, folder := range allFolders {
parents = append(parents, folder.ID)
}
}
files, _ := model.GetFilesByKeywords(fs.User.ID, parents, keywords...)
fs.SetTargetFile(&files)
return fs.listObjects(ctx, "/", files, nil, nil), nil
+4
View File
@@ -284,6 +284,10 @@ func HookPopPlaceholderToFile(picInfo string) Hook {
return func(ctx context.Context, fs *FileSystem, fileHeader fsctx.FileHeader) error {
fileInfo := fileHeader.Info()
fileModel := fileInfo.Model.(*model.File)
if picInfo == "" && fs.Policy.IsThumbExist(fileInfo.FileName) {
picInfo = "1,1"
}
return fileModel.PopChunkToFile(fileInfo.LastModified, picInfo)
}
}
+19
View File
@@ -671,6 +671,25 @@ func TestHookPopPlaceholderToFile(t *testing.T) {
a.NoError(mock.ExpectationsWereMet())
}
func TestHookPopPlaceholderToFileBySuffix(t *testing.T) {
a := assert.New(t)
fs := &FileSystem{
Policy: &model.Policy{Type: "cos"},
}
file := &fsctx.FileStream{
Name: "1.png",
Model: &model.File{
Model: gorm.Model{ID: 1},
},
}
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
a.NoError(HookPopPlaceholderToFile("")(context.Background(), fs, file))
a.NoError(mock.ExpectationsWereMet())
}
func TestHookDeleteUploadSession(t *testing.T) {
a := assert.New(t)
fs := &FileSystem{}
+6
View File
@@ -80,6 +80,12 @@ const (
CodeInvalidChunkIndex = 400012
// CodeInvalidContentLength 无效的正文长度
CodeInvalidContentLength = 400013
// CodeBatchSourceSize 超出批量获取外链限制
CodeBatchSourceSize = 40014
// CodeBatchAria2Size 超出最大 Aria2 任务数量限制
CodeBatchAria2Size = 40015
// CodeParentNotExist 父目录不存在
CodeParentNotExist = 40016
// CodeDBError 数据库操作失败
CodeDBError = 50001
// CodeEncryptError 加密失败
+8
View File
@@ -76,3 +76,11 @@ func BuildObjectList(parent uint, objects []Object, policy *model.Policy) Object
return res
}
// Sources 获取外链的结果响应
type Sources struct {
URL string `json:"url"`
Name string `json:"name"`
Parent uint `json:"parent"`
Error string `json:"error,omitempty"`
}
+2
View File
@@ -40,6 +40,7 @@ type group struct {
ShareDownload bool `json:"shareDownload"`
CompressEnabled bool `json:"compress"`
WebDAVEnabled bool `json:"webdav"`
SourceBatchSize int `json:"sourceBatch"`
}
type tag struct {
@@ -98,6 +99,7 @@ func BuildUser(user model.User) User {
ShareDownload: user.Group.OptionsSerialized.ShareDownload,
CompressEnabled: user.Group.OptionsSerialized.ArchiveTask,
WebDAVEnabled: user.Group.WebDAVEnabled,
SourceBatchSize: user.Group.OptionsSerialized.SourceBatchSize,
},
Tags: buildTagRes(tags),
}
+2 -2
View File
@@ -11,7 +11,7 @@ import (
// AddAria2URL 添加离线下载URL
func AddAria2URL(c *gin.Context) {
var addService aria2.AddURLService
var addService aria2.BatchAddURLService
if err := c.ShouldBindJSON(&addService); err == nil {
res := addService.Add(c, common.URLTask)
c.JSON(200, res)
@@ -52,7 +52,7 @@ func AddAria2Torrent(c *gin.Context) {
if err := c.ShouldBindJSON(&addService); err == nil {
addService.URL = res.Data.(string)
res := addService.Add(c, common.URLTask)
res := addService.Add(c, nil, common.URLTask)
c.JSON(200, res)
} else {
c.JSON(200, ErrorResponse(err))
+17 -32
View File
@@ -3,10 +3,10 @@ package controllers
import (
"context"
"fmt"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"net/http"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/service/explorer"
@@ -102,39 +102,18 @@ func AnonymousPermLink(c *gin.Context) {
}
}
// GetSource 获取文件的外链地址
func GetSource(c *gin.Context) {
// 创建上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
c.JSON(200, serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err))
return
var service explorer.ItemIDService
if err := c.ShouldBindJSON(&service); err == nil {
res := service.Sources(ctx, c)
c.JSON(200, res)
} else {
c.JSON(200, ErrorResponse(err))
}
defer fs.Recycle()
// 获取文件ID
fileID, ok := c.Get("object_id")
if !ok {
c.JSON(200, serializer.ParamErr("文件不存在", err))
return
}
sourceURL, err := fs.GetSource(ctx, fileID.(uint))
if err != nil {
c.JSON(200, serializer.Err(serializer.CodeNotSet, err.Error(), err))
return
}
c.JSON(200, serializer.Response{
Code: 0,
Data: struct {
URL string `json:"url"`
}{URL: sourceURL},
})
}
// Thumb 获取文件缩略图
@@ -383,12 +362,18 @@ func GetUploadSession(c *gin.Context) {
// SearchFile 搜索文件
func SearchFile(c *gin.Context) {
var service explorer.ItemSearchService
if err := c.ShouldBindUri(&service); err == nil {
res := service.Search(c)
c.JSON(200, res)
} else {
if err := c.ShouldBindUri(&service); err != nil {
c.JSON(200, ErrorResponse(err))
return
}
if err := c.ShouldBindQuery(&service); err != nil {
c.JSON(200, ErrorResponse(err))
return
}
res := service.Search(c)
c.JSON(200, res)
}
// CreateFile 创建空白文件
+17
View File
@@ -184,6 +184,23 @@ func ListSharedFolder(c *gin.Context) {
}
}
// SearchSharedFolder 搜索分享的目录下的对象
func SearchSharedFolder(c *gin.Context) {
var service share.SearchService
if err := c.ShouldBindUri(&service); err != nil {
c.JSON(200, ErrorResponse(err))
return
}
if err := c.ShouldBindQuery(&service); err != nil {
c.JSON(200, ErrorResponse(err))
return
}
res := service.Search(c)
c.JSON(200, res)
}
// ArchiveShare 打包要下载的分享
func ArchiveShare(c *gin.Context) {
var service share.ArchiveService
+6 -1
View File
@@ -335,6 +335,11 @@ func InitMasterRouter() *gin.Engine {
middleware.CheckShareUnlocked(),
controllers.ListSharedFolder,
)
// 分享目录搜索
share.GET("search/:id/:type/:keywords",
middleware.CheckShareUnlocked(),
controllers.SearchSharedFolder,
)
// 归档打包下载
share.POST("archive/:id",
middleware.CheckShareUnlocked(),
@@ -558,7 +563,7 @@ func InitMasterRouter() *gin.Engine {
// 获取缩略图
file.GET("thumb/:id", controllers.Thumb)
// 取得文件外链
file.GET("source/:id", controllers.GetSource)
file.POST("source", controllers.GetSource)
// 打包要下载的文件
file.POST("archive", controllers.Archive)
// 创建文件压缩任务
+59 -5
View File
@@ -14,13 +14,13 @@ import (
)
// AddURLService 添加URL离线下载服务
type AddURLService struct {
URL string `json:"url" binding:"required"`
Dst string `json:"dst" binding:"required,min=1"`
type BatchAddURLService struct {
URLs []string `json:"url" binding:"required"`
Dst string `json:"dst" binding:"required,min=1"`
}
// Add 主机创建新的链接离线下载任务
func (service *AddURLService) Add(c *gin.Context, taskType int) serializer.Response {
// Add 主机批量创建新的链接离线下载任务
func (service *BatchAddURLService) Add(c *gin.Context, taskType int) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
@@ -38,6 +38,60 @@ func (service *AddURLService) Add(c *gin.Context, taskType int) serializer.Respo
return serializer.Err(serializer.CodeNotFound, "存放路径不存在", nil)
}
// 检查批量任务数量
limit := fs.User.Group.OptionsSerialized.Aria2BatchSize
if limit > 0 && len(service.URLs) > limit {
return serializer.Err(serializer.CodeBatchAria2Size, "Exceed aria2 batch size", nil)
}
res := make([]serializer.Response, 0, len(service.URLs))
for _, target := range service.URLs {
subService := &AddURLService{
URL: target,
Dst: service.Dst,
}
addRes := subService.Add(c, fs, taskType)
res = append(res, addRes)
}
return serializer.Response{Data: res}
}
// AddURLService 添加URL离线下载服务
type AddURLService struct {
URL string `json:"url" binding:"required"`
Dst string `json:"dst" binding:"required,min=1"`
}
// Add 主机创建新的链接离线下载任务
func (service *AddURLService) Add(c *gin.Context, fs *filesystem.FileSystem, taskType int) serializer.Response {
if fs == nil {
var err error
// 创建文件系统
fs, err = filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err)
}
defer fs.Recycle()
// 检查用户组权限
if !fs.User.Group.OptionsSerialized.Aria2 {
return serializer.Err(serializer.CodeGroupNotAllowed, "当前用户组无法进行此操作", nil)
}
// 存放目录是否存在
if exist, _ := fs.IsPathExist(service.Dst); !exist {
return serializer.Err(serializer.CodeNotFound, "存放路径不存在", nil)
}
}
downloads := model.GetDownloadsByStatusAndUser(0, fs.User.ID, common.Downloading, common.Paused, common.Ready)
limit := fs.User.Group.OptionsSerialized.Aria2BatchSize
if limit > 0 && len(downloads)+1 > limit {
return serializer.Err(serializer.CodeBatchAria2Size, "Exceed aria2 batch size", nil)
}
// 创建任务
task := &model.Download{
Status: common.Ready,
+2 -2
View File
@@ -173,9 +173,9 @@ func (service *OneDriveCallback) PreProcess(c *gin.Context) serializer.Response
actualPath := strings.TrimPrefix(uploadSession.SavePath, "/")
isSizeCheckFailed := uploadSession.Size != info.Size
// SharePoint 会对 Office 文档增加 meta data 导致文件大小不一致,这里增加 100 KB 宽容
// SharePoint 会对 Office 文档增加 meta data 导致文件大小不一致,这里增加 1 MB 宽容
// See: https://github.com/OneDrive/onedrive-api-docs/issues/935
if strings.Contains(fs.Policy.OptionsSerialized.OdDriver, "sharepoint.com") && isSizeCheckFailed && (info.Size > uploadSession.Size) && (info.Size-uploadSession.Size <= 102400) {
if strings.Contains(fs.Policy.OptionsSerialized.OdDriver, "sharepoint.com") && isSizeCheckFailed && (info.Size > uploadSession.Size) && (info.Size-uploadSession.Size <= 1048576) {
isSizeCheckFailed = false
}
+37
View File
@@ -428,3 +428,40 @@ func (service *FileIDService) PutContent(ctx context.Context, c *gin.Context) se
Code: 0,
}
}
// Sources 批量获取对象的外链
func (s *ItemIDService) Sources(ctx context.Context, c *gin.Context) serializer.Response {
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodePolicyNotAllowed, "无法初始化文件系统", err)
}
defer fs.Recycle()
if len(s.Raw().Items) > fs.User.Group.OptionsSerialized.SourceBatchSize {
return serializer.Err(serializer.CodeBatchSourceSize, "超出批量获取外链的最大数量限制", err)
}
res := make([]serializer.Sources, 0, len(s.Raw().Items))
for _, id := range s.Raw().Items {
fs.FileTarget = []model.File{}
sourceURL, err := fs.GetSource(ctx, id)
if len(fs.FileTarget) > 0 {
current := serializer.Sources{
URL: sourceURL,
Name: fs.FileTarget[0].Name,
Parent: fs.FileTarget[0].FolderID,
}
if err != nil {
current.Error = err.Error()
}
res = append(res, current)
}
}
return serializer.Response{
Code: 0,
Data: res,
}
}
+10
View File
@@ -15,6 +15,7 @@ import (
type ItemSearchService struct {
Type string `uri:"type" binding:"required"`
Keywords string `uri:"keywords" binding:"required"`
Path string `form:"path"`
}
// Search 执行搜索
@@ -26,6 +27,15 @@ func (service *ItemSearchService) Search(c *gin.Context) serializer.Response {
}
defer fs.Recycle()
if service.Path != "" {
ok, parent := fs.IsPathExist(service.Path)
if !ok {
return serializer.Err(serializer.CodeParentNotExist, "Cannot find parent folder", nil)
}
fs.Root = parent
}
switch service.Type {
case "keywords":
return service.SearchKeywords(c, fs, "%"+service.Keywords+"%")
+47
View File
@@ -366,3 +366,50 @@ func (service *ArchiveService) Archive(c *gin.Context) serializer.Response {
return subService.Archive(ctx, c)
}
// SearchService 对分享的目录进行搜索
type SearchService struct {
explorer.ItemSearchService
}
// Search 执行搜索
func (service *SearchService) Search(c *gin.Context) serializer.Response {
shareCtx, _ := c.Get("share")
share := shareCtx.(*model.Share)
if !share.IsDir {
return serializer.ParamErr("此分享无法列目录", nil)
}
if service.Path != "" && !path.IsAbs(service.Path) {
return serializer.ParamErr("路径无效", nil)
}
// 创建文件系统
fs, err := filesystem.NewFileSystem(share.Creator())
if err != nil {
return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err)
}
defer fs.Recycle()
// 上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 重设根目录
fs.Root = share.Source().(*model.Folder)
fs.Root.Name = "/"
if service.Path != "" {
ok, parent := fs.IsPathExist(service.Path)
if !ok {
return serializer.Err(serializer.CodeParentNotExist, "Cannot find parent folder", nil)
}
fs.Root = parent
}
// 分享Key上下文
ctx = context.WithValue(ctx, fsctx.ShareKeyCtx, hashid.HashID(share.ID, hashid.ShareID))
return service.SearchKeywords(c, fs, "%"+service.Keywords+"%")
}