github.com/cloudreve/Cloudreve/v3@v3.0.0-20240224133659-3edb00a6484c/service/explorer/objects.go (about)

     1  package explorer
     2  
     3  import (
     4  	"context"
     5  	"encoding/gob"
     6  	"fmt"
     7  	"math"
     8  	"path"
     9  	"strings"
    10  	"time"
    11  
    12  	model "github.com/cloudreve/Cloudreve/v3/models"
    13  	"github.com/cloudreve/Cloudreve/v3/pkg/auth"
    14  	"github.com/cloudreve/Cloudreve/v3/pkg/cache"
    15  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
    16  	"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
    17  	"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
    18  	"github.com/cloudreve/Cloudreve/v3/pkg/task"
    19  	"github.com/cloudreve/Cloudreve/v3/pkg/util"
    20  	"github.com/gin-gonic/gin"
    21  )
    22  
    23  // ItemMoveService 处理多文件/目录移动
    24  type ItemMoveService struct {
    25  	SrcDir string        `json:"src_dir" binding:"required,min=1,max=65535"`
    26  	Src    ItemIDService `json:"src"`
    27  	Dst    string        `json:"dst" binding:"required,min=1,max=65535"`
    28  }
    29  
    30  // ItemRenameService 处理多文件/目录重命名
    31  type ItemRenameService struct {
    32  	Src     ItemIDService `json:"src"`
    33  	NewName string        `json:"new_name" binding:"required,min=1,max=255"`
    34  }
    35  
    36  // ItemService 处理多文件/目录相关服务
    37  type ItemService struct {
    38  	Items []uint `json:"items"`
    39  	Dirs  []uint `json:"dirs"`
    40  }
    41  
    42  // ItemIDService 处理多文件/目录相关服务,字段值为HashID,可通过Raw()方法获取原始ID
    43  type ItemIDService struct {
    44  	Items      []string `json:"items"`
    45  	Dirs       []string `json:"dirs"`
    46  	Source     *ItemService
    47  	Force      bool `json:"force"`
    48  	UnlinkOnly bool `json:"unlink"`
    49  }
    50  
    51  // ItemCompressService 文件压缩任务服务
    52  type ItemCompressService struct {
    53  	Src  ItemIDService `json:"src"`
    54  	Dst  string        `json:"dst" binding:"required,min=1,max=65535"`
    55  	Name string        `json:"name" binding:"required,min=1,max=255"`
    56  }
    57  
    58  // ItemDecompressService 文件解压缩任务服务
    59  type ItemDecompressService struct {
    60  	Src      string `json:"src"`
    61  	Dst      string `json:"dst" binding:"required,min=1,max=65535"`
    62  	Encoding string `json:"encoding"`
    63  }
    64  
    65  // ItemPropertyService 获取对象属性服务
    66  type ItemPropertyService struct {
    67  	ID        string `binding:"required"`
    68  	TraceRoot bool   `form:"trace_root"`
    69  	IsFolder  bool   `form:"is_folder"`
    70  }
    71  
    72  func init() {
    73  	gob.Register(ItemIDService{})
    74  }
    75  
    76  // Raw 批量解码HashID,获取原始ID
    77  func (service *ItemIDService) Raw() *ItemService {
    78  	if service.Source != nil {
    79  		return service.Source
    80  	}
    81  
    82  	service.Source = &ItemService{
    83  		Dirs:  make([]uint, 0, len(service.Dirs)),
    84  		Items: make([]uint, 0, len(service.Items)),
    85  	}
    86  	for _, folder := range service.Dirs {
    87  		id, err := hashid.DecodeHashID(folder, hashid.FolderID)
    88  		if err == nil {
    89  			service.Source.Dirs = append(service.Source.Dirs, id)
    90  		}
    91  	}
    92  	for _, file := range service.Items {
    93  		id, err := hashid.DecodeHashID(file, hashid.FileID)
    94  		if err == nil {
    95  			service.Source.Items = append(service.Source.Items, id)
    96  		}
    97  	}
    98  
    99  	return service.Source
   100  }
   101  
   102  // CreateDecompressTask 创建文件解压缩任务
   103  func (service *ItemDecompressService) CreateDecompressTask(c *gin.Context) serializer.Response {
   104  	// 创建文件系统
   105  	fs, err := filesystem.NewFileSystemFromContext(c)
   106  	if err != nil {
   107  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   108  	}
   109  	defer fs.Recycle()
   110  
   111  	// 检查用户组权限
   112  	if !fs.User.Group.OptionsSerialized.ArchiveTask {
   113  		return serializer.Err(serializer.CodeGroupNotAllowed, "", nil)
   114  	}
   115  
   116  	// 存放目录是否存在
   117  	if exist, _ := fs.IsPathExist(service.Dst); !exist {
   118  		return serializer.Err(serializer.CodeParentNotExist, "", nil)
   119  	}
   120  
   121  	// 压缩包是否存在
   122  	exist, file := fs.IsFileExist(service.Src)
   123  	if !exist {
   124  		return serializer.Err(serializer.CodeFileNotFound, "", nil)
   125  	}
   126  
   127  	// 文件尺寸限制
   128  	if fs.User.Group.OptionsSerialized.DecompressSize != 0 && file.Size > fs.User.Group.
   129  		OptionsSerialized.DecompressSize {
   130  		return serializer.Err(serializer.CodeFileTooLarge, "", nil)
   131  	}
   132  
   133  	// 支持的压缩格式后缀
   134  	var (
   135  		suffixes = []string{".zip", ".gz", ".xz", ".tar", ".rar"}
   136  		matched  bool
   137  	)
   138  	for _, suffix := range suffixes {
   139  		if strings.HasSuffix(file.Name, suffix) {
   140  			matched = true
   141  			break
   142  		}
   143  	}
   144  	if !matched {
   145  		return serializer.Err(serializer.CodeUnsupportedArchiveType, "", nil)
   146  	}
   147  
   148  	// 创建任务
   149  	job, err := task.NewDecompressTask(fs.User, service.Src, service.Dst, service.Encoding)
   150  	if err != nil {
   151  		return serializer.Err(serializer.CodeCreateTaskError, "", err)
   152  	}
   153  	task.TaskPoll.Submit(job)
   154  
   155  	return serializer.Response{}
   156  
   157  }
   158  
   159  // CreateCompressTask 创建文件压缩任务
   160  func (service *ItemCompressService) CreateCompressTask(c *gin.Context) serializer.Response {
   161  	// 创建文件系统
   162  	fs, err := filesystem.NewFileSystemFromContext(c)
   163  	if err != nil {
   164  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   165  	}
   166  	defer fs.Recycle()
   167  
   168  	// 检查用户组权限
   169  	if !fs.User.Group.OptionsSerialized.ArchiveTask {
   170  		return serializer.Err(serializer.CodeGroupNotAllowed, "", nil)
   171  	}
   172  
   173  	// 补齐压缩文件扩展名(如果没有)
   174  	if !strings.HasSuffix(service.Name, ".zip") {
   175  		service.Name += ".zip"
   176  	}
   177  
   178  	// 存放目录是否存在,是否重名
   179  	if exist, _ := fs.IsPathExist(service.Dst); !exist {
   180  		return serializer.Err(serializer.CodeParentNotExist, "", nil)
   181  	}
   182  	if exist, _ := fs.IsFileExist(path.Join(service.Dst, service.Name)); exist {
   183  		return serializer.ParamErr("File "+service.Name+" already exist", nil)
   184  	}
   185  
   186  	// 检查文件名合法性
   187  	if !fs.ValidateLegalName(context.Background(), service.Name) {
   188  		return serializer.Err(serializer.CodeIllegalObjectName, "", nil)
   189  	}
   190  	if !fs.ValidateExtension(context.Background(), service.Name) {
   191  		return serializer.Err(serializer.CodeFileTypeNotAllowed, "", nil)
   192  	}
   193  
   194  	// 递归列出待压缩子目录
   195  	folders, err := model.GetRecursiveChildFolder(service.Src.Raw().Dirs, fs.User.ID, true)
   196  	if err != nil {
   197  		return serializer.DBErr("Failed to list folders", err)
   198  	}
   199  
   200  	// 列出所有待压缩文件
   201  	files, err := model.GetChildFilesOfFolders(&folders)
   202  	if err != nil {
   203  		return serializer.DBErr("Failed to list files", err)
   204  	}
   205  
   206  	// 计算待压缩文件大小
   207  	var totalSize uint64
   208  	for i := 0; i < len(files); i++ {
   209  		totalSize += files[i].Size
   210  	}
   211  
   212  	// 文件尺寸限制
   213  	if fs.User.Group.OptionsSerialized.CompressSize != 0 && totalSize > fs.User.Group.
   214  		OptionsSerialized.CompressSize {
   215  		return serializer.Err(serializer.CodeFileTooLarge, "", nil)
   216  	}
   217  
   218  	// 按照平均压缩率计算用户空间是否足够
   219  	compressRatio := 0.4
   220  	spaceNeeded := uint64(math.Round(float64(totalSize) * compressRatio))
   221  	if fs.User.GetRemainingCapacity() < spaceNeeded {
   222  		return serializer.Err(serializer.CodeInsufficientCapacity, "", err)
   223  	}
   224  
   225  	// 创建任务
   226  	job, err := task.NewCompressTask(fs.User, path.Join(service.Dst, service.Name), service.Src.Raw().Dirs,
   227  		service.Src.Raw().Items)
   228  	if err != nil {
   229  		return serializer.Err(serializer.CodeCreateTaskError, "", err)
   230  	}
   231  	task.TaskPoll.Submit(job)
   232  
   233  	return serializer.Response{}
   234  
   235  }
   236  
   237  // Archive 创建归档
   238  func (service *ItemIDService) Archive(ctx context.Context, c *gin.Context) serializer.Response {
   239  	// 创建文件系统
   240  	fs, err := filesystem.NewFileSystemFromContext(c)
   241  	if err != nil {
   242  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   243  	}
   244  	defer fs.Recycle()
   245  
   246  	// 检查用户组权限
   247  	if !fs.User.Group.OptionsSerialized.ArchiveDownload {
   248  		return serializer.Err(serializer.CodeGroupNotAllowed, "", nil)
   249  	}
   250  
   251  	// 创建打包下载会话
   252  	ttl := model.GetIntSetting("archive_timeout", 30)
   253  	downloadSessionID := util.RandStringRunes(16)
   254  	cache.Set("archive_"+downloadSessionID, *service, ttl)
   255  	cache.Set("archive_user_"+downloadSessionID, *fs.User, ttl)
   256  	signURL, err := auth.SignURI(
   257  		auth.General,
   258  		fmt.Sprintf("/api/v3/file/archive/%s/archive.zip", downloadSessionID),
   259  		int64(ttl),
   260  	)
   261  
   262  	return serializer.Response{
   263  		Code: 0,
   264  		Data: signURL.String(),
   265  	}
   266  }
   267  
   268  // Delete 删除对象
   269  func (service *ItemIDService) Delete(ctx context.Context, c *gin.Context) serializer.Response {
   270  	// 创建文件系统
   271  	fs, err := filesystem.NewFileSystemFromContext(c)
   272  	if err != nil {
   273  		return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err)
   274  	}
   275  	defer fs.Recycle()
   276  
   277  	force, unlink := false, false
   278  	if fs.User.Group.OptionsSerialized.AdvanceDelete {
   279  		force = service.Force
   280  		unlink = service.UnlinkOnly
   281  	}
   282  
   283  	// 删除对象
   284  	items := service.Raw()
   285  	err = fs.Delete(ctx, items.Dirs, items.Items, force, unlink)
   286  	if err != nil {
   287  		return serializer.Err(serializer.CodeNotSet, err.Error(), err)
   288  	}
   289  
   290  	return serializer.Response{
   291  		Code: 0,
   292  	}
   293  
   294  }
   295  
   296  // Move 移动对象
   297  func (service *ItemMoveService) Move(ctx context.Context, c *gin.Context) serializer.Response {
   298  	// 创建文件系统
   299  	fs, err := filesystem.NewFileSystemFromContext(c)
   300  	if err != nil {
   301  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   302  	}
   303  	defer fs.Recycle()
   304  
   305  	// 移动对象
   306  	items := service.Src.Raw()
   307  	err = fs.Move(ctx, items.Dirs, items.Items, service.SrcDir, service.Dst)
   308  	if err != nil {
   309  		return serializer.Err(serializer.CodeNotSet, err.Error(), err)
   310  	}
   311  
   312  	return serializer.Response{
   313  		Code: 0,
   314  	}
   315  
   316  }
   317  
   318  // Copy 复制对象
   319  func (service *ItemMoveService) Copy(ctx context.Context, c *gin.Context) serializer.Response {
   320  	// 复制操作只能对一个目录或文件对象进行操作
   321  	if len(service.Src.Items)+len(service.Src.Dirs) > 1 {
   322  		return filesystem.ErrOneObjectOnly
   323  	}
   324  
   325  	// 创建文件系统
   326  	fs, err := filesystem.NewFileSystemFromContext(c)
   327  	if err != nil {
   328  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   329  	}
   330  	defer fs.Recycle()
   331  
   332  	// 复制对象
   333  	err = fs.Copy(ctx, service.Src.Raw().Dirs, service.Src.Raw().Items, service.SrcDir, service.Dst)
   334  	if err != nil {
   335  		return serializer.Err(serializer.CodeNotSet, err.Error(), err)
   336  	}
   337  
   338  	return serializer.Response{
   339  		Code: 0,
   340  	}
   341  
   342  }
   343  
   344  // Rename 重命名对象
   345  func (service *ItemRenameService) Rename(ctx context.Context, c *gin.Context) serializer.Response {
   346  	// 重命名作只能对一个目录或文件对象进行操作
   347  	if len(service.Src.Items)+len(service.Src.Dirs) > 1 {
   348  		return filesystem.ErrOneObjectOnly
   349  	}
   350  
   351  	// 创建文件系统
   352  	fs, err := filesystem.NewFileSystemFromContext(c)
   353  	if err != nil {
   354  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   355  	}
   356  	defer fs.Recycle()
   357  
   358  	// 重命名对象
   359  	err = fs.Rename(ctx, service.Src.Raw().Dirs, service.Src.Raw().Items, service.NewName)
   360  	if err != nil {
   361  		return serializer.Err(serializer.CodeNotSet, err.Error(), err)
   362  	}
   363  
   364  	return serializer.Response{
   365  		Code: 0,
   366  	}
   367  }
   368  
   369  // GetProperty 获取对象的属性
   370  func (service *ItemPropertyService) GetProperty(ctx context.Context, c *gin.Context) serializer.Response {
   371  	userCtx, _ := c.Get("user")
   372  	user := userCtx.(*model.User)
   373  
   374  	var props serializer.ObjectProps
   375  	props.QueryDate = time.Now()
   376  
   377  	// 如果是文件对象
   378  	if !service.IsFolder {
   379  		res, err := hashid.DecodeHashID(service.ID, hashid.FileID)
   380  		if err != nil {
   381  			return serializer.Err(serializer.CodeNotFound, "", err)
   382  		}
   383  
   384  		file, err := model.GetFilesByIDs([]uint{res}, user.ID)
   385  		if err != nil {
   386  			return serializer.DBErr("Failed to query file records", err)
   387  		}
   388  
   389  		props.CreatedAt = file[0].CreatedAt
   390  		props.UpdatedAt = file[0].UpdatedAt
   391  		props.Policy = file[0].GetPolicy().Name
   392  		props.Size = file[0].Size
   393  
   394  		// 查找父目录
   395  		if service.TraceRoot {
   396  			parent, err := model.GetFoldersByIDs([]uint{file[0].FolderID}, user.ID)
   397  			if err != nil {
   398  				return serializer.DBErr("Parent folder record not exist", err)
   399  			}
   400  
   401  			if err := parent[0].TraceRoot(); err != nil {
   402  				return serializer.DBErr("Failed to trace root folder", err)
   403  			}
   404  
   405  			props.Path = path.Join(parent[0].Position, parent[0].Name)
   406  		}
   407  	} else {
   408  		res, err := hashid.DecodeHashID(service.ID, hashid.FolderID)
   409  		if err != nil {
   410  			return serializer.Err(serializer.CodeNotFound, "", err)
   411  		}
   412  
   413  		folder, err := model.GetFoldersByIDs([]uint{res}, user.ID)
   414  		if err != nil {
   415  			return serializer.DBErr("Failed to query folder records", err)
   416  		}
   417  
   418  		props.CreatedAt = folder[0].CreatedAt
   419  		props.UpdatedAt = folder[0].UpdatedAt
   420  
   421  		// 如果对象是目录, 先尝试返回缓存结果
   422  		if cacheRes, ok := cache.Get(fmt.Sprintf("folder_props_%d", res)); ok {
   423  			res := cacheRes.(serializer.ObjectProps)
   424  			res.CreatedAt = props.CreatedAt
   425  			res.UpdatedAt = props.UpdatedAt
   426  			return serializer.Response{Data: res}
   427  		}
   428  
   429  		// 统计子目录
   430  		childFolders, err := model.GetRecursiveChildFolder([]uint{folder[0].ID},
   431  			user.ID, true)
   432  		if err != nil {
   433  			return serializer.DBErr("Failed to list child folders", err)
   434  		}
   435  		props.ChildFolderNum = len(childFolders) - 1
   436  
   437  		// 统计子文件
   438  		files, err := model.GetChildFilesOfFolders(&childFolders)
   439  		if err != nil {
   440  			return serializer.DBErr("Failed to list child files", err)
   441  		}
   442  
   443  		// 统计子文件个数和大小
   444  		props.ChildFileNum = len(files)
   445  		for i := 0; i < len(files); i++ {
   446  			props.Size += files[i].Size
   447  		}
   448  
   449  		// 查找父目录
   450  		if service.TraceRoot {
   451  			if err := folder[0].TraceRoot(); err != nil {
   452  				return serializer.DBErr("Failed to list child folders", err)
   453  			}
   454  
   455  			props.Path = folder[0].Position
   456  		}
   457  
   458  		// 如果列取对象是目录,则缓存结果
   459  		cache.Set(fmt.Sprintf("folder_props_%d", res), props,
   460  			model.GetIntSetting("folder_props_timeout", 300))
   461  	}
   462  
   463  	return serializer.Response{
   464  		Code: 0,
   465  		Data: props,
   466  	}
   467  }