forked from TrueCloudLab/distribution
2ebc373d91
This change started out as simply updating the existing inmemory driver to implement the new Stat call. After struggling with the map based implementation, it has been refactored to be a tree-based implementation. This process has exposed a few missing error cases in the StorageDriver API that should be addressed in the coming weeks.
329 lines
5.9 KiB
Go
329 lines
5.9 KiB
Go
package inmemory
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
errExists = fmt.Errorf("exists")
|
|
errNotExists = fmt.Errorf("exists")
|
|
errIsNotDir = fmt.Errorf("notdir")
|
|
errIsDir = fmt.Errorf("isdir")
|
|
)
|
|
|
|
type node interface {
|
|
name() string
|
|
path() string
|
|
isdir() bool
|
|
modtime() time.Time
|
|
}
|
|
|
|
// dir is the central type for the memory-based storagedriver. All operations
|
|
// are dispatched from a root dir.
|
|
type dir struct {
|
|
common
|
|
|
|
// TODO(stevvooe): Use sorted slice + search.
|
|
children map[string]node
|
|
}
|
|
|
|
var _ node = &dir{}
|
|
|
|
func (d *dir) isdir() bool {
|
|
return true
|
|
}
|
|
|
|
// add places the node n into dir d.
|
|
func (d *dir) add(n node) {
|
|
if d.children == nil {
|
|
d.children = make(map[string]node)
|
|
}
|
|
|
|
d.children[n.name()] = n
|
|
d.mod = time.Now()
|
|
}
|
|
|
|
// find searches for the node, given path q in dir. If the node is found, it
|
|
// will be returned. If the node is not found, the closet existing parent. If
|
|
// the node is found, the returned (node).path() will match q.
|
|
func (d *dir) find(q string) node {
|
|
q = strings.Trim(q, "/")
|
|
i := strings.Index(q, "/")
|
|
|
|
if q == "" {
|
|
return d
|
|
}
|
|
|
|
if i == 0 {
|
|
panic("shouldn't happen, no root paths")
|
|
}
|
|
|
|
var component string
|
|
if i < 0 {
|
|
// No more path components
|
|
component = q
|
|
} else {
|
|
component = q[:i]
|
|
}
|
|
|
|
child, ok := d.children[component]
|
|
if !ok {
|
|
// Node was not found. Return p and the current node.
|
|
return d
|
|
}
|
|
|
|
if child.isdir() {
|
|
// traverse down!
|
|
q = q[i+1:]
|
|
return child.(*dir).find(q)
|
|
}
|
|
|
|
return child
|
|
}
|
|
|
|
func (d *dir) list(p string) ([]string, error) {
|
|
n := d.find(p)
|
|
|
|
if n.path() != p {
|
|
return nil, errNotExists
|
|
}
|
|
|
|
if !n.isdir() {
|
|
return nil, errIsNotDir
|
|
}
|
|
|
|
var children []string
|
|
for _, child := range n.(*dir).children {
|
|
children = append(children, child.path())
|
|
}
|
|
|
|
sort.Strings(children)
|
|
return children, nil
|
|
}
|
|
|
|
// mkfile or return the existing one. returns an error if it exists and is a
|
|
// directory. Essentially, this is open or create.
|
|
func (d *dir) mkfile(p string) (*file, error) {
|
|
n := d.find(p)
|
|
if n.path() == p {
|
|
if n.isdir() {
|
|
return nil, errIsDir
|
|
}
|
|
|
|
return n.(*file), nil
|
|
}
|
|
|
|
dirpath, filename := path.Split(p)
|
|
// Make any non-existent directories
|
|
n, err := d.mkdirs(dirpath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dd := n.(*dir)
|
|
n = &file{
|
|
common: common{
|
|
p: path.Join(dd.path(), filename),
|
|
mod: time.Now(),
|
|
},
|
|
}
|
|
|
|
dd.add(n)
|
|
return n.(*file), nil
|
|
}
|
|
|
|
// mkdirs creates any missing directory entries in p and returns the result.
|
|
func (d *dir) mkdirs(p string) (*dir, error) {
|
|
if p == "" {
|
|
p = "/"
|
|
}
|
|
|
|
n := d.find(p)
|
|
|
|
if !n.isdir() {
|
|
// Found something there
|
|
return nil, errIsNotDir
|
|
}
|
|
|
|
if n.path() == p {
|
|
return n.(*dir), nil
|
|
}
|
|
|
|
dd := n.(*dir)
|
|
|
|
relative := strings.Trim(strings.TrimPrefix(p, n.path()), "/")
|
|
|
|
if relative == "" {
|
|
return dd, nil
|
|
}
|
|
|
|
components := strings.Split(relative, "/")
|
|
for _, component := range components {
|
|
d, err := dd.mkdir(component)
|
|
|
|
if err != nil {
|
|
// This should actually never happen, since there are no children.
|
|
return nil, err
|
|
}
|
|
dd = d
|
|
}
|
|
|
|
return dd, nil
|
|
}
|
|
|
|
// mkdir creates a child directory under d with the given name.
|
|
func (d *dir) mkdir(name string) (*dir, error) {
|
|
if name == "" {
|
|
return nil, fmt.Errorf("invalid dirname")
|
|
}
|
|
|
|
_, ok := d.children[name]
|
|
if ok {
|
|
return nil, errExists
|
|
}
|
|
|
|
child := &dir{
|
|
common: common{
|
|
p: path.Join(d.path(), name),
|
|
mod: time.Now(),
|
|
},
|
|
}
|
|
d.add(child)
|
|
d.mod = time.Now()
|
|
|
|
return child, nil
|
|
}
|
|
|
|
func (d *dir) move(src, dst string) error {
|
|
dstDirname, _ := path.Split(dst)
|
|
|
|
dp, err := d.mkdirs(dstDirname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
srcDirname, srcFilename := path.Split(src)
|
|
sp := d.find(srcDirname)
|
|
|
|
if sp.path() != srcDirname {
|
|
return errNotExists
|
|
}
|
|
|
|
s, ok := sp.(*dir).children[srcFilename]
|
|
if !ok {
|
|
return errNotExists
|
|
}
|
|
|
|
delete(sp.(*dir).children, srcFilename)
|
|
|
|
switch n := s.(type) {
|
|
case *dir:
|
|
n.p = dst
|
|
case *file:
|
|
n.p = dst
|
|
}
|
|
|
|
dp.add(s)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *dir) delete(p string) error {
|
|
dirname, filename := path.Split(p)
|
|
parent := d.find(dirname)
|
|
|
|
if dirname != parent.path() {
|
|
return errNotExists
|
|
}
|
|
|
|
if _, ok := parent.(*dir).children[filename]; !ok {
|
|
return errNotExists
|
|
}
|
|
|
|
delete(parent.(*dir).children, filename)
|
|
return nil
|
|
}
|
|
|
|
// dump outputs a primitive directory structure to stdout.
|
|
func (d *dir) dump(indent string) {
|
|
fmt.Println(indent, d.name()+"/")
|
|
|
|
for _, child := range d.children {
|
|
if child.isdir() {
|
|
child.(*dir).dump(indent + "\t")
|
|
} else {
|
|
fmt.Println(indent, child.name())
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func (d *dir) String() string {
|
|
return fmt.Sprintf("&dir{path: %v, children: %v}", d.p, d.children)
|
|
}
|
|
|
|
// file stores actual data in the fs tree. It acts like an open, seekable file
|
|
// where operations are conducted through ReadAt and WriteAt. Use it with
|
|
// SectionReader for the best effect.
|
|
type file struct {
|
|
common
|
|
data []byte
|
|
}
|
|
|
|
var _ node = &file{}
|
|
|
|
func (f *file) isdir() bool {
|
|
return false
|
|
}
|
|
|
|
func (f *file) truncate() {
|
|
f.data = f.data[:0]
|
|
}
|
|
|
|
func (f *file) sectionReader(offset int64) io.Reader {
|
|
return io.NewSectionReader(f, offset, int64(len(f.data))-offset)
|
|
}
|
|
|
|
func (f *file) ReadAt(p []byte, offset int64) (n int, err error) {
|
|
return copy(p, f.data[offset:]), nil
|
|
}
|
|
|
|
func (f *file) WriteAt(p []byte, offset int64) (n int, err error) {
|
|
if len(f.data) > 0 && offset >= int64(len(f.data)) {
|
|
// Extend missing region with a zero pad, while also preallocating out to size of p.
|
|
pad := offset - int64(len(f.data))
|
|
size := len(p) + int(pad)
|
|
f.data = append(f.data, make([]byte, pad, size)...)
|
|
}
|
|
|
|
f.data = append(f.data, p...)
|
|
return len(p), nil
|
|
}
|
|
|
|
func (f *file) String() string {
|
|
return fmt.Sprintf("&file{path: %q}", f.p)
|
|
}
|
|
|
|
// common provides shared fields and methods for node implementations.
|
|
type common struct {
|
|
p string
|
|
mod time.Time
|
|
}
|
|
|
|
func (c *common) name() string {
|
|
_, name := path.Split(c.p)
|
|
return name
|
|
}
|
|
|
|
func (c *common) path() string {
|
|
return c.p
|
|
}
|
|
|
|
func (c *common) modtime() time.Time {
|
|
return c.mod
|
|
}
|