package service import ( "bytes" "context" "errors" "fmt" "io" "os" "path/filepath" "strings" "dubbo.apache.org/dubbo-go/v3/common/logger" filesApi "github.com/filebrowser/filebrowser/v2/api/files" "github.com/filebrowser/filebrowser/v2/files" "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/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) return new(filesApi.CreateResp), fs.MkdirAll(req.Path, files.PermDir) } 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: 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, }, nil } default: return nil, fmt.Errorf("目前只支持image类型的预览") } } 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 getFs(UserSpacePath string) afero.Fs { bashAbs, _ := filepath.Abs(BASE_PATH) if !strings.HasPrefix(UserSpacePath, "/") { UserSpacePath = "/" + UserSpacePath } return afero.NewBasePathFs(afero.NewOsFs(), bashAbs+UserSpacePath) }