feat(archive): archive manage (#7817)

* feat(archive): archive management

* fix(ftp-server): remove duplicate ReadAtSeeker realization

* fix(archive): bad seeking of SeekableStream

* fix(archive): split internal and driver extraction api

* feat(archive): patch

* fix(shutdown): clear decompress upload tasks

* chore

* feat(archive): support .iso format

* chore
This commit is contained in:
KirCute_ECT
2025-01-18 23:28:12 +08:00
committed by GitHub
parent ab22cf8233
commit bb40e2e2cd
36 changed files with 2854 additions and 127 deletions

View File

@ -0,0 +1,126 @@
package archives
import (
"github.com/alist-org/alist/v3/internal/archive/tool"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/pkg/utils"
"io"
"io/fs"
"os"
stdpath "path"
"strings"
)
type Archives struct {
}
func (_ *Archives) AcceptedExtensions() []string {
return []string{
".br", ".bz2", ".gz", ".lz4", ".lz", ".sz", ".s2", ".xz", ".zz", ".zst", ".tar", ".rar", ".7z",
}
}
func (_ *Archives) GetMeta(ss *stream.SeekableStream, args model.ArchiveArgs) (model.ArchiveMeta, error) {
fsys, err := getFs(ss, args)
if err != nil {
return nil, err
}
_, err = fsys.ReadDir(".")
if err != nil {
return nil, filterPassword(err)
}
return &model.ArchiveMetaInfo{
Comment: "",
Encrypted: false,
}, nil
}
func (_ *Archives) List(ss *stream.SeekableStream, args model.ArchiveInnerArgs) ([]model.Obj, error) {
fsys, err := getFs(ss, args.ArchiveArgs)
if err != nil {
return nil, err
}
innerPath := strings.TrimPrefix(args.InnerPath, "/")
if innerPath == "" {
innerPath = "."
}
obj, err := fsys.ReadDir(innerPath)
if err != nil {
return nil, filterPassword(err)
}
return utils.SliceConvert(obj, func(src os.DirEntry) (model.Obj, error) {
info, err := src.Info()
if err != nil {
return nil, err
}
return toModelObj(info), nil
})
}
func (_ *Archives) Extract(ss *stream.SeekableStream, args model.ArchiveInnerArgs) (io.ReadCloser, int64, error) {
fsys, err := getFs(ss, args.ArchiveArgs)
if err != nil {
return nil, 0, err
}
file, err := fsys.Open(strings.TrimPrefix(args.InnerPath, "/"))
if err != nil {
return nil, 0, filterPassword(err)
}
stat, err := file.Stat()
if err != nil {
return nil, 0, filterPassword(err)
}
return file, stat.Size(), nil
}
func (_ *Archives) Decompress(ss *stream.SeekableStream, outputPath string, args model.ArchiveInnerArgs, up model.UpdateProgress) error {
fsys, err := getFs(ss, args.ArchiveArgs)
if err != nil {
return err
}
isDir := false
path := strings.TrimPrefix(args.InnerPath, "/")
if path == "" {
isDir = true
path = "."
} else {
stat, err := fsys.Stat(path)
if err != nil {
return filterPassword(err)
}
if stat.IsDir() {
isDir = true
outputPath = stdpath.Join(outputPath, stat.Name())
err = os.Mkdir(outputPath, 0700)
if err != nil {
return filterPassword(err)
}
}
}
if isDir {
err = fs.WalkDir(fsys, path, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
relPath := strings.TrimPrefix(p, path+"/")
dstPath := stdpath.Join(outputPath, relPath)
if d.IsDir() {
err = os.MkdirAll(dstPath, 0700)
} else {
dir := stdpath.Dir(dstPath)
err = decompress(fsys, p, dir, func(_ float64) {})
}
return err
})
} else {
err = decompress(fsys, path, outputPath, up)
}
return filterPassword(err)
}
var _ tool.Tool = (*Archives)(nil)
func init() {
tool.RegisterTool(&Archives{})
}

View File

@ -0,0 +1,80 @@
package archives
import (
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/mholt/archives"
"io"
fs2 "io/fs"
"os"
stdpath "path"
"strings"
)
func getFs(ss *stream.SeekableStream, args model.ArchiveArgs) (*archives.ArchiveFS, error) {
reader, err := stream.NewReadAtSeeker(ss, 0)
if err != nil {
return nil, err
}
format, _, err := archives.Identify(ss.Ctx, ss.GetName(), reader)
if err != nil {
return nil, errs.UnknownArchiveFormat
}
extractor, ok := format.(archives.Extractor)
if !ok {
return nil, errs.UnknownArchiveFormat
}
switch f := format.(type) {
case archives.SevenZip:
f.Password = args.Password
case archives.Rar:
f.Password = args.Password
}
return &archives.ArchiveFS{
Stream: io.NewSectionReader(reader, 0, ss.GetSize()),
Format: extractor,
Context: ss.Ctx,
}, nil
}
func toModelObj(file os.FileInfo) *model.Object {
return &model.Object{
Name: file.Name(),
Size: file.Size(),
Modified: file.ModTime(),
IsFolder: file.IsDir(),
}
}
func filterPassword(err error) error {
if err != nil && strings.Contains(err.Error(), "password") {
return errs.WrongArchivePassword
}
return err
}
func decompress(fsys fs2.FS, filePath, targetPath string, up model.UpdateProgress) error {
rc, err := fsys.Open(filePath)
if err != nil {
return err
}
defer rc.Close()
stat, err := rc.Stat()
if err != nil {
return err
}
f, err := os.OpenFile(stdpath.Join(targetPath, stat.Name()), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, &stream.ReaderUpdatingProgress{
Reader: &stream.SimpleReaderWithSize{
Reader: rc,
Size: stat.Size(),
},
UpdateProgress: up,
})
return err
}