packages/fs.go (189 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. package packages import ( "archive/zip" "fmt" "io" "io/fs" "os" "path" "path/filepath" "strings" ) // PackageFile is the interface that files in the file system need to implement. // Seeker interface is needed for http helpers to serve files. type PackageFile = io.ReadSeekCloser type PackageFileSystem interface { Stat(name string) (os.FileInfo, error) Open(name string) (PackageFile, error) Glob(pattern string) (matches []string, err error) Close() error } // ExtractedPackageFileSystem provides utils to access files in an extracted package. type ExtractedPackageFileSystem struct { path string root fs.FS } func NewExtractedPackageFileSystem(p *Package) (*ExtractedPackageFileSystem, error) { return &ExtractedPackageFileSystem{path: p.BasePath, root: os.DirFS(p.BasePath)}, nil } func (fsys *ExtractedPackageFileSystem) Stat(name string) (os.FileInfo, error) { name = strings.TrimPrefix(name, "/") return fs.Stat(fsys.root, name) } func (fsys *ExtractedPackageFileSystem) Open(name string) (PackageFile, error) { name = strings.TrimPrefix(name, "/") f, err := fsys.root.Open(name) if err != nil { return nil, err } // f is a plain os.File expected to implement the PackageFile interface. pf, ok := f.(PackageFile) if !ok { defer f.Close() return nil, fmt.Errorf("file does not implement PackageFile interface: %q", filepath.Join(fsys.path, filepath.FromSlash(name))) } return pf, nil } func (fsys *ExtractedPackageFileSystem) Glob(pattern string) (matches []string, err error) { err = fs.WalkDir(fsys.root, ".", func(p string, d fs.DirEntry, err error) error { if err != nil { // Path related error: returning it will cause WalkDir to stop walking the entire tree. return fmt.Errorf("failed to walk path %q: %w", p, err) } match, err := path.Match(pattern, p) if err != nil { // ErrBadPattern error: returning it will cause WalkDir to stop walking the entire tree. return fmt.Errorf("failed to match path %q against pattern %s: %w", p, pattern, err) } if match { name := p matches = append(matches, name) } return nil }) if err != nil { return nil, fmt.Errorf("failed to obtain path under package root path (%s): %w", pattern, err) } return } func (fs *ExtractedPackageFileSystem) Close() error { return nil } // ZipPackageFileSystem provides utils to access files in a zipped package. type ZipPackageFileSystem struct { root string reader *zip.ReadCloser } func NewZipPackageFileSystem(p *Package) (*ZipPackageFileSystem, error) { reader, err := zip.OpenReader(p.BasePath) if err != nil { return nil, err } var root string for _, f := range reader.File { name := path.Clean(f.Name) dir, base := path.Split(name) dir = path.Clean(dir) // Find manifest files at the root of the package if dir != "." && path.Dir(dir) == "." && base == "manifest.yml" { root = dir break } } if root == "" { return nil, fmt.Errorf("failed to determine root directory in package (path: %s)", p.BasePath) } return &ZipPackageFileSystem{ root: root, reader: reader, }, nil } func (fs *ZipPackageFileSystem) Stat(name string) (os.FileInfo, error) { path := path.Join(fs.root, name) f, err := fs.reader.Open(path) if err != nil { return nil, err } defer f.Close() return f.Stat() } func (fs *ZipPackageFileSystem) Open(name string) (PackageFile, error) { path := path.Join(fs.root, name) f, err := fs.reader.Open(path) if err != nil { return nil, err } return &zipFileSeeker{ File: f, reader: fs.reader, path: path, }, nil } func (fs *ZipPackageFileSystem) Glob(pattern string) (matches []string, err error) { pattern = path.Join(fs.root, pattern) for _, f := range fs.reader.File { match, err := path.Match(pattern, path.Clean(f.Name)) if err != nil { return nil, err } if match { name := strings.TrimPrefix(f.Name, fs.root+"/") matches = append(matches, name) } } return } func (fs *ZipPackageFileSystem) Close() error { return fs.reader.Close() } // zipFileSeeker implements the seeker interface for zip files. type zipFileSeeker struct { fs.File reader *zip.ReadCloser path string } // Seek implements the seeker interface for zip files. This is inefficient, it shouldn't // be frequently used. func (f *zipFileSeeker) Seek(offset int64, whence int) (n int64, err error) { switch whence { case io.SeekStart: f.File.Close() f.File, err = f.reader.Open(f.path) if err != nil { return -1, err } if offset > 0 { r := io.LimitReader(f.File, offset) n, err = io.Copy(io.Discard, r) if err != nil { return -1, err } offset = int64(n) } return offset, nil case io.SeekEnd: if offset != 0 { return -1, fmt.Errorf("unsupported offset") } info, err := f.File.Stat() if err != nil { return -1, err } _, err = io.Copy(io.Discard, f.File) if err != nil { return -1, err } return info.Size(), nil default: return -1, fmt.Errorf("unsupported whence") } } func ReadAll(fs PackageFileSystem, name string) ([]byte, error) { f, err := fs.Open(name) if err != nil { return nil, err } defer f.Close() return io.ReadAll(f) } // VirtualPackageFileSystem provide utils for package objects that don't correspond to // any real package in any backend. Used mainly for testing purpouses. type VirtualPackageFileSystem struct{} func NewVirtualPackageFileSystem() (*VirtualPackageFileSystem, error) { return &VirtualPackageFileSystem{}, nil } func (fs *VirtualPackageFileSystem) Stat(name string) (os.FileInfo, error) { return nil, os.ErrNotExist } func (fs *VirtualPackageFileSystem) Open(name string) (PackageFile, error) { return nil, os.ErrNotExist } func (fs *VirtualPackageFileSystem) Glob(pattern string) (matches []string, err error) { return []string{}, nil } func (fs *VirtualPackageFileSystem) Close() error { return nil }