diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2018-03-11 19:38:27 +0100 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2018-03-11 19:41:33 +0100 |
| commit | d23b4052c0866698b14ac13ac98581d9f5440a9b (patch) | |
| tree | 51c93559594f8fd5136fd51b7397a30415ea93aa | |
| parent | aaf61ac55e18a04fd68b9b6ee4fd4fce49659eeb (diff) | |
| download | go-schwift-d23b4052c0866698b14ac13ac98581d9f5440a9b.tar.gz | |
revamp the Headers API
1. Move common methods of AccountHeaders, ContainerHeaders,
ObjectHeaders into a base type Headers.
2. Fold Headers into RequestOptions to remove one of the two optional
arguments on request methods. The new Headers.ToOpts() method
offers a nice experience for users passing Headers to request
methods.
The Update() methods keep the explicit Headers argument since the
Headers argument is not optional there.
The only downside is that we lose a bit of type-safety because
RequestOptions takes any Headers instance, so e.g. ContainerHeaders
could be passed to Object.Upload(). I believe the benefits outweigh
this problem.
| -rw-r--r-- | account.go | 19 | ||||
| -rw-r--r-- | bulk.go | 39 | ||||
| -rw-r--r-- | container.go | 15 | ||||
| -rw-r--r-- | container_iterator.go | 4 | ||||
| -rw-r--r-- | download.go | 16 | ||||
| -rw-r--r-- | errors.go | 2 | ||||
| -rw-r--r-- | field_metadata.go | 10 | ||||
| -rw-r--r-- | field_string.go | 10 | ||||
| -rw-r--r-- | field_time.go | 34 | ||||
| -rw-r--r-- | field_uint64.go | 12 | ||||
| -rw-r--r-- | generated.go | 174 | ||||
| -rw-r--r-- | generated.go.in | 50 | ||||
| -rw-r--r-- | headers.go | 76 | ||||
| -rw-r--r-- | iterator.go | 30 | ||||
| -rw-r--r-- | object.go | 55 | ||||
| -rw-r--r-- | object_iterator.go | 6 | ||||
| -rw-r--r-- | request.go | 34 | ||||
| -rw-r--r-- | tests/account_test.go | 6 | ||||
| -rw-r--r-- | tests/bulk_delete_test.go | 10 | ||||
| -rw-r--r-- | tests/bulk_upload_test.go | 6 | ||||
| -rw-r--r-- | tests/container_iterator_test.go | 2 | ||||
| -rw-r--r-- | tests/container_test.go | 8 | ||||
| -rw-r--r-- | tests/field_test.go | 46 | ||||
| -rw-r--r-- | tests/headers_test.go | 12 | ||||
| -rw-r--r-- | tests/object_iterator_test.go | 4 | ||||
| -rw-r--r-- | tests/object_test.go | 36 | ||||
| -rw-r--r-- | tests/shared_test.go | 4 |
27 files changed, 335 insertions, 385 deletions
@@ -98,7 +98,7 @@ func (a *Account) Headers() (AccountHeaders, error) { return AccountHeaders{}, err } - headers := AccountHeaders(headersFromHTTP(resp.Header)) + headers := AccountHeaders{headersFromHTTP(resp.Header)} err = headers.Validate() if err != nil { return headers, err @@ -113,15 +113,14 @@ func (a *Account) Invalidate() { a.headers = nil } -//Update updates the account using a POST request. To add URL parameters, pass -//a non-nil *RequestOptions. +//Update updates the account using a POST request. The headers in the headers +//attribute take precedence over those in opts.Headers. // //A successful POST request implies Invalidate() since it may change metadata. func (a *Account) Update(headers AccountHeaders, opts *RequestOptions) error { _, err := Request{ Method: "POST", - Headers: headersToHTTP(headers), - Options: opts, + Options: cloneRequestOptions(opts, headers.Headers), ExpectStatusCodes: []int{204}, }.Do(a.backend) if err == nil { @@ -130,17 +129,13 @@ func (a *Account) Update(headers AccountHeaders, opts *RequestOptions) error { return err } -//Create creates the account using a PUT request. To add URL parameters, pass -//a non-nil *RequestOptions. -// -//Note that this operation is only available to reseller admins, not to regular -//users. +//Create creates the account using a PUT request. This operation is only +//available to reseller admins, not to regular users. // //A successful PUT request implies Invalidate() since it may change metadata. -func (a *Account) Create(headers AccountHeaders, opts *RequestOptions) error { +func (a *Account) Create(opts *RequestOptions) error { _, err := Request{ Method: "PUT", - Headers: headersToHTTP(headers), Options: opts, ExpectStatusCodes: []int{201, 202}, DrainResponseBody: true, @@ -47,11 +47,11 @@ const ( //For example, when uploading an archive that contains the file "a/b/c": // // //This uploads the file into the container "a" as object "b/c". -// account.BulkUpload("", format, contents, nil, nil) +// account.BulkUpload("", format, contents, nil) // //This uploads the file into the container "foo" as object "a/b/c". -// account.BulkUpload("foo", format, contents, nil, nil) +// account.BulkUpload("foo", format, contents, nil) // //This uploads the file into the container "foo" as object "bar/baz/a/b/c". -// account.BulkUpload("foo/bar/baz", format, contents, nil, nil) +// account.BulkUpload("foo/bar/baz", format, contents, nil) // //The first return value indicates the number of files that have been created //on the server side. This may be lower than the number of files in the archive @@ -63,7 +63,7 @@ const ( // //This operation returns (0, ErrNotSupported) if the server does not support //bulk-uploading. -func (a *Account) BulkUpload(uploadPath string, format BulkUploadFormat, contents io.Reader, headers AccountHeaders, opts *RequestOptions) (int, error) { +func (a *Account) BulkUpload(uploadPath string, format BulkUploadFormat, contents io.Reader, opts *RequestOptions) (int, error) { caps, err := a.Capabilities() if err != nil { return 0, err @@ -75,11 +75,10 @@ func (a *Account) BulkUpload(uploadPath string, format BulkUploadFormat, content req := Request{ Method: "PUT", Body: contents, - Headers: headersToHTTP(headers), - Options: cloneRequestOptions(opts), + Options: cloneRequestOptions(opts, nil), ExpectStatusCodes: []int{200}, } - req.Headers.Set("Accept", "application/json") + req.Options.Headers.Set("Accept", "application/json") req.Options.Values.Set("extract-archive", string(format)) fields := strings.SplitN(strings.Trim(uploadPath, "/"), "/", 2) @@ -134,9 +133,8 @@ func makeBulkObjectError(fullName string, statusCode int) BulkObjectError { // numDeleted, numNotFound, err := container.Account().BulkDelete( // objects, []*schwift.Container{container}, nil, nil) // -//If the server does not support bulk-deletion, this function will just call -//Object.Delete() for each given object and Container.Delete() for each given -//container, and aggregate the result. +//If the server does not support bulk-deletion, this function falls back to +//deleting each object and container individually, and aggregates the result. // //If not nil, the error return value is *usually* an instance of //BulkError. @@ -144,7 +142,7 @@ func makeBulkObjectError(fullName string, statusCode int) BulkObjectError { //The objects may be located in multiple containers, but they and the //containers must all be located in the given account. (Otherwise, //ErrAccountMismatch is returned.) -func (a *Account) BulkDelete(objects []*Object, containers []*Container, headers AccountHeaders, opts *RequestOptions) (numDeleted int, numNotFound int, deleteError error) { +func (a *Account) BulkDelete(objects []*Object, containers []*Container, opts *RequestOptions) (numDeleted int, numNotFound int, deleteError error) { //validate that all given objects are in this account for _, obj := range objects { other := obj.Container().Account() @@ -165,7 +163,7 @@ func (a *Account) BulkDelete(objects []*Object, containers []*Container, headers return 0, 0, err } if caps.BulkDelete == nil { - return a.bulkDeleteSingle(objects, containers, headers, opts) + return a.bulkDeleteSingle(objects, containers, opts) } chunkSize := int(caps.BulkDelete.MaximumDeletesPerRequest) @@ -193,7 +191,7 @@ func (a *Account) BulkDelete(objects []*Object, containers []*Container, headers chunk := names[0:chunkSize] names = names[chunkSize:] - numDeletedNow, numNotFoundNow, err := a.bulkDelete(chunk, headers, opts) + numDeletedNow, numNotFoundNow, err := a.bulkDelete(chunk, opts) numDeleted += numDeletedNow numNotFound += numNotFoundNow if err != nil { @@ -206,7 +204,7 @@ func (a *Account) BulkDelete(objects []*Object, containers []*Container, headers //Implementation of BulkDelete() for servers that *do not* support bulk //deletion. -func (a *Account) bulkDeleteSingle(objects []*Object, containers []*Container, headers AccountHeaders, opts *RequestOptions) (int, int, error) { +func (a *Account) bulkDeleteSingle(objects []*Object, containers []*Container, opts *RequestOptions) (int, int, error) { var ( numDeleted = 0 numNotFound = 0 @@ -235,7 +233,7 @@ func (a *Account) bulkDeleteSingle(objects []*Object, containers []*Container, h } for _, obj := range objects { - err := obj.Delete(ObjectHeaders(headers), opts) //this implies Invalidate() + err := obj.Delete(opts) //this implies Invalidate() err = handleSingleError(obj.Container().Name(), obj.Name(), err) if err != nil { return numDeleted, numNotFound, err @@ -243,7 +241,7 @@ func (a *Account) bulkDeleteSingle(objects []*Object, containers []*Container, h } for _, container := range containers { - err := container.Delete(ContainerHeaders(headers), opts) //this implies Invalidate() + err := container.Delete(opts) //this implies Invalidate() err = handleSingleError(container.Name(), "", err) if err != nil { return numDeleted, numNotFound, err @@ -263,16 +261,15 @@ func (a *Account) bulkDeleteSingle(objects []*Object, containers []*Container, h //Implementation of BulkDelete() for servers that *do* support bulk deletion. //This function is called *after* chunking, so `len(names) <= //account.Capabilities.BulkDelete.MaximumDeletesPerRequest`. -func (a *Account) bulkDelete(names []string, headers AccountHeaders, opts *RequestOptions) (int, int, error) { +func (a *Account) bulkDelete(names []string, opts *RequestOptions) (int, int, error) { req := Request{ Method: "DELETE", Body: strings.NewReader(strings.Join(names, "\n") + "\n"), - Headers: headersToHTTP(headers), - Options: cloneRequestOptions(opts), + Options: cloneRequestOptions(opts, nil), ExpectStatusCodes: []int{200}, } - req.Headers.Set("Accept", "application/json") - req.Headers.Set("Content-Type", "text/plain") + req.Options.Headers.Set("Accept", "application/json") + req.Options.Headers.Set("Content-Type", "text/plain") req.Options.Values.Set("bulk-delete", "true") resp, err := req.Do(a.backend) if err != nil { diff --git a/container.go b/container.go index 741350e..1d310aa 100644 --- a/container.go +++ b/container.go @@ -36,7 +36,7 @@ type Container struct { //container's existence, or chain this function with the EnsureExists() //function like so: // -// container, err := account.Container("documents").EnsureExists() +// container, err := account.Container("documents").EnsureExists() func (a *Account) Container(name string) *Container { return &Container{a: a, name: name} } @@ -81,7 +81,7 @@ func (c *Container) Headers() (ContainerHeaders, error) { return ContainerHeaders{}, err } - headers := ContainerHeaders(headersFromHTTP(resp.Header)) + headers := ContainerHeaders{headersFromHTTP(resp.Header)} err = headers.Validate() if err != nil { return headers, err @@ -100,8 +100,7 @@ func (c *Container) Update(headers ContainerHeaders, opts *RequestOptions) error _, err := Request{ Method: "POST", ContainerName: c.name, - Headers: headersToHTTP(headers), - Options: opts, + Options: cloneRequestOptions(opts, headers.Headers), ExpectStatusCodes: []int{204}, }.Do(c.a.backend) if err == nil { @@ -116,11 +115,10 @@ func (c *Container) Update(headers ContainerHeaders, opts *RequestOptions) error //This function can be used regardless of whether the container exists or not. // //A successful PUT request implies Invalidate() since it may change metadata. -func (c *Container) Create(headers ContainerHeaders, opts *RequestOptions) error { +func (c *Container) Create(opts *RequestOptions) error { _, err := Request{ Method: "PUT", ContainerName: c.name, - Headers: headersToHTTP(headers), Options: opts, ExpectStatusCodes: []int{201, 202}, DrainResponseBody: true, @@ -139,11 +137,10 @@ func (c *Container) Create(headers ContainerHeaders, opts *RequestOptions) error //This operation fails with http.StatusNotFound if the container does not exist. // //A successful DELETE request implies Invalidate(). -func (c *Container) Delete(headers ContainerHeaders, opts *RequestOptions) error { +func (c *Container) Delete(opts *RequestOptions) error { _, err := Request{ Method: "DELETE", ContainerName: c.name, - Headers: headersToHTTP(headers), Options: opts, ExpectStatusCodes: []int{204}, }.Do(c.a.backend) @@ -165,7 +162,7 @@ func (c *Container) Invalidate() { //This function returns the same container again, because its intended use is //with freshly constructed Container instances like so: // -// container, err := account.Container("documents").EnsureExists() +// container, err := account.Container("documents").EnsureExists() func (c *Container) EnsureExists() (*Container, error) { _, err := Request{ Method: "PUT", diff --git a/container_iterator.go b/container_iterator.go index c428335..debe475 100644 --- a/container_iterator.go +++ b/container_iterator.go @@ -64,9 +64,7 @@ type ContainerIterator struct { //When Prefix is set, only containers whose name starts with this string are //returned. Prefix string - //Headers may contain additional headers to include with the GET request. - Headers map[string]string - //Options may contain additional query parameters for the GET request. + //Options may contain additional headers and query parameters for the GET request. Options *RequestOptions base *iteratorBase diff --git a/download.go b/download.go index 6515bbf..dfd9a07 100644 --- a/download.go +++ b/download.go @@ -30,28 +30,26 @@ import ( // var obj *swift.Object // // //Do NOT do this! -// reader, err := obj.Download(nil, nil).AsReadCloser() +// reader, err := obj.Download(nil).AsReadCloser() // bytes, err := ioutil.ReadAll(reader) // err := reader.Close() // str := string(bytes) // // //Do this instead: -// str, err := obj.Download(nil, nil).AsString() +// str, err := obj.Download(nil).AsString() // -//Since the AsByteSlice and AsString method consume only the unread portion of -//the ReadCloser, and since they drain the ReadCloser irreversibly, the -//idiomatic way of using DownloadedObject is to call one of its members -//immediately, without storing the DownloadedObject instance in a variable -//first. +//Since all methods on DownloadedObject are irreversible, the idiomatic way of +//using DownloadedObject is to call one of its members immediately, without +//storing the DownloadedObject instance in a variable first. // // var obj *swift.Object // // //Do NOT do this! -// downloaded := obj.Download(nil, nil) +// downloaded := obj.Download(nil) // reader, err := downloaded.AsReadCloser() // // //Do this instead: -// reader, err := obj.Download(nil, nil).AsReadCloser() +// reader, err := obj.Download(nil).AsReadCloser() type DownloadedObject struct { r io.ReadCloser err error @@ -116,7 +116,7 @@ func (e BulkError) Error() string { //Is checks if the given error is an UnexpectedStatusCodeError for that status //code. For example: // -// err := container.Delete(nil, nil) +// err := container.Delete(nil) // if err != nil { // if schwift.Is(err, http.StatusNotFound) { // //container does not exist -> just what we wanted diff --git a/field_metadata.go b/field_metadata.go index 05dee32..90993d4 100644 --- a/field_metadata.go +++ b/field_metadata.go @@ -22,12 +22,12 @@ package schwift //in a headers instance. It cannot be directly constructed, but each headers //type has a method "Metadata" returning this type. For example: // -// hdr := make(ObjectHeaders) -// //the following two statements are equivalent -// hdr["X-Object-Meta-Access"] = "strictly confidential" -// hdr.Metadata().Set("Access", "strictly confidential") +// hdr := NewObjectHeaders() +// //the following two statements are equivalent +// hdr["X-Object-Meta-Access"] = "strictly confidential" +// hdr.Metadata().Set("Access", "strictly confidential") type FieldMetadata struct { - h headerInterface + h Headers k string } diff --git a/field_string.go b/field_string.go index f12d2d5..e0007e9 100644 --- a/field_string.go +++ b/field_string.go @@ -22,12 +22,12 @@ package schwift //whose value is a string. It cannot be directly constructed, but methods on //the Headers types return this type. For example: // -// hdr := make(AccountHeaders) -// //the following two statements are equivalent: -// hdr["X-Container-Read"] = ".r:*,.rlistings" -// hdr.ReadACL().Set(".r:*,.rlistings") +// hdr := NewAccountHeaders() +// //the following two statements are equivalent: +// hdr["X-Container-Read"] = ".r:*,.rlistings" +// hdr.ReadACL().Set(".r:*,.rlistings") type FieldString struct { - h headerInterface + h Headers k string } diff --git a/field_time.go b/field_time.go index 2506f3c..d367f68 100644 --- a/field_time.go +++ b/field_time.go @@ -28,25 +28,25 @@ import ( //FieldHTTPTimeReadonly is a helper type that provides type-safe access to a //readonly Swift header whose value is a HTTP timestamp like this: // -// Mon, 02 Jan 2006 15:04:05 GMT +// Mon, 02 Jan 2006 15:04:05 GMT // //It cannot be directly constructed, but methods on the Headers types return //this type. For example: // -// //suppose you have: -// hdr, err := obj.Headers() +// //suppose you have: +// hdr, err := obj.Headers() // -// //you could do this: -// time, err := time.Parse(time.RFC1123, hdr.Get("Last-Modified")) +// //you could do this: +// time, err := time.Parse(time.RFC1123, hdr.Get("Last-Modified")) // -// //or you can just: -// time := hdr.UpdatedAt().Get() +// //or you can just: +// time := hdr.UpdatedAt().Get() // //Don't worry about the missing `err` in the last line. When the header fails //to parse, Object.Headers() already returns the corresponding //MalformedHeaderError. type FieldHTTPTimeReadonly struct { - h headerInterface + h Headers k string } @@ -83,21 +83,21 @@ func (f FieldHTTPTimeReadonly) validate() error { //header whose value is a UNIX timestamp. It cannot be directly constructed, //but methods on the Headers types return this type. For example: // -// //suppose you have: -// hdr, err := obj.Headers() +// //suppose you have: +// hdr, err := obj.Headers() // -// //you could do all this: -// sec, err := strconv.ParseFloat(hdr.Get("X-Delete-At"), 64) -// time := time.Unix(0, int64(1e9 * sec)) +// //you could do all this: +// sec, err := strconv.ParseFloat(hdr.Get("X-Delete-At"), 64) +// time := time.Unix(0, int64(1e9 * sec)) // -// //or you can just: -// time := hdr.ExpiresAt().Get() +// //or you can just: +// time := hdr.ExpiresAt().Get() // //Don't worry about the missing `err` in the last line. When the header fails //to parse, Object.Headers() already returns the corresponding //MalformedHeaderError. type FieldUnixTime struct { - h headerInterface + h Headers k string } @@ -151,7 +151,7 @@ func (f FieldUnixTime) validate() error { //FieldUnixTimeReadonly is a readonly variant of FieldUnixTime. It is used for //fields that cannot be set by the client. type FieldUnixTimeReadonly struct { - h headerInterface + h Headers k string } diff --git a/field_uint64.go b/field_uint64.go index a14f558..2a542ae 100644 --- a/field_uint64.go +++ b/field_uint64.go @@ -26,12 +26,12 @@ import ( //whose value is an unsigned integer. It cannot be directly constructed, but //methods on the Headers types return this type. For example: // -// hdr := make(AccountHeaders) -// //the following two statements are equivalent: -// hdr["X-Account-Meta-Quota-Bytes"] = "1048576" -// hdr.QuotaBytes().Set(1 << 20) +// hdr := NewAccountHeaders() +// //the following two statements are equivalent: +// hdr["X-Account-Meta-Quota-Bytes"] = "1048576" +// hdr.BytesUsedQuota().Set(1 << 20) type FieldUint64 struct { - h headerInterface + h Headers k string } @@ -85,7 +85,7 @@ func (f FieldUint64) validate() error { //FieldUint64Readonly is a readonly variant of FieldUint64. It is used for //fields that cannot be set by the client. type FieldUint64Readonly struct { - h headerInterface + h Headers k string } diff --git a/generated.go b/generated.go index 2c109c1..abc4f90 100644 --- a/generated.go +++ b/generated.go @@ -8,38 +8,18 @@ package schwift -import "net/textproto" - //AccountHeaders contains the headers for a schwift.Account instance. // //To read and write well-known headers, use the methods on this type. -//To read and write arbitary headers, use the http.Header-like methods Get(), -//Set(), Clear(), Del(). -type AccountHeaders map[string]string - -//Clear sets the value for the specified header to the empty string. When the -//Headers instance is then sent to the server with Update(), the server will -//delete the value for that header; cf. Del(). -func (h AccountHeaders) Clear(key string) { - h[textproto.CanonicalMIMEHeaderKey(key)] = "" +//To read and write arbitary headers, use the methods on the Headers supertype. +type AccountHeaders struct { + Headers } -//Del deletes a key from the Headers instance. When the Headers instance is -//then sent to the server with Update(), a key which has been deleted with -//Del() will remain unchanged on the server. -func (h AccountHeaders) Del(key string) { - delete(h, textproto.CanonicalMIMEHeaderKey(key)) -} - -//Get returns the value for the specified header. -func (h AccountHeaders) Get(key string) string { - return h[textproto.CanonicalMIMEHeaderKey(key)] -} - -//Set sets a new value for the specified header. Any existing value will be -//overwritten. -func (h AccountHeaders) Set(key, value string) { - h[textproto.CanonicalMIMEHeaderKey(key)] = value +//NewAccountHeaders creates a new AccountHeaders instance. The return value +//will have the Headers attribute initialized to a non-nil map. +func NewAccountHeaders() AccountHeaders { + return AccountHeaders{make(Headers)} } //Validate returns MalformedHeaderError if the value of any well-known header @@ -77,74 +57,56 @@ func (h AccountHeaders) Validate() error { //BytesUsed provides type-safe access to X-Account-Bytes-Used headers. func (h AccountHeaders) BytesUsed() FieldUint64Readonly { - return FieldUint64Readonly{h, "X-Account-Bytes-Used"} + return FieldUint64Readonly{h.Headers, "X-Account-Bytes-Used"} } //ContainerCount provides type-safe access to X-Account-Container-Count headers. func (h AccountHeaders) ContainerCount() FieldUint64Readonly { - return FieldUint64Readonly{h, "X-Account-Container-Count"} + return FieldUint64Readonly{h.Headers, "X-Account-Container-Count"} } //Metadata provides type-safe access to X-Account-Meta- headers. func (h AccountHeaders) Metadata() FieldMetadata { - return FieldMetadata{h, "X-Account-Meta-"} + return FieldMetadata{h.Headers, "X-Account-Meta-"} } //BytesUsedQuota provides type-safe access to X-Account-Meta-Quota-Bytes headers. func (h AccountHeaders) BytesUsedQuota() FieldUint64 { - return FieldUint64{h, "X-Account-Meta-Quota-Bytes"} + return FieldUint64{h.Headers, "X-Account-Meta-Quota-Bytes"} } //TempURLKey2 provides type-safe access to X-Account-Meta-Temp-URL-Key-2 headers. func (h AccountHeaders) TempURLKey2() FieldString { - return FieldString{h, "X-Account-Meta-Temp-URL-Key-2"} + return FieldString{h.Headers, "X-Account-Meta-Temp-URL-Key-2"} } //TempURLKey provides type-safe access to X-Account-Meta-Temp-URL-Key headers. func (h AccountHeaders) TempURLKey() FieldString { - return FieldString{h, "X-Account-Meta-Temp-URL-Key"} + return FieldString{h.Headers, "X-Account-Meta-Temp-URL-Key"} } //ObjectCount provides type-safe access to X-Account-Object-Count headers. func (h AccountHeaders) ObjectCount() FieldUint64Readonly { - return FieldUint64Readonly{h, "X-Account-Object-Count"} + return FieldUint64Readonly{h.Headers, "X-Account-Object-Count"} } //CreatedAt provides type-safe access to X-Timestamp headers. func (h AccountHeaders) CreatedAt() FieldUnixTimeReadonly { - return FieldUnixTimeReadonly{h, "X-Timestamp"} + return FieldUnixTimeReadonly{h.Headers, "X-Timestamp"} } //ContainerHeaders contains the headers for a schwift.Container instance. // //To read and write well-known headers, use the methods on this type. -//To read and write arbitary headers, use the http.Header-like methods Get(), -//Set(), Clear(), Del(). -type ContainerHeaders map[string]string - -//Clear sets the value for the specified header to the empty string. When the -//Headers instance is then sent to the server with Update(), the server will -//delete the value for that header; cf. Del(). -func (h ContainerHeaders) Clear(key string) { - h[textproto.CanonicalMIMEHeaderKey(key)] = "" -} - -//Del deletes a key from the Headers instance. When the Headers instance is -//then sent to the server with Update(), a key which has been deleted with -//Del() will remain unchanged on the server. -func (h ContainerHeaders) Del(key string) { - delete(h, textproto.CanonicalMIMEHeaderKey(key)) +//To read and write arbitary headers, use the methods on the Headers supertype. +type ContainerHeaders struct { + Headers } -//Get returns the value for the specified header. -func (h ContainerHeaders) Get(key string) string { - return h[textproto.CanonicalMIMEHeaderKey(key)] -} - -//Set sets a new value for the specified header. Any existing value will be -//overwritten. -func (h ContainerHeaders) Set(key, value string) { - h[textproto.CanonicalMIMEHeaderKey(key)] = value +//NewContainerHeaders creates a new ContainerHeaders instance. The return value +//will have the Headers attribute initialized to a non-nil map. +func NewContainerHeaders() ContainerHeaders { + return ContainerHeaders{make(Headers)} } //Validate returns MalformedHeaderError if the value of any well-known header @@ -203,117 +165,91 @@ func (h ContainerHeaders) Validate() error { //BytesUsed provides type-safe access to X-Container-Bytes-Used headers. func (h ContainerHeaders) BytesUsed() FieldUint64Readonly { - return FieldUint64Readonly{h, "X-Container-Bytes-Used"} + return FieldUint64Readonly{h.Headers, "X-Container-Bytes-Used"} } //Metadata provides type-safe access to X-Container-Meta- headers. func (h ContainerHeaders) Metadata() FieldMetadata { - return FieldMetadata{h, "X-Container-Meta-"} + return FieldMetadata{h.Headers, "X-Container-Meta-"} } //BytesUsedQuota provides type-safe access to X-Container-Meta-Quota-Bytes headers. func (h ContainerHeaders) BytesUsedQuota() FieldUint64 { - return FieldUint64{h, "X-Container-Meta-Quota-Bytes"} + return FieldUint64{h.Headers, "X-Container-Meta-Quota-Bytes"} } //ObjectCountQuota provides type-safe access to X-Container-Meta-Quota-Count headers. func (h ContainerHeaders) ObjectCountQuota() FieldUint64 { - return FieldUint64{h, "X-Container-Meta-Quota-Count"} + return FieldUint64{h.Headers, "X-Container-Meta-Quota-Count"} } //TempURLKey2 provides type-safe access to X-Container-Meta-Temp-URL-Key-2 headers. func (h ContainerHeaders) TempURLKey2() FieldString { - return FieldString{h, "X-Container-Meta-Temp-URL-Key-2"} + return FieldString{h.Headers, "X-Container-Meta-Temp-URL-Key-2"} } //TempURLKey provides type-safe access to X-Container-Meta-Temp-URL-Key headers. func (h ContainerHeaders) TempURLKey() FieldString { - return FieldString{h, "X-Container-Meta-Temp-URL-Key"} + return FieldString{h.Headers, "X-Container-Meta-Temp-URL-Key"} } //ObjectCount provides type-safe access to X-Container-Object-Count headers. func (h ContainerHeaders) ObjectCount() FieldUint64Readonly { - return FieldUint64Readonly{h, "X-Container-Object-Count"} + return FieldUint64Readonly{h.Headers, "X-Container-Object-Count"} } //ReadACL provides type-safe access to X-Container-Read headers. func (h ContainerHeaders) ReadACL() FieldString { - return FieldString{h, "X-Container-Read"} + return FieldString{h.Headers, "X-Container-Read"} } //SyncKey provides type-safe access to X-Container-Sync-Key headers. func (h ContainerHeaders) SyncKey() FieldString { - return FieldString{h, "X-Container-Sync-Key"} + return FieldString{h.Headers, "X-Container-Sync-Key"} } //SyncTo provides type-safe access to X-Container-Sync-To headers. func (h ContainerHeaders) SyncTo() FieldString { - return FieldString{h, "X-Container-Sync-To"} + return FieldString{h.Headers, "X-Container-Sync-To"} } //WriteACL provides type-safe access to X-Container-Write headers. func (h ContainerHeaders) WriteACL() FieldString { - return FieldString{h, "X-Container-Write"} + return FieldString{h.Headers, "X-Container-Write"} } //HistoryLocation provides type-safe access to X-History-Location headers. func (h ContainerHeaders) HistoryLocation() FieldString { - return FieldString{h, "X-History-Location"} + return FieldString{h.Headers, "X-History-Location"} } //StoragePolicy provides type-safe access to X-Storage-Policy headers. func (h ContainerHeaders) StoragePolicy() FieldString { - return FieldString{h, "X-Storage-Policy"} + return FieldString{h.Headers, "X-Storage-Policy"} } //CreatedAt provides type-safe access to X-Timestamp headers. func (h ContainerHeaders) CreatedAt() FieldUnixTimeReadonly { - return FieldUnixTimeReadonly{h, "X-Timestamp"} + return FieldUnixTimeReadonly{h.Headers, "X-Timestamp"} } //VersionsLocation provides type-safe access to X-Versions-Location headers. func (h ContainerHeaders) VersionsLocation() FieldString { - return FieldString{h, "X-Versions-Location"} + return FieldString{h.Headers, "X-Versions-Location"} } //ObjectHeaders contains the headers for a schwift.Object instance. // //To read and write well-known headers, use the methods on this type. -//To read and write arbitary headers, use the http.Header-like methods Get(), -//Set(), Clear(), Del(). -type ObjectHeaders map[string]string - -//Clear sets the value for the specified header to the empty string. When the -//Headers instance is then sent to the server with Update(), the server will -//delete the value for that header; cf. Del(). -func (h ObjectHeaders) Clear(key string) { - h[textproto.CanonicalMIMEHeaderKey(key)] = "" -} - -//Del deletes a key from the Headers instance. When the Headers instance -//is then sent to the server with Update(), Del() has different effects -//depending on context because of Swift's inconsistent API: -// -//For most writable attributes, a key which has been deleted with Del() will -//remain unchanged on the server. To remove the key on the server, use Clear() -//instead. -// -//For object metadata (but not other object attributes), deleting a key will -//cause that key to be deleted on the server. Del() is identical to Clear() in -//this case. -func (h ObjectHeaders) Del(key string) { - delete(h, textproto.CanonicalMIMEHeaderKey(key)) -} - -//Get returns the value for the specified header. -func (h ObjectHeaders) Get(key string) string { - return h[textproto.CanonicalMIMEHeaderKey(key)] +//To read and write arbitary headers, use the methods on the Headers supertype. +type ObjectHeaders struct { + Headers } -//Set sets a new value for the specified header. Any existing value will be -//overwritten. -func (h ObjectHeaders) Set(key, value string) { - h[textproto.CanonicalMIMEHeaderKey(key)] = value +//NewObjectHeaders creates a new ObjectHeaders instance. The return value +//will have the Headers attribute initialized to a non-nil map. +func NewObjectHeaders() ObjectHeaders { + return ObjectHeaders{make(Headers)} } //Validate returns MalformedHeaderError if the value of any well-known header @@ -360,57 +296,57 @@ func (h ObjectHeaders) Validate() error { //ContentDisposition provides type-safe access to Content-Disposition headers. func (h ObjectHeaders) ContentDisposition() FieldString { - return FieldString{h, "Content-Disposition"} + return FieldString{h.Headers, "Content-Disposition"} } //ContentEncoding provides type-safe access to Content-Encoding headers. func (h ObjectHeaders) ContentEncoding() FieldString { - return FieldString{h, "Content-Encoding"} + return FieldString{h.Headers, "Content-Encoding"} } //SizeBytes provides type-safe access to Content-Length headers. func (h ObjectHeaders) SizeBytes() FieldUint64 { - return FieldUint64{h, "Content-Length"} + return FieldUint64{h.Headers, "Content-Length"} } //ContentType provides type-safe access to Content-Type headers. func (h ObjectHeaders) ContentType() FieldString { - return FieldString{h, "Content-Type"} + return FieldString{h.Headers, "Content-Type"} } //Etag provides type-safe access to Etag headers. func (h ObjectHeaders) Etag() FieldString { - return FieldString{h, "Etag"} + return FieldString{h.Headers, "Etag"} } //UpdatedAt provides type-safe access to Last-Modified headers. func (h ObjectHeaders) UpdatedAt() FieldHTTPTimeReadonly { - return FieldHTTPTimeReadonly{h, "Last-Modified"} + return FieldHTTPTimeReadonly{h.Headers, "Last-Modified"} } //ExpiresAt provides type-safe access to X-Delete-At headers. func (h ObjectHeaders) ExpiresAt() FieldUnixTime { - return FieldUnixTime{h, "X-Delete-At"} + return FieldUnixTime{h.Headers, "X-Delete-At"} } //Metadata provides type-safe access to X-Object-Meta- headers. func (h ObjectHeaders) Metadata() FieldMetadata { - return FieldMetadata{h, "X-Object-Meta-"} + return FieldMetadata{h.Headers, "X-Object-Meta-"} } //SymlinkTargetAccount provides type-safe access to X-Symlink-Target-Account headers. func (h ObjectHeaders) SymlinkTargetAccount() FieldString { - return FieldString{h, "X-Symlink-Target-Account"} + return FieldString{h.Headers, "X-Symlink-Target-Account"} } //SymlinkTarget provides type-safe access to X-Symlink-Target headers. func (h ObjectHeaders) SymlinkTarget() FieldString { - return FieldString{h, "X-Symlink-Target"} + return FieldString{h.Headers, "X-Symlink-Target"} } //CreatedAt provides type-safe access to X-Timestamp headers. func (h ObjectHeaders) CreatedAt() FieldUnixTimeReadonly { - return FieldUnixTimeReadonly{h, "X-Timestamp"} + return FieldUnixTimeReadonly{h.Headers, "X-Timestamp"} } func evadeGolintComplaint1() error { diff --git a/generated.go.in b/generated.go.in index 85c257a..2858bcb 100644 --- a/generated.go.in +++ b/generated.go.in @@ -57,54 +57,20 @@ package schwift -import "net/textproto" - {{- range $htype, $hmeta := . }} //{{$htype}}Headers contains the headers for a schwift.{{$htype}} instance. // //To read and write well-known headers, use the methods on this type. -//To read and write arbitary headers, use the http.Header-like methods Get(), -//Set(), Clear(), Del(). -type {{$htype}}Headers map[string]string - -//Clear sets the value for the specified header to the empty string. When the -//Headers instance is then sent to the server with Update(), the server will -//delete the value for that header; cf. Del(). -func (h {{$htype}}Headers) Clear(key string) { - h[textproto.CanonicalMIMEHeaderKey(key)] = "" -} -{{/* */}} -{{- if eq $htype "Object" }} -//Del deletes a key from the Headers instance. When the Headers instance -//is then sent to the server with Update(), Del() has different effects -//depending on context because of Swift's inconsistent API: -// -//For most writable attributes, a key which has been deleted with Del() will -//remain unchanged on the server. To remove the key on the server, use Clear() -//instead. -// -//For object metadata (but not other object attributes), deleting a key will -//cause that key to be deleted on the server. Del() is identical to Clear() in -//this case. -{{- else }} -//Del deletes a key from the Headers instance. When the Headers instance is -//then sent to the server with Update(), a key which has been deleted with -//Del() will remain unchanged on the server. -{{- end }} -func (h {{$htype}}Headers) Del(key string) { - delete(h, textproto.CanonicalMIMEHeaderKey(key)) -} - -//Get returns the value for the specified header. -func (h {{$htype}}Headers) Get(key string) string { - return h[textproto.CanonicalMIMEHeaderKey(key)] +//To read and write arbitary headers, use the methods on the Headers supertype. +type {{$htype}}Headers struct { + Headers } -//Set sets a new value for the specified header. Any existing value will be -//overwritten. -func (h {{$htype}}Headers) Set(key, value string) { - h[textproto.CanonicalMIMEHeaderKey(key)] = value +//New{{$htype}}Headers creates a new {{$htype}}Headers instance. The return value +//will have the Headers attribute initialized to a non-nil map. +func New{{$htype}}Headers() {{$htype}}Headers { + return {{$htype}}Headers{make(Headers)} } //Validate returns MalformedHeaderError if the value of any well-known header @@ -125,7 +91,7 @@ func (h {{$htype}}Headers) Validate() error { //{{$field.Attribute}} provides type-safe access to {{$field.Header}} headers. func (h {{$htype}}Headers) {{$field.Attribute}}() Field{{$field.Type}} { - return Field{{$field.Type}}{h, "{{$field.Header}}"} + return Field{{$field.Type}}{h.Headers, "{{$field.Header}}"} } {{- end }} {{- end }} @@ -23,7 +23,56 @@ import ( "net/textproto" ) -func headersToHTTP(h map[string]string) http.Header { +//Headers represents a set of request headers or response headers. +// +//Users will typically use one of the subtypes (AccountHeaders, +//ContainerHeaders, ObjectHeaders) instead, which provide type-safe access to +//well-known headers. The http.Header-like interface on this type can be used +//read and write arbitary headers. For example, the following calls are +//equivalent: +// +// h := make(AccountHeaders) +// h.Headers.Set("X-Account-Meta-Quota-Bytes", "1048576") +// h.BytesUsedQuota().Set(1048576) +// +type Headers map[string]string + +//Clear sets the value for the specified header to the empty string. When the +//Headers instance is then sent to the server with Update(), the server will +//delete the value for that header; cf. Del(). +func (h Headers) Clear(key string) { + h[textproto.CanonicalMIMEHeaderKey(key)] = "" +} + +//Del deletes a key from the Headers instance. When the Headers instance is +//then sent to the server with Update(), a key which has been deleted with +//Del() will remain unchanged on the server. +// +//For most writable attributes, a key which has been deleted with Del() will +//remain unchanged on the server. To remove the key on the server, use Clear() +//instead. +// +//For object metadata (but not other object attributes), deleting a key will +//cause that key to be deleted on the server. Del() is identical to Clear() in +//this case. +func (h Headers) Del(key string) { + delete(h, textproto.CanonicalMIMEHeaderKey(key)) +} + +//Get returns the value for the specified header. +func (h Headers) Get(key string) string { + return h[textproto.CanonicalMIMEHeaderKey(key)] +} + +//Set sets a new value for the specified header. Any existing value will be +//overwritten. +func (h Headers) Set(key, value string) { + h[textproto.CanonicalMIMEHeaderKey(key)] = value +} + +//ToHTTP converts this Headers instance into the equivalent http.Header +//instance. The return value is guaranteed to be non-nil. +func (h Headers) ToHTTP() http.Header { dest := make(http.Header, len(h)) for k, v := range h { dest.Set(k, v) @@ -31,19 +80,24 @@ func headersToHTTP(h map[string]string) http.Header { return dest } -func headersFromHTTP(src http.Header) map[string]string { - h := make(map[string]string, len(src)) +//ToOpts wraps this Headers instance into a RequestOpts instance, so that it +//can be passed to Schwift's various request methods. +// +// hdr := NewObjectHeaders() +// hdr.ContentType().Set("image/png") +// hdr.Metadata().Set("color", "blue") +// obj.Upload(content, hdr.ToOpts()) +// +func (h Headers) ToOpts() *RequestOptions { + return &RequestOptions{Headers: h} +} + +func headersFromHTTP(src http.Header) Headers { + h := make(Headers, len(src)) for k, v := range src { if len(v) > 0 { - h[textproto.CanonicalMIMEHeaderKey(k)] = v[0] + h.Set(k, v[0]) } } return h } - -type headerInterface interface { - Clear(string) - Del(string) - Get(string) string - Set(string, string) -} diff --git a/iterator.go b/iterator.go index e460edd..04fb492 100644 --- a/iterator.go +++ b/iterator.go @@ -31,21 +31,19 @@ type iteratorInterface interface { getAccount() *Account getContainerName() string getPrefix() string - getHeaders() map[string]string getOptions() *RequestOptions //putHeader initializes the AccountHeaders/ContainerHeaders field of the //Account/Container using the response headers from the GET request. putHeader(http.Header) error } -func (i ContainerIterator) getAccount() *Account { return i.Account } -func (i ContainerIterator) getContainerName() string { return "" } -func (i ContainerIterator) getPrefix() string { return i.Prefix } -func (i ContainerIterator) getHeaders() map[string]string { return i.Headers } -func (i ContainerIterator) getOptions() *RequestOptions { return i.Options } +func (i ContainerIterator) getAccount() *Account { return i.Account } +func (i ContainerIterator) getContainerName() string { return "" } +func (i ContainerIterator) getPrefix() string { return i.Prefix } +func (i ContainerIterator) getOptions() *RequestOptions { return i.Options } func (i ContainerIterator) putHeader(hdr http.Header) error { - headers := AccountHeaders(headersFromHTTP(hdr)) + headers := AccountHeaders{headersFromHTTP(hdr)} if err := headers.Validate(); err != nil { return err } @@ -53,14 +51,13 @@ func (i ContainerIterator) putHeader(hdr http.Header) error { return nil } -func (i ObjectIterator) getAccount() *Account { return i.Container.Account() } -func (i ObjectIterator) getContainerName() string { return i.Container.Name() } -func (i ObjectIterator) getPrefix() string { return i.Prefix } -func (i ObjectIterator) getHeaders() map[string]string { return i.Headers } -func (i ObjectIterator) getOptions() *RequestOptions { return i.Options } +func (i ObjectIterator) getAccount() *Account { return i.Container.Account() } +func (i ObjectIterator) getContainerName() string { return i.Container.Name() } +func (i ObjectIterator) getPrefix() string { return i.Prefix } +func (i ObjectIterator) getOptions() *RequestOptions { return i.Options } func (i ObjectIterator) putHeader(hdr http.Header) error { - headers := ContainerHeaders(headersFromHTTP(hdr)) + headers := ContainerHeaders{headersFromHTTP(hdr)} if err := headers.Validate(); err != nil { return err } @@ -79,8 +76,7 @@ func (b *iteratorBase) request(limit int, detailed bool) Request { r := Request{ Method: "GET", ContainerName: b.i.getContainerName(), - Headers: headersToHTTP(b.i.getHeaders()), - Options: cloneRequestOptions(b.i.getOptions()), + Options: cloneRequestOptions(b.i.getOptions(), nil), } if prefix := b.i.getPrefix(); prefix != "" { @@ -100,11 +96,11 @@ func (b *iteratorBase) request(limit int, detailed bool) Request { } if detailed { - r.Headers.Set("Accept", "application/json") + r.Options.Headers.Set("Accept", "application/json") r.Options.Values.Set("format", "json") r.ExpectStatusCodes = []int{200} } else { - r.Headers.Set("Accept", "text/plain") + r.Options.Headers.Set("Accept", "text/plain") r.Options.Values.Set("format", "plain") r.ExpectStatusCodes = []int{200, 204} } @@ -104,7 +104,7 @@ func (o *Object) Headers() (ObjectHeaders, error) { return ObjectHeaders{}, err } - headers := ObjectHeaders(headersFromHTTP(resp.Header)) + headers := ObjectHeaders{headersFromHTTP(resp.Header)} err = headers.Validate() if err != nil { return headers, err @@ -124,8 +124,7 @@ func (o *Object) Update(headers ObjectHeaders, opts *RequestOptions) error { Method: "POST", ContainerName: o.c.name, ObjectName: o.name, - Headers: headersToHTTP(headers), - Options: opts, + Options: cloneRequestOptions(opts, headers.Headers), ExpectStatusCodes: []int{202}, }.Do(o.c.a.backend) if err == nil { @@ -134,18 +133,17 @@ func (o *Object) Update(headers ObjectHeaders, opts *RequestOptions) error { return err } -//Upload creates the object using a PUT request. To add URL parameters, pass -//a non-nil *RequestOptions. +//Upload creates the object using a PUT request. // //If you do not have an io.Reader, but you have a []byte or string instance //containing the object, wrap it in a *bytes.Reader instance like so: // // var buffer []byte -// o.Upload(bytes.NewReader(buffer), headers, opts) +// o.Upload(bytes.NewReader(buffer), opts) // // //or... // var buffer string -// o.Upload(bytes.NewReader([]byte(buffer)), headers, opts) +// o.Upload(bytes.NewReader([]byte(buffer)), opts) // //If you have neither an io.Reader nor a []byte or string, but you have a //function that generates the object's content into an io.Writer, use @@ -166,16 +164,15 @@ func (o *Object) Update(headers ObjectHeaders, opts *RequestOptions) error { //This function can be used regardless of whether the object exists or not. // //A successful PUT request implies Invalidate() since it may change metadata. -func (o *Object) Upload(content io.Reader, headers ObjectHeaders, opts *RequestOptions) error { - if headers == nil { - headers = make(ObjectHeaders) - } - tryComputeContentLength(content, headers) - tryComputeEtag(content, headers) +func (o *Object) Upload(content io.Reader, opts *RequestOptions) error { + opts = cloneRequestOptions(opts, nil) + hdr := ObjectHeaders{opts.Headers} + tryComputeContentLength(content, hdr) + tryComputeEtag(content, hdr) //could not compute Etag in advance -> need to check on the fly var hasher hash.Hash - if !headers.Etag().Exists() { + if !hdr.Etag().Exists() { hasher = md5.New() if content != nil { content = io.TeeReader(content, hasher) @@ -186,7 +183,6 @@ func (o *Object) Upload(content io.Reader, headers ObjectHeaders, opts *RequestO Method: "PUT", ContainerName: o.c.name, ObjectName: o.name, - Headers: headersToHTTP(headers), Options: opts, Body: content, ExpectStatusCodes: []int{201}, @@ -253,7 +249,7 @@ func tryComputeEtag(content io.Reader, headers ObjectHeaders) { // } // // obj := container.Object("greeting-for-susan-and-jeffrey") -// err := obj.UploadWithWriter(nil, nil, func(w io.Writer) error { +// err := obj.UploadWithWriter(nil, func(w io.Writer) error { // err := greeting(w, "Susan") // if err == nil { // err = greeting(w, "Jeffrey") @@ -262,11 +258,11 @@ func tryComputeEtag(content io.Reader, headers ObjectHeaders) { // }) // //If you do not need an io.Writer, always use Upload instead. -func (o *Object) UploadWithWriter(headers ObjectHeaders, opts *RequestOptions, callback func(io.Writer) error) error { +func (o *Object) UploadWithWriter(opts *RequestOptions, callback func(io.Writer) error) error { reader, writer := io.Pipe() errChan := make(chan error) go func() { - err := o.Upload(reader, headers, opts) + err := o.Upload(reader, opts) reader.CloseWithError(err) //stop the writer if it is still writing errChan <- err }() @@ -280,12 +276,11 @@ func (o *Object) UploadWithWriter(headers ObjectHeaders, opts *RequestOptions, c //This operation fails with http.StatusNotFound if the object does not exist. // //A successful DELETE request implies Invalidate(). -func (o *Object) Delete(headers ObjectHeaders, opts *RequestOptions) error { +func (o *Object) Delete(opts *RequestOptions) error { _, err := Request{ Method: "DELETE", ContainerName: o.c.name, ObjectName: o.name, - Headers: headersToHTTP(headers), Options: opts, ExpectStatusCodes: []int{204}, }.Do(o.c.a.backend) @@ -313,18 +308,17 @@ func (o *Object) Invalidate() { // str, err := object.Download(nil, nil).AsString() // //See struct DownloadedObject for details. -func (o *Object) Download(headers ObjectHeaders, opts *RequestOptions) DownloadedObject { +func (o *Object) Download(opts *RequestOptions) DownloadedObject { resp, err := Request{ Method: "GET", ContainerName: o.c.name, ObjectName: o.name, - Headers: headersToHTTP(headers), Options: opts, ExpectStatusCodes: []int{200}, }.Do(o.c.a.backend) var body io.ReadCloser if err == nil { - newHeaders := ObjectHeaders(headersFromHTTP(resp.Header)) + newHeaders := ObjectHeaders{headersFromHTTP(resp.Header)} err = newHeaders.Validate() if err == nil { o.headers = &newHeaders @@ -347,18 +341,17 @@ func (o *Object) Download(headers ObjectHeaders, opts *RequestOptions) Downloade // //A successful COPY implies target.Invalidate() since it may change the //target's metadata. -func (o *Object) CopyTo(target *Object, headers ObjectHeaders, opts *RequestOptions) error { - hdr := headersToHTTP(headers) - hdr.Set("Destination", target.FullName()) +func (o *Object) CopyTo(target *Object, opts *RequestOptions) error { + opts = cloneRequestOptions(opts, nil) + opts.Headers.Set("Destination", target.FullName()) if o.c.a.name != target.c.a.name { - hdr.Set("Destination-Account", target.c.a.name) + opts.Headers.Set("Destination-Account", target.c.a.name) } _, err := Request{ Method: "COPY", ContainerName: o.c.name, ObjectName: o.name, - Headers: hdr, Options: opts, ExpectStatusCodes: []int{201}, DrainResponseBody: true, @@ -373,10 +366,10 @@ func (o *Object) CopyTo(target *Object, headers ObjectHeaders, opts *RequestOpti //a DELETE request on the source object. // //A successful move implies Invalidate() on both the source and target side. -func (o *Object) MoveTo(target *Object, headers ObjectHeaders, opts *RequestOptions) error { - err := o.CopyTo(target, headers, opts) +func (o *Object) MoveTo(target *Object, copyOpts *RequestOptions, deleteOpts *RequestOptions) error { + err := o.CopyTo(target, copyOpts) if err != nil { return err } - return o.Delete(nil, nil) + return o.Delete(deleteOpts) } diff --git a/object_iterator.go b/object_iterator.go index 6d93ccf..3df03dd 100644 --- a/object_iterator.go +++ b/object_iterator.go @@ -65,12 +65,10 @@ type ObjectIterator struct { //When Prefix is set, only objects whose name starts with this string are //returned. Prefix string - //Headers may contain additional headers to include with the GET request. - Headers map[string]string - //Options may contain additional query parameters for the GET request. + //Options may contain additional headers and query parameters for the GET request. Options *RequestOptions - //TODO: Delimter field (and check if other stuff is missing) + //TODO: Delimiter field (and check if other stuff is missing) base *iteratorBase } @@ -26,20 +26,39 @@ import ( "strings" ) -//RequestOptions contains additional headers and values for a request. +//RequestOptions is used to pass additional headers and values to aa request. +// +//When preparing a RequestOptions instance with additional headers, the +//preferred way is to create an AccountHeaders, ContainerHeaders and +//ObjectHeaders instance and use the type-safe API on these types. Then use the +//ToOpts() method on that instance. For example: +// +// hdr := NewObjectHeaders() +// hdr.ContentType().Set("image/png") +// hdr.Metadata().Set("color", "blue") +// opts := hdr.ToOpts() //type *schwift.RequestOptions +// type RequestOptions struct { - Values url.Values + Headers Headers + Values url.Values } -func cloneRequestOptions(orig *RequestOptions) *RequestOptions { +func cloneRequestOptions(orig *RequestOptions, additional Headers) *RequestOptions { result := RequestOptions{ - Values: make(url.Values), + Headers: make(Headers), + Values: make(url.Values), } if orig != nil { + for k, v := range orig.Headers { + result.Headers[k] = v + } for k, v := range orig.Values { result.Values[k] = v } } + for k, v := range additional { + result.Headers[k] = v + } return &result } @@ -48,7 +67,6 @@ type Request struct { Method string //"GET", "HEAD", "PUT", "POST" or "DELETE" ContainerName string //empty for requests on accounts ObjectName string //empty for requests on accounts/containers - Headers http.Header Options *RequestOptions Body io.Reader //ExpectStatusCodes can be left empty to disable this check, otherwise @@ -102,8 +120,10 @@ func (r Request) Do(backend Backend) (*http.Response, error) { return nil, err } - for k, v := range r.Headers { - req.Header[k] = v + if r.Options != nil { + for k, v := range r.Options.Headers { + req.Header[k] = []string{v} + } } if r.Body != nil { req.Header.Set("Expect", "100-continue") diff --git a/tests/account_test.go b/tests/account_test.go index 310d4ce..2885bfa 100644 --- a/tests/account_test.go +++ b/tests/account_test.go @@ -41,7 +41,7 @@ func TestAccountBasic(t *testing.T) { func TestAccountMetadata(t *testing.T) { testWithAccount(t, func(a *schwift.Account) { //test creating some metadata - hdr := make(schwift.AccountHeaders) + hdr := schwift.NewAccountHeaders() hdr.Metadata().Set("schwift-test1", "first") hdr.Metadata().Set("schwift-test2", "second") err := a.Update(hdr, nil) @@ -57,7 +57,7 @@ func TestAccountMetadata(t *testing.T) { expectString(t, hdr.Metadata().Get("schwift-test2"), "second") //test deleting some metadata - hdr = make(schwift.AccountHeaders) + hdr = schwift.NewAccountHeaders() hdr.Metadata().Clear("schwift-test1") err = a.Update(hdr, nil) if !expectSuccess(t, err) { @@ -72,7 +72,7 @@ func TestAccountMetadata(t *testing.T) { expectString(t, hdr.Metadata().Get("schwift-test2"), "second") //test updating some metadata - hdr = make(schwift.AccountHeaders) + hdr = schwift.NewAccountHeaders() hdr.Metadata().Set("schwift-test1", "will not be set") hdr.Metadata().Del("schwift-test1") hdr.Metadata().Set("schwift-test2", "changed") diff --git a/tests/bulk_delete_test.go b/tests/bulk_delete_test.go index 52294ba..c498419 100644 --- a/tests/bulk_delete_test.go +++ b/tests/bulk_delete_test.go @@ -33,13 +33,13 @@ func TestBulkDeleteSuccess(t *testing.T) { objs, err := createTestObjects(c) expectSuccess(t, err) - numDeleted, numNotFound, err := c.Account().BulkDelete(objs, nil, nil, nil) + numDeleted, numNotFound, err := c.Account().BulkDelete(objs, nil, nil) expectSuccess(t, err) expectInt(t, numDeleted, len(objs)) expectInt(t, numNotFound, 0) expectContainerExistence(t, c, true) - numDeleted, numNotFound, err = c.Account().BulkDelete(objs, nil, nil, nil) + numDeleted, numNotFound, err = c.Account().BulkDelete(objs, nil, nil) expectSuccess(t, err) expectInt(t, numDeleted, 0) expectInt(t, numNotFound, len(objs)) @@ -49,7 +49,7 @@ func TestBulkDeleteSuccess(t *testing.T) { expectSuccess(t, err) cs := []*schwift.Container{c} - numDeleted, numNotFound, err = c.Account().BulkDelete(objs, cs, nil, nil) + numDeleted, numNotFound, err = c.Account().BulkDelete(objs, cs, nil) expectSuccess(t, err) expectInt(t, numDeleted, len(objs)+1) expectInt(t, numNotFound, 0) @@ -65,7 +65,7 @@ func TestBulkDeleteError(t *testing.T) { cs := []*schwift.Container{c} //not deleting all objects should lead to 409 Conflict when deleting the Container - numDeleted, numNotFound, err := c.Account().BulkDelete(objs, cs, nil, nil) + numDeleted, numNotFound, err := c.Account().BulkDelete(objs, cs, nil) expectInt(t, numDeleted, len(objs)) expectInt(t, numNotFound, 0) expectError(t, err, "400 Bad Request (+1 object errors)") @@ -77,7 +77,7 @@ func createTestObjects(c *schwift.Container) ([]*schwift.Object, error) { var objs []*schwift.Object for idx := 1; idx <= 5; idx++ { obj := c.Object(fmt.Sprintf("object%d", idx)) - err := obj.Upload(strings.NewReader("example"), nil, nil) + err := obj.Upload(strings.NewReader("example"), nil) if err != nil { return nil, err } diff --git a/tests/bulk_upload_test.go b/tests/bulk_upload_test.go index 68cb791..6cbae42 100644 --- a/tests/bulk_upload_test.go +++ b/tests/bulk_upload_test.go @@ -40,7 +40,7 @@ func TestBulkUploadSuccess(t *testing.T) { "", //upload path schwift.BulkUploadTar, bytes.NewReader(archive), - nil, nil, + nil, ) expectInt(t, n, 2) expectSuccess(t, err) @@ -58,7 +58,7 @@ func TestBulkUploadArchiveError(t *testing.T) { c.Name(), //upload path schwift.BulkUploadTar, strings.NewReader("This is not the TAR archive you're looking for."), - nil, nil, + nil, ) expectInt(t, n, 0) expectError(t, err, "400 Bad Request: Invalid Tar File: truncated header") @@ -83,7 +83,7 @@ func TestBulkUploadObjectError(t *testing.T) { c.Name(), //upload path schwift.BulkUploadTar, bytes.NewReader(archive), - nil, nil, + nil, ) expectInt(t, n, 1) expectError(t, err, "400 Bad Request (+1 object errors)") diff --git a/tests/container_iterator_test.go b/tests/container_iterator_test.go index b9813db..d682a59 100644 --- a/tests/container_iterator_test.go +++ b/tests/container_iterator_test.go @@ -126,7 +126,7 @@ func TestContainerIterator(t *testing.T) { iter = a.Containers() iter.Prefix = "schwift-test-listing" expectSuccess(t, iter.Foreach(func(c *schwift.Container) error { - return c.Delete(nil, nil) + return c.Delete(nil) })) }) } diff --git a/tests/container_test.go b/tests/container_test.go index c90a339..0efc4e6 100644 --- a/tests/container_test.go +++ b/tests/container_test.go @@ -46,17 +46,17 @@ func TestContainerLifecycle(t *testing.T) { //DELETE should be idempotent and not return success on non-existence, but //OpenStack LOVES to be inconsistent with everything (including, notably, itself) - err = c.Delete(nil, nil) + err = c.Delete(nil) expectError(t, err, "expected 204 response, got 404 instead: <html><h1>Not Found</h1><p>The resource could not be found.</p></html>") - err = c.Create(nil, nil) + err = c.Create(nil) expectSuccess(t, err) exists, err = c.Exists() expectSuccess(t, err) expectBool(t, exists, true) - err = c.Delete(nil, nil) + err = c.Delete(nil) expectSuccess(t, err) }) } @@ -69,7 +69,7 @@ func TestContainerUpdate(t *testing.T) { expectBool(t, hdr.ObjectCount().Exists(), true) expectUint64(t, hdr.ObjectCount().Get(), 0) - hdr = make(schwift.ContainerHeaders) + hdr = schwift.NewContainerHeaders() hdr.ObjectCountQuota().Set(23) hdr.BytesUsedQuota().Set(42) diff --git a/tests/field_test.go b/tests/field_test.go index f3c03ca..8166f2d 100644 --- a/tests/field_test.go +++ b/tests/field_test.go @@ -27,33 +27,33 @@ import ( ) func TestFieldString(t *testing.T) { - hdr := make(schwift.AccountHeaders) + hdr := schwift.NewAccountHeaders() expectBool(t, hdr.TempURLKey().Exists(), false) expectString(t, hdr.TempURLKey().Get(), "") expectSuccess(t, hdr.Validate()) - hdr["X-Account-Meta-Temp-Url-Key"] = "" + hdr.Headers["X-Account-Meta-Temp-Url-Key"] = "" expectBool(t, hdr.TempURLKey().Exists(), false) expectString(t, hdr.TempURLKey().Get(), "") expectSuccess(t, hdr.Validate()) - hdr["X-Account-Meta-Temp-Url-Key"] = "foo" + hdr.Headers["X-Account-Meta-Temp-Url-Key"] = "foo" expectBool(t, hdr.TempURLKey().Exists(), true) expectString(t, hdr.TempURLKey().Get(), "foo") expectSuccess(t, hdr.Validate()) hdr.TempURLKey().Set("bar") - expectHeaders(t, hdr, map[string]string{ + expectHeaders(t, hdr.Headers, map[string]string{ "X-Account-Meta-Temp-Url-Key": "bar", }) hdr.TempURLKey().Clear() - expectHeaders(t, hdr, map[string]string{ + expectHeaders(t, hdr.Headers, map[string]string{ "X-Account-Meta-Temp-Url-Key": "", }) hdr.TempURLKey().Del() - expectHeaders(t, hdr, nil) + expectHeaders(t, hdr.Headers, nil) hdr.TempURLKey().Clear() - expectHeaders(t, hdr, map[string]string{ + expectHeaders(t, hdr.Headers, map[string]string{ "X-Account-Meta-Temp-Url-Key": "", }) } @@ -70,16 +70,16 @@ func TestFieldTimestamp(t *testing.T) { expectBool(t, hdr.CreatedAt().Exists(), true) actual := float64(hdr.CreatedAt().Get().UnixNano()) / 1e9 - expected, _ := strconv.ParseFloat(hdr["X-Timestamp"], 64) + expected, _ := strconv.ParseFloat(hdr.Headers["X-Timestamp"], 64) expectFloat64(t, actual, expected) }) - hdr := make(schwift.AccountHeaders) + hdr := schwift.NewAccountHeaders() expectBool(t, hdr.CreatedAt().Exists(), false) expectBool(t, hdr.CreatedAt().Get().IsZero(), true) expectSuccess(t, hdr.Validate()) - hdr["X-Timestamp"] = "wtf" + hdr.Headers["X-Timestamp"] = "wtf" expectBool(t, hdr.CreatedAt().Exists(), true) expectBool(t, hdr.CreatedAt().Get().IsZero(), true) expectError(t, hdr.Validate(), `Bad header X-Timestamp: strconv.ParseFloat: parsing "wtf": invalid syntax`) @@ -88,7 +88,7 @@ func TestFieldTimestamp(t *testing.T) { func TestFieldHTTPTimestamp(t *testing.T) { testWithContainer(t, func(c *schwift.Container) { obj := c.Object("test") - err := obj.Upload(nil, nil, nil) + err := obj.Upload(nil, nil) if !expectSuccess(t, err) { return } @@ -104,12 +104,12 @@ func TestFieldHTTPTimestamp(t *testing.T) { expectInt64(t, actual.Unix(), expected.Unix()) }) - hdr := make(schwift.ObjectHeaders) + hdr := schwift.NewObjectHeaders() expectBool(t, hdr.UpdatedAt().Exists(), false) expectBool(t, hdr.UpdatedAt().Get().IsZero(), true) expectSuccess(t, hdr.Validate()) - hdr["Last-Modified"] = "wtf" + hdr.Headers["Last-Modified"] = "wtf" expectBool(t, hdr.UpdatedAt().Exists(), true) expectBool(t, hdr.UpdatedAt().Get().IsZero(), true) expectError(t, hdr.Validate(), `Bad header Last-Modified: parsing time "wtf" as "Mon Jan _2 15:04:05 2006": cannot parse "wtf" as "Mon"`) @@ -118,49 +118,49 @@ func TestFieldHTTPTimestamp(t *testing.T) { //////////////////////////////////////////////////////////////////////////////// func TestFieldUint64(t *testing.T) { - hdr := make(schwift.AccountHeaders) + hdr := schwift.NewAccountHeaders() expectBool(t, hdr.BytesUsedQuota().Exists(), false) expectUint64(t, hdr.BytesUsedQuota().Get(), 0) expectSuccess(t, hdr.Validate()) - hdr["X-Account-Meta-Quota-Bytes"] = "23" + hdr.Headers["X-Account-Meta-Quota-Bytes"] = "23" expectBool(t, hdr.BytesUsedQuota().Exists(), true) expectUint64(t, hdr.BytesUsedQuota().Get(), 23) expectSuccess(t, hdr.Validate()) - hdr["X-Account-Meta-Quota-Bytes"] = "-23" + hdr.Headers["X-Account-Meta-Quota-Bytes"] = "-23" expectBool(t, hdr.BytesUsedQuota().Exists(), true) expectUint64(t, hdr.BytesUsedQuota().Get(), 0) expectError(t, hdr.Validate(), `Bad header X-Account-Meta-Quota-Bytes: strconv.ParseUint: parsing "-23": invalid syntax`) hdr.BytesUsedQuota().Set(9001) - expectHeaders(t, hdr, map[string]string{ + expectHeaders(t, hdr.Headers, map[string]string{ "X-Account-Meta-Quota-Bytes": "9001", }) hdr.BytesUsedQuota().Clear() - expectHeaders(t, hdr, map[string]string{ + expectHeaders(t, hdr.Headers, map[string]string{ "X-Account-Meta-Quota-Bytes": "", }) hdr.BytesUsedQuota().Del() - expectHeaders(t, hdr, nil) + expectHeaders(t, hdr.Headers, nil) hdr.BytesUsedQuota().Clear() - expectHeaders(t, hdr, map[string]string{ + expectHeaders(t, hdr.Headers, map[string]string{ "X-Account-Meta-Quota-Bytes": "", }) } func TestFieldUint64Readonly(t *testing.T) { - hdr := make(schwift.AccountHeaders) + hdr := schwift.NewAccountHeaders() expectBool(t, hdr.BytesUsed().Exists(), false) expectUint64(t, hdr.BytesUsed().Get(), 0) expectSuccess(t, hdr.Validate()) - hdr["X-Account-Bytes-Used"] = "23" + hdr.Headers["X-Account-Bytes-Used"] = "23" expectBool(t, hdr.BytesUsed().Exists(), true) expectUint64(t, hdr.BytesUsed().Get(), 23) expectSuccess(t, hdr.Validate()) - hdr["X-Account-Bytes-Used"] = "-23" + hdr.Headers["X-Account-Bytes-Used"] = "-23" expectBool(t, hdr.BytesUsed().Exists(), true) expectUint64(t, hdr.BytesUsed().Get(), 0) expectError(t, hdr.Validate(), `Bad header X-Account-Bytes-Used: strconv.ParseUint: parsing "-23": invalid syntax`) diff --git a/tests/headers_test.go b/tests/headers_test.go index 8067ef4..6b229ae 100644 --- a/tests/headers_test.go +++ b/tests/headers_test.go @@ -26,11 +26,13 @@ import ( func TestParseAccountHeadersSuccess(t *testing.T) { headers := schwift.AccountHeaders{ - "X-Account-Bytes-Used": "1234", - "X-Account-Object-Count": "42", - "X-Account-Container-Count": "23", - "X-Account-Meta-Quota-Bytes": "1048576", - "X-Account-Meta-Foo": "bar", + Headers: schwift.Headers{ + "X-Account-Bytes-Used": "1234", + "X-Account-Object-Count": "42", + "X-Account-Container-Count": "23", + "X-Account-Meta-Quota-Bytes": "1048576", + "X-Account-Meta-Foo": "bar", + }, } expectSuccess(t, headers.Validate()) diff --git a/tests/object_iterator_test.go b/tests/object_iterator_test.go index 5112324..009189d 100644 --- a/tests/object_iterator_test.go +++ b/tests/object_iterator_test.go @@ -37,9 +37,9 @@ func TestObjectIterator(t *testing.T) { //create test objects that can be listed for idx := 1; idx <= 4; idx++ { - hdr := make(schwift.ObjectHeaders) + hdr := schwift.NewObjectHeaders() hdr.ContentType().Set("application/json") - err := c.Object(oname(idx)).Upload(bytes.NewReader(objectExampleContent), hdr, nil) + err := c.Object(oname(idx)).Upload(bytes.NewReader(objectExampleContent), hdr.ToOpts()) expectSuccess(t, err) } diff --git a/tests/object_test.go b/tests/object_test.go index 090fc48..b42294a 100644 --- a/tests/object_test.go +++ b/tests/object_test.go @@ -47,15 +47,15 @@ func TestObjectLifecycle(t *testing.T) { //DELETE should be idempotent and not return success on non-existence, but //OpenStack LOVES to be inconsistent with everything (including, notably, itself) - err = o.Delete(nil, nil) + err = o.Delete(nil) expectError(t, err, "expected 204 response, got 404 instead: <html><h1>Not Found</h1><p>The resource could not be found.</p></html>") - err = o.Upload(bytes.NewReader([]byte("test")), nil, nil) + err = o.Upload(bytes.NewReader([]byte("test")), nil) expectSuccess(t, err) expectObjectExistence(t, o, true) - err = o.Delete(nil, nil) + err = o.Delete(nil) expectSuccess(t, err) }) } @@ -65,25 +65,25 @@ func TestObjectUpload(t *testing.T) { //test upload with bytes.Reader obj := c.Object("upload1") - err := obj.Upload(bytes.NewReader(objectExampleContent), nil, nil) + err := obj.Upload(bytes.NewReader(objectExampleContent), nil) expectSuccess(t, err) expectObjectContent(t, obj, objectExampleContent) //test upload with bytes.Buffer obj = c.Object("upload2") - err = obj.Upload(bytes.NewBuffer(objectExampleContent), nil, nil) + err = obj.Upload(bytes.NewBuffer(objectExampleContent), nil) expectSuccess(t, err) expectObjectContent(t, obj, objectExampleContent) //test upload with opaque io.Reader obj = c.Object("upload3") - err = obj.Upload(opaqueReader{bytes.NewReader(objectExampleContent)}, nil, nil) + err = obj.Upload(opaqueReader{bytes.NewReader(objectExampleContent)}, nil) expectSuccess(t, err) expectObjectContent(t, obj, objectExampleContent) //test upload with io.Writer obj = c.Object("upload4") - err = obj.UploadWithWriter(nil, nil, func(w io.Writer) error { + err = obj.UploadWithWriter(nil, func(w io.Writer) error { _, err := w.Write(objectExampleContent) return err }) @@ -92,13 +92,13 @@ func TestObjectUpload(t *testing.T) { //test upload with empty reader (should create zero-byte-sized object) obj = c.Object("upload5") - err = obj.Upload(eofReader{}, nil, nil) + err = obj.Upload(eofReader{}, nil) expectSuccess(t, err) expectObjectContent(t, obj, nil) //test upload without reader (should create zero-byte-sized object) obj = c.Object("upload6") - err = obj.Upload(nil, nil, nil) + err = obj.Upload(nil, nil) expectSuccess(t, err) expectObjectContent(t, obj, nil) }) @@ -122,21 +122,21 @@ func TestObjectDownload(t *testing.T) { testWithContainer(t, func(c *schwift.Container) { //upload example object obj := c.Object("example") - err := obj.Upload(bytes.NewReader(objectExampleContent), nil, nil) + err := obj.Upload(bytes.NewReader(objectExampleContent), nil) expectSuccess(t, err) //test download as string - str, err := obj.Download(nil, nil).AsString() + str, err := obj.Download(nil).AsString() expectSuccess(t, err) expectString(t, str, string(objectExampleContent)) //test download as byte slice - buf, err := obj.Download(nil, nil).AsByteSlice() + buf, err := obj.Download(nil).AsByteSlice() expectSuccess(t, err) expectString(t, string(buf), string(objectExampleContent)) //test download as io.ReadCloser slice - reader, err := obj.Download(nil, nil).AsReadCloser() + reader, err := obj.Download(nil).AsReadCloser() expectSuccess(t, err) buf = make([]byte, 4) _, err = reader.Read(buf) @@ -156,14 +156,14 @@ func TestObjectUpdate(t *testing.T) { obj := c.Object("example") //test that metadata update fails for non-existing object - newHeaders := make(schwift.ObjectHeaders) + newHeaders := schwift.NewObjectHeaders() newHeaders.ContentType().Set("application/json") err := obj.Update(newHeaders, nil) expectBool(t, schwift.Is(err, http.StatusNotFound), true) expectError(t, err, "expected 202 response, got 404 instead: <html><h1>Not Found</h1><p>The resource could not be found.</p></html>") //create object - err = obj.Upload(nil, nil, nil) + err = obj.Upload(nil, nil) expectSuccess(t, err) hdr, err := obj.Headers() @@ -183,12 +183,12 @@ func TestObjectUpdate(t *testing.T) { func TestObjectCopyMove(t *testing.T) { testWithContainer(t, func(c *schwift.Container) { obj1 := c.Object("location1") - err := obj1.Upload(bytes.NewReader(objectExampleContent), nil, nil) + err := obj1.Upload(bytes.NewReader(objectExampleContent), nil) expectSuccess(t, err) expectObjectExistence(t, obj1, true) obj2 := c.Object("location2") - expectSuccess(t, obj1.CopyTo(obj2, nil, nil)) + expectSuccess(t, obj1.CopyTo(obj2, nil)) expectObjectExistence(t, obj1, true) expectObjectExistence(t, obj2, true) expectObjectContent(t, obj2, objectExampleContent) @@ -212,7 +212,7 @@ func expectObjectExistence(t *testing.T, obj *schwift.Object, expectedExists boo func expectObjectContent(t *testing.T, obj *schwift.Object, expected []byte) { t.Helper() - str, err := obj.Download(nil, nil).AsString() + str, err := obj.Download(nil).AsString() expectSuccess(t, err) expectString(t, str, string(expected)) obj.Invalidate() diff --git a/tests/shared_test.go b/tests/shared_test.go index 187d0b4..c35951f 100644 --- a/tests/shared_test.go +++ b/tests/shared_test.go @@ -99,9 +99,9 @@ func testWithContainer(t *testing.T, testCode func(c *schwift.Container)) { expectSuccess(t, err) if exists { expectSuccess(t, container.Objects().Foreach(func(o *schwift.Object) error { - return o.Delete(nil, nil) + return o.Delete(nil) })) - err = container.Delete(nil, nil) + err = container.Delete(nil) expectSuccess(t, err) } }) |
