2025-05-22 06:26:07 +00:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
2025-05-22 08:23:11 +00:00
|
|
|
|
"bytes"
|
2025-05-22 06:26:07 +00:00
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
2025-05-22 08:23:11 +00:00
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2025-06-03 02:50:42 +00:00
|
|
|
|
"log"
|
2025-05-22 06:26:07 +00:00
|
|
|
|
"os"
|
2025-05-30 08:17:28 +00:00
|
|
|
|
"path"
|
2025-05-22 06:26:07 +00:00
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2025-05-23 03:31:46 +00:00
|
|
|
|
"dubbo.apache.org/dubbo-go/v3/common/logger"
|
2025-06-03 02:50:42 +00:00
|
|
|
|
"github.com/dubbogo/grpc-go/metadata"
|
2025-05-22 06:26:07 +00:00
|
|
|
|
filesApi "github.com/filebrowser/filebrowser/v2/api/files"
|
2025-05-30 08:17:28 +00:00
|
|
|
|
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
2025-05-22 06:26:07 +00:00
|
|
|
|
"github.com/filebrowser/filebrowser/v2/files"
|
2025-05-30 08:17:28 +00:00
|
|
|
|
"github.com/filebrowser/filebrowser/v2/fileutils"
|
2025-05-26 03:03:37 +00:00
|
|
|
|
"github.com/filebrowser/filebrowser/v2/http"
|
|
|
|
|
"github.com/filebrowser/filebrowser/v2/img"
|
2025-05-22 06:26:07 +00:00
|
|
|
|
"github.com/filebrowser/filebrowser/v2/rules"
|
|
|
|
|
"github.com/filebrowser/filebrowser/v2/search"
|
2025-06-03 02:50:42 +00:00
|
|
|
|
"github.com/mholt/archiver/v3"
|
|
|
|
|
"github.com/samber/lo"
|
2025-05-22 06:26:07 +00:00
|
|
|
|
"github.com/spf13/afero"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type FilesProvider struct {
|
|
|
|
|
filesApi.UnimplementedFileServer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const BASE_PATH = "../"
|
|
|
|
|
|
2025-05-26 03:03:37 +00:00
|
|
|
|
var imgSvc = img.New(4) // 图片预览可用协程数
|
|
|
|
|
|
2025-05-22 06:26:07 +00:00
|
|
|
|
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)
|
2025-06-03 07:52:13 +00:00
|
|
|
|
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
|
2025-05-22 06:26:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2025-05-22 08:23:11 +00:00
|
|
|
|
defer fi.Close()
|
2025-05-22 06:26:07 +00:00
|
|
|
|
_, 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
|
|
|
|
|
}
|
2025-05-22 08:23:11 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
2025-05-23 03:31:46 +00:00
|
|
|
|
logger.Infof("创建空文件,%v", openFile.Name())
|
2025-05-22 08:23:11 +00:00
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 03:31:46 +00:00
|
|
|
|
logger.Infof("写入文件块,offset:%v,块大小:%vbyte", req.UploadOffset, len(req.Content))
|
2025-05-22 08:23:11 +00:00
|
|
|
|
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{
|
2025-06-03 07:52:13 +00:00
|
|
|
|
UploadOffset: req.UploadOffset + bytesWritten,
|
2025-05-22 08:23:11 +00:00
|
|
|
|
}, nil
|
2025-05-22 06:26:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 02:35:22 +00:00
|
|
|
|
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()
|
2025-05-23 03:31:46 +00:00
|
|
|
|
logger.Debugf("设置文件读取,offset:%v,length:%v", req.Offset, req.Length)
|
2025-05-23 02:35:22 +00:00
|
|
|
|
_, 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-26 03:03:37 +00:00
|
|
|
|
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{
|
2025-06-03 07:52:13 +00:00
|
|
|
|
Content: b,
|
|
|
|
|
FileName: file.Name,
|
|
|
|
|
ModTime: file.ModTime.UnixMilli(),
|
2025-05-26 03:03:37 +00:00
|
|
|
|
}, nil
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
default:
|
2025-06-03 02:50:42 +00:00
|
|
|
|
return nil, errors.New("目前只支持图片类型的预览")
|
2025-05-26 03:03:37 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 02:50:42 +00:00
|
|
|
|
func (f *FilesProvider) createPreview(imgSvc http.ImgService,
|
2025-05-26 03:03:37 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
2025-05-30 08:17:28 +00:00
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 02:50:42 +00:00
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 06:10:33 +00:00
|
|
|
|
func getFs(userSpacePath string) afero.Fs {
|
2025-05-22 06:26:07 +00:00
|
|
|
|
bashAbs, _ := filepath.Abs(BASE_PATH)
|
2025-06-03 06:10:33 +00:00
|
|
|
|
|
|
|
|
|
if !strings.HasPrefix(userSpacePath, "/") {
|
|
|
|
|
userSpacePath = "/" + userSpacePath
|
|
|
|
|
}
|
|
|
|
|
_, err := os.Stat(bashAbs + userSpacePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
os.MkdirAll(bashAbs+userSpacePath, os.ModeDir)
|
2025-05-22 06:26:07 +00:00
|
|
|
|
}
|
2025-06-03 06:10:33 +00:00
|
|
|
|
return afero.NewBasePathFs(afero.NewOsFs(), bashAbs+userSpacePath)
|
2025-05-22 06:26:07 +00:00
|
|
|
|
}
|
2025-05-30 08:17:28 +00:00
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-03 02:50:42 +00:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|