Better support of empty directories (#5890)

Better support of HEAD and listing of zero sized objects with trailing
slash (a.k.a empty directory). For that, isLeafDir function is added
to indicate if the specified object is an empty directory or not. Each
backend (xl, fs) has the responsibility to store that information.
Currently, in both of XL & FS, an empty directory is represented by
an empty directory in the backend.

isLeafDir() checks if the given path is an empty directory or not,
since dir listing is costly if the latter contains too many objects,
readDirN() is added in this PR to list only N number of entries.
In isLeadDir(), we will only list one entry to check if a directory
is empty or not.
This commit is contained in:
Anis Elleuch 2018-05-08 19:08:21 -07:00 committed by Harshavardhana
parent 32700fca52
commit 6d5f2a4391
22 changed files with 268 additions and 55 deletions

View File

@ -357,8 +357,16 @@ func (c cacheObjects) listCacheObjects(ctx context.Context, bucket, prefix, mark
return err == nil return err == nil
} }
isLeafDir := func(bucket, object string) bool {
fs, err := c.cache.getCacheFS(ctx, bucket, object)
if err != nil {
return false
}
return fs.isObjectDir(bucket, object)
}
listDir := listDirCacheFactory(isLeaf, cacheTreeWalkIgnoredErrs, c.cache.cfs) listDir := listDirCacheFactory(isLeaf, cacheTreeWalkIgnoredErrs, c.cache.cfs)
walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, isLeaf, endWalkCh) walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, isLeaf, isLeafDir, endWalkCh)
} }
for i := 0; i < maxKeys; { for i := 0; i < maxKeys; {
@ -417,7 +425,7 @@ func (c cacheObjects) listCacheObjects(ctx context.Context, bucket, prefix, mark
result = ListObjectsInfo{IsTruncated: !eof} result = ListObjectsInfo{IsTruncated: !eof}
for _, objInfo := range objInfos { for _, objInfo := range objInfos {
result.NextMarker = objInfo.Name result.NextMarker = objInfo.Name
if objInfo.IsDir { if objInfo.IsDir && delimiter == slashSeparator {
result.Prefixes = append(result.Prefixes, objInfo.Name) result.Prefixes = append(result.Prefixes, objInfo.Name)
continue continue
} }

View File

@ -627,6 +627,10 @@ func (fs *FSObjects) getObjectInfoWithLock(ctx context.Context, bucket, object s
return oi, toObjectErr(err, bucket) return oi, toObjectErr(err, bucket)
} }
if strings.HasSuffix(object, slashSeparator) && !fs.isObjectDir(bucket, object) {
return oi, errFileNotFound
}
return fs.getObjectInfo(ctx, bucket, object) return fs.getObjectInfo(ctx, bucket, object)
} }
@ -890,6 +894,17 @@ func (fs *FSObjects) listDirFactory(isLeaf isLeafFunc) listDirFunc {
return listDir return listDir
} }
// isObjectDir returns true if the specified bucket & prefix exists
// and the prefix represents an empty directory. An S3 empty directory
// is also an empty directory in the FS backend.
func (fs *FSObjects) isObjectDir(bucket, prefix string) bool {
entries, err := readDirN(pathJoin(fs.fsPath, bucket, prefix), 1)
if err != nil {
return false
}
return len(entries) == 0
}
// getObjectETag is a helper function, which returns only the md5sum // getObjectETag is a helper function, which returns only the md5sum
// of the file on the disk. // of the file on the disk.
func (fs *FSObjects) getObjectETag(ctx context.Context, bucket, entry string, lock bool) (string, error) { func (fs *FSObjects) getObjectETag(ctx context.Context, bucket, entry string, lock bool) (string, error) {
@ -1019,8 +1034,15 @@ func (fs *FSObjects) ListObjects(ctx context.Context, bucket, prefix, marker, de
// object string does not end with "/". // object string does not end with "/".
return !hasSuffix(object, slashSeparator) return !hasSuffix(object, slashSeparator)
} }
// Return true if the specified object is an empty directory
isLeafDir := func(bucket, object string) bool {
if !hasSuffix(object, slashSeparator) {
return false
}
return fs.isObjectDir(bucket, object)
}
listDir := fs.listDirFactory(isLeaf) listDir := fs.listDirFactory(isLeaf)
walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, isLeaf, endWalkCh) walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, isLeaf, isLeafDir, endWalkCh)
} }
var objInfos []ObjectInfo var objInfos []ObjectInfo
@ -1065,7 +1087,7 @@ func (fs *FSObjects) ListObjects(ctx context.Context, bucket, prefix, marker, de
result := ListObjectsInfo{IsTruncated: !eof} result := ListObjectsInfo{IsTruncated: !eof}
for _, objInfo := range objInfos { for _, objInfo := range objInfos {
result.NextMarker = objInfo.Name result.NextMarker = objInfo.Name
if objInfo.IsDir { if objInfo.IsDir && delimiter == slashSeparator {
result.Prefixes = append(result.Prefixes, objInfo.Name) result.Prefixes = append(result.Prefixes, objInfo.Name)
continue continue
} }

View File

@ -108,11 +108,11 @@ func (d *naughtyDisk) DeleteVol(volume string) (err error) {
return d.disk.DeleteVol(volume) return d.disk.DeleteVol(volume)
} }
func (d *naughtyDisk) ListDir(volume, path string) (entries []string, err error) { func (d *naughtyDisk) ListDir(volume, path string, count int) (entries []string, err error) {
if err := d.calcError(); err != nil { if err := d.calcError(); err != nil {
return []string{}, err return []string{}, err
} }
return d.disk.ListDir(volume, path) return d.disk.ListDir(volume, path, count)
} }
func (d *naughtyDisk) ReadFile(volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error) { func (d *naughtyDisk) ReadFile(volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error) {

View File

@ -116,7 +116,7 @@ func cleanupDir(ctx context.Context, storage StorageAPI, volume, dirPath string)
} }
// If it's a directory, list and call delFunc() for each entry. // If it's a directory, list and call delFunc() for each entry.
entries, err := storage.ListDir(volume, entryPath) entries, err := storage.ListDir(volume, entryPath, -1)
// If entryPath prefix never existed, safe to ignore. // If entryPath prefix never existed, safe to ignore.
if err == errFileNotFound { if err == errFileNotFound {
return nil return nil

View File

@ -112,6 +112,12 @@ var readDirBufPool = sync.Pool{
// Return all the entries at the directory dirPath. // Return all the entries at the directory dirPath.
func readDir(dirPath string) (entries []string, err error) { func readDir(dirPath string) (entries []string, err error) {
return readDirN(dirPath, -1)
}
// Return count entries at the directory dirPath and all entries
// if count is set to -1
func readDirN(dirPath string, count int) (entries []string, err error) {
bufp := readDirBufPool.Get().(*[]byte) bufp := readDirBufPool.Get().(*[]byte)
buf := *bufp buf := *bufp
defer readDirBufPool.Put(bufp) defer readDirBufPool.Put(bufp)
@ -135,7 +141,11 @@ func readDir(dirPath string) (entries []string, err error) {
defer d.Close() defer d.Close()
fd := int(d.Fd()) fd := int(d.Fd())
for {
remaining := count
done := false
for !done {
nbuf, err := syscall.ReadDirent(fd, buf) nbuf, err := syscall.ReadDirent(fd, buf)
if err != nil { if err != nil {
return nil, err return nil, err
@ -147,6 +157,13 @@ func readDir(dirPath string) (entries []string, err error) {
if tmpEntries, err = parseDirents(dirPath, buf[:nbuf]); err != nil { if tmpEntries, err = parseDirents(dirPath, buf[:nbuf]); err != nil {
return nil, err return nil, err
} }
if count > 0 {
if remaining <= len(tmpEntries) {
tmpEntries = tmpEntries[:remaining]
done = true
}
remaining -= len(tmpEntries)
}
entries = append(entries, tmpEntries...) entries = append(entries, tmpEntries...)
} }
return return

View File

@ -30,7 +30,12 @@ import (
// Return all the entries at the directory dirPath. // Return all the entries at the directory dirPath.
func readDir(dirPath string) (entries []string, err error) { func readDir(dirPath string) (entries []string, err error) {
d, err := os.Open((dirPath)) return readDirN(dirPath, -1)
}
// Return N entries at the directory dirPath. If count is -1, return all entries
func readDirN(dirPath string, count int) (entries []string, err error) {
d, err := os.Open(dirPath)
if err != nil { if err != nil {
// File is really not found. // File is really not found.
if os.IsNotExist(err) { if os.IsNotExist(err) {
@ -45,20 +50,34 @@ func readDir(dirPath string) (entries []string, err error) {
} }
defer d.Close() defer d.Close()
for { maxEntries := 1000
// Read 1000 entries. if count > 0 && count < maxEntries {
fis, err := d.Readdir(1000) maxEntries = count
}
done := false
remaining := count
for !done {
// Read up to max number of entries.
fis, err := d.Readdir(maxEntries)
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
break break
} }
return nil, err return nil, err
} }
if count > 0 {
if remaining <= len(fis) {
fis = fis[:remaining]
done = true
}
}
for _, fi := range fis { for _, fi := range fis {
// Stat symbolic link and follow to get the final value. // Stat symbolic link and follow to get the final value.
if fi.Mode()&os.ModeSymlink == os.ModeSymlink { if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
var st os.FileInfo var st os.FileInfo
st, err = os.Stat((path.Join(dirPath, fi.Name()))) st, err = os.Stat(path.Join(dirPath, fi.Name()))
if err != nil { if err != nil {
reqInfo := (&logger.ReqInfo{}).AppendTags("path", path.Join(dirPath, fi.Name())) reqInfo := (&logger.ReqInfo{}).AppendTags("path", path.Join(dirPath, fi.Name()))
ctx := logger.SetReqInfo(context.Background(), reqInfo) ctx := logger.SetReqInfo(context.Background(), reqInfo)
@ -71,6 +90,9 @@ func readDir(dirPath string) (entries []string, err error) {
} else if st.Mode().IsRegular() { } else if st.Mode().IsRegular() {
entries = append(entries, fi.Name()) entries = append(entries, fi.Name())
} }
if count > 0 {
remaining--
}
continue continue
} }
if fi.Mode().IsDir() { if fi.Mode().IsDir() {
@ -79,6 +101,9 @@ func readDir(dirPath string) (entries []string, err error) {
} else if fi.Mode().IsRegular() { } else if fi.Mode().IsRegular() {
entries = append(entries, fi.Name()) entries = append(entries, fi.Name())
} }
if count > 0 {
remaining--
}
} }
} }
return entries, nil return entries, nil

View File

@ -201,3 +201,41 @@ func TestReadDir(t *testing.T) {
} }
} }
} }
func TestReadDirN(t *testing.T) {
testCases := []struct {
numFiles int
n int
expectedNum int
}{
{0, 0, 0},
{0, 1, 0},
{1, 0, 1},
{0, -1, 0},
{1, -1, 1},
{10, -1, 10},
{1, 1, 1},
{2, 1, 1},
{10, 9, 9},
{10, 10, 10},
{10, 11, 10},
}
for i, testCase := range testCases {
dir := mustSetupDir(t)
defer os.RemoveAll(dir)
for c := 1; c <= testCase.numFiles; c++ {
if err := ioutil.WriteFile(filepath.Join(dir, fmt.Sprintf("%d", c)), []byte{}, os.ModePerm); err != nil {
t.Fatalf("Unable to create a file, %s", err)
}
}
entries, err := readDirN(dir, testCase.n)
if err != nil {
t.Fatalf("Unable to read entries, %s", err)
}
if len(entries) != testCase.expectedNum {
t.Fatalf("Test %d: unexpected number of entries, waiting for %d, but found %d", i+1, testCase.expectedNum, len(entries))
}
}
}

View File

@ -467,7 +467,7 @@ func (s *posix) DeleteVol(volume string) (err error) {
// ListDir - return all the entries at the given directory path. // ListDir - return all the entries at the given directory path.
// If an entry is a directory it will be returned with a trailing "/". // If an entry is a directory it will be returned with a trailing "/".
func (s *posix) ListDir(volume, dirPath string) (entries []string, err error) { func (s *posix) ListDir(volume, dirPath string, count int) (entries []string, err error) {
defer func() { defer func() {
if err == syscall.EIO { if err == syscall.EIO {
atomic.AddInt32(&s.ioErrCount, 1) atomic.AddInt32(&s.ioErrCount, 1)
@ -495,7 +495,12 @@ func (s *posix) ListDir(volume, dirPath string) (entries []string, err error) {
} }
return nil, err return nil, err
} }
return readDir(pathJoin(volumeDir, dirPath))
dirPath = pathJoin(volumeDir, dirPath)
if count > 0 {
return readDirN(dirPath, count)
}
return readDir(dirPath)
} }
// ReadAll reads from r until an error or EOF and returns the data it read. // ReadAll reads from r until an error or EOF and returns the data it read.

View File

@ -785,7 +785,7 @@ func TestPosixPosixListDir(t *testing.T) {
} else { } else {
t.Errorf("Expected the StorageAPI to be of type *posix") t.Errorf("Expected the StorageAPI to be of type *posix")
} }
dirList, err = posixStorage.ListDir(testCase.srcVol, testCase.srcPath) dirList, err = posixStorage.ListDir(testCase.srcVol, testCase.srcPath, -1)
if err != testCase.expectedErr { if err != testCase.expectedErr {
t.Fatalf("TestPosix case %d: Expected: \"%s\", got: \"%s\"", i+1, testCase.expectedErr, err) t.Fatalf("TestPosix case %d: Expected: \"%s\", got: \"%s\"", i+1, testCase.expectedErr, err)
} }

View File

@ -39,7 +39,7 @@ type StorageAPI interface {
DeleteVol(volume string) (err error) DeleteVol(volume string) (err error)
// File operations. // File operations.
ListDir(volume, dirPath string) ([]string, error) ListDir(volume, dirPath string, count int) ([]string, error)
ReadFile(volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error) ReadFile(volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error)
PrepareFile(volume string, path string, len int64) (err error) PrepareFile(volume string, path string, len int64) (err error)
AppendFile(volume string, path string, buf []byte) (err error) AppendFile(volume string, path string, buf []byte) (err error)

View File

@ -283,10 +283,11 @@ func (n *networkStorage) ReadFile(volume string, path string, offset int64, buff
} }
// ListDir - list all entries at prefix. // ListDir - list all entries at prefix.
func (n *networkStorage) ListDir(volume, path string) (entries []string, err error) { func (n *networkStorage) ListDir(volume, path string, count int) (entries []string, err error) {
if err = n.call("Storage.ListDirHandler", &ListDirArgs{ if err = n.call("Storage.ListDirHandler", &ListDirArgs{
Vol: volume, Vol: volume,
Path: path, Path: path,
Count: count,
}, &entries); err != nil { }, &entries); err != nil {
return nil, err return nil, err
} }

View File

@ -435,7 +435,7 @@ func (s *TestRPCStorageSuite) testRPCStorageListDir(t *testing.T) {
t.Error("Unable to initiate MakeVol", err) t.Error("Unable to initiate MakeVol", err)
} }
} }
dirs, err := storageDisk.ListDir("myvol", "") dirs, err := storageDisk.ListDir("myvol", "", -1)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -448,7 +448,7 @@ func (s *TestRPCStorageSuite) testRPCStorageListDir(t *testing.T) {
t.Error("Unable to initiate DeleteVol", err) t.Error("Unable to initiate DeleteVol", err)
} }
} }
dirs, err = storageDisk.ListDir("myvol", "") dirs, err = storageDisk.ListDir("myvol", "", -1)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }

View File

@ -134,6 +134,9 @@ type ListDirArgs struct {
// Name of the path. // Name of the path.
Path string Path string
// Number of wanted results
Count int
} }
// RenameFileArgs represents rename file RPC arguments. // RenameFileArgs represents rename file RPC arguments.

View File

@ -120,7 +120,7 @@ func (s *storageServer) ListDirHandler(args *ListDirArgs, reply *[]string) error
return err return err
} }
entries, err := s.storage.ListDir(args.Vol, args.Path) entries, err := s.storage.ListDir(args.Vol, args.Path, args.Count)
if err != nil { if err != nil {
return err return err
} }

View File

@ -98,6 +98,9 @@ type listDirFunc func(bucket, prefixDir, prefixEntry string) (entries []string,
// 4. XL backend multipart listing - isLeaf is true if the entry is a directory and contains uploads.json // 4. XL backend multipart listing - isLeaf is true if the entry is a directory and contains uploads.json
type isLeafFunc func(string, string) bool type isLeafFunc func(string, string) bool
// A function isLeafDir of type isLeafDirFunc is used to detect if an entry represents an empty directory.
type isLeafDirFunc func(string, string) bool
func filterListEntries(bucket, prefixDir string, entries []string, prefixEntry string, isLeaf isLeafFunc) ([]string, bool) { func filterListEntries(bucket, prefixDir string, entries []string, prefixEntry string, isLeaf isLeafFunc) ([]string, bool) {
// Listing needs to be sorted. // Listing needs to be sorted.
sort.Strings(entries) sort.Strings(entries)
@ -125,7 +128,7 @@ func filterListEntries(bucket, prefixDir string, entries []string, prefixEntry s
} }
// treeWalk walks directory tree recursively pushing treeWalkResult into the channel as and when it encounters files. // treeWalk walks directory tree recursively pushing treeWalkResult into the channel as and when it encounters files.
func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker string, recursive bool, listDir listDirFunc, isLeaf isLeafFunc, resultCh chan treeWalkResult, endWalkCh chan struct{}, isEnd bool) error { func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker string, recursive bool, listDir listDirFunc, isLeaf isLeafFunc, isLeafDir isLeafDirFunc, resultCh chan treeWalkResult, endWalkCh chan struct{}, isEnd bool) error {
// Example: // Example:
// if prefixDir="one/two/three/" and marker="four/five.txt" treeWalk is recursively // if prefixDir="one/two/three/" and marker="four/five.txt" treeWalk is recursively
// called with prefixDir="one/two/three/four/" and marker="five.txt" // called with prefixDir="one/two/three/four/" and marker="five.txt"
@ -167,17 +170,30 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
return nil return nil
} }
for i, entry := range entries { for i, entry := range entries {
var leaf, leafDir bool
// Decision to do isLeaf check was pushed from listDir() to here. // Decision to do isLeaf check was pushed from listDir() to here.
if delayIsLeaf && isLeaf(bucket, pathJoin(prefixDir, entry)) { if delayIsLeaf {
entry = strings.TrimSuffix(entry, slashSeparator) leaf = isLeaf(bucket, pathJoin(prefixDir, entry))
if leaf {
entry = strings.TrimSuffix(entry, slashSeparator)
}
} else {
leaf = !strings.HasSuffix(entry, slashSeparator)
} }
if strings.HasSuffix(entry, slashSeparator) {
leafDir = isLeafDir(bucket, pathJoin(prefixDir, entry))
}
isDir := !leafDir && !leaf
if i == 0 && markerDir == entry { if i == 0 && markerDir == entry {
if !recursive { if !recursive {
// Skip as the marker would already be listed in the previous listing. // Skip as the marker would already be listed in the previous listing.
continue continue
} }
if recursive && !hasSuffix(entry, slashSeparator) { if recursive && !isDir {
// We should not skip for recursive listing and if markerDir is a directory // We should not skip for recursive listing and if markerDir is a directory
// for ex. if marker is "four/five.txt" markerDir will be "four/" which // for ex. if marker is "four/five.txt" markerDir will be "four/" which
// should not be skipped, instead it will need to be treeWalk()'ed into. // should not be skipped, instead it will need to be treeWalk()'ed into.
@ -186,7 +202,7 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
continue continue
} }
} }
if recursive && hasSuffix(entry, slashSeparator) { if recursive && isDir {
// If the entry is a directory, we will need recurse into it. // If the entry is a directory, we will need recurse into it.
markerArg := "" markerArg := ""
if entry == markerDir { if entry == markerDir {
@ -198,7 +214,7 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
// markIsEnd is passed to this entry's treeWalk() so that treeWalker.end can be marked // markIsEnd is passed to this entry's treeWalk() so that treeWalker.end can be marked
// true at the end of the treeWalk stream. // true at the end of the treeWalk stream.
markIsEnd := i == len(entries)-1 && isEnd markIsEnd := i == len(entries)-1 && isEnd
if tErr := doTreeWalk(ctx, bucket, pathJoin(prefixDir, entry), prefixMatch, markerArg, recursive, listDir, isLeaf, resultCh, endWalkCh, markIsEnd); tErr != nil { if tErr := doTreeWalk(ctx, bucket, pathJoin(prefixDir, entry), prefixMatch, markerArg, recursive, listDir, isLeaf, isLeafDir, resultCh, endWalkCh, markIsEnd); tErr != nil {
return tErr return tErr
} }
continue continue
@ -218,7 +234,7 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
} }
// Initiate a new treeWalk in a goroutine. // Initiate a new treeWalk in a goroutine.
func startTreeWalk(ctx context.Context, bucket, prefix, marker string, recursive bool, listDir listDirFunc, isLeaf isLeafFunc, endWalkCh chan struct{}) chan treeWalkResult { func startTreeWalk(ctx context.Context, bucket, prefix, marker string, recursive bool, listDir listDirFunc, isLeaf isLeafFunc, isLeafDir isLeafDirFunc, endWalkCh chan struct{}) chan treeWalkResult {
// Example 1 // Example 1
// If prefix is "one/two/three/" and marker is "one/two/three/four/five.txt" // If prefix is "one/two/three/" and marker is "one/two/three/four/five.txt"
// treeWalk is called with prefixDir="one/two/three/" and marker="four/five.txt" // treeWalk is called with prefixDir="one/two/three/" and marker="four/five.txt"
@ -240,7 +256,7 @@ func startTreeWalk(ctx context.Context, bucket, prefix, marker string, recursive
marker = strings.TrimPrefix(marker, prefixDir) marker = strings.TrimPrefix(marker, prefixDir)
go func() { go func() {
isEnd := true // Indication to start walking the tree with end as true. isEnd := true // Indication to start walking the tree with end as true.
doTreeWalk(ctx, bucket, prefixDir, entryPrefixMatch, marker, recursive, listDir, isLeaf, resultCh, endWalkCh, isEnd) doTreeWalk(ctx, bucket, prefixDir, entryPrefixMatch, marker, recursive, listDir, isLeaf, isLeafDir, resultCh, endWalkCh, isEnd)
close(resultCh) close(resultCh)
}() }()
return resultCh return resultCh

View File

@ -128,11 +128,11 @@ func createNamespace(disk StorageAPI, volume string, files []string) error {
// Test if tree walker returns entries matching prefix alone are received // Test if tree walker returns entries matching prefix alone are received
// when a non empty prefix is supplied. // when a non empty prefix is supplied.
func testTreeWalkPrefix(t *testing.T, listDir listDirFunc, isLeaf isLeafFunc) { func testTreeWalkPrefix(t *testing.T, listDir listDirFunc, isLeaf isLeafFunc, isLeafDir isLeafDirFunc) {
// Start the tree walk go-routine. // Start the tree walk go-routine.
prefix := "d/" prefix := "d/"
endWalkCh := make(chan struct{}) endWalkCh := make(chan struct{})
twResultCh := startTreeWalk(context.Background(), volume, prefix, "", true, listDir, isLeaf, endWalkCh) twResultCh := startTreeWalk(context.Background(), volume, prefix, "", true, listDir, isLeaf, isLeafDir, endWalkCh)
// Check if all entries received on the channel match the prefix. // Check if all entries received on the channel match the prefix.
for res := range twResultCh { for res := range twResultCh {
@ -143,11 +143,11 @@ func testTreeWalkPrefix(t *testing.T, listDir listDirFunc, isLeaf isLeafFunc) {
} }
// Test if entries received on tree walk's channel appear after the supplied marker. // Test if entries received on tree walk's channel appear after the supplied marker.
func testTreeWalkMarker(t *testing.T, listDir listDirFunc, isLeaf isLeafFunc) { func testTreeWalkMarker(t *testing.T, listDir listDirFunc, isLeaf isLeafFunc, isLeafDir isLeafDirFunc) {
// Start the tree walk go-routine. // Start the tree walk go-routine.
prefix := "" prefix := ""
endWalkCh := make(chan struct{}) endWalkCh := make(chan struct{})
twResultCh := startTreeWalk(context.Background(), volume, prefix, "d/g", true, listDir, isLeaf, endWalkCh) twResultCh := startTreeWalk(context.Background(), volume, prefix, "d/g", true, listDir, isLeaf, isLeafDir, endWalkCh)
// Check if only 3 entries, namely d/g/h, i/j/k, lmn are received on the channel. // Check if only 3 entries, namely d/g/h, i/j/k, lmn are received on the channel.
expectedCount := 3 expectedCount := 3
@ -187,11 +187,20 @@ func TestTreeWalk(t *testing.T) {
isLeaf := func(volume, prefix string) bool { isLeaf := func(volume, prefix string) bool {
return !hasSuffix(prefix, slashSeparator) return !hasSuffix(prefix, slashSeparator)
} }
isLeafDir := func(volume, prefix string) bool {
entries, listErr := disk.ListDir(volume, prefix, 1)
if listErr != nil {
return false
}
return len(entries) == 0
}
listDir := listDirFactory(context.Background(), isLeaf, xlTreeWalkIgnoredErrs, disk) listDir := listDirFactory(context.Background(), isLeaf, xlTreeWalkIgnoredErrs, disk)
// Simple test for prefix based walk. // Simple test for prefix based walk.
testTreeWalkPrefix(t, listDir, isLeaf) testTreeWalkPrefix(t, listDir, isLeaf, isLeafDir)
// Simple test when marker is set. // Simple test when marker is set.
testTreeWalkMarker(t, listDir, isLeaf) testTreeWalkMarker(t, listDir, isLeaf, isLeafDir)
err = os.RemoveAll(fsDir) err = os.RemoveAll(fsDir)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -222,6 +231,15 @@ func TestTreeWalkTimeout(t *testing.T) {
isLeaf := func(volume, prefix string) bool { isLeaf := func(volume, prefix string) bool {
return !hasSuffix(prefix, slashSeparator) return !hasSuffix(prefix, slashSeparator)
} }
isLeafDir := func(volume, prefix string) bool {
entries, listErr := disk.ListDir(volume, prefix, 1)
if listErr != nil {
return false
}
return len(entries) == 0
}
listDir := listDirFactory(context.Background(), isLeaf, xlTreeWalkIgnoredErrs, disk) listDir := listDirFactory(context.Background(), isLeaf, xlTreeWalkIgnoredErrs, disk)
// TreeWalk pool with 2 seconds timeout for tree-walk go routines. // TreeWalk pool with 2 seconds timeout for tree-walk go routines.
@ -231,7 +249,7 @@ func TestTreeWalkTimeout(t *testing.T) {
prefix := "" prefix := ""
marker := "" marker := ""
recursive := true recursive := true
resultCh := startTreeWalk(context.Background(), volume, prefix, marker, recursive, listDir, isLeaf, endWalkCh) resultCh := startTreeWalk(context.Background(), volume, prefix, marker, recursive, listDir, isLeaf, isLeafDir, endWalkCh)
params := listParams{ params := listParams{
bucket: volume, bucket: volume,
@ -373,6 +391,14 @@ func TestRecursiveTreeWalk(t *testing.T) {
return !hasSuffix(prefix, slashSeparator) return !hasSuffix(prefix, slashSeparator)
} }
isLeafDir := func(volume, prefix string) bool {
entries, listErr := disk1.ListDir(volume, prefix, 1)
if listErr != nil {
return false
}
return len(entries) == 0
}
// Create listDir function. // Create listDir function.
listDir := listDirFactory(context.Background(), isLeaf, xlTreeWalkIgnoredErrs, disk1) listDir := listDirFactory(context.Background(), isLeaf, xlTreeWalkIgnoredErrs, disk1)
@ -450,7 +476,7 @@ func TestRecursiveTreeWalk(t *testing.T) {
for i, testCase := range testCases { for i, testCase := range testCases {
for entry := range startTreeWalk(context.Background(), volume, for entry := range startTreeWalk(context.Background(), volume,
testCase.prefix, testCase.marker, testCase.recursive, testCase.prefix, testCase.marker, testCase.recursive,
listDir, isLeaf, endWalkCh) { listDir, isLeaf, isLeafDir, endWalkCh) {
if _, found := testCase.expected[entry.entry]; !found { if _, found := testCase.expected[entry.entry]; !found {
t.Errorf("Test %d: Expected %s, but couldn't find", i+1, entry.entry) t.Errorf("Test %d: Expected %s, but couldn't find", i+1, entry.entry)
} }
@ -479,6 +505,15 @@ func TestSortedness(t *testing.T) {
isLeaf := func(volume, prefix string) bool { isLeaf := func(volume, prefix string) bool {
return !hasSuffix(prefix, slashSeparator) return !hasSuffix(prefix, slashSeparator)
} }
isLeafDir := func(volume, prefix string) bool {
entries, listErr := disk1.ListDir(volume, prefix, 1)
if listErr != nil {
return false
}
return len(entries) == 0
}
// Create listDir function. // Create listDir function.
listDir := listDirFactory(context.Background(), isLeaf, xlTreeWalkIgnoredErrs, disk1) listDir := listDirFactory(context.Background(), isLeaf, xlTreeWalkIgnoredErrs, disk1)
@ -522,7 +557,7 @@ func TestSortedness(t *testing.T) {
var actualEntries []string var actualEntries []string
for entry := range startTreeWalk(context.Background(), volume, for entry := range startTreeWalk(context.Background(), volume,
test.prefix, test.marker, test.recursive, test.prefix, test.marker, test.recursive,
listDir, isLeaf, endWalkCh) { listDir, isLeaf, isLeafDir, endWalkCh) {
actualEntries = append(actualEntries, entry.entry) actualEntries = append(actualEntries, entry.entry)
} }
if !sort.IsSorted(sort.StringSlice(actualEntries)) { if !sort.IsSorted(sort.StringSlice(actualEntries)) {
@ -553,6 +588,15 @@ func TestTreeWalkIsEnd(t *testing.T) {
isLeaf := func(volume, prefix string) bool { isLeaf := func(volume, prefix string) bool {
return !hasSuffix(prefix, slashSeparator) return !hasSuffix(prefix, slashSeparator)
} }
isLeafDir := func(volume, prefix string) bool {
entries, listErr := disk1.ListDir(volume, prefix, 1)
if listErr != nil {
return false
}
return len(entries) == 0
}
// Create listDir function. // Create listDir function.
listDir := listDirFactory(context.Background(), isLeaf, xlTreeWalkIgnoredErrs, disk1) listDir := listDirFactory(context.Background(), isLeaf, xlTreeWalkIgnoredErrs, disk1)
@ -595,7 +639,7 @@ func TestTreeWalkIsEnd(t *testing.T) {
} }
for i, test := range testCases { for i, test := range testCases {
var entry treeWalkResult var entry treeWalkResult
for entry = range startTreeWalk(context.Background(), volume, test.prefix, test.marker, test.recursive, listDir, isLeaf, endWalkCh) { for entry = range startTreeWalk(context.Background(), volume, test.prefix, test.marker, test.recursive, listDir, isLeaf, isLeafDir, endWalkCh) {
} }
if entry.entry != test.expectedEntry { if entry.entry != test.expectedEntry {
t.Errorf("Test %d: Expected entry %s, but received %s with the EOF marker", i, test.expectedEntry, entry.entry) t.Errorf("Test %d: Expected entry %s, but received %s with the EOF marker", i, test.expectedEntry, entry.entry)

View File

@ -623,7 +623,7 @@ func (s *xlSets) CopyObject(ctx context.Context, srcBucket, srcObject, destBucke
// Returns function "listDir" of the type listDirFunc. // Returns function "listDir" of the type listDirFunc.
// isLeaf - is used by listDir function to check if an entry is a leaf or non-leaf entry. // isLeaf - is used by listDir function to check if an entry is a leaf or non-leaf entry.
// disks - used for doing disk.ListDir(). Sets passes set of disks. // disks - used for doing disk.ListDir(). Sets passes set of disks.
func listDirSetsFactory(ctx context.Context, isLeaf isLeafFunc, treeWalkIgnoredErrs []error, sets ...[]StorageAPI) listDirFunc { func listDirSetsFactory(ctx context.Context, isLeaf isLeafFunc, isLeafDir isLeafDirFunc, treeWalkIgnoredErrs []error, sets ...[]StorageAPI) listDirFunc {
listDirInternal := func(bucket, prefixDir, prefixEntry string, disks []StorageAPI) (mergedEntries []string, err error) { listDirInternal := func(bucket, prefixDir, prefixEntry string, disks []StorageAPI) (mergedEntries []string, err error) {
for _, disk := range disks { for _, disk := range disks {
if disk == nil { if disk == nil {
@ -632,7 +632,7 @@ func listDirSetsFactory(ctx context.Context, isLeaf isLeafFunc, treeWalkIgnoredE
var entries []string var entries []string
var newEntries []string var newEntries []string
entries, err = disk.ListDir(bucket, prefixDir) entries, err = disk.ListDir(bucket, prefixDir, -1)
if err != nil { if err != nil {
// For any reason disk was deleted or goes offline, continue // For any reason disk was deleted or goes offline, continue
// and list from other disks if possible. // and list from other disks if possible.
@ -723,13 +723,17 @@ func (s *xlSets) ListObjects(ctx context.Context, bucket, prefix, marker, delimi
return s.getHashedSet(entry).isObject(bucket, entry) return s.getHashedSet(entry).isObject(bucket, entry)
} }
isLeafDir := func(bucket, entry string) bool {
return s.getHashedSet(entry).isObjectDir(bucket, entry)
}
var setDisks = make([][]StorageAPI, len(s.sets)) var setDisks = make([][]StorageAPI, len(s.sets))
for _, set := range s.sets { for _, set := range s.sets {
setDisks = append(setDisks, set.getLoadBalancedDisks()) setDisks = append(setDisks, set.getLoadBalancedDisks())
} }
listDir := listDirSetsFactory(ctx, isLeaf, xlTreeWalkIgnoredErrs, setDisks...) listDir := listDirSetsFactory(ctx, isLeaf, isLeafDir, xlTreeWalkIgnoredErrs, setDisks...)
walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, isLeaf, endWalkCh) walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, isLeaf, isLeafDir, endWalkCh)
} }
for i := 0; i < maxKeys; { for i := 0; i < maxKeys; {
@ -781,7 +785,7 @@ func (s *xlSets) ListObjects(ctx context.Context, bucket, prefix, marker, delimi
result = ListObjectsInfo{IsTruncated: !eof} result = ListObjectsInfo{IsTruncated: !eof}
for _, objInfo := range objInfos { for _, objInfo := range objInfos {
result.NextMarker = objInfo.Name result.NextMarker = objInfo.Name
if objInfo.IsDir { if objInfo.IsDir && delimiter == slashSeparator {
result.Prefixes = append(result.Prefixes, objInfo.Name) result.Prefixes = append(result.Prefixes, objInfo.Name)
continue continue
} }
@ -1270,7 +1274,7 @@ func listDirSetsHealFactory(isLeaf isLeafFunc, sets ...[]StorageAPI) listDirFunc
} }
var entries []string var entries []string
var newEntries []string var newEntries []string
entries, err = disk.ListDir(bucket, prefixDir) entries, err = disk.ListDir(bucket, prefixDir, -1)
if err != nil { if err != nil {
continue continue
} }
@ -1362,7 +1366,7 @@ func (s *xlSets) listObjectsHeal(ctx context.Context, bucket, prefix, marker, de
} }
listDir := listDirSetsHealFactory(isLeaf, setDisks...) listDir := listDirSetsHealFactory(isLeaf, setDisks...)
walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, nil, endWalkCh) walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, nil, nil, endWalkCh)
} }
var objInfos []ObjectInfo var objInfos []ObjectInfo

View File

@ -76,6 +76,32 @@ func (xl xlObjects) isObject(bucket, prefix string) (ok bool) {
return false return false
} }
// isObjectDir returns if the specified path represents an empty directory.
func (xl xlObjects) isObjectDir(bucket, prefix string) (ok bool) {
for _, disk := range xl.getLoadBalancedDisks() {
if disk == nil {
continue
}
// Check if 'prefix' is an object on this 'disk', else continue the check the next disk
ctnts, err := disk.ListDir(bucket, prefix, 1)
if err == nil {
if len(ctnts) == 0 {
return true
}
return false
}
// Ignore for file not found, disk not found or faulty disk.
if IsErrIgnored(err, xlTreeWalkIgnoredErrs...) {
continue
}
reqInfo := &logger.ReqInfo{BucketName: bucket}
reqInfo.AppendTags("prefix", prefix)
ctx := logger.SetReqInfo(context.Background(), reqInfo)
logger.LogIf(ctx, err)
} // Exhausted all disks - return false.
return false
}
// Calculate the space occupied by an object in a single disk // Calculate the space occupied by an object in a single disk
func (xl xlObjects) sizeOnDisk(fileSize int64, blockSize int64, dataBlocks int) int64 { func (xl xlObjects) sizeOnDisk(fileSize int64, blockSize int64, dataBlocks int) int64 {
numBlocks := fileSize / blockSize numBlocks := fileSize / blockSize

View File

@ -422,7 +422,7 @@ func healObject(ctx context.Context, storageDisks []StorageAPI, bucket string, o
} }
// List and delete the object directory, // List and delete the object directory,
files, derr := disk.ListDir(bucket, object) files, derr := disk.ListDir(bucket, object, -1)
if derr == nil { if derr == nil {
for _, entry := range files { for _, entry := range files {
_ = disk.DeleteFile(bucket, _ = disk.DeleteFile(bucket,

View File

@ -33,7 +33,7 @@ func listDirFactory(ctx context.Context, isLeaf isLeafFunc, treeWalkIgnoredErrs
} }
var entries []string var entries []string
var newEntries []string var newEntries []string
entries, err = disk.ListDir(bucket, prefixDir) entries, err = disk.ListDir(bucket, prefixDir, -1)
if err != nil { if err != nil {
// For any reason disk was deleted or goes offline, continue // For any reason disk was deleted or goes offline, continue
// and list from other disks if possible. // and list from other disks if possible.
@ -78,8 +78,9 @@ func (xl xlObjects) listObjects(ctx context.Context, bucket, prefix, marker, del
if walkResultCh == nil { if walkResultCh == nil {
endWalkCh = make(chan struct{}) endWalkCh = make(chan struct{})
isLeaf := xl.isObject isLeaf := xl.isObject
isLeafDir := xl.isObjectDir
listDir := listDirFactory(ctx, isLeaf, xlTreeWalkIgnoredErrs, xl.getLoadBalancedDisks()...) listDir := listDirFactory(ctx, isLeaf, xlTreeWalkIgnoredErrs, xl.getLoadBalancedDisks()...)
walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, isLeaf, endWalkCh) walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, isLeaf, isLeafDir, endWalkCh)
} }
var objInfos []ObjectInfo var objInfos []ObjectInfo
@ -136,7 +137,7 @@ func (xl xlObjects) listObjects(ctx context.Context, bucket, prefix, marker, del
result := ListObjectsInfo{IsTruncated: !eof} result := ListObjectsInfo{IsTruncated: !eof}
for _, objInfo := range objInfos { for _, objInfo := range objInfos {
result.NextMarker = objInfo.Name result.NextMarker = objInfo.Name
if objInfo.IsDir { if objInfo.IsDir && delimiter == slashSeparator {
result.Prefixes = append(result.Prefixes, objInfo.Name) result.Prefixes = append(result.Prefixes, objInfo.Name)
continue continue
} }

View File

@ -156,7 +156,7 @@ func (xl xlObjects) ListMultipartUploads(ctx context.Context, bucket, object, ke
if disk == nil { if disk == nil {
continue continue
} }
uploadIDs, err := disk.ListDir(minioMetaMultipartBucket, xl.getMultipartSHADir(bucket, object)) uploadIDs, err := disk.ListDir(minioMetaMultipartBucket, xl.getMultipartSHADir(bucket, object), -1)
if err != nil { if err != nil {
if err == errFileNotFound { if err == errFileNotFound {
return result, nil return result, nil
@ -862,12 +862,12 @@ func (xl xlObjects) cleanupStaleMultipartUploads(ctx context.Context, cleanupInt
// Remove the old multipart uploads on the given disk. // Remove the old multipart uploads on the given disk.
func (xl xlObjects) cleanupStaleMultipartUploadsOnDisk(ctx context.Context, disk StorageAPI, expiry time.Duration) { func (xl xlObjects) cleanupStaleMultipartUploadsOnDisk(ctx context.Context, disk StorageAPI, expiry time.Duration) {
now := time.Now() now := time.Now()
shaDirs, err := disk.ListDir(minioMetaMultipartBucket, "") shaDirs, err := disk.ListDir(minioMetaMultipartBucket, "", -1)
if err != nil { if err != nil {
return return
} }
for _, shaDir := range shaDirs { for _, shaDir := range shaDirs {
uploadIDDirs, err := disk.ListDir(minioMetaMultipartBucket, shaDir) uploadIDDirs, err := disk.ListDir(minioMetaMultipartBucket, shaDir, -1)
if err != nil { if err != nil {
continue continue
} }

View File

@ -359,6 +359,9 @@ func (xl xlObjects) GetObjectInfo(ctx context.Context, bucket, object string) (o
} }
if hasSuffix(object, slashSeparator) { if hasSuffix(object, slashSeparator) {
if !xl.isObjectDir(bucket, object) {
return oi, toObjectErr(errFileNotFound, bucket, object)
}
if oi, e = xl.getObjectInfoDir(ctx, bucket, object); e != nil { if oi, e = xl.getObjectInfoDir(ctx, bucket, object); e != nil {
return oi, toObjectErr(e, bucket, object) return oi, toObjectErr(e, bucket, object)
} }