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 }