contoller: Implement controlled healing and trigger (#2381)
This patch introduces new command line 'control' - minio control TO manage minio server connecting through GoRPC API frontend. - minio control heal Is implemented for healing objects.master
parent
0b7dfab17a
commit
e2498edb45
@ -0,0 +1,134 @@ |
||||
/* |
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strings" |
||||
|
||||
"net/rpc" |
||||
"net/url" |
||||
|
||||
"github.com/minio/cli" |
||||
) |
||||
|
||||
// "minio control" command.
|
||||
var controlCmd = cli.Command{ |
||||
Name: "control", |
||||
Usage: "Control and manage minio server.", |
||||
Action: mainControl, |
||||
Subcommands: []cli.Command{ |
||||
healCmd, |
||||
}, |
||||
} |
||||
|
||||
func mainControl(c *cli.Context) { |
||||
cli.ShowCommandHelp(c, "") |
||||
} |
||||
|
||||
var healCmd = cli.Command{ |
||||
Name: "heal", |
||||
Usage: "To heal objects.", |
||||
Action: healControl, |
||||
CustomHelpTemplate: `NAME: |
||||
minio {{.Name}} - {{.Usage}} |
||||
|
||||
USAGE: |
||||
minio {{.Name}} heal |
||||
|
||||
EAMPLES: |
||||
1. Heal an object. |
||||
$ minio control heal http://localhost:9000/songs/classical/western/piano.mp3
|
||||
|
||||
2. Heal all objects in a bucket recursively. |
||||
$ minio control heal http://localhost:9000/songs
|
||||
|
||||
3. Heall all objects with a given prefix recursively. |
||||
$ minio control heal http://localhost:9000/songs/classical/
|
||||
`, |
||||
} |
||||
|
||||
// "minio control heal" entry point.
|
||||
func healControl(c *cli.Context) { |
||||
// Parse bucket and object from url.URL.Path
|
||||
parseBucketObject := func(path string) (bucketName string, objectName string) { |
||||
splits := strings.SplitN(path, string(slashSeparator), 3) |
||||
switch len(splits) { |
||||
case 0, 1: |
||||
bucketName = "" |
||||
objectName = "" |
||||
case 2: |
||||
bucketName = splits[1] |
||||
objectName = "" |
||||
case 3: |
||||
bucketName = splits[1] |
||||
objectName = splits[2] |
||||
|
||||
} |
||||
return bucketName, objectName |
||||
} |
||||
|
||||
if len(c.Args()) != 1 { |
||||
cli.ShowCommandHelpAndExit(c, "heal", 1) |
||||
} |
||||
|
||||
parsedURL, err := url.ParseRequestURI(c.Args()[0]) |
||||
fatalIf(err, "Unable to parse URL") |
||||
|
||||
bucketName, objectName := parseBucketObject(parsedURL.Path) |
||||
if bucketName == "" { |
||||
cli.ShowCommandHelpAndExit(c, "heal", 1) |
||||
} |
||||
|
||||
client, err := rpc.DialHTTPPath("tcp", parsedURL.Host, healPath) |
||||
fatalIf(err, "Unable to connect to %s", parsedURL.Host) |
||||
|
||||
// If object does not have trailing "/" then it's an object, hence heal it.
|
||||
if objectName != "" && !strings.HasSuffix(objectName, slashSeparator) { |
||||
fmt.Printf("Healing : /%s/%s", bucketName, objectName) |
||||
args := &HealObjectArgs{bucketName, objectName} |
||||
reply := &HealObjectReply{} |
||||
err = client.Call("Heal.HealObject", args, reply) |
||||
fatalIf(err, "RPC Heal.HealObject call failed") |
||||
fmt.Println() |
||||
return |
||||
} |
||||
|
||||
// Recursively list and heal the objects.
|
||||
prefix := objectName |
||||
marker := "" |
||||
for { |
||||
args := HealListArgs{bucketName, prefix, marker, "", 1000} |
||||
reply := &HealListReply{} |
||||
err = client.Call("Heal.ListObjects", args, reply) |
||||
fatalIf(err, "RPC Heal.ListObjects call failed") |
||||
|
||||
// Heal the objects returned in the ListObjects reply.
|
||||
for _, obj := range reply.Objects { |
||||
fmt.Printf("Healing : /%s/%s", bucketName, obj) |
||||
reply := &HealObjectReply{} |
||||
err = client.Call("Heal.HealObject", HealObjectArgs{bucketName, obj}, reply) |
||||
fatalIf(err, "RPC Heal.HealObject call failed") |
||||
fmt.Println() |
||||
} |
||||
if !reply.IsTruncated { |
||||
// End of listing.
|
||||
break |
||||
} |
||||
marker = reply.NextMarker |
||||
} |
||||
} |
@ -0,0 +1,84 @@ |
||||
/* |
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package main |
||||
|
||||
import "encoding/hex" |
||||
|
||||
// Heals the erasure coded file. reedsolomon.Reconstruct() is used to reconstruct the missing parts.
|
||||
func erasureHealFile(latestDisks []StorageAPI, outDatedDisks []StorageAPI, volume, path, healBucket, healPath string, size int64, blockSize int64, dataBlocks int, parityBlocks int, algo string) (checkSums []string, err error) { |
||||
var offset int64 |
||||
remainingSize := size |
||||
|
||||
// Hash for bitrot protection.
|
||||
hashWriters := newHashWriters(len(outDatedDisks), bitRotAlgo) |
||||
|
||||
for remainingSize > 0 { |
||||
curBlockSize := blockSize |
||||
if remainingSize < curBlockSize { |
||||
curBlockSize = remainingSize |
||||
} |
||||
|
||||
// Calculate the block size that needs to be read from each disk.
|
||||
curEncBlockSize := getChunkSize(curBlockSize, dataBlocks) |
||||
|
||||
// Memory for reading data from disks and reconstructing missing data using erasure coding.
|
||||
enBlocks := make([][]byte, len(latestDisks)) |
||||
|
||||
// Read data from the latest disks.
|
||||
// FIXME: no need to read from all the disks. dataBlocks+1 is enough.
|
||||
for index, disk := range latestDisks { |
||||
if disk == nil { |
||||
continue |
||||
} |
||||
enBlocks[index] = make([]byte, curEncBlockSize) |
||||
_, err := disk.ReadFile(volume, path, offset, enBlocks[index]) |
||||
if err != nil { |
||||
enBlocks[index] = nil |
||||
} |
||||
} |
||||
|
||||
// Reconstruct missing data.
|
||||
err := decodeData(enBlocks, dataBlocks, parityBlocks) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// Write to the healPath file.
|
||||
for index, disk := range outDatedDisks { |
||||
if disk == nil { |
||||
continue |
||||
} |
||||
err := disk.AppendFile(healBucket, healPath, enBlocks[index]) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
hashWriters[index].Write(enBlocks[index]) |
||||
} |
||||
remainingSize -= curBlockSize |
||||
offset += curEncBlockSize |
||||
} |
||||
|
||||
// Checksums for the bit rot.
|
||||
checkSums = make([]string, len(outDatedDisks)) |
||||
for index, disk := range outDatedDisks { |
||||
if disk == nil { |
||||
continue |
||||
} |
||||
checkSums[index] = hex.EncodeToString(hashWriters[index].Sum(nil)) |
||||
} |
||||
return checkSums, nil |
||||
} |
@ -0,0 +1,123 @@ |
||||
/* |
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"bytes" |
||||
"crypto/rand" |
||||
"os" |
||||
"path" |
||||
"testing" |
||||
) |
||||
|
||||
// Test erasureHealFile()
|
||||
func TestErasureHealFile(t *testing.T) { |
||||
// Initialize environment needed for the test.
|
||||
dataBlocks := 7 |
||||
parityBlocks := 7 |
||||
blockSize := int64(blockSizeV1) |
||||
setup, err := newErasureTestSetup(dataBlocks, parityBlocks, blockSize) |
||||
if err != nil { |
||||
t.Error(err) |
||||
return |
||||
} |
||||
defer setup.Remove() |
||||
|
||||
disks := setup.disks |
||||
|
||||
// Prepare a slice of 1MB with random data.
|
||||
data := make([]byte, 1*1024*1024) |
||||
_, err = rand.Read(data) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
// Create a test file.
|
||||
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject1", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if size != int64(len(data)) { |
||||
t.Errorf("erasureCreateFile returned %d, expected %d", size, len(data)) |
||||
} |
||||
|
||||
latest := make([]StorageAPI, len(disks)) // Slice of latest disks
|
||||
outDated := make([]StorageAPI, len(disks)) // Slice of outdated disks
|
||||
|
||||
// Test case when one part needs to be healed.
|
||||
dataPath := path.Join(setup.diskPaths[0], "testbucket", "testobject1") |
||||
err = os.Remove(dataPath) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
copy(latest, disks) |
||||
latest[0] = nil |
||||
outDated[0] = disks[0] |
||||
healCheckSums, err := erasureHealFile(latest, outDated, "testbucket", "testobject1", "testbucket", "testobject1", 1*1024*1024, blockSize, dataBlocks, parityBlocks, bitRotAlgo) |
||||
// Checksum of the healed file should match.
|
||||
if checkSums[0] != healCheckSums[0] { |
||||
t.Error("Healing failed, data does not match.") |
||||
} |
||||
|
||||
// Test case when parityBlocks number of disks need to be healed.
|
||||
// Should succeed.
|
||||
copy(latest, disks) |
||||
for index := 0; index < parityBlocks; index++ { |
||||
dataPath := path.Join(setup.diskPaths[index], "testbucket", "testobject1") |
||||
err = os.Remove(dataPath) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
latest[index] = nil |
||||
outDated[index] = disks[index] |
||||
} |
||||
|
||||
healCheckSums, err = erasureHealFile(latest, outDated, "testbucket", "testobject1", "testbucket", "testobject1", 1*1024*1024, blockSize, dataBlocks, parityBlocks, bitRotAlgo) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// Checksums of the healed files should match.
|
||||
for index := 0; index < parityBlocks; index++ { |
||||
if checkSums[index] != healCheckSums[index] { |
||||
t.Error("Healing failed, data does not match.") |
||||
} |
||||
} |
||||
for index := dataBlocks; index < len(disks); index++ { |
||||
if healCheckSums[index] != "" { |
||||
t.Errorf("expected healCheckSums[%d] to be empty", index) |
||||
} |
||||
} |
||||
|
||||
// Test case when parityBlocks+1 number of disks need to be healed.
|
||||
// Should fail.
|
||||
copy(latest, disks) |
||||
for index := 0; index < parityBlocks+1; index++ { |
||||
dataPath := path.Join(setup.diskPaths[index], "testbucket", "testobject1") |
||||
err = os.Remove(dataPath) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
latest[index] = nil |
||||
outDated[index] = disks[index] |
||||
} |
||||
healCheckSums, err = erasureHealFile(latest, outDated, "testbucket", "testobject1", "testbucket", "testobject1", 1*1024*1024, blockSize, dataBlocks, parityBlocks, bitRotAlgo) |
||||
if err == nil { |
||||
t.Error("Expected erasureHealFile() to fail when the number of available disks <= parityBlocks") |
||||
} |
||||
} |
@ -0,0 +1,85 @@ |
||||
/* |
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"net/rpc" |
||||
|
||||
router "github.com/gorilla/mux" |
||||
) |
||||
|
||||
// Routes paths for "minio control" commands.
|
||||
const ( |
||||
controlRPCPath = reservedBucket + "/control" |
||||
healPath = controlRPCPath + "/heal" |
||||
) |
||||
|
||||
// Register control RPC handlers.
|
||||
func registerControlRPCRouter(mux *router.Router, objAPI ObjectLayer) { |
||||
healRPCServer := rpc.NewServer() |
||||
healRPCServer.RegisterName("Heal", &healHandler{objAPI}) |
||||
mux.Path(healPath).Handler(healRPCServer) |
||||
} |
||||
|
||||
// Handler for object healing.
|
||||
type healHandler struct { |
||||
ObjectAPI ObjectLayer |
||||
} |
||||
|
||||
// HealListArgs - argument for ListObjects RPC.
|
||||
type HealListArgs struct { |
||||
Bucket string |
||||
Prefix string |
||||
Marker string |
||||
Delimiter string |
||||
MaxKeys int |
||||
} |
||||
|
||||
// HealListReply - reply by ListObjects RPC.
|
||||
type HealListReply struct { |
||||
IsTruncated bool |
||||
NextMarker string |
||||
Objects []string |
||||
} |
||||
|
||||
// ListObjects - list objects.
|
||||
func (h healHandler) ListObjects(arg *HealListArgs, reply *HealListReply) error { |
||||
info, err := h.ObjectAPI.ListObjectsHeal(arg.Bucket, arg.Prefix, arg.Marker, arg.Delimiter, arg.MaxKeys) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
reply.IsTruncated = info.IsTruncated |
||||
reply.NextMarker = info.NextMarker |
||||
for _, obj := range info.Objects { |
||||
reply.Objects = append(reply.Objects, obj.Name) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// HealObjectArgs - argument for HealObject RPC.
|
||||
type HealObjectArgs struct { |
||||
Bucket string |
||||
Object string |
||||
} |
||||
|
||||
// HealObjectReply - reply by HealObject RPC.
|
||||
type HealObjectReply struct{} |
||||
|
||||
// HealObject - heal the object.
|
||||
func (h healHandler) HealObject(arg *HealObjectArgs, reply *HealObjectReply) error { |
||||
return h.ObjectAPI.HealObject(arg.Bucket, arg.Object) |
||||
} |
@ -0,0 +1,206 @@ |
||||
/* |
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"path" |
||||
"sort" |
||||
"strings" |
||||
) |
||||
|
||||
func listDirHealFactory(disks ...StorageAPI) listDirFunc { |
||||
// Returns sorted merged entries from all the disks.
|
||||
listDir := func(bucket, prefixDir, prefixEntry string) (mergedentries []string, delayIsLeaf bool, err error) { |
||||
for _, disk := range disks { |
||||
var entries []string |
||||
var newEntries []string |
||||
entries, err = disk.ListDir(bucket, prefixDir) |
||||
if err != nil { |
||||
// Skip the disk of listDir returns error.
|
||||
continue |
||||
} |
||||
|
||||
for i, entry := range entries { |
||||
if strings.HasSuffix(entry, slashSeparator) { |
||||
if _, err = disk.StatFile(bucket, path.Join(prefixDir, entry, xlMetaJSONFile)); err == nil { |
||||
// If it is an object trim the trailing "/"
|
||||
entries[i] = strings.TrimSuffix(entry, slashSeparator) |
||||
} |
||||
} |
||||
} |
||||
|
||||
if len(mergedentries) == 0 { |
||||
// For the first successful disk.ListDir()
|
||||
mergedentries = entries |
||||
sort.Strings(mergedentries) |
||||
continue |
||||
} |
||||
|
||||
// find elements in entries which are not in mergedentries
|
||||
for _, entry := range entries { |
||||
idx := sort.SearchStrings(mergedentries, entry) |
||||
if mergedentries[idx] == entry { |
||||
continue |
||||
} |
||||
newEntries = append(newEntries, entry) |
||||
} |
||||
|
||||
if len(newEntries) > 0 { |
||||
// Merge the entries and sort it.
|
||||
mergedentries = append(mergedentries, newEntries...) |
||||
sort.Strings(mergedentries) |
||||
} |
||||
} |
||||
return mergedentries, false, nil |
||||
} |
||||
return listDir |
||||
} |
||||
|
||||
// listObjectsHeal - wrapper function implemented over file tree walk.
|
||||
func (xl xlObjects) listObjectsHeal(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) { |
||||
// Default is recursive, if delimiter is set then list non recursive.
|
||||
recursive := true |
||||
if delimiter == slashSeparator { |
||||
recursive = false |
||||
} |
||||
|
||||
// "heal" true for listObjectsHeal() and false for listObjects()
|
||||
heal := true |
||||
walkResultCh, endWalkCh := xl.listPool.Release(listParams{bucket, recursive, marker, prefix, heal}) |
||||
if walkResultCh == nil { |
||||
endWalkCh = make(chan struct{}) |
||||
listDir := listDirHealFactory(xl.storageDisks...) |
||||
walkResultCh = startTreeWalk(bucket, prefix, marker, recursive, listDir, nil, endWalkCh) |
||||
} |
||||
|
||||
var objInfos []ObjectInfo |
||||
var eof bool |
||||
var nextMarker string |
||||
for i := 0; i < maxKeys; { |
||||
walkResult, ok := <-walkResultCh |
||||
if !ok { |
||||
// Closed channel.
|
||||
eof = true |
||||
break |
||||
} |
||||
// For any walk error return right away.
|
||||
if walkResult.err != nil { |
||||
// File not found is a valid case.
|
||||
if walkResult.err == errFileNotFound { |
||||
return ListObjectsInfo{}, nil |
||||
} |
||||
return ListObjectsInfo{}, toObjectErr(walkResult.err, bucket, prefix) |
||||
} |
||||
entry := walkResult.entry |
||||
var objInfo ObjectInfo |
||||
if strings.HasSuffix(entry, slashSeparator) { |
||||
// Object name needs to be full path.
|
||||
objInfo.Bucket = bucket |
||||
objInfo.Name = entry |
||||
objInfo.IsDir = true |
||||
} else { |
||||
objInfo.Bucket = bucket |
||||
objInfo.Name = entry |
||||
} |
||||
nextMarker = objInfo.Name |
||||
objInfos = append(objInfos, objInfo) |
||||
i++ |
||||
if walkResult.end == true { |
||||
eof = true |
||||
break |
||||
} |
||||
} |
||||
|
||||
params := listParams{bucket, recursive, nextMarker, prefix, heal} |
||||
if !eof { |
||||
xl.listPool.Set(params, walkResultCh, endWalkCh) |
||||
} |
||||
|
||||
result := ListObjectsInfo{IsTruncated: !eof} |
||||
for _, objInfo := range objInfos { |
||||
result.NextMarker = objInfo.Name |
||||
if objInfo.IsDir { |
||||
result.Prefixes = append(result.Prefixes, objInfo.Name) |
||||
continue |
||||
} |
||||
result.Objects = append(result.Objects, ObjectInfo{ |
||||
Name: objInfo.Name, |
||||
ModTime: objInfo.ModTime, |
||||
Size: objInfo.Size, |
||||
IsDir: false, |
||||
}) |
||||
} |
||||
return result, nil |
||||
} |
||||
|
||||
// ListObjects - list all objects at prefix, delimited by '/'.
|
||||
func (xl xlObjects) ListObjectsHeal(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) { |
||||
// Verify if bucket is valid.
|
||||
if !IsValidBucketName(bucket) { |
||||
return ListObjectsInfo{}, BucketNameInvalid{Bucket: bucket} |
||||
} |
||||
// Verify if bucket exists.
|
||||
if !xl.isBucketExist(bucket) { |
||||
return ListObjectsInfo{}, BucketNotFound{Bucket: bucket} |
||||
} |
||||
if !IsValidObjectPrefix(prefix) { |
||||
return ListObjectsInfo{}, ObjectNameInvalid{Bucket: bucket, Object: prefix} |
||||
} |
||||
// Verify if delimiter is anything other than '/', which we do not support.
|
||||
if delimiter != "" && delimiter != slashSeparator { |
||||
return ListObjectsInfo{}, UnsupportedDelimiter{ |
||||
Delimiter: delimiter, |
||||
} |
||||
} |
||||
// Verify if marker has prefix.
|
||||
if marker != "" { |
||||
if !strings.HasPrefix(marker, prefix) { |
||||
return ListObjectsInfo{}, InvalidMarkerPrefixCombination{ |
||||
Marker: marker, |
||||
Prefix: prefix, |
||||
} |
||||
} |
||||
} |
||||
|
||||
// With max keys of zero we have reached eof, return right here.
|
||||
if maxKeys == 0 { |
||||
return ListObjectsInfo{}, nil |
||||
} |
||||
|
||||
// For delimiter and prefix as '/' we do not list anything at all
|
||||
// since according to s3 spec we stop at the 'delimiter' along
|
||||
// with the prefix. On a flat namespace with 'prefix' as '/'
|
||||
// we don't have any entries, since all the keys are of form 'keyName/...'
|
||||
if delimiter == slashSeparator && prefix == slashSeparator { |
||||
return ListObjectsInfo{}, nil |
||||
} |
||||
|
||||
// Over flowing count - reset to maxObjectList.
|
||||
if maxKeys < 0 || maxKeys > maxObjectList { |
||||
maxKeys = maxObjectList |
||||
} |
||||
|
||||
// Initiate a list operation, if successful filter and return quickly.
|
||||
listObjInfo, err := xl.listObjectsHeal(bucket, prefix, marker, delimiter, maxKeys) |
||||
if err == nil { |
||||
// We got the entries successfully return.
|
||||
return listObjInfo, nil |
||||
} |
||||
|
||||
// Return error at the end.
|
||||
return ListObjectsInfo{}, toObjectErr(err, bucket, prefix) |
||||
} |
Loading…
Reference in new issue