414 lines
8 KiB
Go
414 lines
8 KiB
Go
// Package memfs is a variant of "github.com/go-git/go-billy/v5/memfs" with
|
|
// stable mtimes for items.
|
|
package memfs
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/go-git/go-billy/v5"
|
|
"github.com/go-git/go-billy/v5/helper/chroot"
|
|
"github.com/go-git/go-billy/v5/util"
|
|
)
|
|
|
|
const separator = filepath.Separator
|
|
|
|
// Memory a very convenient filesystem based on memory files
|
|
type Memory struct {
|
|
s *storage
|
|
}
|
|
|
|
// New returns a new Memory filesystem.
|
|
func New() billy.Filesystem {
|
|
fs := &Memory{s: newStorage()}
|
|
return chroot.New(fs, string(separator))
|
|
}
|
|
|
|
func (fs *Memory) Create(filename string) (billy.File, error) {
|
|
return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
|
}
|
|
|
|
func (fs *Memory) Open(filename string) (billy.File, error) {
|
|
return fs.OpenFile(filename, os.O_RDONLY, 0)
|
|
}
|
|
|
|
func (fs *Memory) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
|
|
f, has := fs.s.Get(filename)
|
|
if !has {
|
|
if !isCreate(flag) {
|
|
return nil, os.ErrNotExist
|
|
}
|
|
|
|
var err error
|
|
f, err = fs.s.New(filename, perm, flag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if isExclusive(flag) {
|
|
return nil, os.ErrExist
|
|
}
|
|
|
|
if target, isLink := fs.resolveLink(filename, f); isLink {
|
|
return fs.OpenFile(target, flag, perm)
|
|
}
|
|
}
|
|
|
|
if f.mode.IsDir() {
|
|
return nil, fmt.Errorf("cannot open directory: %s", filename)
|
|
}
|
|
|
|
return f.Duplicate(filename, perm, flag), nil
|
|
}
|
|
|
|
func (fs *Memory) resolveLink(fullpath string, f *file) (target string, isLink bool) {
|
|
if !isSymlink(f.mode) {
|
|
return fullpath, false
|
|
}
|
|
|
|
target = string(f.content.bytes)
|
|
if !isAbs(target) {
|
|
target = fs.Join(filepath.Dir(fullpath), target)
|
|
}
|
|
|
|
return target, true
|
|
}
|
|
|
|
// On Windows OS, IsAbs validates if a path is valid based on if stars with a
|
|
// unit (eg.: `C:\`) to assert that is absolute, but in this mem implementation
|
|
// any path starting by `separator` is also considered absolute.
|
|
func isAbs(path string) bool {
|
|
return filepath.IsAbs(path) || strings.HasPrefix(path, string(separator))
|
|
}
|
|
|
|
func (fs *Memory) Stat(filename string) (os.FileInfo, error) {
|
|
f, has := fs.s.Get(filename)
|
|
if !has {
|
|
return nil, os.ErrNotExist
|
|
}
|
|
|
|
fi, _ := f.Stat()
|
|
|
|
var err error
|
|
if target, isLink := fs.resolveLink(filename, f); isLink {
|
|
fi, err = fs.Stat(target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// the name of the file should always the name of the stated file, so we
|
|
// overwrite the Stat returned from the storage with it, since the
|
|
// filename may belong to a link.
|
|
fi.(*fileInfo).name = filepath.Base(filename)
|
|
return fi, nil
|
|
}
|
|
|
|
func (fs *Memory) Lstat(filename string) (os.FileInfo, error) {
|
|
f, has := fs.s.Get(filename)
|
|
if !has {
|
|
return nil, os.ErrNotExist
|
|
}
|
|
|
|
return f.Stat()
|
|
}
|
|
|
|
type ByName []os.FileInfo
|
|
|
|
func (a ByName) Len() int { return len(a) }
|
|
func (a ByName) Less(i, j int) bool { return a[i].Name() < a[j].Name() }
|
|
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
|
|
func (fs *Memory) ReadDir(path string) ([]os.FileInfo, error) {
|
|
if f, has := fs.s.Get(path); has {
|
|
if target, isLink := fs.resolveLink(path, f); isLink {
|
|
return fs.ReadDir(target)
|
|
}
|
|
} else {
|
|
return nil, &os.PathError{Op: "open", Path: path, Err: syscall.ENOENT}
|
|
}
|
|
|
|
var entries []os.FileInfo
|
|
for _, f := range fs.s.Children(path) {
|
|
fi, _ := f.Stat()
|
|
entries = append(entries, fi)
|
|
}
|
|
|
|
sort.Sort(ByName(entries))
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
func (fs *Memory) MkdirAll(path string, perm os.FileMode) error {
|
|
_, err := fs.s.New(path, perm|os.ModeDir, 0)
|
|
return err
|
|
}
|
|
|
|
func (fs *Memory) TempFile(dir, prefix string) (billy.File, error) {
|
|
return util.TempFile(fs, dir, prefix)
|
|
}
|
|
|
|
func (fs *Memory) Rename(from, to string) error {
|
|
return fs.s.Rename(from, to)
|
|
}
|
|
|
|
func (fs *Memory) Remove(filename string) error {
|
|
return fs.s.Remove(filename)
|
|
}
|
|
|
|
func (fs *Memory) Join(elem ...string) string {
|
|
return filepath.Join(elem...)
|
|
}
|
|
|
|
func (fs *Memory) Symlink(target, link string) error {
|
|
_, err := fs.Stat(link)
|
|
if err == nil {
|
|
return os.ErrExist
|
|
}
|
|
|
|
if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
return util.WriteFile(fs, link, []byte(target), 0777|os.ModeSymlink)
|
|
}
|
|
|
|
func (fs *Memory) Readlink(link string) (string, error) {
|
|
f, has := fs.s.Get(link)
|
|
if !has {
|
|
return "", os.ErrNotExist
|
|
}
|
|
|
|
if !isSymlink(f.mode) {
|
|
return "", &os.PathError{
|
|
Op: "readlink",
|
|
Path: link,
|
|
Err: fmt.Errorf("not a symlink"),
|
|
}
|
|
}
|
|
|
|
return string(f.content.bytes), nil
|
|
}
|
|
|
|
// Capabilities implements the Capable interface.
|
|
func (fs *Memory) Capabilities() billy.Capability {
|
|
return billy.WriteCapability |
|
|
billy.ReadCapability |
|
|
billy.ReadAndWriteCapability |
|
|
billy.SeekCapability |
|
|
billy.TruncateCapability
|
|
}
|
|
|
|
type file struct {
|
|
name string
|
|
content *content
|
|
position int64
|
|
flag int
|
|
mode os.FileMode
|
|
mtime time.Time
|
|
|
|
isClosed bool
|
|
}
|
|
|
|
func (f *file) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
func (f *file) Read(b []byte) (int, error) {
|
|
n, err := f.ReadAt(b, f.position)
|
|
f.position += int64(n)
|
|
|
|
if err == io.EOF && n != 0 {
|
|
err = nil
|
|
}
|
|
|
|
return n, err
|
|
}
|
|
|
|
func (f *file) ReadAt(b []byte, off int64) (int, error) {
|
|
if f.isClosed {
|
|
return 0, os.ErrClosed
|
|
}
|
|
|
|
if !isReadAndWrite(f.flag) && !isReadOnly(f.flag) {
|
|
return 0, errors.New("read not supported")
|
|
}
|
|
|
|
n, err := f.content.ReadAt(b, off)
|
|
|
|
return n, err
|
|
}
|
|
|
|
func (f *file) Seek(offset int64, whence int) (int64, error) {
|
|
if f.isClosed {
|
|
return 0, os.ErrClosed
|
|
}
|
|
|
|
switch whence {
|
|
case io.SeekCurrent:
|
|
f.position += offset
|
|
case io.SeekStart:
|
|
f.position = offset
|
|
case io.SeekEnd:
|
|
f.position = int64(f.content.Len()) + offset
|
|
}
|
|
|
|
return f.position, nil
|
|
}
|
|
|
|
func (f *file) Write(p []byte) (int, error) {
|
|
return f.WriteAt(p, f.position)
|
|
}
|
|
|
|
func (f *file) WriteAt(p []byte, off int64) (int, error) {
|
|
if f.isClosed {
|
|
return 0, os.ErrClosed
|
|
}
|
|
|
|
if !isReadAndWrite(f.flag) && !isWriteOnly(f.flag) {
|
|
return 0, errors.New("write not supported")
|
|
}
|
|
|
|
n, err := f.content.WriteAt(p, off)
|
|
f.position = off + int64(n)
|
|
f.mtime = time.Now()
|
|
|
|
return n, err
|
|
}
|
|
|
|
func (f *file) Close() error {
|
|
if f.isClosed {
|
|
return os.ErrClosed
|
|
}
|
|
|
|
f.isClosed = true
|
|
return nil
|
|
}
|
|
|
|
func (f *file) Truncate(size int64) error {
|
|
if size < int64(len(f.content.bytes)) {
|
|
f.content.bytes = f.content.bytes[:size]
|
|
} else if more := int(size) - len(f.content.bytes); more > 0 {
|
|
f.content.bytes = append(f.content.bytes, make([]byte, more)...)
|
|
}
|
|
f.mtime = time.Now()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *file) Duplicate(filename string, mode os.FileMode, flag int) billy.File {
|
|
new := &file{
|
|
name: filename,
|
|
content: f.content,
|
|
mode: mode,
|
|
flag: flag,
|
|
mtime: time.Now(),
|
|
}
|
|
|
|
if isTruncate(flag) {
|
|
new.content.Truncate()
|
|
}
|
|
|
|
if isAppend(flag) {
|
|
new.position = int64(new.content.Len())
|
|
}
|
|
|
|
return new
|
|
}
|
|
|
|
func (f *file) Stat() (os.FileInfo, error) {
|
|
return &fileInfo{
|
|
name: f.Name(),
|
|
mode: f.mode,
|
|
size: f.content.Len(),
|
|
mtime: f.mtime,
|
|
}, nil
|
|
}
|
|
|
|
// Lock is a no-op in memfs.
|
|
func (f *file) Lock() error {
|
|
return nil
|
|
}
|
|
|
|
// Unlock is a no-op in memfs.
|
|
func (f *file) Unlock() error {
|
|
return nil
|
|
}
|
|
|
|
type fileInfo struct {
|
|
name string
|
|
size int
|
|
mode os.FileMode
|
|
mtime time.Time
|
|
}
|
|
|
|
func (fi *fileInfo) Name() string {
|
|
return fi.name
|
|
}
|
|
|
|
func (fi *fileInfo) Size() int64 {
|
|
return int64(fi.size)
|
|
}
|
|
|
|
func (fi *fileInfo) Mode() os.FileMode {
|
|
return fi.mode
|
|
}
|
|
|
|
func (fi *fileInfo) ModTime() time.Time {
|
|
return fi.mtime
|
|
}
|
|
|
|
func (fi *fileInfo) IsDir() bool {
|
|
return fi.mode.IsDir()
|
|
}
|
|
|
|
func (*fileInfo) Sys() interface{} {
|
|
return nil
|
|
}
|
|
|
|
func (c *content) Truncate() {
|
|
c.bytes = make([]byte, 0)
|
|
}
|
|
|
|
func (c *content) Len() int {
|
|
return len(c.bytes)
|
|
}
|
|
|
|
func isCreate(flag int) bool {
|
|
return flag&os.O_CREATE != 0
|
|
}
|
|
|
|
func isExclusive(flag int) bool {
|
|
return flag&os.O_EXCL != 0
|
|
}
|
|
|
|
func isAppend(flag int) bool {
|
|
return flag&os.O_APPEND != 0
|
|
}
|
|
|
|
func isTruncate(flag int) bool {
|
|
return flag&os.O_TRUNC != 0
|
|
}
|
|
|
|
func isReadAndWrite(flag int) bool {
|
|
return flag&os.O_RDWR != 0
|
|
}
|
|
|
|
func isReadOnly(flag int) bool {
|
|
return flag == os.O_RDONLY
|
|
}
|
|
|
|
func isWriteOnly(flag int) bool {
|
|
return flag&os.O_WRONLY != 0
|
|
}
|
|
|
|
func isSymlink(m os.FileMode) bool {
|
|
return m&os.ModeSymlink != 0
|
|
}
|