package vfs import ( "archive/zip" "context" "fmt" "io" "io/fs" "path" "strings" "sync" "time" "git.kmsign.ru/royalcat/tstor/pkg/ctxio" "github.com/bodgit/sevenzip" "github.com/nwaples/rardecode/v2" ) var ArchiveFactories = map[string]FsFactory{ ".zip": func(ctx context.Context, f File) (Filesystem, error) { stat, err := f.Info() if err != nil { return nil, err } return NewArchive(ctx, stat.Name(), f, stat.Size(), ZipLoader) }, ".rar": func(ctx context.Context, f File) (Filesystem, error) { stat, err := f.Info() if err != nil { return nil, err } return NewArchive(ctx, stat.Name(), f, stat.Size(), RarLoader) }, ".7z": func(ctx context.Context, f File) (Filesystem, error) { stat, err := f.Info() if err != nil { return nil, err } return NewArchive(ctx, stat.Name(), f, stat.Size(), SevenZipLoader) }, } type archiveLoader func(ctx context.Context, r ctxio.ReaderAt, size int64) (map[string]*archiveFile, error) var _ Filesystem = &ArchiveFS{} type ArchiveFS struct { name string size int64 files map[string]File } // 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, name string, r ctxio.ReaderAt, size int64, loader archiveLoader) (*ArchiveFS, error) { archiveFiles, err := loader(ctx, r, 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]File, 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 files["/.forcegallery"] = NewMemoryFile(".forcegallery", []byte{}) 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) { return GetFile(a.files, filename) } func (a *ArchiveFS) ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error) { return ListDirFromFiles(a.files, path) } // Stat implements Filesystem. func (afs *ArchiveFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) { if file, ok := afs.files[filename]; ok { return file.Info() } 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, af archiveFileReaderFactory) *archiveFile { return &archiveFile{ name: name, size: size, af: af, buffer: ctxio.NewFileBuffer(nil), } } const readahead = 1024 * 16 type archiveFile struct { name string size int64 af archiveFileReaderFactory m sync.Mutex offset int64 readen int64 buffer *ctxio.FileBuffer } // Name implements File. func (d *archiveFile) Name() string { return d.name } // Type implements File. func (d *archiveFile) Type() fs.FileMode { return ROMode } 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) Close(ctx context.Context) error { return d.buffer.Close(ctx) } func (d *archiveFile) loadMore(ctx context.Context, to int64) error { d.m.Lock() defer d.m.Unlock() if to < d.readen { return nil } reader, err := d.af(ctx) if err != nil { return fmt.Errorf("failed to get file reader: %w", err) } defer reader.Close() _, err = d.buffer.Seek(0, io.SeekStart) if err != nil { return fmt.Errorf("failed to seek to start of the file: %w", err) } d.readen, err = ctxio.CopyN(ctx, d.buffer, ctxio.WrapIoReader(reader), to+readahead) if err != nil && err != io.EOF { return fmt.Errorf("error copying from archive file reader: %w", err) } return nil } func (d *archiveFile) Read(ctx context.Context, p []byte) (n int, err error) { err = d.loadMore(ctx, d.offset+int64(len(p))) if err != nil { return 0, fmt.Errorf("failed to load more from archive file: %w", err) } n, err = d.buffer.Read(ctx, p) if err != nil && err != io.EOF { return n, fmt.Errorf("failed to read from buffer: %w", err) } return n, nil } func (d *archiveFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) { err = d.loadMore(ctx, off+int64(len(p))) if err != nil { return 0, fmt.Errorf("failed to load more from archive file: %w", err) } n, err = d.buffer.ReadAt(ctx, p, off) if err != nil && err != io.EOF { return n, fmt.Errorf("failed to read from buffer: %w", err) } return n, nil } type archiveFileReaderFactory func(ctx context.Context) (io.ReadCloser, error) var _ archiveLoader = ZipLoader func ZipLoader(ctx context.Context, ctxreader ctxio.ReaderAt, size int64) (map[string]*archiveFile, error) { reader := ctxio.IoReaderAt(ctx, ctxreader) zr, err := zip.NewReader(reader, size) if err != nil { return nil, err } out := make(map[string]*archiveFile) for i := range zr.File { zipFile := zr.File[i] if zipFile.FileInfo().IsDir() { continue } i := i af := func(ctx context.Context) (io.ReadCloser, error) { reader := ctxio.IoReaderAt(ctx, ctxreader) zr, err := zip.NewReader(reader, size) if err != nil { return nil, err } rc, err := zr.File[i].Open() if err != nil { return nil, err } return rc, nil } out[AbsPath(zipFile.Name)] = NewArchiveFile(zipFile.Name, zipFile.FileInfo().Size(), af) } return out, nil } var _ archiveLoader = SevenZipLoader func SevenZipLoader(ctx context.Context, ctxreader ctxio.ReaderAt, size int64) (map[string]*archiveFile, error) { reader := ctxio.IoReaderAt(context.Background(), ctxreader) r, err := sevenzip.NewReader(reader, size) if err != nil { return nil, err } out := make(map[string]*archiveFile) for i, f := range r.File { f := f if f.FileInfo().IsDir() { continue } i := i af := func(ctx context.Context) (io.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 rc, nil } out[AbsPath(f.Name)] = NewArchiveFile(f.Name, f.FileInfo().Size(), af) } return out, nil } var _ archiveLoader = RarLoader func RarLoader(ctx context.Context, ctxreader ctxio.ReaderAt, size int64) (map[string]*archiveFile, error) { reader := ctxio.WrapIoReadSeeker(ctx, ctxreader, size) r, err := rardecode.NewReader(reader) if err != nil { return nil, err } out := make(map[string]*archiveFile) for { header, err := r.Next() if err == io.EOF { break } if err != nil { return nil, err } name := header.Name af := func(ctx context.Context) (io.ReadCloser, error) { reader := ctxio.WrapIoReadSeeker(ctx, ctxreader, 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 io.NopCloser(r), nil } } return nil, fmt.Errorf("file with name '%s' not found", name) } out[AbsPath(header.Name)] = NewArchiveFile(header.Name, header.UnPackedSize, af) } return out, nil }