This commit is contained in:
2024-02-25 08:30:34 +08:00
commit 4947f39e74
273 changed files with 45396 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
package explorer
import (
"context"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/gin-gonic/gin"
)
// DirectoryService 创建新目录服务
type DirectoryService struct {
Path string `uri:"path" json:"path" binding:"required,min=1,max=65535"`
}
// ListDirectory 列出目录内容
func (service *DirectoryService) ListDirectory(c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 获取子项目
objects, err := fs.List(ctx, service.Path, nil)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
var parentID uint
if len(fs.DirTarget) > 0 {
parentID = fs.DirTarget[0].ID
}
// 获取目录的存储策略
if err := fs.SetPolicyFromPath(service.Path); err != nil {
return serializer.Err(serializer.CodePolicyNotExist, "", err)
}
return serializer.Response{
Code: 0,
Data: serializer.BuildObjectList(parentID, objects, fs.Policy),
}
}
// CreateDirectory 创建目录
func (service *DirectoryService) CreateDirectory(c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 创建目录
_, err = fs.CreateDirectory(ctx, service.Path)
if err != nil {
return serializer.Err(serializer.CodeCreateFolderFailed, err.Error(), err)
}
return serializer.Response{
Code: 0,
}
}

537
service/explorer/file.go Normal file
View File

@@ -0,0 +1,537 @@
package explorer
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path"
"strconv"
"strings"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
"github.com/gin-gonic/gin"
)
// SingleFileService 对单文件进行操作的五福path为文件完整路径
type SingleFileService struct {
Path string `uri:"path" json:"path" binding:"required,min=1,max=65535"`
}
// FileIDService 通过文件ID对文件进行操作的服务
type FileIDService struct {
}
// FileAnonymousGetService 匿名(外链)获取文件服务
type FileAnonymousGetService struct {
ID uint `uri:"id" binding:"required,min=1"`
Name string `uri:"name" binding:"required"`
}
// DownloadService 文件下載服务
type DownloadService struct {
ID string `uri:"id" binding:"required"`
}
// ArchiveService 文件流式打包下載服务
type ArchiveService struct {
ID string `uri:"sessionID" binding:"required"`
}
// New 创建新文件
func (service *SingleFileService) Create(c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
baseDir := path.Dir(service.Path)
fs.SetPolicyFromPath(baseDir)
// 上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 给文件系统分配钩子
fs.Use("BeforeUpload", filesystem.HookValidateFile)
fs.Use("AfterUpload", filesystem.GenericAfterUpload)
// 上传空文件
err = fs.Upload(ctx, &fsctx.FileStream{
File: ioutil.NopCloser(strings.NewReader("")),
Size: 0,
VirtualPath: baseDir,
Name: path.Base(service.Path),
})
if err != nil {
return serializer.Err(serializer.CodeUploadFailed, err.Error(), err)
}
return serializer.Response{
Code: 0,
}
}
// List 列出从机上的文件
func (service *SlaveListService) List(c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewAnonymousFileSystem()
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
objects, err := fs.Handler.List(context.Background(), service.Path, service.Recursive)
if err != nil {
return serializer.Err(serializer.CodeIOFailed, "Cannot list files", err)
}
res, _ := json.Marshal(objects)
return serializer.Response{Data: string(res)}
}
// DownloadArchived 通过预签名 URL 打包下载
func (service *ArchiveService) DownloadArchived(ctx context.Context, c *gin.Context) serializer.Response {
userRaw, exist := cache.Get("archive_user_" + service.ID)
if !exist {
return serializer.Err(serializer.CodeNotFound, "Archive session not exist", nil)
}
user := userRaw.(model.User)
// 创建文件系统
fs, err := filesystem.NewFileSystem(&user)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 查找打包的临时文件
archiveSession, exist := cache.Get("archive_" + service.ID)
if !exist {
return serializer.Err(serializer.CodeNotFound, "Archive session not exist", nil)
}
// 开始打包
c.Header("Content-Disposition", "attachment;")
c.Header("Content-Type", "application/zip")
itemService := archiveSession.(ItemIDService)
items := itemService.Raw()
ctx = context.WithValue(ctx, fsctx.GinCtx, c)
err = fs.Compress(ctx, c.Writer, items.Dirs, items.Items, true)
if err != nil {
return serializer.Err(serializer.CodeNotSet, "Failed to compress file", err)
}
return serializer.Response{
Code: 0,
}
}
// Download 签名的匿名文件下载
func (service *FileAnonymousGetService) Download(ctx context.Context, c *gin.Context) serializer.Response {
fs, err := filesystem.NewAnonymousFileSystem()
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 查找文件
err = fs.SetTargetFileByIDs([]uint{service.ID})
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
// 获取文件流
rs, err := fs.GetDownloadContent(ctx, 0)
defer rs.Close()
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
// 发送文件
http.ServeContent(c.Writer, c.Request, service.Name, fs.FileTarget[0].UpdatedAt, rs)
return serializer.Response{
Code: 0,
}
}
// Source 重定向到文件的有效原始链接
func (service *FileAnonymousGetService) Source(ctx context.Context, c *gin.Context) serializer.Response {
fs, err := filesystem.NewAnonymousFileSystem()
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 查找文件
err = fs.SetTargetFileByIDs([]uint{service.ID})
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
// 获取文件流
ttl := int64(model.GetIntSetting("preview_timeout", 60))
res, err := fs.SignURL(ctx, &fs.FileTarget[0], ttl, false)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
c.Header("Cache-Control", fmt.Sprintf("max-age=%d", ttl))
return serializer.Response{
Code: -302,
Data: res,
}
}
// CreateDocPreviewSession 创建DOC文件预览会话返回预览地址
func (service *FileIDService) CreateDocPreviewSession(ctx context.Context, c *gin.Context, editable bool) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 获取对象id
objectID, _ := c.Get("object_id")
// 如果上下文中已有File对象则重设目标
if file, ok := ctx.Value(fsctx.FileModelCtx).(*model.File); ok {
fs.SetTargetFile(&[]model.File{*file})
objectID = uint(0)
}
// 如果上下文中已有Folder对象则重设根目录
if folder, ok := ctx.Value(fsctx.FolderModelCtx).(*model.Folder); ok {
fs.Root = folder
path := ctx.Value(fsctx.PathCtx).(string)
err := fs.ResetFileIfNotExist(ctx, path)
if err != nil {
return serializer.Err(serializer.CodeNotFound, err.Error(), err)
}
objectID = uint(0)
}
// 获取文件临时下载地址
downloadURL, err := fs.GetDownloadURL(ctx, objectID.(uint), "doc_preview_timeout")
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
// For newer version of Cloudreve - Local Policy
// When do not use a cdn, the downloadURL withouts hosts, like "/api/v3/file/download/xxx"
if strings.HasPrefix(downloadURL, "/") {
downloadURI, err := url.Parse(downloadURL)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
downloadURL = model.GetSiteURL().ResolveReference(downloadURI).String()
}
var resp serializer.DocPreviewSession
// Use WOPI preview if available
if model.IsTrueVal(model.GetSettingByName("wopi_enabled")) && wopi.Default != nil {
maxSize := model.GetIntSetting("maxEditSize", 0)
if maxSize > 0 && fs.FileTarget[0].Size > uint64(maxSize) {
return serializer.Err(serializer.CodeFileTooLarge, "", nil)
}
action := wopi.ActionPreview
if editable {
action = wopi.ActionEdit
}
session, err := wopi.Default.NewSession(fs.FileTarget[0].UserID, &fs.FileTarget[0], action)
if err != nil {
return serializer.Err(serializer.CodeInternalSetting, "Failed to create WOPI session", err)
}
resp.URL = session.ActionURL.String()
resp.AccessTokenTTL = session.AccessTokenTTL
resp.AccessToken = session.AccessToken
return serializer.Response{
Code: 0,
Data: resp,
}
}
// 生成最终的预览器地址
srcB64 := base64.StdEncoding.EncodeToString([]byte(downloadURL))
srcEncoded := url.QueryEscape(downloadURL)
srcB64Encoded := url.QueryEscape(srcB64)
resp.URL = util.Replace(map[string]string{
"{$src}": srcEncoded,
"{$srcB64}": srcB64Encoded,
"{$name}": url.QueryEscape(fs.FileTarget[0].Name),
}, model.GetSettingByName("office_preview_service"))
return serializer.Response{
Code: 0,
Data: resp,
}
}
// CreateDownloadSession 创建下载会话获取下载URL
func (service *FileIDService) CreateDownloadSession(ctx context.Context, c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 获取对象id
objectID, _ := c.Get("object_id")
// 获取下载地址
downloadURL, err := fs.GetDownloadURL(ctx, objectID.(uint), "download_timeout")
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
return serializer.Response{
Code: 0,
Data: downloadURL,
}
}
// Download 通过签名URL的文件下载无需登录
func (service *DownloadService) Download(ctx context.Context, c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 查找打包的临时文件
file, exist := cache.Get("download_" + service.ID)
if !exist {
return serializer.Err(serializer.CodeNotFound, "Download session not exist", nil)
}
fs.FileTarget = []model.File{file.(model.File)}
// 开始处理下载
ctx = context.WithValue(ctx, fsctx.GinCtx, c)
rs, err := fs.GetDownloadContent(ctx, 0)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
defer rs.Close()
// 设置文件名
c.Header("Content-Disposition", "attachment; filename=\""+url.PathEscape(fs.FileTarget[0].Name)+"\"")
if fs.User.Group.OptionsSerialized.OneTimeDownload {
// 清理资源,删除临时文件
_ = cache.Deletes([]string{service.ID}, "download_")
}
// 发送文件
http.ServeContent(c.Writer, c.Request, fs.FileTarget[0].Name, fs.FileTarget[0].UpdatedAt, rs)
return serializer.Response{
Code: 0,
}
}
// PreviewContent 预览文件,需要登录会话, isText - 是否为文本文件,文本文件会
// 强制经由服务端中转
func (service *FileIDService) PreviewContent(ctx context.Context, c *gin.Context, isText bool) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 获取对象id
objectID, _ := c.Get("object_id")
// 如果上下文中已有File对象则重设目标
if file, ok := ctx.Value(fsctx.FileModelCtx).(*model.File); ok {
fs.SetTargetFile(&[]model.File{*file})
objectID = uint(0)
}
// 如果上下文中已有Folder对象则重设根目录
if folder, ok := ctx.Value(fsctx.FolderModelCtx).(*model.Folder); ok {
fs.Root = folder
path := ctx.Value(fsctx.PathCtx).(string)
err := fs.ResetFileIfNotExist(ctx, path)
if err != nil {
return serializer.Err(serializer.CodeFileNotFound, err.Error(), err)
}
objectID = uint(0)
}
// 获取文件预览响应
resp, err := fs.Preview(ctx, objectID.(uint), isText)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
// 重定向到文件源
if resp.Redirect {
c.Header("Cache-Control", fmt.Sprintf("max-age=%d", resp.MaxAge))
return serializer.Response{
Code: -301,
Data: resp.URL,
}
}
// 直接返回文件内容
defer resp.Content.Close()
if isText {
c.Header("Cache-Control", "no-cache")
}
http.ServeContent(c.Writer, c.Request, fs.FileTarget[0].Name, fs.FileTarget[0].UpdatedAt, resp.Content)
return serializer.Response{
Code: 0,
}
}
// PutContent 更新文件内容
func (service *FileIDService) PutContent(ctx context.Context, c *gin.Context) serializer.Response {
// 创建上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 取得文件大小
fileSize, err := strconv.ParseUint(c.Request.Header.Get("Content-Length"), 10, 64)
if err != nil {
return serializer.ParamErr("Invalid content-length value", err)
}
fileData := fsctx.FileStream{
MimeType: c.Request.Header.Get("Content-Type"),
File: c.Request.Body,
Size: fileSize,
Mode: fsctx.Overwrite,
}
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
uploadCtx := context.WithValue(ctx, fsctx.GinCtx, c)
// 取得现有文件
fileID, _ := c.Get("object_id")
originFile, _ := model.GetFilesByIDs([]uint{fileID.(uint)}, fs.User.ID)
if len(originFile) == 0 {
return serializer.Err(serializer.CodeFileNotFound, "", nil)
}
fileData.Name = originFile[0].Name
// 检查此文件是否有软链接
fileList, err := model.RemoveFilesWithSoftLinks([]model.File{originFile[0]})
if err == nil && len(fileList) == 0 {
// 如果包含软连接应重新生成新文件副本并更新source_name
originFile[0].SourceName = fs.GenerateSavePath(uploadCtx, &fileData)
fileData.Mode &= ^fsctx.Overwrite
fs.Use("AfterUpload", filesystem.HookUpdateSourceName)
fs.Use("AfterUploadCanceled", filesystem.HookUpdateSourceName)
fs.Use("AfterUploadCanceled", filesystem.HookCleanFileContent)
fs.Use("AfterUploadCanceled", filesystem.HookClearFileSize)
fs.Use("AfterValidateFailed", filesystem.HookUpdateSourceName)
fs.Use("AfterValidateFailed", filesystem.HookCleanFileContent)
fs.Use("AfterValidateFailed", filesystem.HookClearFileSize)
}
// 给文件系统分配钩子
fs.Use("BeforeUpload", filesystem.HookResetPolicy)
fs.Use("BeforeUpload", filesystem.HookValidateFile)
fs.Use("BeforeUpload", filesystem.HookValidateCapacityDiff)
fs.Use("AfterUpload", filesystem.GenericAfterUpdate)
// 执行上传
uploadCtx = context.WithValue(uploadCtx, fsctx.FileModelCtx, originFile[0])
err = fs.Upload(uploadCtx, &fileData)
if err != nil {
return serializer.Err(serializer.CodeUploadFailed, err.Error(), err)
}
return serializer.Response{
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.CodeCreateFSError, "", 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))
files, err := model.GetFilesByIDs(s.Raw().Items, fs.User.ID)
if err != nil || len(files) == 0 {
return serializer.Err(serializer.CodeFileNotFound, "", err)
}
getSourceFunc := func(file model.File) (string, error) {
fs.FileTarget = []model.File{file}
return fs.GetSource(ctx, file.ID)
}
// Create redirected source link if needed
if fs.User.Group.OptionsSerialized.RedirectedSource {
getSourceFunc = func(file model.File) (string, error) {
source, err := file.CreateOrGetSourceLink()
if err != nil {
return "", err
}
sourceLinkURL, err := source.Link()
if err != nil {
return "", err
}
return sourceLinkURL, nil
}
}
for _, file := range files {
sourceURL, err := getSourceFunc(file)
current := serializer.Sources{
URL: sourceURL,
Name: file.Name,
Parent: file.FolderID,
}
if err != nil {
current.Error = err.Error()
}
res = append(res, current)
}
return serializer.Response{
Code: 0,
Data: res,
}
}

529
service/explorer/objects.go Normal file
View File

@@ -0,0 +1,529 @@
package explorer
import (
"context"
"encoding/gob"
"fmt"
"math"
"path"
"strings"
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/task"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-gonic/gin"
)
// ItemMoveService 处理多文件/目录移动
type ItemMoveService struct {
SrcDir string `json:"src_dir" binding:"required,min=1,max=65535"`
Src ItemIDService `json:"src"`
Dst string `json:"dst" binding:"required,min=1,max=65535"`
}
// ItemRenameService 处理多文件/目录重命名
type ItemRenameService struct {
Src ItemIDService `json:"src"`
NewName string `json:"new_name" binding:"required,min=1,max=255"`
}
// ItemService 处理多文件/目录相关服务
type ItemService struct {
Items []uint `json:"items"`
Dirs []uint `json:"dirs"`
}
// ItemIDService 处理多文件/目录相关服务字段值为HashID可通过Raw()方法获取原始ID
type ItemIDService struct {
Items []string `json:"items"`
Dirs []string `json:"dirs"`
Source *ItemService
Force bool `json:"force"`
UnlinkOnly bool `json:"unlink"`
}
// ItemCompressService 文件压缩任务服务
type ItemCompressService struct {
Src ItemIDService `json:"src"`
Dst string `json:"dst" binding:"required,min=1,max=65535"`
Name string `json:"name" binding:"required,min=1,max=255"`
}
// ItemRelocateService 文件转移任务服务
type ItemRelocateService struct {
Src ItemIDService `json:"src"`
DstPolicyID string `json:"dst_policy_id" binding:"required"`
}
// ItemDecompressService 文件解压缩任务服务
type ItemDecompressService struct {
Src string `json:"src"`
Dst string `json:"dst" binding:"required,min=1,max=65535"`
Encoding string `json:"encoding"`
}
// ItemPropertyService 获取对象属性服务
type ItemPropertyService struct {
ID string `binding:"required"`
TraceRoot bool `form:"trace_root"`
IsFolder bool `form:"is_folder"`
}
func init() {
gob.Register(ItemIDService{})
}
// Raw 批量解码HashID获取原始ID
func (service *ItemIDService) Raw() *ItemService {
if service.Source != nil {
return service.Source
}
service.Source = &ItemService{
Dirs: make([]uint, 0, len(service.Dirs)),
Items: make([]uint, 0, len(service.Items)),
}
for _, folder := range service.Dirs {
id, err := hashid.DecodeHashID(folder, hashid.FolderID)
if err == nil {
service.Source.Dirs = append(service.Source.Dirs, id)
}
}
for _, file := range service.Items {
id, err := hashid.DecodeHashID(file, hashid.FileID)
if err == nil {
service.Source.Items = append(service.Source.Items, id)
}
}
return service.Source
}
// CreateDecompressTask 创建文件解压缩任务
func (service *ItemDecompressService) CreateDecompressTask(c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 检查用户组权限
if !fs.User.Group.OptionsSerialized.ArchiveTask {
return serializer.Err(serializer.CodeGroupNotAllowed, "", nil)
}
// 存放目录是否存在
if exist, _ := fs.IsPathExist(service.Dst); !exist {
return serializer.Err(serializer.CodeParentNotExist, "", nil)
}
// 压缩包是否存在
exist, file := fs.IsFileExist(service.Src)
if !exist {
return serializer.Err(serializer.CodeFileNotFound, "", nil)
}
// 文件尺寸限制
if fs.User.Group.OptionsSerialized.DecompressSize != 0 && file.Size > fs.User.Group.
OptionsSerialized.DecompressSize {
return serializer.Err(serializer.CodeFileTooLarge, "", nil)
}
// 支持的压缩格式后缀
var (
suffixes = []string{".zip", ".gz", ".xz", ".tar", ".rar"}
matched bool
)
for _, suffix := range suffixes {
if strings.HasSuffix(file.Name, suffix) {
matched = true
break
}
}
if !matched {
return serializer.Err(serializer.CodeUnsupportedArchiveType, "", nil)
}
// 创建任务
job, err := task.NewDecompressTask(fs.User, service.Src, service.Dst, service.Encoding)
if err != nil {
return serializer.Err(serializer.CodeCreateTaskError, "", err)
}
task.TaskPoll.Submit(job)
return serializer.Response{}
}
// CreateCompressTask 创建文件压缩任务
func (service *ItemCompressService) CreateCompressTask(c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 检查用户组权限
if !fs.User.Group.OptionsSerialized.ArchiveTask {
return serializer.Err(serializer.CodeGroupNotAllowed, "", nil)
}
// 补齐压缩文件扩展名(如果没有)
if !strings.HasSuffix(service.Name, ".zip") {
service.Name += ".zip"
}
// 存放目录是否存在,是否重名
if exist, _ := fs.IsPathExist(service.Dst); !exist {
return serializer.Err(serializer.CodeParentNotExist, "", nil)
}
if exist, _ := fs.IsFileExist(path.Join(service.Dst, service.Name)); exist {
return serializer.ParamErr("File "+service.Name+" already exist", nil)
}
// 检查文件名合法性
if !fs.ValidateLegalName(context.Background(), service.Name) {
return serializer.Err(serializer.CodeIllegalObjectName, "", nil)
}
if !fs.ValidateExtension(context.Background(), service.Name) {
return serializer.Err(serializer.CodeFileTypeNotAllowed, "", nil)
}
// 递归列出待压缩子目录
folders, err := model.GetRecursiveChildFolder(service.Src.Raw().Dirs, fs.User.ID, true)
if err != nil {
return serializer.DBErr("Failed to list folders", err)
}
// 列出所有待压缩文件
files, err := model.GetChildFilesOfFolders(&folders)
if err != nil {
return serializer.DBErr("Failed to list files", err)
}
// 计算待压缩文件大小
var totalSize uint64
for i := 0; i < len(files); i++ {
totalSize += files[i].Size
}
// 文件尺寸限制
if fs.User.Group.OptionsSerialized.CompressSize != 0 && totalSize > fs.User.Group.
OptionsSerialized.CompressSize {
return serializer.Err(serializer.CodeFileTooLarge, "", nil)
}
// 按照平均压缩率计算用户空间是否足够
compressRatio := 0.4
spaceNeeded := uint64(math.Round(float64(totalSize) * compressRatio))
if fs.User.GetRemainingCapacity() < spaceNeeded {
return serializer.Err(serializer.CodeInsufficientCapacity, "", err)
}
// 创建任务
job, err := task.NewCompressTask(fs.User, path.Join(service.Dst, service.Name), service.Src.Raw().Dirs,
service.Src.Raw().Items)
if err != nil {
return serializer.Err(serializer.CodeCreateTaskError, "", err)
}
task.TaskPoll.Submit(job)
return serializer.Response{}
}
// CreateRelocateTask 创建文件转移任务
func (service *ItemRelocateService) CreateRelocateTask(c *gin.Context) serializer.Response {
userCtx, _ := c.Get("user")
user := userCtx.(*model.User)
// 取得存储策略的ID
rawID, err := hashid.DecodeHashID(service.DstPolicyID, hashid.PolicyID)
if err != nil {
return serializer.Err(serializer.CodePolicyNotExist, "", err)
}
// 检查用户组权限
if !user.Group.OptionsSerialized.Relocate {
return serializer.Err(serializer.CodeGroupNotAllowed, "", nil)
}
// 用户是否可以使用目的存储策略
if !util.ContainsUint(user.Group.PolicyList, rawID) {
return serializer.ParamErr("Storage policy is not available", nil)
}
// 查找存储策略
if _, err := model.GetPolicyByID(rawID); err != nil {
return serializer.ParamErr("Storage policy is not available", nil)
}
// 查找是否有正在进行中的转存任务
var tasks []model.Task
model.DB.Where("status in (?) and user_id = ? and type = ?",
[]int{task.Queued, task.Processing}, user.ID,
task.RelocateTaskType).Find(&tasks)
if len(tasks) > 0 {
return serializer.Response{
Code: serializer.CodeConflict,
Msg: "There's ongoing relocate task, please wait for the previous task to finish",
}
}
IDRaw := service.Src.Raw()
// 创建任务
job, err := task.NewRelocateTask(user, rawID, IDRaw.Dirs,
IDRaw.Items)
if err != nil {
return serializer.Err(serializer.CodeCreateTaskError, "", err)
}
task.TaskPoll.Submit(job)
return serializer.Response{}
}
// Archive 创建归档
func (service *ItemIDService) Archive(ctx context.Context, c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 检查用户组权限
if !fs.User.Group.OptionsSerialized.ArchiveDownload {
return serializer.Err(serializer.CodeGroupNotAllowed, "", nil)
}
// 创建打包下载会话
ttl := model.GetIntSetting("archive_timeout", 30)
downloadSessionID := util.RandStringRunes(16)
cache.Set("archive_"+downloadSessionID, *service, ttl)
cache.Set("archive_user_"+downloadSessionID, *fs.User, ttl)
signURL, err := auth.SignURI(
auth.General,
fmt.Sprintf("/api/v3/file/archive/%s/archive.zip", downloadSessionID),
int64(ttl),
)
return serializer.Response{
Code: 0,
Data: signURL.String(),
}
}
// Delete 删除对象
func (service *ItemIDService) Delete(ctx context.Context, c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
force, unlink := false, false
if fs.User.Group.OptionsSerialized.AdvanceDelete {
force = service.Force
unlink = service.UnlinkOnly
}
// 删除对象
items := service.Raw()
err = fs.Delete(ctx, items.Dirs, items.Items, force, unlink)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
return serializer.Response{
Code: 0,
}
}
// Move 移动对象
func (service *ItemMoveService) Move(ctx context.Context, c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 移动对象
items := service.Src.Raw()
err = fs.Move(ctx, items.Dirs, items.Items, service.SrcDir, service.Dst)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
return serializer.Response{
Code: 0,
}
}
// Copy 复制对象
func (service *ItemMoveService) Copy(ctx context.Context, c *gin.Context) serializer.Response {
// 复制操作只能对一个目录或文件对象进行操作
if len(service.Src.Items)+len(service.Src.Dirs) > 1 {
return filesystem.ErrOneObjectOnly
}
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 复制对象
err = fs.Copy(ctx, service.Src.Raw().Dirs, service.Src.Raw().Items, service.SrcDir, service.Dst)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
return serializer.Response{
Code: 0,
}
}
// Rename 重命名对象
func (service *ItemRenameService) Rename(ctx context.Context, c *gin.Context) serializer.Response {
// 重命名作只能对一个目录或文件对象进行操作
if len(service.Src.Items)+len(service.Src.Dirs) > 1 {
return filesystem.ErrOneObjectOnly
}
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err)
}
defer fs.Recycle()
// 重命名对象
err = fs.Rename(ctx, service.Src.Raw().Dirs, service.Src.Raw().Items, service.NewName)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
return serializer.Response{
Code: 0,
}
}
// GetProperty 获取对象的属性
func (service *ItemPropertyService) GetProperty(ctx context.Context, c *gin.Context) serializer.Response {
userCtx, _ := c.Get("user")
user := userCtx.(*model.User)
var props serializer.ObjectProps
props.QueryDate = time.Now()
// 如果是文件对象
if !service.IsFolder {
res, err := hashid.DecodeHashID(service.ID, hashid.FileID)
if err != nil {
return serializer.Err(serializer.CodeNotFound, "", err)
}
file, err := model.GetFilesByIDs([]uint{res}, user.ID)
if err != nil {
return serializer.DBErr("Failed to query file records", err)
}
props.CreatedAt = file[0].CreatedAt
props.UpdatedAt = file[0].UpdatedAt
props.Policy = file[0].GetPolicy().Name
props.Size = file[0].Size
// 查找父目录
if service.TraceRoot {
parent, err := model.GetFoldersByIDs([]uint{file[0].FolderID}, user.ID)
if err != nil {
return serializer.DBErr("Parent folder record not exist", err)
}
if err := parent[0].TraceRoot(); err != nil {
return serializer.DBErr("Failed to trace root folder", err)
}
props.Path = path.Join(parent[0].Position, parent[0].Name)
}
} else {
res, err := hashid.DecodeHashID(service.ID, hashid.FolderID)
if err != nil {
return serializer.Err(serializer.CodeNotFound, "", err)
}
folder, err := model.GetFoldersByIDs([]uint{res}, user.ID)
if err != nil {
return serializer.DBErr("Failed to query folder records", err)
}
policy := user.GetPolicyID(&folder[0])
if folder[0].PolicyID > 0 {
props.Policy = policy.Name
}
props.CreatedAt = folder[0].CreatedAt
props.UpdatedAt = folder[0].UpdatedAt
// 如果对象是目录, 先尝试返回缓存结果
if cacheRes, ok := cache.Get(fmt.Sprintf("folder_props_%d", res)); ok {
res := cacheRes.(serializer.ObjectProps)
res.CreatedAt = props.CreatedAt
res.UpdatedAt = props.UpdatedAt
res.Policy = props.Policy
return serializer.Response{Data: res}
}
// 统计子目录
childFolders, err := model.GetRecursiveChildFolder([]uint{folder[0].ID},
user.ID, true)
if err != nil {
return serializer.DBErr("Failed to list child folders", err)
}
props.ChildFolderNum = len(childFolders) - 1
// 统计子文件
files, err := model.GetChildFilesOfFolders(&childFolders)
if err != nil {
return serializer.DBErr("Failed to list child files", err)
}
// 统计子文件个数和大小
props.ChildFileNum = len(files)
for i := 0; i < len(files); i++ {
props.Size += files[i].Size
}
// 查找父目录
if service.TraceRoot {
if err := folder[0].TraceRoot(); err != nil {
return serializer.DBErr("Failed to list child folders", err)
}
props.Path = folder[0].Position
}
// 如果列取对象是目录,则缓存结果
cache.Set(fmt.Sprintf("folder_props_%d", res), props,
model.GetIntSetting("folder_props_timeout", 300))
}
return serializer.Response{
Code: 0,
Data: props,
}
}

View File

@@ -0,0 +1,88 @@
package explorer
import (
"context"
"strings"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/gin-gonic/gin"
)
// ItemSearchService 文件搜索服务
type ItemSearchService struct {
Type string `uri:"type" binding:"required"`
Keywords string `uri:"keywords" binding:"required"`
Path string `form:"path"`
}
// Search 执行搜索
func (service *ItemSearchService) Search(c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
if service.Path != "" {
ok, parent := fs.IsPathExist(service.Path)
if !ok {
return serializer.Err(serializer.CodeParentNotExist, "", nil)
}
fs.Root = parent
}
switch service.Type {
case "keywords":
return service.SearchKeywords(c, fs, "%"+service.Keywords+"%")
case "image":
return service.SearchKeywords(c, fs, "%.bmp", "%.iff", "%.png", "%.gif", "%.jpg", "%.jpeg", "%.psd", "%.svg", "%.webp")
case "video":
return service.SearchKeywords(c, fs, "%.mp4", "%.flv", "%.avi", "%.wmv", "%.mkv", "%.rm", "%.rmvb", "%.mov", "%.ogv")
case "audio":
return service.SearchKeywords(c, fs, "%.mp3", "%.flac", "%.ape", "%.wav", "%.acc", "%.ogg", "%.midi", "%.mid")
case "doc":
return service.SearchKeywords(c, fs, "%.txt", "%.md", "%.pdf", "%.doc", "%.docx", "%.ppt", "%.pptx", "%.xls", "%.xlsx", "%.pub")
case "tag":
if tid, err := hashid.DecodeHashID(service.Keywords, hashid.TagID); err == nil {
if tag, err := model.GetTagsByID(tid, fs.User.ID); err == nil {
if tag.Type == model.FileTagType {
exp := strings.Split(tag.Expression, "\n")
expInput := make([]interface{}, len(exp))
for i := 0; i < len(exp); i++ {
expInput[i] = exp[i]
}
return service.SearchKeywords(c, fs, expInput...)
}
}
}
return serializer.Err(serializer.CodeNotFound, "", nil)
default:
return serializer.ParamErr("Unknown search type", nil)
}
}
// SearchKeywords 根据关键字搜索文件
func (service *ItemSearchService) SearchKeywords(c *gin.Context, fs *filesystem.FileSystem, keywords ...interface{}) serializer.Response {
// 上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 获取子项目
objects, err := fs.Search(ctx, keywords...)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
return serializer.Response{
Code: 0,
Data: map[string]interface{}{
"parent": 0,
"objects": objects,
},
}
}

193
service/explorer/slave.go Normal file
View File

@@ -0,0 +1,193 @@
package explorer
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/task"
"github.com/cloudreve/Cloudreve/v3/pkg/task/slavetask"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
)
// SlaveDownloadService 从机文件下載服务
type SlaveDownloadService struct {
PathEncoded string `uri:"path" binding:"required"`
Name string `uri:"name" binding:"required"`
Speed int `uri:"speed" binding:"min=0"`
}
// SlaveFileService 从机单文件文件相关服务
type SlaveFileService struct {
PathEncoded string `uri:"path" binding:"required"`
Ext string `uri:"ext"`
}
// SlaveFilesService 从机多文件相关服务
type SlaveFilesService struct {
Files []string `json:"files" binding:"required,gt=0"`
}
// SlaveListService 从机列表服务
type SlaveListService struct {
Path string `json:"path" binding:"required,min=1,max=65535"`
Recursive bool `json:"recursive"`
}
// ServeFile 通过签名的URL下载从机文件
func (service *SlaveDownloadService) ServeFile(ctx context.Context, c *gin.Context, isDownload bool) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewAnonymousFileSystem()
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 解码文件路径
fileSource, err := base64.RawURLEncoding.DecodeString(service.PathEncoded)
if err != nil {
return serializer.Err(serializer.CodeFileNotFound, "", err)
}
// 根据URL里的信息创建一个文件对象和用户对象
file := model.File{
Name: service.Name,
SourceName: string(fileSource),
Policy: model.Policy{
Model: gorm.Model{ID: 1},
Type: "local",
},
}
fs.User = &model.User{
Group: model.Group{SpeedLimit: service.Speed},
}
fs.FileTarget = []model.File{file}
// 开始处理下载
ctx = context.WithValue(ctx, fsctx.GinCtx, c)
rs, err := fs.GetDownloadContent(ctx, 0)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
defer rs.Close()
// 设置下载文件名
if isDownload {
c.Header("Content-Disposition", "attachment; filename=\""+url.PathEscape(fs.FileTarget[0].Name)+"\"")
}
// 发送文件
http.ServeContent(c.Writer, c.Request, fs.FileTarget[0].Name, time.Now(), rs)
return serializer.Response{}
}
// Delete 通过签名的URL删除从机文件
func (service *SlaveFilesService) Delete(ctx context.Context, c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewAnonymousFileSystem()
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 删除文件
failed, err := fs.Handler.Delete(ctx, service.Files)
if err != nil {
// 将Data字段写为字符串方便主控端解析
data, _ := json.Marshal(serializer.RemoteDeleteRequest{Files: failed})
return serializer.Response{
Code: serializer.CodeNotFullySuccess,
Data: string(data),
Msg: fmt.Sprintf("Failed to delete %d files(s)", len(failed)),
Error: err.Error(),
}
}
return serializer.Response{}
}
// Thumb 通过签名URL获取从机文件缩略图
func (service *SlaveFileService) Thumb(ctx context.Context, c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewAnonymousFileSystem()
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 解码文件路径
fileSource, err := base64.RawURLEncoding.DecodeString(service.PathEncoded)
if err != nil {
return serializer.Err(serializer.CodeFileNotFound, "", err)
}
fs.FileTarget = []model.File{{SourceName: string(fileSource), Name: fmt.Sprintf("%s.%s", fileSource, service.Ext), PicInfo: "1,1"}}
// 获取缩略图
resp, err := fs.GetThumb(ctx, 0)
if err != nil {
return serializer.Err(serializer.CodeNotSet, "Failed to get thumb", err)
}
defer resp.Content.Close()
http.ServeContent(c.Writer, c.Request, "thumb.png", time.Now(), resp.Content)
return serializer.Response{}
}
// CreateTransferTask 创建从机文件转存任务
func CreateTransferTask(c *gin.Context, req *serializer.SlaveTransferReq) serializer.Response {
if id, ok := c.Get("MasterSiteID"); ok {
job := &slavetask.TransferTask{
Req: req,
MasterID: id.(string),
}
if err := cluster.DefaultController.SubmitTask(job.MasterID, job, req.Hash(job.MasterID), func(job interface{}) {
task.TaskPoll.Submit(job.(task.Job))
}); err != nil {
return serializer.Err(serializer.CodeCreateTaskError, "", err)
}
return serializer.Response{}
}
return serializer.ParamErr("未知的主机节点ID", nil)
}
// SlaveListService 从机上传会话服务
type SlaveCreateUploadSessionService struct {
Session serializer.UploadSession `json:"session" binding:"required"`
TTL int64 `json:"ttl"`
Overwrite bool `json:"overwrite"`
}
// Create 从机创建上传会话
func (service *SlaveCreateUploadSessionService) Create(ctx context.Context, c *gin.Context) serializer.Response {
if !service.Overwrite && util.Exists(service.Session.SavePath) {
return serializer.Err(serializer.CodeConflict, "placeholder file already exist", nil)
}
err := cache.Set(
filesystem.UploadSessionCachePrefix+service.Session.Key,
service.Session,
int(service.TTL),
)
if err != nil {
return serializer.Err(serializer.CodeCacheOperation, "Failed to create upload session in slave node", err)
}
return serializer.Response{}
}

88
service/explorer/tag.go Normal file
View File

@@ -0,0 +1,88 @@
package explorer
import (
"fmt"
"strings"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/gin-gonic/gin"
)
// FilterTagCreateService 文件分类标签创建服务
type FilterTagCreateService struct {
Expression string `json:"expression" binding:"required,min=1,max=65535"`
Icon string `json:"icon" binding:"required,min=1,max=255"`
Name string `json:"name" binding:"required,min=1,max=255"`
Color string `json:"color" binding:"hexcolor|rgb|rgba|hsl"`
}
// LinkTagCreateService 目录快捷方式标签创建服务
type LinkTagCreateService struct {
Path string `json:"path" binding:"required,min=1,max=65535"`
Name string `json:"name" binding:"required,min=1,max=255"`
}
// TagService 标签服务
type TagService struct {
}
// Delete 删除标签
func (service *TagService) Delete(c *gin.Context, user *model.User) serializer.Response {
id, _ := c.Get("object_id")
if err := model.DeleteTagByID(id.(uint), user.ID); err != nil {
return serializer.DBErr("Failed to delete a tag", err)
}
return serializer.Response{}
}
// Create 创建标签
func (service *LinkTagCreateService) Create(c *gin.Context, user *model.User) serializer.Response {
// 创建标签
tag := model.Tag{
Name: service.Name,
Icon: "FolderHeartOutline",
Type: model.DirectoryLinkType,
Expression: service.Path,
UserID: user.ID,
}
id, err := tag.Create()
if err != nil {
return serializer.DBErr("Failed to create a tag", err)
}
return serializer.Response{
Data: hashid.HashID(id, hashid.TagID),
}
}
// Create 创建标签
func (service *FilterTagCreateService) Create(c *gin.Context, user *model.User) serializer.Response {
// 分割表达式将通配符转换为SQL内的%
expressions := strings.Split(service.Expression, "\n")
for i := 0; i < len(expressions); i++ {
expressions[i] = strings.ReplaceAll(expressions[i], "*", "%")
if expressions[i] == "" {
return serializer.ParamErr(fmt.Sprintf("The %d line contains an empty match expression", i+1), nil)
}
}
// 创建标签
tag := model.Tag{
Name: service.Name,
Icon: service.Icon,
Color: service.Color,
Type: model.FileTagType,
Expression: strings.Join(expressions, "\n"),
UserID: user.ID,
}
id, err := tag.Create()
if err != nil {
return serializer.DBErr("Failed to create a tag", err)
}
return serializer.Response{
Data: hashid.HashID(id, hashid.TagID),
}
}

298
service/explorer/upload.go Normal file
View File

@@ -0,0 +1,298 @@
package explorer
import (
"context"
"fmt"
"io/ioutil"
"strconv"
"strings"
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/local"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-gonic/gin"
)
// CreateUploadSessionService 获取上传凭证服务
type CreateUploadSessionService struct {
Path string `json:"path" binding:"required"`
Size uint64 `json:"size" binding:"min=0"`
Name string `json:"name" binding:"required"`
PolicyID string `json:"policy_id" binding:"required"`
LastModified int64 `json:"last_modified"`
MimeType string `json:"mime_type"`
}
// Create 创建新的上传会话
func (service *CreateUploadSessionService) Create(ctx context.Context, c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
// 取得存储策略的ID
rawID, err := hashid.DecodeHashID(service.PolicyID, hashid.PolicyID)
if err != nil {
return serializer.Err(serializer.CodePolicyNotExist, "", err)
}
// 分配并检查存储策略
if err := fs.SetPolicyFromPreference(rawID); err != nil {
return serializer.Err(serializer.CodePolicyNotAllowed, "", err)
}
if fs.Policy.ID != rawID {
return serializer.Err(serializer.CodePolicyChanged, "", nil)
}
file := &fsctx.FileStream{
Size: service.Size,
Name: service.Name,
VirtualPath: service.Path,
File: ioutil.NopCloser(strings.NewReader("")),
MimeType: service.MimeType,
}
if service.LastModified > 0 {
lastModified := time.UnixMilli(service.LastModified)
file.LastModified = &lastModified
}
credential, err := fs.CreateUploadSession(ctx, file)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
return serializer.Response{
Code: 0,
Data: credential,
}
}
// UploadService 本机及从机策略上传服务
type UploadService struct {
ID string `uri:"sessionId" binding:"required"`
Index int `uri:"index" form:"index" binding:"min=0"`
}
// LocalUpload 处理本机文件分片上传
func (service *UploadService) LocalUpload(ctx context.Context, c *gin.Context) serializer.Response {
uploadSessionRaw, ok := cache.Get(filesystem.UploadSessionCachePrefix + service.ID)
if !ok {
return serializer.Err(serializer.CodeUploadSessionExpired, "", nil)
}
uploadSession := uploadSessionRaw.(serializer.UploadSession)
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err)
}
if uploadSession.UID != fs.User.ID {
return serializer.Err(serializer.CodeUploadSessionExpired, "", nil)
}
// 查找上传会话创建的占位文件
file, err := model.GetFilesByUploadSession(service.ID, fs.User.ID)
if err != nil {
return serializer.Err(serializer.CodeUploadSessionExpired, "", err)
}
// 重设 fs 存储策略
if !uploadSession.Policy.IsTransitUpload(uploadSession.Size) {
return serializer.Err(serializer.CodePolicyNotAllowed, "", err)
}
fs.Policy = &uploadSession.Policy
if err := fs.DispatchHandler(); err != nil {
return serializer.Err(serializer.CodePolicyNotExist, "", err)
}
expectedSizeStart := file.Size
actualSizeStart := uint64(service.Index) * uploadSession.Policy.OptionsSerialized.ChunkSize
if uploadSession.Policy.OptionsSerialized.ChunkSize == 0 && service.Index > 0 {
return serializer.Err(serializer.CodeInvalidChunkIndex, "Chunk index cannot be greater than 0", nil)
}
if expectedSizeStart < actualSizeStart {
return serializer.Err(serializer.CodeInvalidChunkIndex, "Chunk must be uploaded in order", nil)
}
if expectedSizeStart > actualSizeStart {
util.Log().Info("Trying to overwrite chunk[%d] Start=%d", service.Index, actualSizeStart)
}
return processChunkUpload(ctx, c, fs, &uploadSession, service.Index, file, fsctx.Append)
}
// SlaveUpload 处理从机文件分片上传
func (service *UploadService) SlaveUpload(ctx context.Context, c *gin.Context) serializer.Response {
uploadSessionRaw, ok := cache.Get(filesystem.UploadSessionCachePrefix + service.ID)
if !ok {
return serializer.Err(serializer.CodeUploadSessionExpired, "", nil)
}
uploadSession := uploadSessionRaw.(serializer.UploadSession)
fs, err := filesystem.NewAnonymousFileSystem()
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
fs.Handler = local.Driver{}
// 解析需要的参数
service.Index, _ = strconv.Atoi(c.Query("chunk"))
mode := fsctx.Append
if c.GetHeader(auth.CrHeaderPrefix+"Overwrite") == "true" {
mode |= fsctx.Overwrite
}
return processChunkUpload(ctx, c, fs, &uploadSession, service.Index, nil, mode)
}
func processChunkUpload(ctx context.Context, c *gin.Context, fs *filesystem.FileSystem, session *serializer.UploadSession, index int, file *model.File, mode fsctx.WriteMode) serializer.Response {
// 取得并校验文件大小是否符合分片要求
chunkSize := session.Policy.OptionsSerialized.ChunkSize
isLastChunk := session.Policy.OptionsSerialized.ChunkSize == 0 || uint64(index+1)*chunkSize >= session.Size
expectedLength := chunkSize
if isLastChunk {
expectedLength = session.Size - uint64(index)*chunkSize
}
fileSize, err := strconv.ParseUint(c.Request.Header.Get("Content-Length"), 10, 64)
if err != nil || (expectedLength != fileSize) {
return serializer.Err(
serializer.CodeInvalidContentLength,
fmt.Sprintf("Invalid Content-Length (expected: %d)", expectedLength),
err,
)
}
// 非首个分片时需要允许覆盖
if index > 0 {
mode |= fsctx.Overwrite
}
fileData := fsctx.FileStream{
MimeType: c.Request.Header.Get("Content-Type"),
File: c.Request.Body,
Size: fileSize,
Name: session.Name,
VirtualPath: session.VirtualPath,
SavePath: session.SavePath,
Mode: mode,
AppendStart: chunkSize * uint64(index),
Model: file,
LastModified: session.LastModified,
}
// 给文件系统分配钩子
fs.Use("AfterUploadCanceled", filesystem.HookTruncateFileTo(fileData.AppendStart))
fs.Use("AfterValidateFailed", filesystem.HookTruncateFileTo(fileData.AppendStart))
if file != nil {
fs.Use("BeforeUpload", filesystem.HookValidateCapacity)
fs.Use("AfterUpload", filesystem.HookChunkUploaded)
fs.Use("AfterValidateFailed", filesystem.HookChunkUploadFailed)
if isLastChunk {
fs.Use("AfterUpload", filesystem.HookPopPlaceholderToFile(""))
fs.Use("AfterUpload", filesystem.HookDeleteUploadSession(session.Key))
}
} else {
if isLastChunk {
fs.Use("AfterUpload", filesystem.SlaveAfterUpload(session))
fs.Use("AfterUpload", filesystem.HookDeleteUploadSession(session.Key))
}
}
// 执行上传
uploadCtx := context.WithValue(ctx, fsctx.GinCtx, c)
err = fs.Upload(uploadCtx, &fileData)
if err != nil {
return serializer.Err(serializer.CodeUploadFailed, err.Error(), err)
}
return serializer.Response{}
}
// UploadSessionService 上传会话服务
type UploadSessionService struct {
ID string `uri:"sessionId" binding:"required"`
}
// Delete 删除指定上传会话
func (service *UploadSessionService) Delete(ctx context.Context, c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 查找需要删除的上传会话的占位文件
file, err := model.GetFilesByUploadSession(service.ID, fs.User.ID)
if err != nil {
return serializer.Err(serializer.CodeUploadSessionExpired, "", err)
}
// 删除文件
if err := fs.Delete(ctx, []uint{}, []uint{file.ID}, false, false); err != nil {
return serializer.Err(serializer.CodeInternalSetting, "Failed to delete upload session", err)
}
return serializer.Response{}
}
// SlaveDelete 从机删除指定上传会话
func (service *UploadSessionService) SlaveDelete(ctx context.Context, c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewAnonymousFileSystem()
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
session, ok := cache.Get(filesystem.UploadSessionCachePrefix + service.ID)
if !ok {
return serializer.Err(serializer.CodeUploadSessionExpired, "", nil)
}
if _, err := fs.Handler.Delete(ctx, []string{session.(serializer.UploadSession).SavePath}); err != nil {
return serializer.Err(serializer.CodeInternalSetting, "Failed to delete temp file", err)
}
cache.Deletes([]string{service.ID}, filesystem.UploadSessionCachePrefix)
return serializer.Response{}
}
// DeleteAllUploadSession 删除当前用户的全部上传绘会话
func DeleteAllUploadSession(ctx context.Context, c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 查找需要删除的上传会话的占位文件
files := model.GetUploadPlaceholderFiles(fs.User.ID)
fileIDs := make([]uint, len(files))
for i, file := range files {
fileIDs[i] = file.ID
}
// 删除文件
if err := fs.Delete(ctx, []uint{}, fileIDs, false, false); err != nil {
return serializer.Err(serializer.CodeInternalSetting, "Failed to cleanup upload session", err)
}
return serializer.Response{}
}

138
service/explorer/wopi.go Normal file
View File

@@ -0,0 +1,138 @@
package explorer
import (
"errors"
"fmt"
"github.com/cloudreve/Cloudreve/v3/middleware"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
type WopiService struct {
}
func (service *WopiService) Rename(c *gin.Context) error {
fs, _, err := service.prepareFs(c)
if err != nil {
return err
}
defer fs.Recycle()
return fs.Rename(c, []uint{}, []uint{c.MustGet("object_id").(uint)}, c.GetHeader(wopi.RenameRequestHeader))
}
func (service *WopiService) GetFile(c *gin.Context) error {
fs, _, err := service.prepareFs(c)
if err != nil {
return err
}
defer fs.Recycle()
resp, err := fs.Preview(c, fs.FileTarget[0].ID, true)
if err != nil {
return fmt.Errorf("failed to pull file content: %w", err)
}
// 重定向到文件源
if resp.Redirect {
return fmt.Errorf("redirect not supported in WOPI")
}
// 直接返回文件内容
defer resp.Content.Close()
c.Header("Cache-Control", "no-cache")
http.ServeContent(c.Writer, c.Request, fs.FileTarget[0].Name, fs.FileTarget[0].UpdatedAt, resp.Content)
return nil
}
func (service *WopiService) FileInfo(c *gin.Context) (*serializer.WopiFileInfo, error) {
fs, session, err := service.prepareFs(c)
if err != nil {
return nil, err
}
defer fs.Recycle()
parent, err := model.GetFoldersByIDs([]uint{fs.FileTarget[0].FolderID}, fs.User.ID)
if err != nil {
return nil, err
}
if len(parent) == 0 {
return nil, fmt.Errorf("failed to find parent folder")
}
parent[0].TraceRoot()
siteUrl := model.GetSiteURL()
// Generate url for parent folder
parentUrl := model.GetSiteURL()
parentUrl.Path = "/home"
query := parentUrl.Query()
query.Set("path", parent[0].Position)
parentUrl.RawQuery = query.Encode()
info := &serializer.WopiFileInfo{
BaseFileName: fs.FileTarget[0].Name,
Version: fs.FileTarget[0].Model.UpdatedAt.String(),
BreadcrumbBrandName: model.GetSettingByName("siteName"),
BreadcrumbBrandUrl: siteUrl.String(),
FileSharingPostMessage: false,
PostMessageOrigin: "*",
FileNameMaxLength: 256,
LastModifiedTime: fs.FileTarget[0].Model.UpdatedAt.Format(time.RFC3339),
IsAnonymousUser: true,
ReadOnly: true,
ClosePostMessage: true,
Size: int64(fs.FileTarget[0].Size),
OwnerId: hashid.HashID(fs.FileTarget[0].UserID, hashid.UserID),
}
if session.Action == wopi.ActionEdit {
info.FileSharingPostMessage = true
info.IsAnonymousUser = false
info.SupportsRename = true
info.SupportsReviewing = true
info.SupportsUpdate = true
info.UserFriendlyName = fs.User.Nick
info.UserId = hashid.HashID(fs.User.ID, hashid.UserID)
info.UserCanRename = true
info.UserCanReview = true
info.UserCanWrite = true
info.ReadOnly = false
info.BreadcrumbFolderName = parent[0].Name
info.BreadcrumbFolderUrl = parentUrl.String()
}
return info, nil
}
func (service *WopiService) prepareFs(c *gin.Context) (*filesystem.FileSystem, *wopi.SessionCache, error) {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return nil, nil, err
}
session := c.MustGet(middleware.WopiSessionCtx).(*wopi.SessionCache)
if err := fs.SetTargetFileByIDs([]uint{session.FileID}); err != nil {
fs.Recycle()
return nil, nil, fmt.Errorf("failed to find file: %w", err)
}
maxSize := model.GetIntSetting("maxEditSize", 0)
if maxSize > 0 && fs.FileTarget[0].Size > uint64(maxSize) {
return nil, nil, errors.New("file too large")
}
return fs, session, nil
}