From b65158017829cee6fba71a6d730d1502026280a7 Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Mon, 7 May 2018 14:07:11 +0200 Subject: add support for symlinks to ObjectIterator Closes #2. --- CONTRIBUTING.md | 3 ++ object.go | 5 +++ object_iterator.go | 19 ++++++++ tests/object_iterator_test.go | 100 ++++++++++++++++++++++++++++++++++++------ 4 files changed, 113 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 96ae415..aa145a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,3 +37,6 @@ container](https://github.com/bouncestorage/docker-swift). 2. Run the tests with `./testing/with-saio.sh go test`. The script will find how to access the Swift API inside the container, and configure the auth environment variables accordingly. You can use this with any command that requires Swift credentials, e.g. `./testing/with-saio.sh swift stat`. + +**WARNING:** At the time of this writing, https://github.com/bouncestorage/docker-swift/pull/30 has not been merged, so +you need to patch the proxy-server.conf manually to get the symlink-related tests to pass. diff --git a/object.go b/object.go index 4f80e9f..affa9c3 100644 --- a/object.go +++ b/object.go @@ -510,6 +510,11 @@ func (o *Object) SymlinkTo(target *Object, opts *SymlinkOptions, ropts *RequestO if !target.c.a.isEqualTo(o.c.a) { ropts.Headers.Set("X-Symlink-Target-Account", target.c.a.Name()) } + if ropts.Headers.Get("Content-Type") == "" { + //recommended Content-Type for symlinks as per + // + ropts.Headers.Set("Content-Type", "application/symlink") + } var uopts *UploadOptions if opts != nil { diff --git a/object_iterator.go b/object_iterator.go index 6231292..a94d7c9 100644 --- a/object_iterator.go +++ b/object_iterator.go @@ -20,6 +20,7 @@ package schwift import ( "fmt" + "regexp" "time" ) @@ -33,6 +34,8 @@ type ObjectInfo struct { ContentType string Etag string LastModified time.Time + //SymlinkTarget is only set for symlinks. + SymlinkTarget *Object //If the ObjectInfo refers to an actual object, then SubDirectory is empty. //If the ObjectInfo refers to a pseudo-directory, then SubDirectory contains //the path of the pseudo-directory and all other fields are nil/zero/empty. @@ -113,6 +116,9 @@ func (i *ObjectIterator) NextPage(limit int) ([]*Object, error) { return result, nil } +//The symlink_path attribute looks like "/v1/AUTH_foo/containername/obje/ctna/me". +var symlinkPathRx = regexp.MustCompile(`^/v1/([^/]+)/([^/]+)/(.+)$`) + //NextPageDetailed is like NextPage, but includes basic metadata. func (i *ObjectIterator) NextPageDetailed(limit int) ([]ObjectInfo, error) { b := i.getBase() @@ -124,6 +130,7 @@ func (i *ObjectIterator) NextPageDetailed(limit int) ([]ObjectInfo, error) { Etag string `json:"hash"` LastModifiedStr string `json:"last_modified"` Name string `json:"name"` + SymlinkPath string `json:"symlink_path"` //or just this: Subdir string `json:"subdir"` } @@ -150,6 +157,18 @@ func (i *ObjectIterator) NextPageDetailed(limit int) ([]ObjectInfo, error) { //this error is sufficiently obscure that we don't need to expose a type for it return nil, fmt.Errorf("Bad field objects[%d].last_modified: %s", idx, err.Error()) } + if data.SymlinkPath != "" { + match := symlinkPathRx.FindStringSubmatch(data.SymlinkPath) + if match == nil { + //like above + return nil, fmt.Errorf("Bad field objects[%d].symlink_path: %q", idx, data.SymlinkPath) + } + a := i.Container.a + if a.Name() != match[1] { + a = a.SwitchAccount(match[1]) + } + result[idx].SymlinkTarget = a.Container(match[2]).Object(match[3]) + } } else { marker = data.Subdir result[idx].SubDirectory = data.Subdir diff --git a/tests/object_iterator_test.go b/tests/object_iterator_test.go index e4a9c6e..a1334d1 100644 --- a/tests/object_iterator_test.go +++ b/tests/object_iterator_test.go @@ -180,6 +180,35 @@ func TestPseudoDirectories(t *testing.T) { }) } +func TestObjectIteratorWithSymlinks(t *testing.T) { + testWithContainer(t, func(c *schwift.Container) { + //create test objects that can be listed + objectNames := []string{ + "foo/1", + "foo/3", + } + for _, name := range objectNames { + hdr := schwift.NewObjectHeaders() + hdr.ContentType().Set("application/json") + err := c.Object(name).Upload(bytes.NewReader(objectExampleContent), nil, hdr.ToOpts()) + expectSuccess(t, err) + } + + //create a test symlink + expectSuccess(t, c.Object("foo/2").SymlinkTo(c.Object("foo/1"), nil, nil)) + + iter := c.Objects() + os, err := iter.Collect() + expectSuccess(t, err) + expectObjectNames(t, os, "foo/1", "foo/2", "foo/3") + + iter = c.Objects() + ois, err := iter.CollectDetailed() + expectSuccess(t, err) + expectObjectInfos(t, ois, "foo/1", "symlink:foo/2>foo/1", "foo/3") + }) +} + func expectContainerHeadersCached(t *testing.T, c *schwift.Container) { requestCountBefore := c.Account().Backend().(*RequestCountingBackend).Count _, err := c.Headers() @@ -215,6 +244,7 @@ func expectObjectInfos(t *testing.T, actualInfos []schwift.ObjectInfo, expectedN return } for idx, info := range actualInfos { + //case 1: pseudo-directory if strings.HasPrefix(expectedNames[idx], "subdir:") { expectedSubdir := strings.TrimPrefix(expectedNames[idx], "subdir:") if expectedSubdir != info.SubDirectory { @@ -225,33 +255,75 @@ func expectObjectInfos(t *testing.T, actualInfos []schwift.ObjectInfo, expectedN t.Errorf("expected objects[%d] to be a subdir, got %#v", idx, info) } - } else { - if info.SubDirectory != "" { - t.Errorf("expected objects[%d] to be an object, got subdir = %q", - idx, info.SubDirectory) - } + continue + } + if info.SubDirectory != "" { + t.Errorf("expected objects[%d] to be an object, got subdir = %q", + idx, info.SubDirectory) + } + + //case 2: symlink + if strings.HasPrefix(expectedNames[idx], "symlink:") { + fields := strings.SplitN(strings.TrimPrefix(expectedNames[idx], "symlink:"), ">", 2) + expectedName, expectedTargetName := fields[0], fields[1] + if info.Object == nil { t.Errorf("expected objects[%d].Name() == %q, got object == nil", idx, expectedNames[idx]) - } else if info.Object.Name() != expectedNames[idx] { + } else if info.Object.Name() != expectedName { t.Errorf("expected objects[%d].Name() == %q, got %q", - idx, expectedNames[idx], info.Object.Name()) + idx, expectedName, info.Object.Name()) } - if info.SizeBytes != uint64(len(objectExampleContent)) { - t.Errorf("expected objects[%d] sizeBytes == %d, got %d", - idx, len(objectExampleContent), info.SizeBytes) + + if info.SymlinkTarget == nil { + t.Errorf("expected objects[%d] symlinkTarget.Name() == %q, got symlinkTarget == nil", + idx, expectedTargetName) + } else if info.SymlinkTarget.Name() != expectedTargetName { + t.Errorf("expected objects[%d] symlinkTarget.Name() == %q, got %q", + idx, expectedTargetName, info.SymlinkTarget.Name()) } - if info.ContentType != "application/json" { - t.Errorf(`expected objects[%d] contentType == "application/json", got %q`, + + if info.SizeBytes != 0 { + t.Errorf("expected objects[%d] sizeBytes == 0, got %d", + idx, info.SizeBytes) + } + if info.ContentType != "application/symlink" { + t.Errorf(`expected objects[%d] contentType == "application/symlink", got %q`, idx, info.ContentType) } - if info.Etag != objectExampleContentEtag { + emptyEtag := etagOf(nil) + if info.Etag != emptyEtag { t.Errorf("expected objects[%d] etag == %q, got %q", - idx, objectExampleContentEtag, info.Etag) + idx, emptyEtag, info.Etag) } if info.LastModified.IsZero() { t.Errorf("objects[%d].LastModified is zero", idx) } + continue + } + + //case 3: regular object + if info.Object == nil { + t.Errorf("expected objects[%d].Name() == %q, got object == nil", + idx, expectedNames[idx]) + } else if info.Object.Name() != expectedNames[idx] { + t.Errorf("expected objects[%d].Name() == %q, got %q", + idx, expectedNames[idx], info.Object.Name()) + } + if info.SizeBytes != uint64(len(objectExampleContent)) { + t.Errorf("expected objects[%d] sizeBytes == %d, got %d", + idx, len(objectExampleContent), info.SizeBytes) + } + if info.ContentType != "application/json" { + t.Errorf(`expected objects[%d] contentType == "application/json", got %q`, + idx, info.ContentType) + } + if info.Etag != objectExampleContentEtag { + t.Errorf("expected objects[%d] etag == %q, got %q", + idx, objectExampleContentEtag, info.Etag) + } + if info.LastModified.IsZero() { + t.Errorf("objects[%d].LastModified is zero", idx) } } } -- cgit v1.2.3