package service import ( "bytes" "context" "errors" "fmt" "io" "log" "os" "path" "path/filepath" "strings" "dubbo.apache.org/dubbo-go/v3/common/logger" "github.com/dubbogo/grpc-go/metadata" filesApi "github.com/filebrowser/filebrowser/v2/api/files" fbErrors "github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/files" "github.com/filebrowser/filebrowser/v2/fileutils" "github.com/filebrowser/filebrowser/v2/http" "github.com/filebrowser/filebrowser/v2/img" "github.com/filebrowser/filebrowser/v2/rules" "github.com/filebrowser/filebrowser/v2/search" "github.com/mholt/archiver/v3" "github.com/samber/lo" "github.com/spf13/afero" ) type FilesProvider struct { filesApi.UnimplementedFileServer } const BASE_PATH = "../" var imgSvc = img.New(4) // 图片预览可用协程数 func (f *FilesProvider) List(ctx context.Context, req *filesApi.FileListReq) (*filesApi.FileListResp, error) { file, err := files.NewFileInfo(&files.FileOptions{ Fs: getFs(req.UserSpacePath), Path: req.Path, Modify: true, Expand: true, ReadHeader: true, Content: true, Checker: rules.EmptyChecker, }) if err != nil { return nil, err } if !file.IsDir { return nil, errors.New("路径不是文件夹") } file.Listing.Sorting.Asc = req.Sorting.Asc file.Listing.Sorting.By = req.Sorting.By file.Listing.ApplySort() result := &filesApi.FileListResp{ NumDirs: int32(file.NumDirs), NumFiles: int32(file.NumFiles), Sorting: &filesApi.Sorting{ By: file.Sorting.By, Asc: file.Sorting.Asc, }, Path: file.Path, Name: file.Name, Size: file.Size, Extension: file.Extension, Mode: file.Mode.String(), IsDir: file.IsDir, IsSymlink: file.IsSymlink, Type: file.Type, Items: make([]*filesApi.Items, len(file.Listing.Items)), } for _, v := range file.Listing.Items { result.Items = append(result.Items, &filesApi.Items{ Path: v.Path, Name: v.Name, Size: v.Size, Extension: v.Extension, Mode: v.Mode.String(), IsDir: v.IsDir, IsSymlink: v.IsSymlink, Type: v.Type, }) } return result, err } func (f *FilesProvider) Create(ctx context.Context, req *filesApi.CreateReq) (*filesApi.CreateResp, error) { fs := getFs(req.UserSpacePath) if strings.HasSuffix(req.Path, "/") { return new(filesApi.CreateResp), fs.MkdirAll(req.Path, files.PermDir) } _, err := fs.Create(req.Path) return new(filesApi.CreateResp), err } func (f *FilesProvider) Delete(ctx context.Context, req *filesApi.DeleteReq) (*filesApi.DeleteResp, error) { fs := getFs(req.UserSpacePath) return new(filesApi.DeleteResp), fs.RemoveAll(req.Path) } func (f *FilesProvider) Upload(ctx context.Context, req *filesApi.UploadReq) (*filesApi.UploadResp, error) { fs := getFs(req.UserSpacePath) fi, err := fs.Create(req.Path) if err != nil { return nil, err } defer fi.Close() _, err = fi.Write(req.Content) if err != nil { return nil, err } return new(filesApi.UploadResp), nil } func (f *FilesProvider) Search(ctx context.Context, req *filesApi.SearchReq) (*filesApi.SearchResp, error) { fs := getFs(req.UserSpacePath) result := new(filesApi.SearchResp) err := search.Search(fs, req.Path, req.Query, rules.EmptyChecker, func(path string, f os.FileInfo) error { result.Items = append(result.Items, &filesApi.SearchResp_Nested{ Dir: f.IsDir(), Path: path, }) return nil }) if err != nil { return nil, err } return result, err } func (f *FilesProvider) TusCreate(ctx context.Context, req *filesApi.TusCreateReq) (*filesApi.TusCreateResp, error) { fs := getFs(req.UserSpacePath) file, err := files.NewFileInfo(&files.FileOptions{ Fs: fs, Path: req.Path, Modify: true, Expand: false, ReadHeader: true, Checker: rules.EmptyChecker, }) switch { case errors.Is(err, afero.ErrFileNotFound): dirPath := filepath.Dir(req.Path) if _, statErr := fs.Stat(dirPath); os.IsNotExist(statErr) { if mkdirErr := fs.MkdirAll(dirPath, files.PermDir); mkdirErr != nil { return nil, err } } case err != nil: return nil, err } fileFlags := os.O_CREATE | os.O_WRONLY if req.Override { fileFlags |= os.O_TRUNC } // if file exists if file != nil { if file.IsDir { return nil, fmt.Errorf("上传路径不能是文件夹 %s", file.RealPath()) } } openFile, err := fs.OpenFile(req.Path, fileFlags, files.PermFile) if err != nil { return nil, err } if err := openFile.Close(); err != nil { return nil, err } logger.Infof("创建空文件,%v", openFile.Name()) return &filesApi.TusCreateResp{ UploadLength: -1, UploadOffset: 0, }, nil } func (f *FilesProvider) TusUpload(ctx context.Context, req *filesApi.TusUploadReq) (*filesApi.TusUploadResp, error) { fs := getFs(req.UserSpacePath) file, err := files.NewFileInfo(&files.FileOptions{ Fs: fs, Path: req.Path, Modify: true, Expand: false, ReadHeader: true, Checker: rules.EmptyChecker, }) switch { case errors.Is(err, afero.ErrFileNotFound): return nil, errors.New("未找到文件") case err != nil: return nil, err } switch { case file.IsDir: return nil, fmt.Errorf("上传路径不能是文件夹 %s", file.RealPath()) case file.Size != req.UploadOffset: return nil, fmt.Errorf( "%s 文件大小符合: %d", file.RealPath(), req.UploadOffset, ) } openFile, err := fs.OpenFile(req.Path, os.O_WRONLY|os.O_APPEND, files.PermFile) if err != nil { return nil, fmt.Errorf("could not open file: %w", err) } defer openFile.Close() _, err = openFile.Seek(req.UploadOffset, 0) if err != nil { return nil, fmt.Errorf("could not seek file: %w", err) } logger.Infof("写入文件块,offset:%v,块大小:%vbyte", req.UploadOffset, len(req.Content)) bytesWritten, err := io.Copy(openFile, bytes.NewBuffer(req.Content)) if err != nil { return nil, fmt.Errorf("could not write to file: %w", err) } return &filesApi.TusUploadResp{ UploadOffset: req.UploadOffset + bytesWritten, }, nil } func (f *FilesProvider) Info(ctx context.Context, req *filesApi.FileInfoReq) (*filesApi.FileInfoResp, error) { fs := getFs(req.UserSpacePath) file, err := files.NewFileInfo(&files.FileOptions{ Fs: fs, Path: req.Path, Modify: true, Expand: false, ReadHeader: true, Checker: rules.EmptyChecker, }) switch { case errors.Is(err, afero.ErrFileNotFound): return nil, errors.New("未找到文件") case err != nil: return nil, err } result := &filesApi.FileInfoResp{ Path: file.Path, Name: file.Name, Size: file.Size, Extension: file.Extension, Mode: file.Mode.String(), IsDir: file.IsDir, IsSymlink: file.IsSymlink, Type: file.Type, } return result, nil } func (f *FilesProvider) ResumableTransfer(ctx context.Context, req *filesApi.ResumableTransferReq) (*filesApi.ResumableTransferResp, error) { fs := getFs(req.UserSpacePath) _, err := files.NewFileInfo(&files.FileOptions{ Fs: fs, Path: req.Path, Modify: true, Expand: false, ReadHeader: true, Checker: rules.EmptyChecker, }) switch { case errors.Is(err, afero.ErrFileNotFound): return nil, errors.New("未找到文件") case err != nil: return nil, err } openFile, err := fs.Open(req.Path) if err != nil { return nil, fmt.Errorf("could not open file: %w", err) } defer openFile.Close() logger.Debugf("设置文件读取,offset:%v,length:%v", req.Offset, req.Length) _, err = openFile.Seek(req.Offset, 0) if err != nil { return nil, err } b := make([]byte, req.Length) n, err := openFile.Read(b) if err != nil { return nil, err } return &filesApi.ResumableTransferResp{ Content: b[:n], }, nil } func (f *FilesProvider) Preview(ctx context.Context, req *filesApi.PreviewReq) (*filesApi.PreviewResp, error) { fs := getFs(req.UserSpacePath) file, err := files.NewFileInfo(&files.FileOptions{ Fs: fs, Path: req.Path, Modify: true, Expand: true, ReadHeader: true, Checker: rules.EmptyChecker, }) if err != nil { return nil, err } switch file.Type { case "image": { b, err := f.createPreview(imgSvc, file, req.Size) if err != nil { return nil, err } return &filesApi.PreviewResp{ Content: b, FileName: file.Name, ModTime: file.ModTime.UnixMilli(), }, nil } default: return nil, errors.New("目前只支持图片类型的预览") } } func (f *FilesProvider) createPreview(imgSvc http.ImgService, file *files.FileInfo, previewSize uint32) ([]byte, error) { fd, err := file.Fs.Open(file.Path) if err != nil { return nil, err } defer fd.Close() var ( width int height int options []img.Option ) switch { case previewSize == 1: width = 1080 height = 1080 options = append(options, img.WithMode(img.ResizeModeFit), img.WithQuality(img.QualityMedium)) case previewSize == 0: width = 256 height = 256 options = append(options, img.WithMode(img.ResizeModeFill), img.WithQuality(img.QualityLow), img.WithFormat(img.FormatJpeg)) default: return nil, img.ErrUnsupportedFormat } buf := &bytes.Buffer{} if err := imgSvc.Resize(context.Background(), fd, width, height, buf, options...); err != nil { return nil, err } return buf.Bytes(), nil } func (f *FilesProvider) Action(_ context.Context, req *filesApi.ActionReq) (*filesApi.ActionResp, error) { fs := getFs(req.UserSpacePath) if req.Destination == "/" || req.Path == "/" { return nil, errors.New("禁止操作用户空间根目录") } err := checkParent(req.Path, req.Destination) if err != nil { return nil, err } if !req.Override && !req.Rename { if _, err = fs.Stat(req.Destination); err == nil { return nil, err } } if req.Rename { req.Destination = addVersionSuffix(req.Destination, fs) } return &filesApi.ActionResp{}, patchAction(req.Action, req.Path, req.Destination, fs) } func (f *FilesProvider) DirDownload(req *filesApi.DirDownloadReq, stream filesApi.File_DirDownloadServer) error { var filenames []string writer := newFileDownloadWriter(stream) fs := getFs(req.UserSpacePath) file, err := files.NewFileInfo(&files.FileOptions{ Fs: fs, Path: req.Path, Modify: true, Expand: false, ReadHeader: true, Checker: rules.EmptyChecker, }) if err != nil { return err } if len(req.Files) == 0 { filenames = append(filenames, file.Path) } else { filenames = lo.Map(req.Files, func(name string, _ int) string { return filepath.Join(req.Path, slashClean(name)) }) } extension, ar, err := parseQueryAlgorithm(req.Algo) if err != nil { return err } err = ar.Create(writer) if err != nil { return err } defer ar.Close() commonDir := fileutils.CommonPrefix(filepath.Separator, filenames...) name := filepath.Base(commonDir) if name == "." || name == "" || name == string(filepath.Separator) { name = file.Name } // Prefix used to distinguish a filelist generated // archive from the full directory archive if len(filenames) > 1 { name = "_" + name } name += extension stream.SendHeader(metadata.MD{ "filename": []string{name}, }) for _, fname := range filenames { err = addFile(ar, fs, fname, commonDir) if err != nil { log.Printf("Failed to archive %s: %v", fname, err) } } return nil } type fileDownloadWriter struct { // 用于实现archiver.Writer.Create(out io.Writer) error stream filesApi.File_DirDownloadServer } func (f *fileDownloadWriter) Write(p []byte) (n int, err error) { err = f.stream.Send(&filesApi.DirDownloadResp{ Content: p, }) if err == nil { n = len(p) } return } func newFileDownloadWriter(stream filesApi.File_DirDownloadServer) *fileDownloadWriter { return &fileDownloadWriter{ stream: stream, } } func getFs(userSpacePath string) afero.Fs { bashAbs, _ := filepath.Abs(BASE_PATH) if !strings.HasPrefix(userSpacePath, "/") { userSpacePath = "/" + userSpacePath } _, err := os.Stat(bashAbs + userSpacePath) if err != nil { os.MkdirAll(bashAbs+userSpacePath, os.ModeDir) } return afero.NewBasePathFs(afero.NewOsFs(), bashAbs+userSpacePath) } func checkParent(src, dst string) error { rel, err := filepath.Rel(src, dst) if err != nil { return err } rel = filepath.ToSlash(rel) if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." { return fbErrors.ErrSourceIsParent } return nil } func addVersionSuffix(source string, fs afero.Fs) string { counter := 1 dir, name := path.Split(source) ext := filepath.Ext(name) base := strings.TrimSuffix(name, ext) for { if _, err := fs.Stat(source); err != nil { break } renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext) source = path.Join(dir, renamed) counter++ } return source } func patchAction(action, src, dst string, fs afero.Fs) error { switch action { case "copy": return fileutils.Copy(fs, src, dst) case "rename": src = path.Clean("/" + src) dst = path.Clean("/" + dst) _, err := files.NewFileInfo(&files.FileOptions{ Fs: fs, Path: src, Modify: true, Expand: false, ReadHeader: false, Checker: rules.EmptyChecker, }) if err != nil { return err } return fileutils.MoveFile(fs, src, dst) default: return fmt.Errorf("unsupported action %s: %w", action, fbErrors.ErrInvalidRequestParams) } } func slashClean(name string) string { if name == "" || name[0] != '/' { name = "/" + name } return path.Clean(name) } func parseQueryAlgorithm(algo string) (string, archiver.Writer, error) { switch algo { case "zip", "true", "": return ".zip", archiver.NewZip(), nil case "tar": return ".tar", archiver.NewTar(), nil case "targz": return ".tar.gz", archiver.NewTarGz(), nil case "tarbz2": return ".tar.bz2", archiver.NewTarBz2(), nil case "tarxz": return ".tar.xz", archiver.NewTarXz(), nil case "tarlz4": return ".tar.lz4", archiver.NewTarLz4(), nil case "tarsz": return ".tar.sz", archiver.NewTarSz(), nil default: return "", nil, errors.New("format not implemented") } } func addFile(ar archiver.Writer, fs afero.Fs, path, commonPath string) error { info, err := fs.Stat(path) if err != nil { return err } if !info.IsDir() && !info.Mode().IsRegular() { return nil } file, err := fs.Open(path) if err != nil { return err } defer file.Close() if path != commonPath { filename := strings.TrimPrefix(path, commonPath) filename = strings.TrimPrefix(filename, string(filepath.Separator)) err = ar.Write(archiver.File{ FileInfo: archiver.FileInfo{ FileInfo: info, CustomName: filename, }, ReadCloser: file, }) if err != nil { return err } } if info.IsDir() { names, err := file.Readdirnames(0) if err != nil { return err } for _, name := range names { fPath := filepath.Join(path, name) err = addFile(ar, fs, fPath, commonPath) if err != nil { log.Printf("Failed to archive %s: %v", fPath, err) } } } return nil }