| | package local |
| |
|
| | import ( |
| | "bytes" |
| | "context" |
| | "errors" |
| | "fmt" |
| | "io/fs" |
| | "net/http" |
| | "os" |
| | stdpath "path" |
| | "path/filepath" |
| | "strconv" |
| | "strings" |
| | "time" |
| |
|
| | "github.com/alist-org/alist/v3/internal/conf" |
| | "github.com/alist-org/alist/v3/internal/driver" |
| | "github.com/alist-org/alist/v3/internal/errs" |
| | "github.com/alist-org/alist/v3/internal/model" |
| | "github.com/alist-org/alist/v3/internal/sign" |
| | "github.com/alist-org/alist/v3/pkg/utils" |
| | "github.com/alist-org/alist/v3/server/common" |
| | "github.com/alist-org/times" |
| | cp "github.com/otiai10/copy" |
| | log "github.com/sirupsen/logrus" |
| | _ "golang.org/x/image/webp" |
| | ) |
| |
|
| | type Local struct { |
| | model.Storage |
| | Addition |
| | mkdirPerm int32 |
| |
|
| | |
| | thumbConcurrency int |
| | thumbTokenBucket TokenBucket |
| | } |
| |
|
| | func (d *Local) Config() driver.Config { |
| | return config |
| | } |
| |
|
| | func (d *Local) Init(ctx context.Context) error { |
| | if d.MkdirPerm == "" { |
| | d.mkdirPerm = 0777 |
| | } else { |
| | v, err := strconv.ParseUint(d.MkdirPerm, 8, 32) |
| | if err != nil { |
| | return err |
| | } |
| | d.mkdirPerm = int32(v) |
| | } |
| | if !utils.Exists(d.GetRootPath()) { |
| | return fmt.Errorf("root folder %s not exists", d.GetRootPath()) |
| | } |
| | if !filepath.IsAbs(d.GetRootPath()) { |
| | abs, err := filepath.Abs(d.GetRootPath()) |
| | if err != nil { |
| | return err |
| | } |
| | d.Addition.RootFolderPath = abs |
| | } |
| | if d.ThumbCacheFolder != "" && !utils.Exists(d.ThumbCacheFolder) { |
| | err := os.MkdirAll(d.ThumbCacheFolder, os.FileMode(d.mkdirPerm)) |
| | if err != nil { |
| | return err |
| | } |
| | } |
| | if d.ThumbConcurrency != "" { |
| | v, err := strconv.ParseUint(d.ThumbConcurrency, 10, 32) |
| | if err != nil { |
| | return err |
| | } |
| | d.thumbConcurrency = int(v) |
| | } |
| | if d.thumbConcurrency == 0 { |
| | d.thumbTokenBucket = NewNopTokenBucket() |
| | } else { |
| | d.thumbTokenBucket = NewStaticTokenBucketWithMigration(d.thumbTokenBucket, d.thumbConcurrency) |
| | } |
| | return nil |
| | } |
| |
|
| | func (d *Local) Drop(ctx context.Context) error { |
| | return nil |
| | } |
| |
|
| | func (d *Local) GetAddition() driver.Additional { |
| | return &d.Addition |
| | } |
| |
|
| | func (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { |
| | fullPath := dir.GetPath() |
| | rawFiles, err := readDir(fullPath) |
| | if err != nil { |
| | return nil, err |
| | } |
| | var files []model.Obj |
| | for _, f := range rawFiles { |
| | if !d.ShowHidden && strings.HasPrefix(f.Name(), ".") { |
| | continue |
| | } |
| | file := d.FileInfoToObj(ctx, f, args.ReqPath, fullPath) |
| | files = append(files, file) |
| | } |
| | return files, nil |
| | } |
| | func (d *Local) FileInfoToObj(ctx context.Context, f fs.FileInfo, reqPath string, fullPath string) model.Obj { |
| | thumb := "" |
| | if d.Thumbnail { |
| | typeName := utils.GetFileType(f.Name()) |
| | if typeName == conf.IMAGE || typeName == conf.VIDEO { |
| | thumb = common.GetApiUrl(common.GetHttpReq(ctx)) + stdpath.Join("/d", reqPath, f.Name()) |
| | thumb = utils.EncodePath(thumb, true) |
| | thumb += "?type=thumb&sign=" + sign.Sign(stdpath.Join(reqPath, f.Name())) |
| | } |
| | } |
| | isFolder := f.IsDir() || isSymlinkDir(f, fullPath) |
| | var size int64 |
| | if !isFolder { |
| | size = f.Size() |
| | } |
| | var ctime time.Time |
| | t, err := times.Stat(stdpath.Join(fullPath, f.Name())) |
| | if err == nil { |
| | if t.HasBirthTime() { |
| | ctime = t.BirthTime() |
| | } |
| | } |
| |
|
| | file := model.ObjThumb{ |
| | Object: model.Object{ |
| | Path: filepath.Join(fullPath, f.Name()), |
| | Name: f.Name(), |
| | Modified: f.ModTime(), |
| | Size: size, |
| | IsFolder: isFolder, |
| | Ctime: ctime, |
| | }, |
| | Thumbnail: model.Thumbnail{ |
| | Thumbnail: thumb, |
| | }, |
| | } |
| | return &file |
| | } |
| | func (d *Local) GetMeta(ctx context.Context, path string) (model.Obj, error) { |
| | f, err := os.Stat(path) |
| | if err != nil { |
| | return nil, err |
| | } |
| | file := d.FileInfoToObj(ctx, f, path, path) |
| | |
| | |
| | |
| | |
| | return file, nil |
| |
|
| | } |
| |
|
| | func (d *Local) Get(ctx context.Context, path string) (model.Obj, error) { |
| | path = filepath.Join(d.GetRootPath(), path) |
| | f, err := os.Stat(path) |
| | if err != nil { |
| | if strings.Contains(err.Error(), "cannot find the file") { |
| | return nil, errs.ObjectNotFound |
| | } |
| | return nil, err |
| | } |
| | isFolder := f.IsDir() || isSymlinkDir(f, path) |
| | size := f.Size() |
| | if isFolder { |
| | size = 0 |
| | } |
| | var ctime time.Time |
| | t, err := times.Stat(path) |
| | if err == nil { |
| | if t.HasBirthTime() { |
| | ctime = t.BirthTime() |
| | } |
| | } |
| | file := model.Object{ |
| | Path: path, |
| | Name: f.Name(), |
| | Modified: f.ModTime(), |
| | Ctime: ctime, |
| | Size: size, |
| | IsFolder: isFolder, |
| | } |
| | return &file, nil |
| | } |
| |
|
| | func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { |
| | fullPath := file.GetPath() |
| | var link model.Link |
| | if args.Type == "thumb" && utils.Ext(file.GetName()) != "svg" { |
| | var buf *bytes.Buffer |
| | var thumbPath *string |
| | err := d.thumbTokenBucket.Do(ctx, func() error { |
| | var err error |
| | buf, thumbPath, err = d.getThumb(file) |
| | return err |
| | }) |
| | if err != nil { |
| | return nil, err |
| | } |
| | link.Header = http.Header{ |
| | "Content-Type": []string{"image/png"}, |
| | } |
| | if thumbPath != nil { |
| | open, err := os.Open(*thumbPath) |
| | if err != nil { |
| | return nil, err |
| | } |
| | link.MFile = open |
| | } else { |
| | link.MFile = model.NewNopMFile(bytes.NewReader(buf.Bytes())) |
| | |
| | } |
| | } else { |
| | open, err := os.Open(fullPath) |
| | if err != nil { |
| | return nil, err |
| | } |
| | link.MFile = open |
| | } |
| | return &link, nil |
| | } |
| |
|
| | func (d *Local) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { |
| | fullPath := filepath.Join(parentDir.GetPath(), dirName) |
| | err := os.MkdirAll(fullPath, os.FileMode(d.mkdirPerm)) |
| | if err != nil { |
| | return err |
| | } |
| | return nil |
| | } |
| |
|
| | func (d *Local) Move(ctx context.Context, srcObj, dstDir model.Obj) error { |
| | srcPath := srcObj.GetPath() |
| | dstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName()) |
| | if utils.IsSubPath(srcPath, dstPath) { |
| | return fmt.Errorf("the destination folder is a subfolder of the source folder") |
| | } |
| | if err := os.Rename(srcPath, dstPath); err != nil && strings.Contains(err.Error(), "invalid cross-device link") { |
| | |
| | if err = d.Copy(ctx, srcObj, dstDir); err != nil { |
| | return err |
| | } else { |
| | |
| | if srcObj.IsDir() { |
| | err = os.RemoveAll(srcObj.GetPath()) |
| | } else { |
| | err = os.Remove(srcObj.GetPath()) |
| | } |
| | return err |
| | } |
| | } else { |
| | return err |
| | } |
| | } |
| |
|
| | func (d *Local) Rename(ctx context.Context, srcObj model.Obj, newName string) error { |
| | srcPath := srcObj.GetPath() |
| | dstPath := filepath.Join(filepath.Dir(srcPath), newName) |
| | err := os.Rename(srcPath, dstPath) |
| | if err != nil { |
| | return err |
| | } |
| | return nil |
| | } |
| |
|
| | func (d *Local) Copy(_ context.Context, srcObj, dstDir model.Obj) error { |
| | srcPath := srcObj.GetPath() |
| | dstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName()) |
| | if utils.IsSubPath(srcPath, dstPath) { |
| | return fmt.Errorf("the destination folder is a subfolder of the source folder") |
| | } |
| | |
| | return cp.Copy(srcPath, dstPath, cp.Options{ |
| | Sync: true, |
| | PreserveTimes: true, |
| | PreserveOwner: true, |
| | }) |
| | } |
| |
|
| | func (d *Local) Remove(ctx context.Context, obj model.Obj) error { |
| | var err error |
| | if utils.SliceContains([]string{"", "delete permanently"}, d.RecycleBinPath) { |
| | if obj.IsDir() { |
| | err = os.RemoveAll(obj.GetPath()) |
| | } else { |
| | err = os.Remove(obj.GetPath()) |
| | } |
| | } else { |
| | dstPath := filepath.Join(d.RecycleBinPath, obj.GetName()) |
| | if utils.Exists(dstPath) { |
| | dstPath = filepath.Join(d.RecycleBinPath, obj.GetName()+"_"+time.Now().Format("20060102150405")) |
| | } |
| | err = os.Rename(obj.GetPath(), dstPath) |
| | } |
| | if err != nil { |
| | return err |
| | } |
| | return nil |
| | } |
| |
|
| | func (d *Local) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { |
| | fullPath := filepath.Join(dstDir.GetPath(), stream.GetName()) |
| | out, err := os.Create(fullPath) |
| | if err != nil { |
| | return err |
| | } |
| | defer func() { |
| | _ = out.Close() |
| | if errors.Is(err, context.Canceled) { |
| | _ = os.Remove(fullPath) |
| | } |
| | }() |
| | err = utils.CopyWithCtx(ctx, out, stream, stream.GetSize(), up) |
| | if err != nil { |
| | return err |
| | } |
| | err = os.Chtimes(fullPath, stream.ModTime(), stream.ModTime()) |
| | if err != nil { |
| | log.Errorf("[local] failed to change time of %s: %s", fullPath, err) |
| | } |
| | return nil |
| | } |
| |
|
| | var _ driver.Driver = (*Local)(nil) |
| |
|