package main import ( "io" "io/fs" "os" "path/filepath" "slices" "r00t2.io/sysutils/paths" ) /* Append adds fsPaths as fs.FS to the MergedFS (before the embed.FS). If fsPaths is empty, Append is a NO-OP. noExist and dupes have the same meaning as in NewMergedFS. */ func (m *MergedFS) Append(fsPaths ...string) (noExist, dupes []string, err error) { var exists bool var fsPath string var newPaths []string var fsMem *realFsMember var newFs []mergedFsMember m.mut.Lock() defer m.mut.Unlock() newPaths = make([]string, 0, len(fsPaths)) newFs = make([]mergedFsMember, 0, len(fsPaths)) for _, fsPath = range fsPaths { if exists, err = paths.RealPathExists(&fsPath); err != nil { return } if !exists { noExist = append(noExist, fsPath) continue } if fsMem, err = NewRealFsMember(fsPath); err != nil { return } if !m.uniq.Add(fsPath) { dupes = append(dupes, fsPath) continue } newFs = append(newFs, fsMem) newPaths = append(newPaths, fsPath) } if len(newFs) == 0 { return } m.overridePaths = append(m.overridePaths, newPaths...) m.fses = append(m.fses[:len(m.fses)-1], append(newFs, m.fses[len(m.fses)-1])...) return } // Close closes all realFsMember members. This MergedFS will not be able to be used after this is called. func (m *MergedFS) Close() (err error) { var fsMem mergedFsMember m.mut.Lock() defer m.mut.Unlock() for _, fsMem = range m.fses { if err = fsMem.(io.Closer).Close(); err != nil { return } } return } /* Insert adds fsPaths as fs.FS to a MergedFS starting at index idx. If fsPaths is empty, Insert is a NO-OP. If idx is 0 or negative, they will be prepended. If idx is larger than the last index of real directories (excluding the last embed.FS), it will be reduced to the highest valid index. noExist and dupes have the same meaning as in NewMergedFS. */ func (m *MergedFS) Insert(idx int, fsPaths ...string) (noExist, dupes []string, err error) { var exists bool var fsPath string var newPaths []string var fsMem *realFsMember var newFs []mergedFsMember if len(fsPaths) == 0 { return } m.mut.Lock() defer m.mut.Unlock() newPaths = make([]string, 0, len(fsPaths)) newFs = make([]mergedFsMember, 0, len(fsPaths)) for _, fsPath = range fsPaths { if exists, err = paths.RealPathExists(&fsPath); err != nil { return } if !exists { noExist = append(noExist, fsPath) continue } if fsMem, err = NewRealFsMember(fsPath); err != nil { return } if !m.uniq.Add(fsPath) { dupes = append(dupes, fsPath) continue } newPaths = append(newPaths, fsPath) newFs = append(newFs, fsMem) } if len(newFs) == 0 { return } if idx < 0 { m.fses = append(newFs, m.fses...) m.overridePaths = append(newPaths, m.overridePaths...) return } else if idx >= len(m.fses)-1 { idx = len(m.fses) - 2 } m.fses = append(m.fses[:idx], append(newFs, m.fses[idx:]...)...) m.overridePaths = append(m.overridePaths[:idx], append(newPaths, m.overridePaths[idx:]...)...) return } // Open opens filename fnm and returns it as file handler fh. func (m *MergedFS) Open(fnm string) (fh fs.File, err error) { var fsref fs.FS m.mut.RLock() defer m.mut.RUnlock() // Iterate until a match is found. for _, fsref = range m.fses { if fh, err = fsref.Open(fnm); err == nil { return } } // No match found. err = &fs.PathError{ Op: "Open", Path: fnm, Err: fs.ErrNotExist, } return } /* Paths returns a slice of real filesystem paths that are used by this MergedFS. If sorted is true, they will be returned as a sorted slice - otherwise they will be returned in the order they are checked. */ func (m *MergedFS) Paths(sorted bool) (fsPaths []string) { m.mut.RLock() defer m.mut.RUnlock() if sorted { fsPaths = m.uniq.ToSlice() slices.Sort(fsPaths) } else { fsPaths = make([]string, len(m.overridePaths)-1) copy(fsPaths, m.overridePaths[:len(m.overridePaths)-1]) } return } /* ReadDir returns a listing of path matcher fpath. The result is a combination of all filesystems; the first encountered matching name is added. */ func (m *MergedFS) ReadDir(fpath string) (entries []fs.DirEntry, err error) { var idx int var e fs.DirEntry var tmpEntries []fs.DirEntry var seen map[string]bool = make(map[string]bool) m.mut.RLock() defer m.mut.RUnlock() // merge real and embedded, preferring first-seen real paths for idx, _ = range m.fses { if m.overridePaths[idx] == "" { if tmpEntries, err = m.fses[idx].ReadDir(filepath.FromSlash(fpath)); err != nil { continue } } else { if tmpEntries, err = os.ReadDir(filepath.Join(m.overridePaths[idx], filepath.FromSlash(fpath))); err != nil { continue } } for _, e = range tmpEntries { if seen[e.Name()] { continue } seen[e.Name()] = true entries = append(entries, e) } } return } /* ReadFile returns the bytes for the first found fpath. It wraps MergedFS.Open. */ func (m *MergedFS) ReadFile(fpath string) (b []byte, err error) { var fh fs.File if fh, err = m.Open(fpath); nil != err { return } defer func() { if fh != nil { if err = fh.Close(); err != nil { return } } }() if b, err = io.ReadAll(fh); err != nil { return } return } /* Stat returns the fs.FileInfo for the first found fpath. It wraps MergedFS.Open. */ func (m *MergedFS) Stat(fpath string) (stat fs.FileInfo, err error) { var fh fs.File if fh, err = m.Open(fpath); nil != err { return } defer func() { if fh != nil { if err = fh.Close(); err != nil { return } } }() if stat, err = fh.Stat(); err != nil { return } return } /* RemoveFsByIdx removes the filesystem found at index idx. An attempt to remove the final embed.FS filesystem will result in an ErrFsIdxOOB. If idx is larger than the last index of "real" filesystems, an ErrFsIdxOOB will be returned. */ func (m *MergedFS) RemoveFsByIdx(idx uint) (err error) { var fsPath string m.mut.Lock() defer m.mut.Unlock() if idx+1 == uint(len(m.fses)) { return } if idx > uint(len(m.overridePaths)-1) { err = ErrFsIdxOOB return } fsPath = m.overridePaths[idx] if m.uniq.Contains(fsPath) { m.uniq.Remove(fsPath) } m.fses = append(m.fses[:idx], m.fses[idx+1:]...) m.overridePaths = append(m.overridePaths[:idx], m.overridePaths[idx+1:]...) return } /* RemoveFsByPath removes the fileststem with path dirPath. If dirpath is empty, this function NOOPs. If dirpath does not exist in this MergedFS, an ErrFsPathNoExists will be returned. */ func (m *MergedFS) RemoveFsByPath(dirPath string) (err error) { var idx int var fsp string var p string = dirPath if p == "" { return } if err = paths.RealPath(&p); err != nil { return } m.mut.Lock() defer m.mut.Unlock() if !m.uniq.Contains(p) { err = ErrFsPathNoExists return } m.uniq.Remove(p) for idx, fsp = range m.overridePaths { if fsp == p { m.fses = append(m.fses[:idx], m.fses[idx+1:]...) m.overridePaths = append(m.overridePaths[:idx], m.overridePaths[idx+1:]...) return } } err = ErrFsIdxOOB return }