quintodrome/utils/merge/merge_fs.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

101 lines
2.3 KiB
Go
Raw Permalink Normal View History

2024-05-11 15:34:00 -08:00
package merge
import (
2024-06-06 03:09:30 -08:00
"cmp"
2021-07-20 15:50:00 -08:00
"errors"
"io"
2021-07-20 15:50:00 -08:00
"io/fs"
"maps"
2024-06-06 03:09:30 -08:00
"slices"
)
2024-05-11 15:34:00 -08:00
// FS implements a simple merged fs.FS, that can combine a Base FS with an Overlay FS. The semantics are:
2021-07-20 15:50:00 -08:00
// - Files from the Overlay FS will override files with the same name in the Base FS
// - Directories are combined, with priority for the Overlay FS over the Base FS for files with matching names
2024-05-11 15:34:00 -08:00
type FS struct {
2021-07-20 15:50:00 -08:00
Base fs.FS
Overlay fs.FS
2020-07-23 18:00:59 -08:00
}
2024-05-11 15:34:00 -08:00
func (m FS) Open(name string) (fs.File, error) {
2021-07-20 15:50:00 -08:00
file, err := m.Overlay.Open(name)
if err != nil {
2021-07-20 15:50:00 -08:00
return m.Base.Open(name)
}
2021-07-20 15:50:00 -08:00
info, err := file.Stat()
if err != nil {
2021-07-20 15:50:00 -08:00
_ = file.Close()
return nil, err
}
2021-07-20 15:50:00 -08:00
overlayDirFile, ok := file.(fs.ReadDirFile)
if !info.IsDir() || !ok {
return file, nil
}
2021-07-20 15:50:00 -08:00
baseDir, _ := m.Base.Open(name)
defer func() {
_ = baseDir.Close()
2021-07-20 15:50:00 -08:00
_ = file.Close()
}()
2021-07-20 15:50:00 -08:00
baseDirFile, ok := baseDir.(fs.ReadDirFile)
if !ok {
return nil, fs.ErrInvalid
}
return m.mergeDirs(name, info, baseDirFile, overlayDirFile)
}
2024-05-11 15:34:00 -08:00
func (m FS) mergeDirs(name string, info fs.FileInfo, baseDir fs.ReadDirFile, overlayDir fs.ReadDirFile) (fs.File, error) {
2021-07-20 15:50:00 -08:00
baseFiles, err := baseDir.ReadDir(-1)
if err != nil {
return nil, err
}
2021-07-20 15:50:00 -08:00
overlayFiles, err := overlayDir.ReadDir(-1)
if err != nil {
overlayFiles = nil
}
2024-06-06 03:09:30 -08:00
merged := map[string]fs.DirEntry{}
for _, f := range baseFiles {
merged[f.Name()] = f
}
for _, f := range overlayFiles {
merged[f.Name()] = f
}
it := maps.Values(merged)
entries := slices.SortedFunc(it, func(i, j fs.DirEntry) int { return cmp.Compare(i.Name(), j.Name()) })
return &mergedDir{
name: name,
info: info,
entries: entries,
}, nil
}
type mergedDir struct {
name string
2021-07-20 15:50:00 -08:00
info fs.FileInfo
entries []fs.DirEntry
pos int
}
2021-07-20 15:50:00 -08:00
var _ fs.ReadDirFile = (*mergedDir)(nil)
func (d *mergedDir) ReadDir(count int) ([]fs.DirEntry, error) {
if d.pos >= len(d.entries) && count > 0 {
return nil, io.EOF
}
if count <= 0 || count > len(d.entries)-d.pos {
count = len(d.entries) - d.pos
}
2021-07-20 15:50:00 -08:00
entries := d.entries[d.pos : d.pos+count]
d.pos += count
2021-07-20 15:50:00 -08:00
return entries, nil
}
2020-11-05 09:34:56 -09:00
func (d *mergedDir) Close() error { return nil }
2021-07-20 15:50:00 -08:00
func (d *mergedDir) Stat() (fs.FileInfo, error) { return d.info, nil }
func (d *mergedDir) Read([]byte) (int, error) {
2021-07-20 15:50:00 -08:00
return 0, &fs.PathError{Op: "read", Path: d.name, Err: errors.New("is a directory")}
}