init
This commit is contained in:
73
service/explorer/directory.go
Normal file
73
service/explorer/directory.go
Normal 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
537
service/explorer/file.go
Normal 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
529
service/explorer/objects.go
Normal 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,
|
||||
}
|
||||
}
|
88
service/explorer/search.go
Normal file
88
service/explorer/search.go
Normal 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
193
service/explorer/slave.go
Normal 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
88
service/explorer/tag.go
Normal 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
298
service/explorer/upload.go
Normal 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
138
service/explorer/wopi.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user