mirror of
https://github.com/distribution/distribution
synced 2024-11-12 05:45:51 +01:00
043c81bea2
Updates storagedriver tests to better test directory trees
333 lines
5.9 KiB
Go
333 lines
5.9 KiB
Go
package inmemory
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
errExists = fmt.Errorf("exists")
|
|
errNotExists = fmt.Errorf("notexists")
|
|
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) {
|
|
p = normalize(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 normalize(srcDirname) != normalize(sp.path()) {
|
|
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 normalize(dirname) != normalize(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) {
|
|
off := int(offset)
|
|
if cap(f.data) < off+len(p) {
|
|
data := make([]byte, len(f.data), off+len(p))
|
|
copy(data, f.data)
|
|
f.data = data
|
|
}
|
|
|
|
f.data = f.data[:off+len(p)]
|
|
|
|
return copy(f.data[off:off+len(p)], 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
|
|
}
|
|
|
|
func normalize(p string) string {
|
|
return "/" + strings.Trim(p, "/")
|
|
}
|