in internal/storage/fake/bucket.go [546:659]
func (b *bucket) ListObjects(
ctx context.Context,
req *gcs.ListObjectsRequest) (listing *gcs.Listing, err error) {
b.mu.Lock()
defer b.mu.Unlock()
// Set up the result object.
listing = new(gcs.Listing)
// Handle defaults.
maxResults := req.MaxResults
if maxResults == 0 {
maxResults = 1000
}
// Find where in the space of object names to start.
nameStart := req.Prefix
if req.ContinuationToken != "" && req.ContinuationToken > nameStart {
nameStart = req.ContinuationToken
}
// Find the range of indexes within the array to scan.
indexStart := b.objects.lowerBound(nameStart)
prefixLimit := b.objects.prefixUpperBound(req.Prefix)
indexLimit := minInt(indexStart+maxResults, prefixLimit)
// Scan the array.
var lastResultWasPrefix bool
for i := indexStart; i < indexLimit; i++ {
var o fakeObject = b.objects[i]
name := o.metadata.Name
// Search for a delimiter if necessary.
if req.Delimiter != "" {
// Search only in the part after the prefix.
nameMinusQueryPrefix := name[len(req.Prefix):]
delimiterIndex := strings.Index(nameMinusQueryPrefix, req.Delimiter)
if delimiterIndex >= 0 {
resultPrefixLimit := delimiterIndex
// Transform to an index within name.
resultPrefixLimit += len(req.Prefix)
// Include the delimiter in the result.
resultPrefixLimit += len(req.Delimiter)
// Save the result, but only if it's not a duplicate.
resultPrefix := name[:resultPrefixLimit]
if len(listing.CollapsedRuns) == 0 ||
listing.CollapsedRuns[len(listing.CollapsedRuns)-1] != resultPrefix {
listing.CollapsedRuns = append(listing.CollapsedRuns, resultPrefix)
}
// In hierarchical buckets, a directory is represented both as a prefix and a folder.
// Consequently, if a folder entry is discovered, it indicates that it's exclusively a prefix entry.
//
// This check was incorporated because createFolder needs to add an entry to the objects, and
// we cannot distinguish from that entry whether it's solely a prefix.
//
// For example, mkdir test will create both a folder entry and a test/ prefix.
// In our createFolder fake bucket implementation, we created both a folder and an object for
// the given folderName. There, we can't define whether it's only a prefix and not an object.
// Hence, we added this check here.
//
// Note that in a real ListObject call, the entry will appear only as a prefix and not as an object.
folderIndex := b.folders.find(resultPrefix)
if folderIndex < len(b.folders) {
lastResultWasPrefix = true
continue
}
isTrailingDelimiter := (delimiterIndex == len(nameMinusQueryPrefix)-1)
if !isTrailingDelimiter || !req.IncludeTrailingDelimiter {
lastResultWasPrefix = true
continue
}
}
}
lastResultWasPrefix = false
// Otherwise, return as an object result. Make a copy to avoid handing back
// internal state.
listing.MinObjects = append(listing.MinObjects, copyMinObject(&o.metadata))
}
// Set up a cursor for where to start the next scan if we didn't exhaust the
// results.
if indexLimit < prefixLimit {
// If the final object we visited was returned as an element in
// listing.CollapsedRuns, we want to skip all other objects that would
// result in the same so we don't return duplicate elements in
// listing.CollapsedRuns across requests.
if lastResultWasPrefix {
lastResultPrefix := listing.CollapsedRuns[len(listing.CollapsedRuns)-1]
listing.ContinuationToken = prefixSuccessor(lastResultPrefix)
// Check an assumption: prefixSuccessor cannot result in the empty string
// above because object names must be non-empty UTF-8 strings, and there
// is no valid non-empty UTF-8 string that consists of entirely 0xff
// bytes.
if listing.ContinuationToken == "" {
err = errors.New("unexpected empty string from prefixSuccessor")
return
}
} else {
// Otherwise, we'll start scanning at the next object.
listing.ContinuationToken = b.objects[indexLimit].metadata.Name
}
}
return
}