github.com/zntrio/harp/v2@v2.0.9/pkg/sdk/fsutil/targzfs/fs.go (about)

     1  // Licensed to Elasticsearch B.V. under one or more contributor
     2  // license agreements. See the NOTICE file distributed with
     3  // this work for additional information regarding copyright
     4  // ownership. Elasticsearch B.V. licenses this file to you under
     5  // the Apache License, Version 2.0 (the "License"); you may
     6  // not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing,
    12  // software distributed under the License is distributed on an
    13  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    14  // KIND, either express or implied.  See the License for the
    15  // specific language governing permissions and limitations
    16  // under the License.
    17  
    18  package targzfs
    19  
    20  import (
    21  	"bytes"
    22  	"fmt"
    23  	"io/fs"
    24  	"sort"
    25  	"strings"
    26  
    27  	"github.com/gobwas/glob"
    28  )
    29  
    30  var (
    31  	// Block decompression if the TAR archive is larger than 25MB.
    32  	maxDecompressedSize = int64(25 * 1024 * 1024)
    33  	// Maximum file size to load in memory (2MB).
    34  	maxFileSize = int64(2 * 1024 * 1024)
    35  	// Block decompression if the archive has more than 1k files.
    36  	maxFileCount = 1000
    37  )
    38  
    39  type tarGzFs struct {
    40  	files       map[string]*tarEntry
    41  	rootEntries []fs.DirEntry
    42  	rootEntry   *tarEntry
    43  }
    44  
    45  var _ fs.FS = (*tarGzFs)(nil)
    46  
    47  // Open opens the named file.
    48  func (gzfs *tarGzFs) Open(name string) (fs.File, error) {
    49  	// Shortcut if the file is '.'
    50  	if name == "." {
    51  		if gzfs.rootEntries == nil {
    52  			return &rootFile{}, nil
    53  		}
    54  		return &tarFile{
    55  			tarEntry:   *gzfs.rootEntry,
    56  			r:          bytes.NewReader(gzfs.rootEntry.b),
    57  			readDirPos: 0,
    58  		}, nil
    59  	}
    60  
    61  	// Lookup file.
    62  	f, err := gzfs.get(name, "open")
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  
    67  	// Wrapped file content
    68  	return &tarFile{
    69  		tarEntry:   *f,
    70  		r:          bytes.NewReader(f.b),
    71  		readDirPos: 0,
    72  	}, nil
    73  }
    74  
    75  var _ fs.ReadDirFS = (*tarGzFs)(nil)
    76  
    77  // ReadDir is used to enumerate all files from a directory.
    78  func (gzfs *tarGzFs) ReadDir(name string) ([]fs.DirEntry, error) {
    79  	// Shortcut if the file is '.'
    80  	if name == "." {
    81  		return gzfs.rootEntries, nil
    82  	}
    83  
    84  	// Lookup file.
    85  	e, err := gzfs.get(name, "readdir")
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	// Only directory should be used.
    91  	if !e.IsDir() {
    92  		return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid}
    93  	}
    94  
    95  	// Sort results by name.
    96  	sort.Slice(e.entries, func(i, j int) bool {
    97  		return e.entries[i].Name() < e.entries[j].Name()
    98  	})
    99  
   100  	// Return file entries.
   101  	return e.entries, nil
   102  }
   103  
   104  var _ fs.ReadFileFS = (*tarGzFs)(nil)
   105  
   106  // ReadFile is used to retrieve directly the file content.
   107  func (gzfs *tarGzFs) ReadFile(name string) ([]byte, error) {
   108  	// Shortcut if the file is '.'
   109  	if name == "." {
   110  		return nil, &fs.PathError{Op: "readfile", Path: name, Err: fs.ErrInvalid}
   111  	}
   112  
   113  	// Lookup file.
   114  	e, err := gzfs.get(name, "readfile")
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	// Entry must be a file
   120  	if e.IsDir() {
   121  		return nil, &fs.PathError{Op: "readfile", Path: name, Err: fs.ErrInvalid}
   122  	}
   123  
   124  	// Copy content
   125  	buf := make([]byte, len(e.b))
   126  	copy(buf, e.b)
   127  
   128  	// No error
   129  	return buf, nil
   130  }
   131  
   132  var _ fs.StatFS = (*tarGzFs)(nil)
   133  
   134  // Stat query the in-memory file system to get file info.
   135  func (gzfs *tarGzFs) Stat(name string) (fs.FileInfo, error) {
   136  	// Shortcut if the file is '.'
   137  	if name == "." {
   138  		if gzfs.rootEntry == nil {
   139  			return &rootFile{}, nil
   140  		}
   141  
   142  		// Return root fileinfo
   143  		return gzfs.rootEntry.Info()
   144  	}
   145  
   146  	// Lookup file.
   147  	e, err := gzfs.get(name, "stat")
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  
   152  	// Return fileinfo
   153  	return e.h.FileInfo(), nil
   154  }
   155  
   156  var _ fs.GlobFS = (*tarGzFs)(nil)
   157  
   158  func (gzfs *tarGzFs) Glob(pattern string) (matches []string, _ error) {
   159  	// Compile pattern
   160  	g, err := glob.Compile(pattern)
   161  	if err != nil {
   162  		return nil, fmt.Errorf("unable to compile glob pattern: %w", err)
   163  	}
   164  
   165  	// Iterate over file names
   166  	for name := range gzfs.files {
   167  		// Check if pattern match the file name
   168  		if g.Match(name) {
   169  			matches = append(matches, name)
   170  		}
   171  	}
   172  
   173  	// Return results
   174  	return
   175  }
   176  
   177  var _ fs.SubFS = (*tarGzFs)(nil)
   178  
   179  func (gzfs *tarGzFs) Sub(dir string) (fs.FS, error) {
   180  	if dir == "." {
   181  		return gzfs, nil
   182  	}
   183  
   184  	// Lookup directory
   185  	e, err := gzfs.get(dir, "sub")
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  
   190  	// Must be a directory
   191  	if !e.IsDir() {
   192  		return nil, &fs.PathError{Op: "sub", Path: dir, Err: fs.ErrInvalid}
   193  	}
   194  
   195  	// Create a sub-filesystem
   196  	subfs := &tarGzFs{
   197  		files:       make(map[string]*tarEntry),
   198  		rootEntries: e.entries,
   199  		rootEntry:   e,
   200  	}
   201  
   202  	// Copy files and remove directory prefix.
   203  	prefix := dir + "/"
   204  	for name, file := range gzfs.files {
   205  		if strings.HasPrefix(name, prefix) {
   206  			subfs.files[strings.TrimPrefix(name, prefix)] = file
   207  		}
   208  	}
   209  
   210  	// No error
   211  	return subfs, nil
   212  }
   213  
   214  // -----------------------------------------------------------------------------
   215  
   216  func (gzfs *tarGzFs) get(name, op string) (*tarEntry, error) {
   217  	if !fs.ValidPath(name) {
   218  		return nil, &fs.PathError{Op: op, Path: name, Err: fs.ErrInvalid}
   219  	}
   220  
   221  	// Lookup file
   222  	e, ok := gzfs.files[name]
   223  	if !ok {
   224  		return nil, &fs.PathError{Op: op, Path: name, Err: fs.ErrNotExist}
   225  	}
   226  
   227  	return e, nil
   228  }