336 lines
7.4 KiB
Go
336 lines
7.4 KiB
Go
package overlayfs
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"sync"
|
|
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
var (
|
|
_ FilesystemIterator = (*OverlayFs)(nil)
|
|
_ afero.Fs = (*OverlayFs)(nil)
|
|
_ afero.Lstater = (*OverlayFs)(nil)
|
|
_ afero.File = (*Dir)(nil)
|
|
)
|
|
|
|
// FilesystemIterator is an interface for iterating over the wrapped filesystems in order.
|
|
type FilesystemIterator interface {
|
|
Filesystem(i int) afero.Fs
|
|
NumFilesystems() int
|
|
}
|
|
|
|
type Options struct {
|
|
// The filesystems to overlay ordered in priority from left to right.
|
|
Fss []afero.Fs
|
|
|
|
// The OverlayFs is by default read-only, but you can nominate the first filesystem to be writable.
|
|
FirstWritable bool
|
|
|
|
// The DirsMerger is used to merge the contents of two directories.
|
|
// If not provided, the defaultDirMerger is used.
|
|
DirsMerger DirsMerger
|
|
}
|
|
|
|
// OverlayFs is a filesystem that overlays multiple filesystems.
|
|
// It's by default a read-only filesystem, but you can nominate the first filesystem to be writable.
|
|
// For all operations, the filesystems are checked in order until found.
|
|
// If a filesystem implementes FilesystemIterator, those filesystems will be checked before continuing.
|
|
type OverlayFs struct {
|
|
fss []afero.Fs
|
|
|
|
mergeDirs DirsMerger
|
|
firstWritable bool
|
|
}
|
|
|
|
// New creates a new OverlayFs with the given options.
|
|
func New(opts Options) *OverlayFs {
|
|
if opts.DirsMerger == nil {
|
|
opts.DirsMerger = defaultDirMerger
|
|
}
|
|
|
|
return &OverlayFs{
|
|
fss: opts.Fss,
|
|
mergeDirs: opts.DirsMerger,
|
|
firstWritable: opts.FirstWritable,
|
|
}
|
|
}
|
|
|
|
// Append creates a shallow copy of the filesystem and appends the given filesystems to it.
|
|
func (ofs OverlayFs) Append(fss ...afero.Fs) *OverlayFs {
|
|
ofs.fss = append(ofs.fss, fss...)
|
|
return &ofs
|
|
}
|
|
|
|
// Filesystem returns filesystem with index i, nil if not found.
|
|
func (ofs *OverlayFs) Filesystem(i int) afero.Fs {
|
|
if i >= len(ofs.fss) {
|
|
return nil
|
|
}
|
|
return ofs.fss[i]
|
|
}
|
|
|
|
// NumFilesystems returns the number of filesystems in this composite filesystem.
|
|
func (ofs *OverlayFs) NumFilesystems() int {
|
|
return len(ofs.fss)
|
|
}
|
|
|
|
// Name returns the name of this filesystem.
|
|
func (ofs *OverlayFs) Name() string {
|
|
return "overlayfs"
|
|
}
|
|
|
|
func (ofs *OverlayFs) collectDirs(name string, withFs func(fs afero.Fs)) error {
|
|
for _, fs := range ofs.fss {
|
|
if err := ofs.collectDirsRecursive(fs, name, withFs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ofs *OverlayFs) collectDirsRecursive(fs afero.Fs, name string, withFs func(fs afero.Fs)) error {
|
|
if fi, err := fs.Stat(name); err == nil && fi.IsDir() {
|
|
withFs(fs)
|
|
}
|
|
if fsi, ok := fs.(FilesystemIterator); ok {
|
|
for i := 0; i < fsi.NumFilesystems(); i++ {
|
|
if err := ofs.collectDirsRecursive(fsi.Filesystem(i), name, withFs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ofs *OverlayFs) stat(name string, lstatIfPossible bool) (afero.Fs, os.FileInfo, bool, error) {
|
|
for _, fs := range ofs.fss {
|
|
if fs2, fi, ok, err := ofs.statRecursive(fs, name, lstatIfPossible); err == nil || !os.IsNotExist(err) {
|
|
return fs2, fi, ok, err
|
|
}
|
|
}
|
|
return nil, nil, false, os.ErrNotExist
|
|
}
|
|
|
|
func (ofs *OverlayFs) statRecursive(fs afero.Fs, name string, lstatIfPossible bool) (afero.Fs, os.FileInfo, bool, error) {
|
|
if lstatIfPossible {
|
|
if lfs, ok := fs.(afero.Lstater); ok {
|
|
fi, ok, err := lfs.LstatIfPossible(name)
|
|
if err == nil || !os.IsNotExist(err) {
|
|
return fs, fi, ok, err
|
|
}
|
|
} else if fi, err := fs.Stat(name); err == nil || !os.IsNotExist(err) {
|
|
return fs, fi, false, err
|
|
}
|
|
} else if fi, err := fs.Stat(name); err == nil || !os.IsNotExist(err) {
|
|
return fs, fi, false, err
|
|
}
|
|
if fsi, ok := fs.(FilesystemIterator); ok {
|
|
for i := 0; i < fsi.NumFilesystems(); i++ {
|
|
if fs2, fi, ok, err := ofs.statRecursive(fsi.Filesystem(i), name, lstatIfPossible); err == nil || !os.IsNotExist(err) {
|
|
return fs2, fi, ok, err
|
|
}
|
|
}
|
|
}
|
|
return nil, nil, false, os.ErrNotExist
|
|
}
|
|
|
|
func (ofs *OverlayFs) writeFs() afero.Fs {
|
|
if len(ofs.fss) == 0 {
|
|
panic("overlayfs: there are no filesystems to write to")
|
|
}
|
|
return ofs.fss[0]
|
|
}
|
|
|
|
// DirsMerger is used to merge two directories.
|
|
type DirsMerger func(lofi, bofi []os.FileInfo) []os.FileInfo
|
|
|
|
var defaultDirMerger = func(lofi, bofi []os.FileInfo) []os.FileInfo {
|
|
for _, bofi := range bofi {
|
|
var found bool
|
|
for _, lofi := range lofi {
|
|
if bofi.Name() == lofi.Name() {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
lofi = append(lofi, bofi)
|
|
}
|
|
}
|
|
return lofi
|
|
|
|
}
|
|
|
|
var dirPool = &sync.Pool{
|
|
New: func() any {
|
|
return &Dir{}
|
|
},
|
|
}
|
|
|
|
func getDir() *Dir {
|
|
return dirPool.Get().(*Dir)
|
|
}
|
|
|
|
func releaseDir(dir *Dir) {
|
|
dir.fss = dir.fss[:0]
|
|
dir.fis = dir.fis[:0]
|
|
dir.offset = 0
|
|
dir.name = ""
|
|
dir.err = nil
|
|
dirPool.Put(dir)
|
|
}
|
|
|
|
// Dir is an afero.File that represents list of directories that will be merged in Readdir and Readdirnames.
|
|
type Dir struct {
|
|
name string
|
|
fss []afero.Fs
|
|
merge DirsMerger
|
|
|
|
err error
|
|
offset int
|
|
fis []os.FileInfo
|
|
}
|
|
|
|
// Readdir implements afero.File.Readdir.
|
|
// If n > 0, Readdir returns at most n.
|
|
func (d *Dir) Readdir(n int) ([]os.FileInfo, error) {
|
|
if d.err != nil {
|
|
return nil, d.err
|
|
}
|
|
if len(d.fss) == 0 {
|
|
return nil, os.ErrClosed
|
|
}
|
|
|
|
if d.offset == 0 {
|
|
readDir := func(fs afero.Fs) error {
|
|
f, err := fs.Open(d.name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
fi, err := f.Readdir(-1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.fis = d.merge(d.fis, fi)
|
|
return nil
|
|
}
|
|
|
|
for _, fs := range d.fss {
|
|
if err := readDir(fs); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
fis := d.fis[d.offset:]
|
|
|
|
if n <= 0 {
|
|
d.err = io.EOF
|
|
if d.offset > 0 && len(fis) == 0 {
|
|
return nil, d.err
|
|
}
|
|
fisc := make([]os.FileInfo, len(fis))
|
|
copy(fisc, fis)
|
|
return fisc, nil
|
|
}
|
|
|
|
if len(fis) == 0 {
|
|
d.err = io.EOF
|
|
return nil, d.err
|
|
}
|
|
|
|
if n > len(d.fis) {
|
|
n = len(d.fis)
|
|
}
|
|
|
|
defer func() { d.offset += n }()
|
|
|
|
fisc := make([]os.FileInfo, len(fis[:n]))
|
|
copy(fisc, fis[:n])
|
|
|
|
return fisc, nil
|
|
|
|
}
|
|
|
|
// Readdirnames implements afero.File.Readdirnames.
|
|
// If n > 0, Readdirnames returns at most n.
|
|
func (d *Dir) Readdirnames(n int) ([]string, error) {
|
|
if len(d.fss) == 0 {
|
|
return nil, os.ErrClosed
|
|
}
|
|
|
|
fis, err := d.Readdir(n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
names := make([]string, len(fis))
|
|
for i, fi := range fis {
|
|
names[i] = fi.Name()
|
|
}
|
|
return names, nil
|
|
}
|
|
|
|
// Stat implements afero.File.Stat.
|
|
func (d *Dir) Stat() (os.FileInfo, error) {
|
|
if len(d.fss) == 0 {
|
|
return nil, os.ErrClosed
|
|
}
|
|
return d.fss[0].Stat(d.name)
|
|
}
|
|
|
|
// Close implements afero.File.Close.
|
|
// Note that d must not be used after it is closed,
|
|
// as the object may be reused.
|
|
func (d *Dir) Close() error {
|
|
releaseDir(d)
|
|
return nil
|
|
}
|
|
|
|
// Name implements afero.File.Name.
|
|
func (d *Dir) Name() string {
|
|
return d.name
|
|
}
|
|
|
|
// Read is not supported.
|
|
func (d *Dir) Read(p []byte) (n int, err error) {
|
|
panic("not supported")
|
|
}
|
|
|
|
// ReadAt is not supported.
|
|
func (d *Dir) ReadAt(p []byte, off int64) (n int, err error) {
|
|
panic("not supported")
|
|
}
|
|
|
|
// Seek is not supported.
|
|
func (d *Dir) Seek(offset int64, whence int) (int64, error) {
|
|
panic("not supported")
|
|
}
|
|
|
|
// Write is not supported.
|
|
func (d *Dir) Write(p []byte) (n int, err error) {
|
|
panic("not supported")
|
|
}
|
|
|
|
// WriteAt is not supported.
|
|
func (d *Dir) WriteAt(p []byte, off int64) (n int, err error) {
|
|
panic("not supported")
|
|
}
|
|
|
|
// Sync is not supported.
|
|
func (d *Dir) Sync() error {
|
|
panic("not supported")
|
|
}
|
|
|
|
// Truncate is not supported.
|
|
func (d *Dir) Truncate(size int64) error {
|
|
panic("not supported")
|
|
}
|
|
|
|
// WriteString is not supported.
|
|
func (d *Dir) WriteString(s string) (ret int, err error) {
|
|
panic("not supported")
|
|
}
|