aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.md3
-rw-r--r--object.go5
-rw-r--r--object_iterator.go19
-rw-r--r--tests/object_iterator_test.go100
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
+ //<https://docs.openstack.org/swift/latest/middleware.html#symlink>
+ 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)
}
}
}