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 }