Signed-off-by: Matheus Sampaio Queiroga <srherobrine20@gmail.com>
253 lines
6.4 KiB
Go
253 lines
6.4 KiB
Go
package overlayfs
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"iter"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
var ErrNotImplemented = errors.New("not implemented")
|
|
|
|
const (
|
|
OpaqueWhiteout string = ".wh." // File to ignore
|
|
fileCreateFlags = os.O_CREATE | os.O_TRUNC | os.O_WRONLY // Flags to create file, if exists replace data
|
|
)
|
|
|
|
type flages int
|
|
|
|
func (f flages) Flags() (flags []int) {
|
|
if int(f)&os.O_RDONLY != 0 {
|
|
flags = append(flags, os.O_RDONLY)
|
|
}
|
|
if int(f)&os.O_WRONLY != 0 {
|
|
flags = append(flags, os.O_WRONLY)
|
|
}
|
|
if int(f)&os.O_RDWR != 0 {
|
|
flags = append(flags, os.O_RDWR)
|
|
}
|
|
if int(f)&os.O_APPEND != 0 {
|
|
flags = append(flags, os.O_APPEND)
|
|
}
|
|
if int(f)&os.O_CREATE != 0 {
|
|
flags = append(flags, os.O_CREATE)
|
|
}
|
|
if int(f)&os.O_EXCL != 0 {
|
|
flags = append(flags, os.O_EXCL)
|
|
}
|
|
if int(f)&os.O_SYNC != 0 {
|
|
flags = append(flags, os.O_SYNC)
|
|
}
|
|
if int(f)&os.O_TRUNC != 0 {
|
|
flags = append(flags, os.O_TRUNC)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Check flag have write permissions
|
|
func (f flages) IsRead() bool {
|
|
flags := f.Flags()
|
|
return slices.Contains(flags, os.O_RDWR) ||
|
|
slices.Contains(flags, os.O_RDONLY)
|
|
}
|
|
|
|
// Check flag have write permissions
|
|
func (f flages) IsWrite() bool {
|
|
flags := f.Flags()
|
|
return slices.Contains(flags, os.O_RDWR) ||
|
|
slices.Contains(flags, os.O_WRONLY) ||
|
|
slices.Contains(flags, os.O_CREATE) ||
|
|
slices.Contains(flags, os.O_TRUNC)
|
|
}
|
|
|
|
func (f flages) CreateIfNotExist() bool {
|
|
flags := f.Flags()
|
|
return slices.Contains(flags, os.O_CREATE)
|
|
}
|
|
|
|
func rewriteName(name string, err error) error {
|
|
if err == nil {
|
|
return err
|
|
} else if e, ok := err.(*fs.PathError); ok {
|
|
e.Path = name
|
|
} else if e, ok := err.(*os.LinkError); ok {
|
|
e.Old = name
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Check if rw overlayfs
|
|
func (over Overlayfs) isRW() bool {
|
|
rooted := strings.TrimSpace(over.Upper)
|
|
return rooted != "" && (filepath.IsAbs(rooted) || filepath.IsLocal(rooted))
|
|
}
|
|
|
|
type fileLayer struct {
|
|
Layer string // layer Path
|
|
Stat fs.FileInfo // File info
|
|
FromUpper bool // is file present on Upper dir
|
|
}
|
|
|
|
func (over Overlayfs) allLayersSeq() iter.Seq[string] {
|
|
return func(yield func(string) bool) {
|
|
if over.Upper != "" {
|
|
if !yield(over.Upper) {
|
|
return
|
|
}
|
|
}
|
|
for _, layer := range over.Lower {
|
|
if layer = strings.TrimSpace(layer); layer == "" {
|
|
continue
|
|
} else if !yield(layer) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (over Overlayfs) isPathInsideOverlay(path string) bool {
|
|
for layer := range over.allLayersSeq() {
|
|
if !strings.HasPrefix(filepath.Join(layer, path), layer) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (over Overlayfs) retrieveFileFromLayers(name string) ([]fileLayer, error) {
|
|
content, layers := []fileLayer{}, over.allLayersSeq()
|
|
for layer := range layers {
|
|
stat, err := os.Stat(filepath.Join(layer, name))
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return nil, rewriteName(name, err)
|
|
}
|
|
content = append(content, fileLayer{
|
|
FromUpper: layer == over.Upper,
|
|
Layer: layer,
|
|
Stat: stat,
|
|
})
|
|
}
|
|
return content, nil
|
|
}
|
|
|
|
func toMarkedToDeleteFilename(name string) string {
|
|
dir, name := filepath.Split(name)
|
|
return filepath.Join(dir, OpaqueWhiteout+name)
|
|
}
|
|
|
|
// Remove opaque file if exists
|
|
func (over Overlayfs) removeIfDeleted(name string) error {
|
|
if over.isRW() {
|
|
dir, name := filepath.Split(name)
|
|
name = filepath.Join(over.Upper, dir, OpaqueWhiteout+name)
|
|
if _, err := os.Stat(name); err == nil {
|
|
return os.Remove(name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (over Overlayfs) makeFileDeleted(name string) error {
|
|
if over.isRW() {
|
|
fullPath := filepath.Join(over.Upper, toMarkedToDeleteFilename(name))
|
|
if _, err := os.Stat(filepath.Dir(fullPath)); err != nil {
|
|
if err = os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return os.WriteFile(fullPath, nil, 0666)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Check if file marked deleted
|
|
func (over Overlayfs) isFileMarkedDeleted(name string) (bool, error) {
|
|
if over.isRW() && !(name == "." || name == "") {
|
|
for len(name) > 2 {
|
|
var filename string
|
|
name, filename = filepath.Split(name)
|
|
if filename == "" {
|
|
if name[len(name)-1] == filepath.Separator {
|
|
filename = filepath.Base(name)
|
|
name = ""
|
|
}
|
|
}
|
|
|
|
_, err := os.Stat(filepath.Join(name, OpaqueWhiteout+filename))
|
|
if err == nil {
|
|
return true, nil
|
|
} else if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// Clone from lower layer to upper to modifications
|
|
func (over Overlayfs) copyFileToUpperIfAbsent(name string) error {
|
|
files, err := over.retrieveFileFromLayers(name)
|
|
if err != nil {
|
|
return rewriteName(name, err)
|
|
}
|
|
|
|
// Locate layers
|
|
fromUpperIndex, fromFistLowIndex := slices.IndexFunc(files, func(entry fileLayer) bool { return entry.FromUpper }), slices.IndexFunc(files, func(entry fileLayer) bool { return !entry.FromUpper })
|
|
if (fromUpperIndex == -1 && fromFistLowIndex == -1) || fromUpperIndex != -1 {
|
|
return nil // Ignore copy
|
|
} else if fromFistLowIndex == -1 {
|
|
return &fs.PathError{Op: "copy", Path: name, Err: fs.ErrNotExist}
|
|
}
|
|
|
|
// File stats
|
|
fileInfo := files[fromFistLowIndex]
|
|
lowPath, uperPath := filepath.Join(fileInfo.Layer, name), filepath.Join(over.Upper, name)
|
|
|
|
// Remove opaque file
|
|
if err := over.removeIfDeleted(name); err != nil {
|
|
return err
|
|
}
|
|
|
|
return rewriteName(name, over.copyFromTo(fileInfo, name, lowPath, uperPath))
|
|
}
|
|
|
|
func (over Overlayfs) copyFromTo(fileInfo fileLayer, name, lowPath, uperPath string) error {
|
|
switch fileInfo.Stat.Mode().Type() {
|
|
case fs.ModeAppend, fs.ModeExclusive, fs.ModeTemporary, fs.ModeDevice, fs.ModeNamedPipe, fs.ModeSocket, fs.ModeSetuid, fs.ModeSetgid, fs.ModeCharDevice, fs.ModeSticky, fs.ModeIrregular:
|
|
return &fs.PathError{Op: "copy", Path: name, Err: fs.ErrInvalid}
|
|
case fs.ModeDir:
|
|
return os.CopyFS(uperPath, os.DirFS(lowPath)) // Copy dir full dir
|
|
case fs.ModeSymlink:
|
|
target, err := os.Readlink(filepath.Join(lowPath))
|
|
if err == nil {
|
|
err = os.Symlink(target, uperPath)
|
|
}
|
|
return rewriteName(name, err)
|
|
default:
|
|
// Open file
|
|
fromReadFile, err := os.Open(lowPath)
|
|
if err != nil {
|
|
return rewriteName(name, err)
|
|
}
|
|
defer fromReadFile.Close() // Close file
|
|
|
|
// Create target file
|
|
targetWriteFile, err := os.OpenFile(filepath.Join(over.Upper, name), fileCreateFlags, fileInfo.Stat.Mode().Perm())
|
|
if err != nil {
|
|
return rewriteName(name, err)
|
|
}
|
|
defer targetWriteFile.Close()
|
|
|
|
// Copy file
|
|
_, err = io.Copy(targetWriteFile, fromReadFile)
|
|
return rewriteName(name, err)
|
|
}
|
|
}
|