github.com/soypat/rebed@v0.2.3/rebed.go (about)

     1  // Package rebed brings simple embedded file functionality
     2  // to Go's new embed directive.
     3  //
     4  // It can recreate the directory structure
     5  // from the embed.FS type with or without
     6  // the files it contains. This is useful to
     7  // expose the filesystem to the end user so they
     8  // may see and modify the files.
     9  //
    10  // It also provides basic directory walking functionality for
    11  // the embed.FS type.
    12  package rebed
    13  
    14  import (
    15  	"embed"
    16  	"fmt"
    17  	"io"
    18  	"io/fs"
    19  	"os"
    20  	"path/filepath"
    21  	"strings"
    22  )
    23  
    24  // FolderMode MkdirAll is called with this permission to prevent restricted folders
    25  // from being created. Default value is 0755=rwxr-xr-x.
    26  var FolderMode os.FileMode = 0755
    27  
    28  // ErrExist returned by Create when encountering
    29  // a file conflict in filesystem creation
    30  var ErrExist error = os.ErrExist
    31  
    32  // Tree creates the target filesystem folder structure.
    33  func Tree(fsys embed.FS, outputPath string) error {
    34  	return Walk(fsys, ".", func(dirpath string, de fs.DirEntry) error {
    35  		fullpath := filepath.Join(outputPath, dirpath, de.Name())
    36  		if de.IsDir() {
    37  			return os.MkdirAll(fullpath, FolderMode)
    38  		}
    39  		return nil
    40  	})
    41  }
    42  
    43  // Touch creates the target filesystem folder structure in the binary's
    44  // current working directory with empty files. Does not modify
    45  // already existing files.
    46  func Touch(fsys embed.FS, outputPath string) error {
    47  	return Walk(fsys, ".", func(dirpath string, de fs.DirEntry) error {
    48  		fullpath := filepath.Join(outputPath, dirpath, de.Name())
    49  		if de.IsDir() {
    50  			return os.MkdirAll(fullpath, FolderMode)
    51  		}
    52  		// unsure how IsNotExist works. this could be improved
    53  		_, err := os.Stat(fullpath)
    54  		if os.IsNotExist(err) {
    55  			_, err = os.Create(fullpath)
    56  		}
    57  		return err
    58  	})
    59  }
    60  
    61  // Write overwrites files of same path/name
    62  // in binaries current working directory or
    63  // creates new ones if not exist.
    64  func Write(fsys embed.FS, outputPath string) error {
    65  	return Walk(fsys, ".", func(dirpath string, de fs.DirEntry) error {
    66  		embedPath := sanitize(filepath.Join(dirpath, de.Name()))
    67  		fullpath := filepath.Join(outputPath, embedPath)
    68  		if de.IsDir() {
    69  			return os.MkdirAll(fullpath, FolderMode)
    70  		}
    71  		return embedCopyToFile(fsys, embedPath, fullpath)
    72  	})
    73  }
    74  
    75  // Patch creates files which are missing in
    76  // FS filesystem. Does not modify existing files
    77  func Patch(fsys embed.FS, outputPath string) error {
    78  	return Walk(fsys, ".", func(dirpath string, de fs.DirEntry) error {
    79  		embedPath := sanitize(filepath.Join(dirpath, de.Name()))
    80  		fullpath := filepath.Join(outputPath, embedPath)
    81  		if de.IsDir() {
    82  			return os.MkdirAll(fullpath, FolderMode)
    83  		}
    84  		_, err := os.Stat(fullpath)
    85  		if os.IsNotExist(err) {
    86  			err = embedCopyToFile(fsys, embedPath, fullpath)
    87  		}
    88  		return err
    89  	})
    90  }
    91  
    92  // Create attempts to recreate filesystem. It first checks that
    93  // there be no matching files present and returns an error
    94  // if there is an existing file conflict in outputPath.
    95  //
    96  // Folders are not considered to conflict.
    97  func Create(fsys embed.FS, outputPath string) error {
    98  	err := Walk(fsys, ".", func(dirpath string, de fs.DirEntry) error {
    99  		embedPath := filepath.Join(dirpath, de.Name())
   100  		fullpath := filepath.Join(outputPath, embedPath)
   101  		if de.IsDir() {
   102  			return nil
   103  		}
   104  		_, err := os.Stat(fullpath)
   105  		if os.IsNotExist(err) {
   106  			return nil
   107  		}
   108  		if err != nil {
   109  			return err
   110  		}
   111  		return ErrExist
   112  	})
   113  	if err != nil {
   114  		return err
   115  	}
   116  	return Patch(fsys, outputPath)
   117  }
   118  
   119  // Walk expects a relative path within fsys.
   120  // f called on every file/directory found recursively.
   121  //
   122  // f's first argument is the relative/absolute path to directory being scanned.
   123  // "." as startPath will scan all files and folders.
   124  //
   125  // Any error returned by f will cause Walk to return said error immediately.
   126  func Walk(fsys embed.FS, startPath string, f func(path string, de fs.DirEntry) error) error {
   127  	folders := make([]string, 0) // buffer of folders to process
   128  	err := WalkDir(fsys, startPath, func(dirpath string, de fs.DirEntry) error {
   129  		if de.IsDir() {
   130  			folders = append(folders, filepath.Join(dirpath, de.Name()))
   131  		}
   132  		return f(dirpath, de)
   133  	})
   134  	if err != nil {
   135  		if len(folders) == 0 {
   136  			return fmt.Errorf("no folder found: %v", err)
   137  		}
   138  		return err
   139  	}
   140  	n := len(folders)
   141  	for n != 0 {
   142  		for i := 0; i < n; i++ {
   143  			err = WalkDir(fsys, folders[i], func(dirpath string, de fs.DirEntry) error {
   144  				if de.IsDir() {
   145  					folders = append(folders, filepath.Join(dirpath, de.Name()))
   146  				}
   147  				return f(dirpath, de)
   148  			})
   149  			if err != nil {
   150  				return err
   151  			}
   152  		}
   153  		// we process n folders at a time, add new folders while
   154  		//processing n folders, then discard those n folders once finished
   155  		// and resume with a new n list of folders
   156  		var newFolders int = len(folders) - n
   157  		folders = folders[n : n+newFolders] // if found 0 new folders, end
   158  		n = len(folders)
   159  	}
   160  	return nil
   161  }
   162  
   163  // WalkDir applies f to every file/folder in embedded directory fsys.
   164  //
   165  // f's first argument is the relative/absolute path to directory being scanned.
   166  func WalkDir(fsys embed.FS, startPath string, f func(path string, de fs.DirEntry) error) error {
   167  	startPath = sanitize(startPath)
   168  	items, err := fsys.ReadDir(startPath)
   169  	if err != nil {
   170  		return err
   171  	}
   172  	for _, item := range items {
   173  		if err := f(startPath, item); err != nil {
   174  			return err
   175  		}
   176  	}
   177  	return nil
   178  }
   179  
   180  // embedCopyToFile copies an embedded file's contents
   181  // to a file on the host machine.
   182  func embedCopyToFile(fsys embed.FS, embedPath, path string) error {
   183  	embedPath = sanitize(embedPath)
   184  	fi, err := fsys.Open(embedPath)
   185  	if err != nil {
   186  		return fmt.Errorf("opening embedded file %v: %v", embedPath, err)
   187  	}
   188  	fo, err := os.Create(path)
   189  	if err != nil {
   190  		return err
   191  	}
   192  	// Thank you chengziqing for spotting this
   193  	defer fo.Close()
   194  	_, err = io.Copy(fo, fi)
   195  	return err
   196  }
   197  
   198  // sanitize converts windows representation of path to embed.FS representation
   199  func sanitize(embedPath string) string {
   200  	return strings.ReplaceAll(embedPath, "\\", "/")
   201  }