github.com/zntrio/harp/v2@v2.0.9/pkg/bundle/fs/root.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  //go:build go1.16
    19  // +build go1.16
    20  
    21  package fs
    22  
    23  import (
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"io/fs"
    28  	"os"
    29  	"path/filepath"
    30  	"strings"
    31  
    32  	"github.com/awnumar/memguard"
    33  	"google.golang.org/protobuf/proto"
    34  
    35  	bundlev1 "github.com/zntrio/harp/v2/api/gen/go/harp/bundle/v1"
    36  )
    37  
    38  const (
    39  	directoryAccess = 0o555
    40  	fileAccess      = 0o444
    41  )
    42  
    43  type bundleFs struct {
    44  	root *directory
    45  }
    46  
    47  // -----------------------------------------------------------------------------
    48  
    49  // FromBundle initializes an fs.FS object from the given bundle.
    50  func FromBundle(b *bundlev1.Bundle) (BundleFS, error) {
    51  	// Check arguments
    52  	if b == nil {
    53  		return nil, errors.New("unable to create a filesytem from a nil bundle")
    54  	}
    55  
    56  	// Prepare vfs root
    57  	bfs := &bundleFs{
    58  		root: &directory{
    59  			children: map[string]interface{}{},
    60  		},
    61  	}
    62  
    63  	// Prepare filesystem
    64  	for _, p := range b.Packages {
    65  		if p == nil {
    66  			// ignore nil package
    67  			continue
    68  		}
    69  
    70  		// Serialize package
    71  		body, err := proto.Marshal(p)
    72  		if err != nil {
    73  			return nil, fmt.Errorf("unable to serialize package %q: %w", p.Name, err)
    74  		}
    75  
    76  		// Write content
    77  		if errWrite := bfs.WriteFile(p.Name, body, fileAccess); errWrite != nil {
    78  			return nil, fmt.Errorf("unable to write package %q in filesystem: %w", p.Name, err)
    79  		}
    80  	}
    81  
    82  	// Return bundle filesystem
    83  	return bfs, nil
    84  }
    85  
    86  // -----------------------------------------------------------------------------
    87  
    88  func (bfs *bundleFs) Open(name string) (fs.File, error) {
    89  	// Return root as default
    90  	if name == "" {
    91  		return bfs.root, nil
    92  	}
    93  
    94  	// Validate input path
    95  	if !fs.ValidPath(name) {
    96  		return nil, &fs.PathError{
    97  			Op:   "open",
    98  			Path: name,
    99  			Err:  fs.ErrInvalid,
   100  		}
   101  	}
   102  
   103  	// Create directory tree
   104  	dirPath, name := filepath.Split(name)
   105  	dirNames := strings.Split(dirPath, "/")
   106  
   107  	// Browse directory tree
   108  	currentDirectory := bfs.root
   109  	for _, dirName := range dirNames {
   110  		// Skip empty directory name
   111  		if dirName == "" {
   112  			continue
   113  		}
   114  
   115  		it, ok := currentDirectory.children[dirName]
   116  		if !ok {
   117  			return nil, fmt.Errorf("directory %q not found: %w", dirName, fs.ErrNotExist)
   118  		}
   119  		currentDirectory, ok = it.(*directory)
   120  		if !ok {
   121  			return nil, errors.New("invalid directory iterator value")
   122  		}
   123  	}
   124  
   125  	// Get child
   126  	h, ok := currentDirectory.children[name]
   127  	if !ok {
   128  		return nil, fmt.Errorf("item %q not found in directory %q: %w", name, currentDirectory.name, fs.ErrNotExist)
   129  	}
   130  
   131  	switch it := h.(type) {
   132  	case *directory:
   133  		// Return directory
   134  		return it, nil
   135  	case *file:
   136  		// Open enclave
   137  		body, err := it.content.Open()
   138  		if err != nil {
   139  			return nil, fmt.Errorf("file %q could not be opened: %w", name, err)
   140  		}
   141  
   142  		// Assign body reader
   143  		it.bodyReader = body.Reader()
   144  
   145  		// Return file
   146  		return it, nil
   147  	}
   148  
   149  	return nil, fmt.Errorf("unexpected file type in filesystem %s: %w", name, fs.ErrInvalid)
   150  }
   151  
   152  func (bfs *bundleFs) ReadDir(name string) ([]fs.DirEntry, error) {
   153  	// Try to open directory
   154  	h, err := bfs.Open(name)
   155  	if err != nil {
   156  		return nil, fmt.Errorf("unable to open directory: %w", err)
   157  	}
   158  
   159  	// Retrieve directory info
   160  	fi, err := h.Stat()
   161  	if err != nil {
   162  		return nil, fmt.Errorf("unable to retrieve directory info: %w", err)
   163  	}
   164  
   165  	// Confirm it's a directory
   166  	if !fi.IsDir() {
   167  		return nil, fmt.Errorf("path %q point to a file", name)
   168  	}
   169  
   170  	// Convert handle to directory reader
   171  	dir, ok := h.(fs.ReadDirFile)
   172  	if !ok {
   173  		return nil, fmt.Errorf("path %q point to a directory but could not be listed", name)
   174  	}
   175  
   176  	// Delegate to directory list
   177  	return dir.ReadDir(0)
   178  }
   179  
   180  func (bfs *bundleFs) ReadFile(name string) ([]byte, error) {
   181  	// Try to open file
   182  	h, err := bfs.Open(name)
   183  	if err != nil {
   184  		return nil, fmt.Errorf("unable to open file: %w", err)
   185  	}
   186  
   187  	// Delete to file
   188  	return io.ReadAll(h)
   189  }
   190  
   191  func (bfs *bundleFs) WriteFile(name string, data []byte, perm os.FileMode) error {
   192  	// Create directory tree
   193  	dirPath, fname := filepath.Split(name)
   194  	dirNames := strings.Split(dirPath, "/")
   195  
   196  	// MkDirAll
   197  	currentDirectory := bfs.root
   198  	for _, dirName := range dirNames {
   199  		// Skip empty directory name
   200  		if dirName == "" {
   201  			continue
   202  		}
   203  
   204  		currentDirectory.RLock()
   205  		it, ok := currentDirectory.children[dirName]
   206  		currentDirectory.RUnlock()
   207  		if !ok {
   208  			it = &directory{
   209  				name:     dirName,
   210  				perm:     directoryAccess,
   211  				children: map[string]interface{}{},
   212  			}
   213  
   214  			currentDirectory.Lock()
   215  			currentDirectory.children[dirName] = it
   216  			currentDirectory.Unlock()
   217  		}
   218  		currentDirectory, ok = it.(*directory)
   219  		if !ok {
   220  			return errors.New("invalid directory iterator value")
   221  		}
   222  	}
   223  
   224  	// Create file entry
   225  	currentDirectory.Lock()
   226  	currentDirectory.children[fname] = &file{
   227  		name:    fname,
   228  		mode:    fileAccess,
   229  		size:    int64(len(data)),
   230  		content: memguard.NewEnclave(data),
   231  	}
   232  	currentDirectory.Unlock()
   233  
   234  	// No error
   235  	return nil
   236  }
   237  
   238  func (bfs *bundleFs) Stat(name string) (fs.FileInfo, error) {
   239  	// Try to open file
   240  	h, err := bfs.Open(name)
   241  	if err != nil {
   242  		return nil, fmt.Errorf("unable to open file: %w", err)
   243  	}
   244  
   245  	// Delegate to file
   246  	return h.Stat()
   247  }