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

     1  package explorer
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"github.com/cloudreve/Cloudreve/v3/pkg/util"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/url"
    12  	"path"
    13  	"strconv"
    14  	"strings"
    15  
    16  	model "github.com/cloudreve/Cloudreve/v3/models"
    17  	"github.com/cloudreve/Cloudreve/v3/pkg/cache"
    18  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
    19  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
    20  	"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
    21  	"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
    22  	"github.com/gin-gonic/gin"
    23  )
    24  
    25  // SingleFileService 对单文件进行操作的五福,path为文件完整路径
    26  type SingleFileService struct {
    27  	Path string `uri:"path" json:"path" binding:"required,min=1,max=65535"`
    28  }
    29  
    30  // FileIDService 通过文件ID对文件进行操作的服务
    31  type FileIDService struct {
    32  }
    33  
    34  // FileAnonymousGetService 匿名(外链)获取文件服务
    35  type FileAnonymousGetService struct {
    36  	ID   uint   `uri:"id" binding:"required,min=1"`
    37  	Name string `uri:"name" binding:"required"`
    38  }
    39  
    40  // DownloadService 文件下載服务
    41  type DownloadService struct {
    42  	ID string `uri:"id" binding:"required"`
    43  }
    44  
    45  // ArchiveService 文件流式打包下載服务
    46  type ArchiveService struct {
    47  	ID string `uri:"sessionID" binding:"required"`
    48  }
    49  
    50  // New 创建新文件
    51  func (service *SingleFileService) Create(c *gin.Context) serializer.Response {
    52  	// 创建文件系统
    53  	fs, err := filesystem.NewFileSystemFromContext(c)
    54  	if err != nil {
    55  		return serializer.Err(serializer.CodeCreateFSError, "", err)
    56  	}
    57  	defer fs.Recycle()
    58  
    59  	// 上下文
    60  	ctx, cancel := context.WithCancel(context.Background())
    61  	defer cancel()
    62  
    63  	// 给文件系统分配钩子
    64  	fs.Use("BeforeUpload", filesystem.HookValidateFile)
    65  	fs.Use("AfterUpload", filesystem.GenericAfterUpload)
    66  
    67  	// 上传空文件
    68  	err = fs.Upload(ctx, &fsctx.FileStream{
    69  		File:        ioutil.NopCloser(strings.NewReader("")),
    70  		Size:        0,
    71  		VirtualPath: path.Dir(service.Path),
    72  		Name:        path.Base(service.Path),
    73  	})
    74  	if err != nil {
    75  		return serializer.Err(serializer.CodeUploadFailed, err.Error(), err)
    76  	}
    77  
    78  	return serializer.Response{
    79  		Code: 0,
    80  	}
    81  }
    82  
    83  // List 列出从机上的文件
    84  func (service *SlaveListService) List(c *gin.Context) serializer.Response {
    85  	// 创建文件系统
    86  	fs, err := filesystem.NewAnonymousFileSystem()
    87  	if err != nil {
    88  		return serializer.Err(serializer.CodeCreateFSError, "", err)
    89  	}
    90  	defer fs.Recycle()
    91  
    92  	objects, err := fs.Handler.List(context.Background(), service.Path, service.Recursive)
    93  	if err != nil {
    94  		return serializer.Err(serializer.CodeIOFailed, "Cannot list files", err)
    95  	}
    96  
    97  	res, _ := json.Marshal(objects)
    98  	return serializer.Response{Data: string(res)}
    99  }
   100  
   101  // DownloadArchived 通过预签名 URL 打包下载
   102  func (service *ArchiveService) DownloadArchived(ctx context.Context, c *gin.Context) serializer.Response {
   103  	userRaw, exist := cache.Get("archive_user_" + service.ID)
   104  	if !exist {
   105  		return serializer.Err(serializer.CodeNotFound, "Archive session not exist", nil)
   106  	}
   107  	user := userRaw.(model.User)
   108  
   109  	// 创建文件系统
   110  	fs, err := filesystem.NewFileSystem(&user)
   111  	if err != nil {
   112  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   113  	}
   114  	defer fs.Recycle()
   115  
   116  	// 查找打包的临时文件
   117  	archiveSession, exist := cache.Get("archive_" + service.ID)
   118  	if !exist {
   119  		return serializer.Err(serializer.CodeNotFound, "Archive session not exist", nil)
   120  	}
   121  
   122  	// 开始打包
   123  	c.Header("Content-Disposition", "attachment;")
   124  	c.Header("Content-Type", "application/zip")
   125  	itemService := archiveSession.(ItemIDService)
   126  	items := itemService.Raw()
   127  	ctx = context.WithValue(ctx, fsctx.GinCtx, c)
   128  	err = fs.Compress(ctx, c.Writer, items.Dirs, items.Items, true)
   129  	if err != nil {
   130  		return serializer.Err(serializer.CodeNotSet, "Failed to compress file", err)
   131  	}
   132  
   133  	return serializer.Response{
   134  		Code: 0,
   135  	}
   136  }
   137  
   138  // Download 签名的匿名文件下载
   139  func (service *FileAnonymousGetService) Download(ctx context.Context, c *gin.Context) serializer.Response {
   140  	fs, err := filesystem.NewAnonymousFileSystem()
   141  	if err != nil {
   142  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   143  	}
   144  	defer fs.Recycle()
   145  
   146  	// 查找文件
   147  	err = fs.SetTargetFileByIDs([]uint{service.ID})
   148  	if err != nil {
   149  		return serializer.Err(serializer.CodeNotSet, err.Error(), err)
   150  	}
   151  
   152  	// 获取文件流
   153  	rs, err := fs.GetDownloadContent(ctx, 0)
   154  	defer rs.Close()
   155  	if err != nil {
   156  		return serializer.Err(serializer.CodeNotSet, err.Error(), err)
   157  	}
   158  
   159  	// 发送文件
   160  	http.ServeContent(c.Writer, c.Request, service.Name, fs.FileTarget[0].UpdatedAt, rs)
   161  
   162  	return serializer.Response{
   163  		Code: 0,
   164  	}
   165  }
   166  
   167  // Source 重定向到文件的有效原始链接
   168  func (service *FileAnonymousGetService) Source(ctx context.Context, c *gin.Context) serializer.Response {
   169  	fs, err := filesystem.NewAnonymousFileSystem()
   170  	if err != nil {
   171  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   172  	}
   173  	defer fs.Recycle()
   174  
   175  	// 查找文件
   176  	err = fs.SetTargetFileByIDs([]uint{service.ID})
   177  	if err != nil {
   178  		return serializer.Err(serializer.CodeNotSet, err.Error(), err)
   179  	}
   180  
   181  	// 获取文件流
   182  	ttl := int64(model.GetIntSetting("preview_timeout", 60))
   183  	res, err := fs.SignURL(ctx, &fs.FileTarget[0], ttl, false)
   184  	if err != nil {
   185  		return serializer.Err(serializer.CodeNotSet, err.Error(), err)
   186  	}
   187  
   188  	c.Header("Cache-Control", fmt.Sprintf("max-age=%d", ttl))
   189  	return serializer.Response{
   190  		Code: -302,
   191  		Data: res,
   192  	}
   193  }
   194  
   195  // CreateDocPreviewSession 创建DOC文件预览会话,返回预览地址
   196  func (service *FileIDService) CreateDocPreviewSession(ctx context.Context, c *gin.Context, editable bool) serializer.Response {
   197  	// 创建文件系统
   198  	fs, err := filesystem.NewFileSystemFromContext(c)
   199  	if err != nil {
   200  		return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err)
   201  	}
   202  	defer fs.Recycle()
   203  
   204  	// 获取对象id
   205  	objectID, _ := c.Get("object_id")
   206  
   207  	// 如果上下文中已有File对象,则重设目标
   208  	if file, ok := ctx.Value(fsctx.FileModelCtx).(*model.File); ok {
   209  		fs.SetTargetFile(&[]model.File{*file})
   210  		objectID = uint(0)
   211  	}
   212  
   213  	// 如果上下文中已有Folder对象,则重设根目录
   214  	if folder, ok := ctx.Value(fsctx.FolderModelCtx).(*model.Folder); ok {
   215  		fs.Root = folder
   216  		path := ctx.Value(fsctx.PathCtx).(string)
   217  		err := fs.ResetFileIfNotExist(ctx, path)
   218  		if err != nil {
   219  			return serializer.Err(serializer.CodeNotFound, err.Error(), err)
   220  		}
   221  		objectID = uint(0)
   222  	}
   223  
   224  	// 获取文件临时下载地址
   225  	downloadURL, err := fs.GetDownloadURL(ctx, objectID.(uint), "doc_preview_timeout")
   226  	if err != nil {
   227  		return serializer.Err(serializer.CodeNotSet, err.Error(), err)
   228  	}
   229  
   230  	// For newer version of Cloudreve - Local Policy
   231  	// When do not use a cdn, the downloadURL withouts hosts, like "/api/v3/file/download/xxx"
   232  	if strings.HasPrefix(downloadURL, "/") {
   233  		downloadURI, err := url.Parse(downloadURL)
   234  		if err != nil {
   235  			return serializer.Err(serializer.CodeNotSet, err.Error(), err)
   236  		}
   237  		downloadURL = model.GetSiteURL().ResolveReference(downloadURI).String()
   238  	}
   239  
   240  	var resp serializer.DocPreviewSession
   241  
   242  	// Use WOPI preview if available
   243  	if model.IsTrueVal(model.GetSettingByName("wopi_enabled")) && wopi.Default != nil {
   244  		maxSize := model.GetIntSetting("maxEditSize", 0)
   245  		if maxSize > 0 && fs.FileTarget[0].Size > uint64(maxSize) {
   246  			return serializer.Err(serializer.CodeFileTooLarge, "", nil)
   247  		}
   248  
   249  		action := wopi.ActionPreview
   250  		if editable {
   251  			action = wopi.ActionEdit
   252  		}
   253  
   254  		session, err := wopi.Default.NewSession(fs.FileTarget[0].UserID, &fs.FileTarget[0], action)
   255  		if err != nil {
   256  			return serializer.Err(serializer.CodeInternalSetting, "Failed to create WOPI session", err)
   257  		}
   258  
   259  		resp.URL = session.ActionURL.String()
   260  		resp.AccessTokenTTL = session.AccessTokenTTL
   261  		resp.AccessToken = session.AccessToken
   262  		return serializer.Response{
   263  			Code: 0,
   264  			Data: resp,
   265  		}
   266  	}
   267  
   268  	// 生成最终的预览器地址
   269  	srcB64 := base64.StdEncoding.EncodeToString([]byte(downloadURL))
   270  	srcEncoded := url.QueryEscape(downloadURL)
   271  	srcB64Encoded := url.QueryEscape(srcB64)
   272  	resp.URL = util.Replace(map[string]string{
   273  		"{$src}":    srcEncoded,
   274  		"{$srcB64}": srcB64Encoded,
   275  		"{$name}":   url.QueryEscape(fs.FileTarget[0].Name),
   276  	}, model.GetSettingByName("office_preview_service"))
   277  
   278  	return serializer.Response{
   279  		Code: 0,
   280  		Data: resp,
   281  	}
   282  }
   283  
   284  // CreateDownloadSession 创建下载会话,获取下载URL
   285  func (service *FileIDService) CreateDownloadSession(ctx context.Context, c *gin.Context) serializer.Response {
   286  	// 创建文件系统
   287  	fs, err := filesystem.NewFileSystemFromContext(c)
   288  	if err != nil {
   289  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   290  	}
   291  	defer fs.Recycle()
   292  
   293  	// 获取对象id
   294  	objectID, _ := c.Get("object_id")
   295  
   296  	// 获取下载地址
   297  	downloadURL, err := fs.GetDownloadURL(ctx, objectID.(uint), "download_timeout")
   298  	if err != nil {
   299  		return serializer.Err(serializer.CodeNotSet, err.Error(), err)
   300  	}
   301  
   302  	return serializer.Response{
   303  		Code: 0,
   304  		Data: downloadURL,
   305  	}
   306  }
   307  
   308  // Download 通过签名URL的文件下载,无需登录
   309  func (service *DownloadService) Download(ctx context.Context, c *gin.Context) serializer.Response {
   310  	// 创建文件系统
   311  	fs, err := filesystem.NewFileSystemFromContext(c)
   312  	if err != nil {
   313  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   314  	}
   315  	defer fs.Recycle()
   316  
   317  	// 查找打包的临时文件
   318  	file, exist := cache.Get("download_" + service.ID)
   319  	if !exist {
   320  		return serializer.Err(serializer.CodeNotFound, "Download session not exist", nil)
   321  	}
   322  	fs.FileTarget = []model.File{file.(model.File)}
   323  
   324  	// 开始处理下载
   325  	ctx = context.WithValue(ctx, fsctx.GinCtx, c)
   326  	rs, err := fs.GetDownloadContent(ctx, 0)
   327  	if err != nil {
   328  		return serializer.Err(serializer.CodeNotSet, err.Error(), err)
   329  	}
   330  	defer rs.Close()
   331  
   332  	// 设置文件名
   333  	c.Header("Content-Disposition", "attachment; filename=\""+url.PathEscape(fs.FileTarget[0].Name)+"\"")
   334  
   335  	if fs.User.Group.OptionsSerialized.OneTimeDownload {
   336  		// 清理资源,删除临时文件
   337  		_ = cache.Deletes([]string{service.ID}, "download_")
   338  	}
   339  
   340  	// 发送文件
   341  	http.ServeContent(c.Writer, c.Request, fs.FileTarget[0].Name, fs.FileTarget[0].UpdatedAt, rs)
   342  
   343  	return serializer.Response{
   344  		Code: 0,
   345  	}
   346  }
   347  
   348  // PreviewContent 预览文件,需要登录会话, isText - 是否为文本文件,文本文件会
   349  // 强制经由服务端中转
   350  func (service *FileIDService) PreviewContent(ctx context.Context, c *gin.Context, isText bool) serializer.Response {
   351  	// 创建文件系统
   352  	fs, err := filesystem.NewFileSystemFromContext(c)
   353  	if err != nil {
   354  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   355  	}
   356  	defer fs.Recycle()
   357  
   358  	// 获取对象id
   359  	objectID, _ := c.Get("object_id")
   360  
   361  	// 如果上下文中已有File对象,则重设目标
   362  	if file, ok := ctx.Value(fsctx.FileModelCtx).(*model.File); ok {
   363  		fs.SetTargetFile(&[]model.File{*file})
   364  		objectID = uint(0)
   365  	}
   366  
   367  	// 如果上下文中已有Folder对象,则重设根目录
   368  	if folder, ok := ctx.Value(fsctx.FolderModelCtx).(*model.Folder); ok {
   369  		fs.Root = folder
   370  		path := ctx.Value(fsctx.PathCtx).(string)
   371  		err := fs.ResetFileIfNotExist(ctx, path)
   372  		if err != nil {
   373  			return serializer.Err(serializer.CodeFileNotFound, err.Error(), err)
   374  		}
   375  		objectID = uint(0)
   376  	}
   377  
   378  	// 获取文件预览响应
   379  	resp, err := fs.Preview(ctx, objectID.(uint), isText)
   380  	if err != nil {
   381  		return serializer.Err(serializer.CodeNotSet, err.Error(), err)
   382  	}
   383  
   384  	// 重定向到文件源
   385  	if resp.Redirect {
   386  		c.Header("Cache-Control", fmt.Sprintf("max-age=%d", resp.MaxAge))
   387  		return serializer.Response{
   388  			Code: -301,
   389  			Data: resp.URL,
   390  		}
   391  	}
   392  
   393  	// 直接返回文件内容
   394  	defer resp.Content.Close()
   395  
   396  	if isText {
   397  		c.Header("Cache-Control", "no-cache")
   398  	}
   399  
   400  	http.ServeContent(c.Writer, c.Request, fs.FileTarget[0].Name, fs.FileTarget[0].UpdatedAt, resp.Content)
   401  
   402  	return serializer.Response{
   403  		Code: 0,
   404  	}
   405  }
   406  
   407  // PutContent 更新文件内容
   408  func (service *FileIDService) PutContent(ctx context.Context, c *gin.Context) serializer.Response {
   409  	// 创建上下文
   410  	ctx, cancel := context.WithCancel(context.Background())
   411  	defer cancel()
   412  
   413  	// 取得文件大小
   414  	fileSize, err := strconv.ParseUint(c.Request.Header.Get("Content-Length"), 10, 64)
   415  	if err != nil {
   416  
   417  		return serializer.ParamErr("Invalid content-length value", err)
   418  	}
   419  
   420  	fileData := fsctx.FileStream{
   421  		MimeType: c.Request.Header.Get("Content-Type"),
   422  		File:     c.Request.Body,
   423  		Size:     fileSize,
   424  		Mode:     fsctx.Overwrite,
   425  	}
   426  
   427  	// 创建文件系统
   428  	fs, err := filesystem.NewFileSystemFromContext(c)
   429  	if err != nil {
   430  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   431  	}
   432  	uploadCtx := context.WithValue(ctx, fsctx.GinCtx, c)
   433  
   434  	// 取得现有文件
   435  	fileID, _ := c.Get("object_id")
   436  	originFile, _ := model.GetFilesByIDs([]uint{fileID.(uint)}, fs.User.ID)
   437  	if len(originFile) == 0 {
   438  		return serializer.Err(serializer.CodeFileNotFound, "", nil)
   439  	}
   440  	fileData.Name = originFile[0].Name
   441  
   442  	// 检查此文件是否有软链接
   443  	fileList, err := model.RemoveFilesWithSoftLinks([]model.File{originFile[0]})
   444  	if err == nil && len(fileList) == 0 {
   445  		// 如果包含软连接,应重新生成新文件副本,并更新source_name
   446  		originFile[0].SourceName = fs.GenerateSavePath(uploadCtx, &fileData)
   447  		fileData.Mode &= ^fsctx.Overwrite
   448  		fs.Use("AfterUpload", filesystem.HookUpdateSourceName)
   449  		fs.Use("AfterUploadCanceled", filesystem.HookUpdateSourceName)
   450  		fs.Use("AfterUploadCanceled", filesystem.HookCleanFileContent)
   451  		fs.Use("AfterUploadCanceled", filesystem.HookClearFileSize)
   452  		fs.Use("AfterValidateFailed", filesystem.HookUpdateSourceName)
   453  		fs.Use("AfterValidateFailed", filesystem.HookCleanFileContent)
   454  		fs.Use("AfterValidateFailed", filesystem.HookClearFileSize)
   455  	}
   456  
   457  	// 给文件系统分配钩子
   458  	fs.Use("BeforeUpload", filesystem.HookResetPolicy)
   459  	fs.Use("BeforeUpload", filesystem.HookValidateFile)
   460  	fs.Use("BeforeUpload", filesystem.HookValidateCapacityDiff)
   461  	fs.Use("AfterUpload", filesystem.GenericAfterUpdate)
   462  
   463  	// 执行上传
   464  	uploadCtx = context.WithValue(uploadCtx, fsctx.FileModelCtx, originFile[0])
   465  	err = fs.Upload(uploadCtx, &fileData)
   466  	if err != nil {
   467  		return serializer.Err(serializer.CodeUploadFailed, err.Error(), err)
   468  	}
   469  
   470  	return serializer.Response{
   471  		Code: 0,
   472  	}
   473  }
   474  
   475  // Sources 批量获取对象的外链
   476  func (s *ItemIDService) Sources(ctx context.Context, c *gin.Context) serializer.Response {
   477  	fs, err := filesystem.NewFileSystemFromContext(c)
   478  	if err != nil {
   479  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   480  	}
   481  	defer fs.Recycle()
   482  
   483  	if len(s.Raw().Items) > fs.User.Group.OptionsSerialized.SourceBatchSize {
   484  		return serializer.Err(serializer.CodeBatchSourceSize, "", err)
   485  	}
   486  
   487  	res := make([]serializer.Sources, 0, len(s.Raw().Items))
   488  	files, err := model.GetFilesByIDs(s.Raw().Items, fs.User.ID)
   489  	if err != nil || len(files) == 0 {
   490  		return serializer.Err(serializer.CodeFileNotFound, "", err)
   491  	}
   492  
   493  	getSourceFunc := func(file model.File) (string, error) {
   494  		fs.FileTarget = []model.File{file}
   495  		return fs.GetSource(ctx, file.ID)
   496  	}
   497  
   498  	// Create redirected source link if needed
   499  	if fs.User.Group.OptionsSerialized.RedirectedSource {
   500  		getSourceFunc = func(file model.File) (string, error) {
   501  			source, err := file.CreateOrGetSourceLink()
   502  			if err != nil {
   503  				return "", err
   504  			}
   505  
   506  			sourceLinkURL, err := source.Link()
   507  			if err != nil {
   508  				return "", err
   509  			}
   510  
   511  			return sourceLinkURL, nil
   512  		}
   513  	}
   514  
   515  	for _, file := range files {
   516  		sourceURL, err := getSourceFunc(file)
   517  		current := serializer.Sources{
   518  			URL:    sourceURL,
   519  			Name:   file.Name,
   520  			Parent: file.FolderID,
   521  		}
   522  
   523  		if err != nil {
   524  			current.Error = err.Error()
   525  		}
   526  
   527  		res = append(res, current)
   528  	}
   529  
   530  	return serializer.Response{
   531  		Code: 0,
   532  		Data: res,
   533  	}
   534  }