|
| 1 | +package google |
| 2 | + |
| 3 | +import ( |
| 4 | + "errors" |
| 5 | + "fmt" |
| 6 | + "io" |
| 7 | + "io/fs" |
| 8 | + "os" |
| 9 | +) |
| 10 | + |
| 11 | +// internal interface supporting ReadDirFS and ReadFileFS |
| 12 | +// |
| 13 | +// Most sensible FS implementations support these, as an example both `embed.FS` |
| 14 | +// and `os.DirFS(...)` implement these. |
| 15 | +type ReadDirReadFileFS interface { |
| 16 | + fs.ReadDirFS |
| 17 | + fs.ReadFileFS |
| 18 | +} |
| 19 | + |
| 20 | +// overlayFS is an fs.FS implementation which supports the concept of overlays. |
| 21 | +// |
| 22 | +// Given two fs.FS, `overlay` and `base`, NewOverlayFS(overlay, base) builds an FS |
| 23 | +// which prioritizes files in `overlay` over files in `base`. |
| 24 | +// |
| 25 | +// As an example, given: |
| 26 | +// - overlay = { |
| 27 | +// "a/b": "foo", |
| 28 | +// "c": "c", |
| 29 | +// } |
| 30 | +// - base = { |
| 31 | +// "a/b": "bar", |
| 32 | +// "d": "d", |
| 33 | +// } |
| 34 | +// |
| 35 | +// Then: |
| 36 | +// |
| 37 | +// ofs.ReadFile("a/b") -> "foo" |
| 38 | +// ofs.ReadFile("d") -> "d" |
| 39 | +// ofs.ReadDir(".") -> {"a/b":"foo", "c":"c", "d":"d"} |
| 40 | +type overlayFS struct { |
| 41 | + overlay, base ReadDirReadFileFS |
| 42 | +} |
| 43 | + |
| 44 | +func dirAsReadDirReadFileFS(path string) (ReadDirReadFileFS, error) { |
| 45 | + fsys, ok := os.DirFS(path).(ReadDirReadFileFS) |
| 46 | + if !ok { |
| 47 | + return nil, fmt.Errorf("Golang documentations claim that DirFS implements ReadDirFS and ReadFileFS") |
| 48 | + } |
| 49 | + return fsys, nil |
| 50 | +} |
| 51 | + |
| 52 | +// NewOverlayFS create an overlay FS from two directories. |
| 53 | +// overlayDirectory may be empty to |
| 54 | +func NewOverlayFS(overlayDirectory, baseDirectory string) (ReadDirReadFileFS, error) { |
| 55 | + base, err := dirAsReadDirReadFileFS(baseDirectory) |
| 56 | + if err != nil { |
| 57 | + return nil, err |
| 58 | + } |
| 59 | + if overlayDirectory == "" { |
| 60 | + return base, nil |
| 61 | + } |
| 62 | + o, err := dirAsReadDirReadFileFS(overlayDirectory) |
| 63 | + if err != nil { |
| 64 | + return nil, err |
| 65 | + } |
| 66 | + return overlayFS{overlay: o, base: base}, nil |
| 67 | +} |
| 68 | + |
| 69 | +// Open implements the main FS interface. |
| 70 | +func (o overlayFS) Open(name string) (fs.File, error) { |
| 71 | + f, err := o.overlay.Open(name) |
| 72 | + f2, err2 := o.base.Open(name) |
| 73 | + if err != nil { |
| 74 | + return f2, err2 |
| 75 | + } |
| 76 | + if err2 != nil { |
| 77 | + return f, err |
| 78 | + } |
| 79 | + overlay, ok := f.(fs.ReadDirFile) |
| 80 | + if !ok { |
| 81 | + // not a directory, we can return the unmerged overlay file |
| 82 | + return f, err |
| 83 | + } |
| 84 | + base, ok := f2.(fs.ReadDirFile) |
| 85 | + if !ok { |
| 86 | + // inconsistency between the two FSes, surprising. |
| 87 | + return nil, fmt.Errorf("Open(%q)'s base did not return ReadDirFile values", name) |
| 88 | + } |
| 89 | + // As a note, here we could have taken shortcuts and not implemented this: |
| 90 | + // implementations will realize that OverlayFS implements ReadDirFS and |
| 91 | + // call overylayfs.ReadDir(dir) instead of overlayfs.Open(dir)+d.ReadDir(int). |
| 92 | + // |
| 93 | + // That being said we do so to be a compliant FS and pass the fstest.TestFS |
| 94 | + // test battery. |
| 95 | + return &overlayDirFile{overlay: overlay, base: base}, nil |
| 96 | +} |
| 97 | + |
| 98 | +// ReadFile implements the ReadFileFS interface. |
| 99 | +func (o overlayFS) ReadFile(name string) ([]byte, error) { |
| 100 | + b, err := o.overlay.ReadFile(name) |
| 101 | + if err == nil { |
| 102 | + return b, nil |
| 103 | + } |
| 104 | + return o.base.ReadFile(name) |
| 105 | +} |
| 106 | + |
| 107 | +// ReadDir implements the ReadDirFS interface. |
| 108 | +func (o overlayFS) ReadDir(name string) ([]fs.DirEntry, error) { |
| 109 | + a, err1 := o.overlay.ReadDir(name) |
| 110 | + b, err2 := o.base.ReadDir(name) |
| 111 | + return mergeReadDirs(a, b, err1, err2) |
| 112 | +} |
| 113 | + |
| 114 | +func mergeReadDirs(overlay, base []fs.DirEntry, errOverlay, errBase error) ([]fs.DirEntry, error) { |
| 115 | + if errOverlay != nil { |
| 116 | + // No need to merge (and handle both fs errors case). |
| 117 | + return base, errBase |
| 118 | + } |
| 119 | + var merged []fs.DirEntry |
| 120 | + seen := make(map[string]bool) |
| 121 | + for _, e := range overlay { |
| 122 | + seen[e.Name()] = true |
| 123 | + merged = append(merged, e) |
| 124 | + } |
| 125 | + for _, e := range base { |
| 126 | + if _, ok := seen[e.Name()]; !ok { |
| 127 | + merged = append(merged, e) |
| 128 | + } |
| 129 | + } |
| 130 | + return merged, nil |
| 131 | +} |
| 132 | + |
| 133 | +// ReadDirFile implementation when both overlay and base have an existing such |
| 134 | +// directory. |
| 135 | +type overlayDirFile struct { |
| 136 | + overlay, base fs.ReadDirFile |
| 137 | + initialized bool |
| 138 | + entries []fs.DirEntry |
| 139 | + offset int |
| 140 | +} |
| 141 | + |
| 142 | +func (f *overlayDirFile) Stat() (fs.FileInfo, error) { |
| 143 | + return f.overlay.Stat() |
| 144 | +} |
| 145 | + |
| 146 | +func (f *overlayDirFile) Read(b []byte) (int, error) { |
| 147 | + // Will be an error: one can't read directories. |
| 148 | + return f.overlay.Read(b) |
| 149 | +} |
| 150 | + |
| 151 | +func (f *overlayDirFile) Close() error { |
| 152 | + err := f.overlay.Close() |
| 153 | + err2 := f.base.Close() |
| 154 | + return errors.Join(err, err2) |
| 155 | +} |
| 156 | + |
| 157 | +func (f *overlayDirFile) ReadDir(count int) ([]fs.DirEntry, error) { |
| 158 | + if !f.initialized { |
| 159 | + a, err1 := f.overlay.ReadDir(-1) |
| 160 | + b, err2 := f.base.ReadDir(-1) |
| 161 | + if err1 != nil || err2 != nil { |
| 162 | + panic("unexpected error") |
| 163 | + } |
| 164 | + var err error |
| 165 | + f.entries, err = mergeReadDirs(a, b, err1, err2) |
| 166 | + if err != nil { |
| 167 | + panic("unexpected error") |
| 168 | + } |
| 169 | + f.initialized = true |
| 170 | + } |
| 171 | + n := len(f.entries) - f.offset |
| 172 | + if n == 0 { |
| 173 | + if count <= 0 { |
| 174 | + return nil, nil |
| 175 | + } |
| 176 | + return nil, io.EOF |
| 177 | + } |
| 178 | + if count > 0 && n > count { |
| 179 | + n = count |
| 180 | + } |
| 181 | + list := make([]fs.DirEntry, n) |
| 182 | + for i := range list { |
| 183 | + list[i] = f.entries[f.offset+i] |
| 184 | + } |
| 185 | + f.offset += n |
| 186 | + return list, nil |
| 187 | +} |
| 188 | + |
| 189 | +// Verifying interface implementations |
| 190 | +var _ ReadDirReadFileFS = (*overlayFS)(nil) |
| 191 | +var _ fs.ReadDirFile = (*overlayDirFile)(nil) |
0 commit comments