Repository: fsouza/fake-gcs-server (go)

Issue:
[Feature Request] add support for startOffset and endOffset
The GCP storage API allows specifying an inclusive startOffset and exclusive endOffset to fetch a range of objects. The fake storage server ignores both parameters, returning all objects instead.

https://cloud.google.com/storage/docs/json_api/v1/objects/list

Pull request description:
Add support for startOffset and endOffset
Closes #464, adding support for `startOffset` and `endOffset` when listing objects in a bucket

Patch:
diff --git a/fakestorage/object.go b/fakestorage/object.go
index e023fb9..a2763c2 100644
--- a/fakestorage/object.go
+++ b/fakestorage/object.go
@@ -232,10 +232,26 @@ func (s *Server) createObject(obj Object) (Object, error) {
 	return fromBackendObjects([]backend.Object{newObj})[0], nil
 }
 
+type ListOptions struct {
+	Prefix      string
+	Delimiter   string
+	Versions    bool
+	StartOffset string
+	EndOffset   string
+}
+
 // ListObjects returns a sorted list of objects that match the given criteria,
 // or an error if the bucket doesn't exist.
 func (s *Server) ListObjects(bucketName, prefix, delimiter string, versions bool) ([]Object, []string, error) {
-	backendObjects, err := s.backend.ListObjects(bucketName, versions)
+	return s.ListObjectsWithOptions(bucketName, &ListOptions{
+		Prefix:    prefix,
+		Delimiter: delimiter,
+		Versions:  versions,
+	})
+}
+
+func (s *Server) ListObjectsWithOptions(bucketName string, options *ListOptions) ([]Object, []string, error) {
+	backendObjects, err := s.backend.ListObjects(bucketName, options.Versions)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -245,13 +261,18 @@ func (s *Server) ListObjects(bucketName, prefix, delimiter string, versions bool
 	var respObjects []Object
 	prefixes := make(map[string]bool)
 	for _, obj := range olist {
-		if strings.HasPrefix(obj.Name, prefix) {
-			objName := strings.Replace(obj.Name, prefix, "", 1)
-			delimPos := strings.Index(objName, delimiter)
-			if delimiter != "" && delimPos > -1 {
-				prefixes[obj.Name[:len(prefix)+delimPos+1]] = true
+		if strings.HasPrefix(obj.Name, options.Prefix) {
+			objName := strings.Replace(obj.Name, options.Prefix, "", 1)
+			delimPos := strings.Index(objName, options.Delimiter)
+			if options.Delimiter != "" && delimPos > -1 {
+				prefix := obj.Name[:len(options.Prefix)+delimPos+1]
+				if isInOffset(prefix, options.StartOffset, options.EndOffset) {
+					prefixes[prefix] = true
+				}
 			} else {
-				respObjects = append(respObjects, obj)
+				if isInOffset(obj.Name, options.StartOffset, options.EndOffset) {
+					respObjects = append(respObjects, obj)
+				}
 			}
 		}
 	}
@@ -263,6 +284,18 @@ func (s *Server) ListObjects(bucketName, prefix, delimiter string, versions bool
 	return respObjects, respPrefixes, nil
 }
 
+func isInOffset(name, startOffset, endOffset string) bool {
+	if endOffset != "" && startOffset != "" {
+		return strings.Compare(name, endOffset) < 0 && strings.Compare(name, startOffset) >= 0
+	} else if endOffset != "" {
+		return strings.Compare(name, endOffset) < 0
+	} else if startOffset != "" {
+		return strings.Compare(name, startOffset) >= 0
+	} else {
+		return true
+	}
+}
+
 func getCurrentIfZero(date time.Time) time.Time {
 	if date.IsZero() {
 		return time.Now()
@@ -355,11 +388,15 @@ func (s *Server) objectWithGenerationOnValidGeneration(bucketName, objectName, g
 
 func (s *Server) listObjects(r *http.Request) jsonResponse {
 	bucketName := mux.Vars(r)["bucketName"]
-	prefix := r.URL.Query().Get("prefix")
-	delimiter := r.URL.Query().Get("delimiter")
-	versions := r.URL.Query().Get("versions")
+	options := ListOptions{
+		Prefix:      r.URL.Query().Get("prefix"),
+		Delimiter:   r.URL.Query().Get("delimiter"),
+		Versions:    r.URL.Query().Get("versions") == "true",
+		StartOffset: r.URL.Query().Get("startOffset"),
+		EndOffset:   r.URL.Query().Get("endOffset"),
+	}
 
-	objs, prefixes, err := s.ListObjects(bucketName, prefix, delimiter, versions == "true")
+	objs, prefixes, err := s.ListObjectsWithOptions(bucketName, &options)
 
 	if err != nil {
 		return jsonResponse{status: http.StatusNotFound}


Test patch:
diff --git a/fakestorage/object_test.go b/fakestorage/object_test.go
index 10d9a4d..8300c93 100644
--- a/fakestorage/object_test.go
+++ b/fakestorage/object_test.go
@@ -628,6 +628,43 @@ func getTestCasesForListTests(versioningEnabled, withOverwrites bool) []listTest
 			[]string{},
 			nil,
 		},
+		{
+			fmt.Sprintf("filtering endOffset, versioning %t and overwrites %t", versioningEnabled, withOverwrites),
+			"some-bucket",
+			&storage.Query{EndOffset: "img/low-res"},
+			[]string{
+				"img/brand.jpg",
+				"img/hi-res/party-01.jpg",
+				"img/hi-res/party-02.jpg",
+				"img/hi-res/party-03.jpg",
+			},
+			nil,
+		},
+		{
+			fmt.Sprintf("filtering startOffset and endOffset, versioning %t and overwrites %t", versioningEnabled, withOverwrites),
+			"some-bucket",
+			&storage.Query{StartOffset: "img/hi-res", EndOffset: "img/low-res"},
+			[]string{
+				"img/hi-res/party-01.jpg",
+				"img/hi-res/party-02.jpg",
+				"img/hi-res/party-03.jpg",
+			},
+			nil,
+		},
+		{
+			fmt.Sprintf("filtering prefix, delimiter, startOffset and endOffset, versioning %t and overwrites %t", versioningEnabled, withOverwrites),
+			"some-bucket",
+			&storage.Query{Prefix: "img/", Delimiter: "/", StartOffset: "img/hi-res", EndOffset: "img/low-res"},
+			[]string{},
+			[]string{"img/hi-res/"},
+		},
+		{
+			fmt.Sprintf("filtering prefix, delimiter and endOffset, versioning %t and overwrites %t", versioningEnabled, withOverwrites),
+			"some-bucket",
+			&storage.Query{Prefix: "img/", Delimiter: "/", StartOffset: "", EndOffset: "img/low-res"},
+			[]string{"img/brand.jpg"},
+			[]string{"img/hi-res/"},
+		},
 	}
 }