442 lines
9.4 KiB
Go
442 lines
9.4 KiB
Go
package vfs
|
|
|
|
import (
|
|
"archive/zip"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.kmsign.ru/royalcat/tstor/pkg/ioutils"
|
|
"github.com/bodgit/sevenzip"
|
|
"github.com/nwaples/rardecode/v2"
|
|
"github.com/royalcat/ctxio"
|
|
)
|
|
|
|
var ArchiveFactories = map[string]FsFactory{
|
|
".zip": func(ctx context.Context, sourcePath string, f File) (Filesystem, error) {
|
|
stat, err := f.Info()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewArchive(ctx, sourcePath, stat.Name(), f, stat.Size(), ZipLoader)
|
|
},
|
|
".rar": func(ctx context.Context, sourcePath string, f File) (Filesystem, error) {
|
|
stat, err := f.Info()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewArchive(ctx, sourcePath, stat.Name(), f, stat.Size(), RarLoader)
|
|
},
|
|
".7z": func(ctx context.Context, sourcePath string, f File) (Filesystem, error) {
|
|
stat, err := f.Info()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewArchive(ctx, sourcePath, stat.Name(), f, stat.Size(), SevenZipLoader)
|
|
},
|
|
}
|
|
|
|
type archiveLoader func(ctx context.Context, archivePath string, r File, size int64) (map[string]fileEntry, error)
|
|
|
|
var _ Filesystem = &ArchiveFS{}
|
|
|
|
type fileEntry struct {
|
|
fs.FileInfo
|
|
open func(ctx context.Context) (File, error)
|
|
}
|
|
|
|
type ArchiveFS struct {
|
|
name string
|
|
|
|
size int64
|
|
|
|
files map[string]fileEntry
|
|
}
|
|
|
|
// Rename implements Filesystem.
|
|
func (a *ArchiveFS) Rename(ctx context.Context, oldpath string, newpath string) error {
|
|
return ErrNotImplemented
|
|
}
|
|
|
|
// ModTime implements Filesystem.
|
|
func (a *ArchiveFS) ModTime() time.Time {
|
|
return time.Time{}
|
|
}
|
|
|
|
// Mode implements Filesystem.
|
|
func (a *ArchiveFS) Mode() fs.FileMode {
|
|
return fs.ModeDir
|
|
}
|
|
|
|
// Size implements Filesystem.
|
|
func (a *ArchiveFS) Size() int64 {
|
|
return int64(a.size)
|
|
}
|
|
|
|
// Sys implements Filesystem.
|
|
func (a *ArchiveFS) Sys() any {
|
|
return nil
|
|
}
|
|
|
|
// FsName implements Filesystem.
|
|
func (a *ArchiveFS) FsName() string {
|
|
return "archivefs"
|
|
}
|
|
|
|
func NewArchive(ctx context.Context, archivePath, name string, f File, size int64, loader archiveLoader) (*ArchiveFS, error) {
|
|
archiveFiles, err := loader(ctx, archivePath, f, size)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO make optional
|
|
singleDir := true
|
|
for k := range archiveFiles {
|
|
if !strings.HasPrefix(k, "/"+name+"/") {
|
|
singleDir = false
|
|
break
|
|
}
|
|
}
|
|
|
|
files := make(map[string]fileEntry, len(archiveFiles))
|
|
for k, v := range archiveFiles {
|
|
// TODO make optional
|
|
if strings.Contains(k, "/__MACOSX/") {
|
|
continue
|
|
}
|
|
|
|
if singleDir {
|
|
k, _ = strings.CutPrefix(k, "/"+name)
|
|
}
|
|
|
|
files[k] = v
|
|
}
|
|
|
|
// FIXME configurable
|
|
files["/.forcegallery"] = fileEntry{
|
|
FileInfo: NewFileInfo("/.forcegallery", 0),
|
|
open: func(ctx context.Context) (File, error) {
|
|
return NewMemoryFile(".forcegallery", []byte{}), nil
|
|
},
|
|
}
|
|
|
|
return &ArchiveFS{
|
|
name: name,
|
|
size: size,
|
|
files: files,
|
|
}, nil
|
|
}
|
|
|
|
// Unlink implements Filesystem.
|
|
func (a *ArchiveFS) Unlink(ctx context.Context, filename string) error {
|
|
return ErrNotImplemented
|
|
}
|
|
|
|
func (a *ArchiveFS) Open(ctx context.Context, filename string) (File, error) {
|
|
if filename == Separator {
|
|
return NewDirFile(filename), nil
|
|
}
|
|
|
|
f, ok := a.files[filename]
|
|
if ok {
|
|
return f.open(ctx)
|
|
}
|
|
|
|
for p := range a.files {
|
|
if strings.HasPrefix(p, filename) {
|
|
return NewDirFile(filename), nil
|
|
}
|
|
}
|
|
|
|
return nil, ErrNotExist
|
|
}
|
|
|
|
func (a *ArchiveFS) ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error) {
|
|
infos := make(map[string]fs.FileInfo, len(a.files))
|
|
for k, v := range a.files {
|
|
infos[k] = v
|
|
}
|
|
|
|
return ListDirFromInfo(infos, path)
|
|
}
|
|
|
|
// Stat implements Filesystem.
|
|
func (afs *ArchiveFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
|
|
if entry, ok := afs.files[filename]; ok {
|
|
return entry, nil
|
|
}
|
|
|
|
for p, _ := range afs.files {
|
|
if strings.HasPrefix(p, filename) {
|
|
return NewDirInfo(path.Base(filename)), nil
|
|
}
|
|
}
|
|
|
|
return nil, ErrNotExist
|
|
}
|
|
|
|
// Info implements Filesystem.
|
|
func (a *ArchiveFS) Info() (fs.FileInfo, error) {
|
|
return a, nil
|
|
}
|
|
|
|
// IsDir implements Filesystem.
|
|
func (a *ArchiveFS) IsDir() bool {
|
|
return true
|
|
}
|
|
|
|
// Name implements Filesystem.
|
|
func (a *ArchiveFS) Name() string {
|
|
return a.name
|
|
}
|
|
|
|
// Type implements Filesystem.
|
|
func (a *ArchiveFS) Type() fs.FileMode {
|
|
return fs.ModeDir
|
|
}
|
|
|
|
var _ File = (*archiveFile)(nil)
|
|
|
|
func newArchiveFile(name string, size int64, rr *randomReaderFromLinear) *archiveFile {
|
|
return &archiveFile{
|
|
name: name,
|
|
size: size,
|
|
rr: rr,
|
|
}
|
|
}
|
|
|
|
type archiveFile struct {
|
|
name string
|
|
size int64
|
|
|
|
m sync.Mutex
|
|
offset int64
|
|
|
|
rr *randomReaderFromLinear
|
|
}
|
|
|
|
// Seek implements File.
|
|
func (d *archiveFile) Seek(offset int64, whence int) (int64, error) {
|
|
switch whence {
|
|
case io.SeekStart:
|
|
d.offset = offset
|
|
|
|
case io.SeekCurrent:
|
|
d.offset += offset
|
|
case io.SeekEnd:
|
|
d.offset = d.size + offset
|
|
}
|
|
return d.offset, nil
|
|
}
|
|
|
|
// Name implements File.
|
|
func (d *archiveFile) Name() string {
|
|
return d.name
|
|
}
|
|
|
|
// Type implements File.
|
|
func (d *archiveFile) Type() fs.FileMode {
|
|
return ModeFileRO
|
|
}
|
|
|
|
func (d *archiveFile) Info() (fs.FileInfo, error) {
|
|
return NewFileInfo(d.name, d.size), nil
|
|
}
|
|
|
|
func (d *archiveFile) Size() int64 {
|
|
return d.size
|
|
}
|
|
|
|
func (d *archiveFile) IsDir() bool {
|
|
return false
|
|
}
|
|
|
|
func (d *archiveFile) Read(ctx context.Context, p []byte) (n int, err error) {
|
|
ctx, span := tracer.Start(ctx, "archive.File.Read")
|
|
defer span.End()
|
|
|
|
n, err = d.rr.ReadAt(ctx, p, d.offset)
|
|
d.offset += int64(n)
|
|
return n, err
|
|
}
|
|
|
|
func (d *archiveFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
|
|
d.m.Lock()
|
|
defer d.m.Unlock()
|
|
|
|
return d.rr.ReadAt(ctx, p, off)
|
|
}
|
|
|
|
func (d *archiveFile) Close(ctx context.Context) error {
|
|
// FIXME close should do nothing as archive fs currently reuse the same file instances
|
|
return nil
|
|
}
|
|
|
|
type archiveFileReaderFactory func(ctx context.Context) (ctxio.ReadCloser, error)
|
|
|
|
var _ archiveLoader = ZipLoader
|
|
|
|
func ZipLoader(ctx context.Context, archivePath string, f File, size int64) (map[string]fileEntry, error) {
|
|
hash, err := FileHash(ctx, f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reader := ctxio.IoReaderAt(ctx, f)
|
|
zr, err := zip.NewReader(reader, size)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := make(map[string]fileEntry)
|
|
for i := range zr.File {
|
|
zipFile := zr.File[i]
|
|
if zipFile.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
|
|
i := i
|
|
af := func(ctx context.Context) (ctxio.ReadCloser, error) {
|
|
reader := ctxio.IoReaderAt(ctx, f)
|
|
|
|
zr, err := zip.NewReader(reader, size)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create zip reader: %w", err)
|
|
}
|
|
|
|
rc, err := zr.File[i].Open()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open file in zip archive: %w", err)
|
|
}
|
|
|
|
return ctxio.WrapIoReadCloser(rc), nil
|
|
}
|
|
|
|
info := zipFile.FileInfo()
|
|
|
|
rr := newRandomReaderFromLinear(archiveFileIndex{archiveHash: hash, filename: zipFile.Name}, info.Size(), af)
|
|
|
|
out[AbsPath(zipFile.Name)] = fileEntry{
|
|
FileInfo: info,
|
|
open: func(ctx context.Context) (File, error) {
|
|
return newArchiveFile(info.Name(), info.Size(), rr), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
var _ archiveLoader = SevenZipLoader
|
|
|
|
func SevenZipLoader(ctx context.Context, archivePath string, ctxreader File, size int64) (map[string]fileEntry, error) {
|
|
hash, err := FileHash(ctx, ctxreader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reader := ctxio.IoReaderAt(ctx, ctxreader)
|
|
r, err := sevenzip.NewReader(reader, size)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := make(map[string]fileEntry)
|
|
for i, f := range r.File {
|
|
f := f
|
|
if f.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
|
|
i := i
|
|
af := func(ctx context.Context) (ctxio.ReadCloser, error) {
|
|
reader := ctxio.IoReaderAt(ctx, ctxreader)
|
|
zr, err := sevenzip.NewReader(reader, size)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rc, err := zr.File[i].Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ctxio.WrapIoReadCloser(rc), nil
|
|
}
|
|
|
|
info := f.FileInfo()
|
|
|
|
rr := newRandomReaderFromLinear(archiveFileIndex{archiveHash: hash, filename: f.Name}, info.Size(), af)
|
|
|
|
out[AbsPath(f.Name)] = fileEntry{
|
|
FileInfo: f.FileInfo(),
|
|
open: func(ctx context.Context) (File, error) {
|
|
return newArchiveFile(info.Name(), info.Size(), rr), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
var _ archiveLoader = RarLoader
|
|
|
|
func RarLoader(ctx context.Context, archivePath string, f File, size int64) (map[string]fileEntry, error) {
|
|
hash, err := FileHash(ctx, f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reader := ioutils.WrapIoReadSeeker(ctx, f, size)
|
|
|
|
r, err := rardecode.NewReader(reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := make(map[string]fileEntry)
|
|
for {
|
|
header, err := r.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
name := header.Name
|
|
af := func(ctx context.Context) (ctxio.ReadCloser, error) {
|
|
reader := ioutils.WrapIoReadSeeker(ctx, f, size)
|
|
r, err := rardecode.NewReader(reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for header, err := r.Next(); err != io.EOF; header, err = r.Next() {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if header.Name == name {
|
|
return ctxio.NopCloser(ctxio.WrapIoReader(r)), nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("file with name '%s' not found", name)
|
|
}
|
|
|
|
rr := newRandomReaderFromLinear(archiveFileIndex{archiveHash: hash, filename: header.Name}, header.UnPackedSize, af)
|
|
|
|
out[AbsPath(header.Name)] = fileEntry{
|
|
FileInfo: NewFileInfo(header.Name, header.UnPackedSize),
|
|
open: func(ctx context.Context) (File, error) {
|
|
return newArchiveFile(header.Name, header.UnPackedSize, rr), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
return out, nil
|
|
}
|