mirror of https://github.com/Xhofe/alist
448 lines
14 KiB
Go
448 lines
14 KiB
Go
package op
|
|
|
|
import (
|
|
"context"
|
|
stderrors "errors"
|
|
"io"
|
|
stdpath "path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/alist-org/alist/v3/internal/archive/tool"
|
|
"github.com/alist-org/alist/v3/internal/stream"
|
|
|
|
"github.com/Xhofe/go-cache"
|
|
"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/pkg/singleflight"
|
|
"github.com/alist-org/alist/v3/pkg/utils"
|
|
"github.com/pkg/errors"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var archiveMetaCache = cache.NewMemCache(cache.WithShards[*model.ArchiveMetaProvider](64))
|
|
var archiveMetaG singleflight.Group[*model.ArchiveMetaProvider]
|
|
|
|
func GetArchiveMeta(ctx context.Context, storage driver.Driver, path string, args model.ArchiveMetaArgs) (*model.ArchiveMetaProvider, error) {
|
|
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
|
|
return nil, errors.Errorf("storage not init: %s", storage.GetStorage().Status)
|
|
}
|
|
path = utils.FixAndCleanPath(path)
|
|
key := Key(storage, path)
|
|
if !args.Refresh {
|
|
if meta, ok := archiveMetaCache.Get(key); ok {
|
|
log.Debugf("use cache when get %s archive meta", path)
|
|
return meta, nil
|
|
}
|
|
}
|
|
fn := func() (*model.ArchiveMetaProvider, error) {
|
|
_, m, err := getArchiveMeta(ctx, storage, path, args)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to get %s archive met: %+v", path, err)
|
|
}
|
|
if m.Expiration != nil {
|
|
archiveMetaCache.Set(key, m, cache.WithEx[*model.ArchiveMetaProvider](*m.Expiration))
|
|
}
|
|
return m, nil
|
|
}
|
|
if storage.Config().OnlyLocal {
|
|
meta, err := fn()
|
|
return meta, err
|
|
}
|
|
meta, err, _ := archiveMetaG.Do(key, fn)
|
|
return meta, err
|
|
}
|
|
|
|
func getArchiveToolAndStream(ctx context.Context, storage driver.Driver, path string, args model.LinkArgs) (model.Obj, tool.Tool, *stream.SeekableStream, error) {
|
|
l, obj, err := Link(ctx, storage, path, args)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.WithMessagef(err, "failed get [%s] link", path)
|
|
}
|
|
ext := stdpath.Ext(obj.GetName())
|
|
t, err := tool.GetArchiveTool(ext)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.WithMessagef(err, "failed get [%s] archive tool", ext)
|
|
}
|
|
ss, err := stream.NewSeekableStream(stream.FileStream{Ctx: ctx, Obj: obj}, l)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.WithMessagef(err, "failed get [%s] stream", path)
|
|
}
|
|
return obj, t, ss, nil
|
|
}
|
|
|
|
func getArchiveMeta(ctx context.Context, storage driver.Driver, path string, args model.ArchiveMetaArgs) (model.Obj, *model.ArchiveMetaProvider, error) {
|
|
storageAr, ok := storage.(driver.ArchiveReader)
|
|
if ok {
|
|
obj, err := GetUnwrap(ctx, storage, path)
|
|
if err != nil {
|
|
return nil, nil, errors.WithMessage(err, "failed to get file")
|
|
}
|
|
if obj.IsDir() {
|
|
return nil, nil, errors.WithStack(errs.NotFile)
|
|
}
|
|
meta, err := storageAr.GetArchiveMeta(ctx, obj, args.ArchiveArgs)
|
|
if !errors.Is(err, errs.NotImplement) {
|
|
archiveMetaProvider := &model.ArchiveMetaProvider{ArchiveMeta: meta, DriverProviding: true}
|
|
if meta.GetTree() != nil {
|
|
archiveMetaProvider.Sort = &storage.GetStorage().Sort
|
|
}
|
|
if !storage.Config().NoCache {
|
|
Expiration := time.Minute * time.Duration(storage.GetStorage().CacheExpiration)
|
|
archiveMetaProvider.Expiration = &Expiration
|
|
}
|
|
return obj, archiveMetaProvider, err
|
|
}
|
|
}
|
|
obj, t, ss, err := getArchiveToolAndStream(ctx, storage, path, args.LinkArgs)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer func() {
|
|
if err := ss.Close(); err != nil {
|
|
log.Errorf("failed to close file streamer, %v", err)
|
|
}
|
|
}()
|
|
meta, err := t.GetMeta(ss, args.ArchiveArgs)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
archiveMetaProvider := &model.ArchiveMetaProvider{ArchiveMeta: meta, DriverProviding: false}
|
|
if meta.GetTree() != nil {
|
|
archiveMetaProvider.Sort = &storage.GetStorage().Sort
|
|
}
|
|
if !storage.Config().NoCache {
|
|
Expiration := time.Minute * time.Duration(storage.GetStorage().CacheExpiration)
|
|
archiveMetaProvider.Expiration = &Expiration
|
|
} else if ss.Link.MFile == nil {
|
|
// alias、crypt 驱动
|
|
archiveMetaProvider.Expiration = ss.Link.Expiration
|
|
}
|
|
return obj, archiveMetaProvider, err
|
|
}
|
|
|
|
var archiveListCache = cache.NewMemCache(cache.WithShards[[]model.Obj](64))
|
|
var archiveListG singleflight.Group[[]model.Obj]
|
|
|
|
func ListArchive(ctx context.Context, storage driver.Driver, path string, args model.ArchiveListArgs) ([]model.Obj, error) {
|
|
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
|
|
return nil, errors.Errorf("storage not init: %s", storage.GetStorage().Status)
|
|
}
|
|
path = utils.FixAndCleanPath(path)
|
|
metaKey := Key(storage, path)
|
|
key := stdpath.Join(metaKey, args.InnerPath)
|
|
if !args.Refresh {
|
|
if files, ok := archiveListCache.Get(key); ok {
|
|
log.Debugf("use cache when list archive [%s]%s", path, args.InnerPath)
|
|
return files, nil
|
|
}
|
|
// if meta, ok := archiveMetaCache.Get(metaKey); ok {
|
|
// log.Debugf("use meta cache when list archive [%s]%s", path, args.InnerPath)
|
|
// return getChildrenFromArchiveMeta(meta, args.InnerPath)
|
|
// }
|
|
}
|
|
objs, err, _ := archiveListG.Do(key, func() ([]model.Obj, error) {
|
|
obj, files, err := listArchive(ctx, storage, path, args)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to list archive [%s]%s: %+v", path, args.InnerPath, err)
|
|
}
|
|
// set path
|
|
for _, f := range files {
|
|
if s, ok := f.(model.SetPath); ok && f.GetPath() == "" && obj.GetPath() != "" {
|
|
s.SetPath(stdpath.Join(obj.GetPath(), args.InnerPath, f.GetName()))
|
|
}
|
|
}
|
|
// warp obj name
|
|
model.WrapObjsName(files)
|
|
// sort objs
|
|
if storage.Config().LocalSort {
|
|
model.SortFiles(files, storage.GetStorage().OrderBy, storage.GetStorage().OrderDirection)
|
|
}
|
|
model.ExtractFolder(files, storage.GetStorage().ExtractFolder)
|
|
if !storage.Config().NoCache {
|
|
if len(files) > 0 {
|
|
log.Debugf("set cache: %s => %+v", key, files)
|
|
archiveListCache.Set(key, files, cache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))
|
|
} else {
|
|
log.Debugf("del cache: %s", key)
|
|
archiveListCache.Del(key)
|
|
}
|
|
}
|
|
return files, nil
|
|
})
|
|
return objs, err
|
|
}
|
|
|
|
func _listArchive(ctx context.Context, storage driver.Driver, path string, args model.ArchiveListArgs) (model.Obj, []model.Obj, error) {
|
|
storageAr, ok := storage.(driver.ArchiveReader)
|
|
if ok {
|
|
obj, err := GetUnwrap(ctx, storage, path)
|
|
if err != nil {
|
|
return nil, nil, errors.WithMessage(err, "failed to get file")
|
|
}
|
|
if obj.IsDir() {
|
|
return nil, nil, errors.WithStack(errs.NotFile)
|
|
}
|
|
files, err := storageAr.ListArchive(ctx, obj, args.ArchiveInnerArgs)
|
|
if !errors.Is(err, errs.NotImplement) {
|
|
return obj, files, err
|
|
}
|
|
}
|
|
obj, t, ss, err := getArchiveToolAndStream(ctx, storage, path, args.LinkArgs)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer func() {
|
|
if err := ss.Close(); err != nil {
|
|
log.Errorf("failed to close file streamer, %v", err)
|
|
}
|
|
}()
|
|
files, err := t.List(ss, args.ArchiveInnerArgs)
|
|
return obj, files, err
|
|
}
|
|
|
|
func listArchive(ctx context.Context, storage driver.Driver, path string, args model.ArchiveListArgs) (model.Obj, []model.Obj, error) {
|
|
obj, files, err := _listArchive(ctx, storage, path, args)
|
|
if errors.Is(err, errs.NotSupport) {
|
|
var meta model.ArchiveMeta
|
|
meta, err = GetArchiveMeta(ctx, storage, path, model.ArchiveMetaArgs{
|
|
ArchiveArgs: args.ArchiveArgs,
|
|
Refresh: args.Refresh,
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
files, err = getChildrenFromArchiveMeta(meta, args.InnerPath)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
if err == nil && obj == nil {
|
|
obj, err = GetUnwrap(ctx, storage, path)
|
|
}
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return obj, files, err
|
|
}
|
|
|
|
func getChildrenFromArchiveMeta(meta model.ArchiveMeta, innerPath string) ([]model.Obj, error) {
|
|
obj := meta.GetTree()
|
|
if obj == nil {
|
|
return nil, errors.WithStack(errs.NotImplement)
|
|
}
|
|
dirs := splitPath(innerPath)
|
|
for _, dir := range dirs {
|
|
var next model.ObjTree
|
|
for _, c := range obj {
|
|
if c.GetName() == dir {
|
|
next = c
|
|
break
|
|
}
|
|
}
|
|
if next == nil {
|
|
return nil, errors.WithStack(errs.ObjectNotFound)
|
|
}
|
|
if !next.IsDir() || next.GetChildren() == nil {
|
|
return nil, errors.WithStack(errs.NotFolder)
|
|
}
|
|
obj = next.GetChildren()
|
|
}
|
|
return utils.SliceConvert(obj, func(src model.ObjTree) (model.Obj, error) {
|
|
return src, nil
|
|
})
|
|
}
|
|
|
|
func splitPath(path string) []string {
|
|
var parts []string
|
|
for {
|
|
dir, file := stdpath.Split(path)
|
|
if file == "" {
|
|
break
|
|
}
|
|
parts = append([]string{file}, parts...)
|
|
path = strings.TrimSuffix(dir, "/")
|
|
}
|
|
return parts
|
|
}
|
|
|
|
func ArchiveGet(ctx context.Context, storage driver.Driver, path string, args model.ArchiveListArgs) (model.Obj, model.Obj, error) {
|
|
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
|
|
return nil, nil, errors.Errorf("storage not init: %s", storage.GetStorage().Status)
|
|
}
|
|
path = utils.FixAndCleanPath(path)
|
|
af, err := GetUnwrap(ctx, storage, path)
|
|
if err != nil {
|
|
return nil, nil, errors.WithMessage(err, "failed to get file")
|
|
}
|
|
if af.IsDir() {
|
|
return nil, nil, errors.WithStack(errs.NotFile)
|
|
}
|
|
if g, ok := storage.(driver.ArchiveGetter); ok {
|
|
obj, err := g.ArchiveGet(ctx, af, args.ArchiveInnerArgs)
|
|
if err == nil {
|
|
return af, model.WrapObjName(obj), nil
|
|
}
|
|
}
|
|
|
|
if utils.PathEqual(args.InnerPath, "/") {
|
|
return af, &model.ObjWrapName{
|
|
Name: RootName,
|
|
Obj: &model.Object{
|
|
Name: af.GetName(),
|
|
Path: af.GetPath(),
|
|
ID: af.GetID(),
|
|
Size: af.GetSize(),
|
|
Modified: af.ModTime(),
|
|
IsFolder: true,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
innerDir, name := stdpath.Split(args.InnerPath)
|
|
args.InnerPath = strings.TrimSuffix(innerDir, "/")
|
|
files, err := ListArchive(ctx, storage, path, args)
|
|
if err != nil {
|
|
return nil, nil, errors.WithMessage(err, "failed get parent list")
|
|
}
|
|
for _, f := range files {
|
|
if f.GetName() == name {
|
|
return af, f, nil
|
|
}
|
|
}
|
|
return nil, nil, errors.WithStack(errs.ObjectNotFound)
|
|
}
|
|
|
|
type extractLink struct {
|
|
Link *model.Link
|
|
Obj model.Obj
|
|
}
|
|
|
|
var extractCache = cache.NewMemCache(cache.WithShards[*extractLink](16))
|
|
var extractG singleflight.Group[*extractLink]
|
|
|
|
func DriverExtract(ctx context.Context, storage driver.Driver, path string, args model.ArchiveInnerArgs) (*model.Link, model.Obj, error) {
|
|
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
|
|
return nil, nil, errors.Errorf("storage not init: %s", storage.GetStorage().Status)
|
|
}
|
|
key := stdpath.Join(Key(storage, path), args.InnerPath)
|
|
if link, ok := extractCache.Get(key); ok {
|
|
return link.Link, link.Obj, nil
|
|
} else if link, ok := extractCache.Get(key + ":" + args.IP); ok {
|
|
return link.Link, link.Obj, nil
|
|
}
|
|
fn := func() (*extractLink, error) {
|
|
link, err := driverExtract(ctx, storage, path, args)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed extract archive")
|
|
}
|
|
if link.Link.Expiration != nil {
|
|
if link.Link.IPCacheKey {
|
|
key = key + ":" + args.IP
|
|
}
|
|
extractCache.Set(key, link, cache.WithEx[*extractLink](*link.Link.Expiration))
|
|
}
|
|
return link, nil
|
|
}
|
|
if storage.Config().OnlyLocal {
|
|
link, err := fn()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return link.Link, link.Obj, nil
|
|
}
|
|
link, err, _ := extractG.Do(key, fn)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return link.Link, link.Obj, err
|
|
}
|
|
|
|
func driverExtract(ctx context.Context, storage driver.Driver, path string, args model.ArchiveInnerArgs) (*extractLink, error) {
|
|
storageAr, ok := storage.(driver.ArchiveReader)
|
|
if !ok {
|
|
return nil, errs.DriverExtractNotSupported
|
|
}
|
|
archiveFile, extracted, err := ArchiveGet(ctx, storage, path, model.ArchiveListArgs{
|
|
ArchiveInnerArgs: args,
|
|
Refresh: false,
|
|
})
|
|
if err != nil {
|
|
return nil, errors.WithMessage(err, "failed to get file")
|
|
}
|
|
if extracted.IsDir() {
|
|
return nil, errors.WithStack(errs.NotFile)
|
|
}
|
|
link, err := storageAr.Extract(ctx, archiveFile, args)
|
|
return &extractLink{Link: link, Obj: extracted}, err
|
|
}
|
|
|
|
type streamWithParent struct {
|
|
rc io.ReadCloser
|
|
parent *stream.SeekableStream
|
|
}
|
|
|
|
func (s *streamWithParent) Read(p []byte) (int, error) {
|
|
return s.rc.Read(p)
|
|
}
|
|
|
|
func (s *streamWithParent) Close() error {
|
|
err1 := s.rc.Close()
|
|
err2 := s.parent.Close()
|
|
return stderrors.Join(err1, err2)
|
|
}
|
|
|
|
func InternalExtract(ctx context.Context, storage driver.Driver, path string, args model.ArchiveInnerArgs) (io.ReadCloser, int64, error) {
|
|
_, t, ss, err := getArchiveToolAndStream(ctx, storage, path, args.LinkArgs)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
rc, size, err := t.Extract(ss, args)
|
|
if err != nil {
|
|
if e := ss.Close(); e != nil {
|
|
log.Errorf("failed to close file streamer, %v", e)
|
|
}
|
|
return nil, 0, err
|
|
}
|
|
return &streamWithParent{rc: rc, parent: ss}, size, nil
|
|
}
|
|
|
|
func ArchiveDecompress(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string, args model.ArchiveDecompressArgs, lazyCache ...bool) error {
|
|
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
|
|
return errors.Errorf("storage not init: %s", storage.GetStorage().Status)
|
|
}
|
|
srcPath = utils.FixAndCleanPath(srcPath)
|
|
dstDirPath = utils.FixAndCleanPath(dstDirPath)
|
|
srcObj, err := GetUnwrap(ctx, storage, srcPath)
|
|
if err != nil {
|
|
return errors.WithMessage(err, "failed to get src object")
|
|
}
|
|
dstDir, err := GetUnwrap(ctx, storage, dstDirPath)
|
|
if err != nil {
|
|
return errors.WithMessage(err, "failed to get dst dir")
|
|
}
|
|
|
|
switch s := storage.(type) {
|
|
case driver.ArchiveDecompressResult:
|
|
var newObjs []model.Obj
|
|
newObjs, err = s.ArchiveDecompress(ctx, srcObj, dstDir, args)
|
|
if err == nil {
|
|
if newObjs != nil && len(newObjs) > 0 {
|
|
for _, newObj := range newObjs {
|
|
addCacheObj(storage, dstDirPath, model.WrapObjName(newObj))
|
|
}
|
|
} else if !utils.IsBool(lazyCache...) {
|
|
ClearCache(storage, dstDirPath)
|
|
}
|
|
}
|
|
case driver.ArchiveDecompress:
|
|
err = s.ArchiveDecompress(ctx, srcObj, dstDir, args)
|
|
if err == nil && !utils.IsBool(lazyCache...) {
|
|
ClearCache(storage, dstDirPath)
|
|
}
|
|
default:
|
|
return errs.NotImplement
|
|
}
|
|
return errors.WithStack(err)
|
|
}
|