github.com/cloudreve/Cloudreve/v3@v3.0.0-20240224133659-3edb00a6484c/pkg/filesystem/driver/local/handler.go (about)

     1  package local
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/url"
     9  	"os"
    10  	"path/filepath"
    11  
    12  	model "github.com/cloudreve/Cloudreve/v3/models"
    13  	"github.com/cloudreve/Cloudreve/v3/pkg/auth"
    14  	"github.com/cloudreve/Cloudreve/v3/pkg/cache"
    15  	"github.com/cloudreve/Cloudreve/v3/pkg/conf"
    16  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
    17  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
    18  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
    19  	"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
    20  	"github.com/cloudreve/Cloudreve/v3/pkg/util"
    21  )
    22  
    23  const (
    24  	Perm = 0744
    25  )
    26  
    27  // Driver 本地策略适配器
    28  type Driver struct {
    29  	Policy *model.Policy
    30  }
    31  
    32  // List 递归列取给定物理路径下所有文件
    33  func (handler Driver) List(ctx context.Context, path string, recursive bool) ([]response.Object, error) {
    34  	var res []response.Object
    35  
    36  	// 取得起始路径
    37  	root := util.RelativePath(filepath.FromSlash(path))
    38  
    39  	// 开始遍历路径下的文件、目录
    40  	err := filepath.Walk(root,
    41  		func(path string, info os.FileInfo, err error) error {
    42  			// 跳过根目录
    43  			if path == root {
    44  				return nil
    45  			}
    46  
    47  			if err != nil {
    48  				util.Log().Warning("Failed to walk folder %q: %s", path, err)
    49  				return filepath.SkipDir
    50  			}
    51  
    52  			// 将遍历对象的绝对路径转换为相对路径
    53  			rel, err := filepath.Rel(root, path)
    54  			if err != nil {
    55  				return err
    56  			}
    57  
    58  			res = append(res, response.Object{
    59  				Name:         info.Name(),
    60  				RelativePath: filepath.ToSlash(rel),
    61  				Source:       path,
    62  				Size:         uint64(info.Size()),
    63  				IsDir:        info.IsDir(),
    64  				LastModify:   info.ModTime(),
    65  			})
    66  
    67  			// 如果非递归,则不步入目录
    68  			if !recursive && info.IsDir() {
    69  				return filepath.SkipDir
    70  			}
    71  
    72  			return nil
    73  		})
    74  
    75  	return res, err
    76  }
    77  
    78  // Get 获取文件内容
    79  func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
    80  	// 打开文件
    81  	file, err := os.Open(util.RelativePath(path))
    82  	if err != nil {
    83  		util.Log().Debug("Failed to open file: %s", err)
    84  		return nil, err
    85  	}
    86  
    87  	return file, nil
    88  }
    89  
    90  // Put 将文件流保存到指定目录
    91  func (handler Driver) Put(ctx context.Context, file fsctx.FileHeader) error {
    92  	defer file.Close()
    93  	fileInfo := file.Info()
    94  	dst := util.RelativePath(filepath.FromSlash(fileInfo.SavePath))
    95  
    96  	// 如果非 Overwrite,则检查是否有重名冲突
    97  	if fileInfo.Mode&fsctx.Overwrite != fsctx.Overwrite {
    98  		if util.Exists(dst) {
    99  			util.Log().Warning("File with the same name existed or unavailable: %s", dst)
   100  			return errors.New("file with the same name existed or unavailable")
   101  		}
   102  	}
   103  
   104  	// 如果目标目录不存在,创建
   105  	basePath := filepath.Dir(dst)
   106  	if !util.Exists(basePath) {
   107  		err := os.MkdirAll(basePath, Perm)
   108  		if err != nil {
   109  			util.Log().Warning("Failed to create directory: %s", err)
   110  			return err
   111  		}
   112  	}
   113  
   114  	var (
   115  		out *os.File
   116  		err error
   117  	)
   118  
   119  	openMode := os.O_CREATE | os.O_RDWR
   120  	if fileInfo.Mode&fsctx.Append == fsctx.Append {
   121  		openMode |= os.O_APPEND
   122  	} else {
   123  		openMode |= os.O_TRUNC
   124  	}
   125  
   126  	out, err = os.OpenFile(dst, openMode, Perm)
   127  	if err != nil {
   128  		util.Log().Warning("Failed to open or create file: %s", err)
   129  		return err
   130  	}
   131  	defer out.Close()
   132  
   133  	if fileInfo.Mode&fsctx.Append == fsctx.Append {
   134  		stat, err := out.Stat()
   135  		if err != nil {
   136  			util.Log().Warning("Failed to read file info: %s", err)
   137  			return err
   138  		}
   139  
   140  		if uint64(stat.Size()) < fileInfo.AppendStart {
   141  			return errors.New("size of unfinished uploaded chunks is not as expected")
   142  		} else if uint64(stat.Size()) > fileInfo.AppendStart {
   143  			out.Close()
   144  			if err := handler.Truncate(ctx, dst, fileInfo.AppendStart); err != nil {
   145  				return fmt.Errorf("failed to overwrite chunk: %w", err)
   146  			}
   147  
   148  			out, err = os.OpenFile(dst, openMode, Perm)
   149  			defer out.Close()
   150  			if err != nil {
   151  				util.Log().Warning("Failed to create or open file: %s", err)
   152  				return err
   153  			}
   154  		}
   155  	}
   156  
   157  	// 写入文件内容
   158  	_, err = io.Copy(out, file)
   159  	return err
   160  }
   161  
   162  func (handler Driver) Truncate(ctx context.Context, src string, size uint64) error {
   163  	util.Log().Warning("Truncate file %q to [%d].", src, size)
   164  	out, err := os.OpenFile(src, os.O_WRONLY, Perm)
   165  	if err != nil {
   166  		util.Log().Warning("Failed to open file: %s", err)
   167  		return err
   168  	}
   169  
   170  	defer out.Close()
   171  	return out.Truncate(int64(size))
   172  }
   173  
   174  // Delete 删除一个或多个文件,
   175  // 返回未删除的文件,及遇到的最后一个错误
   176  func (handler Driver) Delete(ctx context.Context, files []string) ([]string, error) {
   177  	deleteFailed := make([]string, 0, len(files))
   178  	var retErr error
   179  
   180  	for _, value := range files {
   181  		filePath := util.RelativePath(filepath.FromSlash(value))
   182  		if util.Exists(filePath) {
   183  			err := os.Remove(filePath)
   184  			if err != nil {
   185  				util.Log().Warning("Failed to delete file: %s", err)
   186  				retErr = err
   187  				deleteFailed = append(deleteFailed, value)
   188  			}
   189  		}
   190  
   191  		// 尝试删除文件的缩略图(如果有)
   192  		_ = os.Remove(util.RelativePath(value + model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb")))
   193  	}
   194  
   195  	return deleteFailed, retErr
   196  }
   197  
   198  // Thumb 获取文件缩略图
   199  func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
   200  	// Quick check thumb existence on master.
   201  	if conf.SystemConfig.Mode == "master" && file.MetadataSerialized[model.ThumbStatusMetadataKey] == model.ThumbStatusNotExist {
   202  		// Tell invoker to generate a thumb
   203  		return nil, driver.ErrorThumbNotExist
   204  	}
   205  
   206  	thumbFile, err := handler.Get(ctx, file.ThumbFile())
   207  	if err != nil {
   208  		if errors.Is(err, os.ErrNotExist) {
   209  			err = fmt.Errorf("thumb not exist: %w (%w)", err, driver.ErrorThumbNotExist)
   210  		}
   211  
   212  		return nil, err
   213  	}
   214  
   215  	return &response.ContentResponse{
   216  		Redirect: false,
   217  		Content:  thumbFile,
   218  	}, nil
   219  }
   220  
   221  // Source 获取外链URL
   222  func (handler Driver) Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error) {
   223  	file, ok := ctx.Value(fsctx.FileModelCtx).(model.File)
   224  	if !ok {
   225  		return "", errors.New("failed to read file model context")
   226  	}
   227  
   228  	var baseURL *url.URL
   229  	// 是否启用了CDN
   230  	if handler.Policy.BaseURL != "" {
   231  		cdnURL, err := url.Parse(handler.Policy.BaseURL)
   232  		if err != nil {
   233  			return "", err
   234  		}
   235  		baseURL = cdnURL
   236  	}
   237  
   238  	var (
   239  		signedURI *url.URL
   240  		err       error
   241  	)
   242  	if isDownload {
   243  		// 创建下载会话,将文件信息写入缓存
   244  		downloadSessionID := util.RandStringRunes(16)
   245  		err = cache.Set("download_"+downloadSessionID, file, int(ttl))
   246  		if err != nil {
   247  			return "", serializer.NewError(serializer.CodeCacheOperation, "Failed to create download session", err)
   248  		}
   249  
   250  		// 签名生成文件记录
   251  		signedURI, err = auth.SignURI(
   252  			auth.General,
   253  			fmt.Sprintf("/api/v3/file/download/%s", downloadSessionID),
   254  			ttl,
   255  		)
   256  	} else {
   257  		// 签名生成文件记录
   258  		signedURI, err = auth.SignURI(
   259  			auth.General,
   260  			fmt.Sprintf("/api/v3/file/get/%d/%s", file.ID, file.Name),
   261  			ttl,
   262  		)
   263  	}
   264  
   265  	if err != nil {
   266  		return "", serializer.NewError(serializer.CodeEncryptError, "Failed to sign url", err)
   267  	}
   268  
   269  	finalURL := signedURI.String()
   270  	if baseURL != nil {
   271  		finalURL = baseURL.ResolveReference(signedURI).String()
   272  	}
   273  
   274  	return finalURL, nil
   275  }
   276  
   277  // Token 获取上传策略和认证Token,本地策略直接返回空值
   278  func (handler Driver) Token(ctx context.Context, ttl int64, uploadSession *serializer.UploadSession, file fsctx.FileHeader) (*serializer.UploadCredential, error) {
   279  	if util.Exists(uploadSession.SavePath) {
   280  		return nil, errors.New("placeholder file already exist")
   281  	}
   282  
   283  	return &serializer.UploadCredential{
   284  		SessionID: uploadSession.Key,
   285  		ChunkSize: handler.Policy.OptionsSerialized.ChunkSize,
   286  	}, nil
   287  }
   288  
   289  // 取消上传凭证
   290  func (handler Driver) CancelToken(ctx context.Context, uploadSession *serializer.UploadSession) error {
   291  	return nil
   292  }