github.com/gophercloud/gophercloud@v1.11.0/openstack/objectstorage/v1/objects/requests.go (about)

     1  package objects
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/hmac"
     6  	"crypto/md5"
     7  	"crypto/sha1"
     8  	"crypto/sha256"
     9  	"crypto/sha512"
    10  	"fmt"
    11  	"hash"
    12  	"io"
    13  	"io/ioutil"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/gophercloud/gophercloud"
    18  	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts"
    19  	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers"
    20  	"github.com/gophercloud/gophercloud/pagination"
    21  )
    22  
    23  // ErrTempURLKeyNotFound is an error indicating that the Temp URL key was
    24  // neigther set nor resolved from a container or account metadata.
    25  type ErrTempURLKeyNotFound struct{ gophercloud.ErrMissingInput }
    26  
    27  func (e ErrTempURLKeyNotFound) Error() string {
    28  	return "Unable to obtain the Temp URL key."
    29  }
    30  
    31  // ErrTempURLDigestNotValid is an error indicating that the requested
    32  // cryptographic hash function is not supported.
    33  type ErrTempURLDigestNotValid struct {
    34  	gophercloud.ErrMissingInput
    35  	Digest string
    36  }
    37  
    38  func (e ErrTempURLDigestNotValid) Error() string {
    39  	return fmt.Sprintf("The requested %q digest is not supported.", e.Digest)
    40  }
    41  
    42  // ListOptsBuilder allows extensions to add additional parameters to the List
    43  // request.
    44  type ListOptsBuilder interface {
    45  	ToObjectListParams() (bool, string, error)
    46  }
    47  
    48  // ListOpts is a structure that holds parameters for listing objects.
    49  type ListOpts struct {
    50  	// Full is a true/false value that represents the amount of object information
    51  	// returned. If Full is set to true, then the content-type, number of bytes,
    52  	// hash date last modified, and name are returned. If set to false or not set,
    53  	// then only the object names are returned.
    54  	Full      bool
    55  	Limit     int    `q:"limit"`
    56  	Marker    string `q:"marker"`
    57  	EndMarker string `q:"end_marker"`
    58  	Format    string `q:"format"`
    59  	Prefix    string `q:"prefix"`
    60  	Delimiter string `q:"delimiter"`
    61  	Path      string `q:"path"`
    62  	Versions  bool   `q:"versions"`
    63  }
    64  
    65  // ToObjectListParams formats a ListOpts into a query string and boolean
    66  // representing whether to list complete information for each object.
    67  func (opts ListOpts) ToObjectListParams() (bool, string, error) {
    68  	q, err := gophercloud.BuildQueryString(opts)
    69  	return opts.Full, q.String(), err
    70  }
    71  
    72  // List is a function that retrieves all objects in a container. It also returns
    73  // the details for the container. To extract only the object information or names,
    74  // pass the ListResult response to the ExtractInfo or ExtractNames function,
    75  // respectively.
    76  func List(c *gophercloud.ServiceClient, containerName string, opts ListOptsBuilder) pagination.Pager {
    77  	url, err := listURL(c, containerName)
    78  	if err != nil {
    79  		return pagination.Pager{Err: err}
    80  	}
    81  
    82  	headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
    83  	if opts != nil {
    84  		full, query, err := opts.ToObjectListParams()
    85  		if err != nil {
    86  			return pagination.Pager{Err: err}
    87  		}
    88  		url += query
    89  
    90  		if full {
    91  			headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"}
    92  		}
    93  	}
    94  
    95  	pager := pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
    96  		p := ObjectPage{pagination.MarkerPageBase{PageResult: r}}
    97  		p.MarkerPageBase.Owner = p
    98  		return p
    99  	})
   100  	pager.Headers = headers
   101  	return pager
   102  }
   103  
   104  // DownloadOptsBuilder allows extensions to add additional parameters to the
   105  // Download request.
   106  type DownloadOptsBuilder interface {
   107  	ToObjectDownloadParams() (map[string]string, string, error)
   108  }
   109  
   110  // DownloadOpts is a structure that holds parameters for downloading an object.
   111  type DownloadOpts struct {
   112  	IfMatch           string    `h:"If-Match"`
   113  	IfModifiedSince   time.Time `h:"If-Modified-Since"`
   114  	IfNoneMatch       string    `h:"If-None-Match"`
   115  	IfUnmodifiedSince time.Time `h:"If-Unmodified-Since"`
   116  	Newest            bool      `h:"X-Newest"`
   117  	Range             string    `h:"Range"`
   118  	Expires           string    `q:"expires"`
   119  	MultipartManifest string    `q:"multipart-manifest"`
   120  	Signature         string    `q:"signature"`
   121  	ObjectVersionID   string    `q:"version-id"`
   122  }
   123  
   124  // ToObjectDownloadParams formats a DownloadOpts into a query string and map of
   125  // headers.
   126  func (opts DownloadOpts) ToObjectDownloadParams() (map[string]string, string, error) {
   127  	q, err := gophercloud.BuildQueryString(opts)
   128  	if err != nil {
   129  		return nil, "", err
   130  	}
   131  	h, err := gophercloud.BuildHeaders(opts)
   132  	if err != nil {
   133  		return nil, q.String(), err
   134  	}
   135  	if !opts.IfModifiedSince.IsZero() {
   136  		h["If-Modified-Since"] = opts.IfModifiedSince.Format(time.RFC1123)
   137  	}
   138  	if !opts.IfUnmodifiedSince.IsZero() {
   139  		h["If-Unmodified-Since"] = opts.IfUnmodifiedSince.Format(time.RFC1123)
   140  	}
   141  	return h, q.String(), nil
   142  }
   143  
   144  // Download is a function that retrieves the content and metadata for an object.
   145  // To extract just the content, call the DownloadResult method ExtractContent,
   146  // after checking DownloadResult's Err field.
   147  func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts DownloadOptsBuilder) (r DownloadResult) {
   148  	url, err := downloadURL(c, containerName, objectName)
   149  	if err != nil {
   150  		r.Err = err
   151  		return
   152  	}
   153  
   154  	h := make(map[string]string)
   155  	if opts != nil {
   156  		headers, query, err := opts.ToObjectDownloadParams()
   157  		if err != nil {
   158  			r.Err = err
   159  			return
   160  		}
   161  		for k, v := range headers {
   162  			h[k] = v
   163  		}
   164  		url += query
   165  	}
   166  
   167  	resp, err := c.Get(url, nil, &gophercloud.RequestOpts{
   168  		MoreHeaders:      h,
   169  		OkCodes:          []int{200, 206, 304},
   170  		KeepResponseBody: true,
   171  	})
   172  	r.Body, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   173  	return
   174  }
   175  
   176  // CreateOptsBuilder allows extensions to add additional parameters to the
   177  // Create request.
   178  type CreateOptsBuilder interface {
   179  	ToObjectCreateParams() (io.Reader, map[string]string, string, error)
   180  }
   181  
   182  // CreateOpts is a structure that holds parameters for creating an object.
   183  type CreateOpts struct {
   184  	Content            io.Reader
   185  	Metadata           map[string]string
   186  	NoETag             bool
   187  	CacheControl       string `h:"Cache-Control"`
   188  	ContentDisposition string `h:"Content-Disposition"`
   189  	ContentEncoding    string `h:"Content-Encoding"`
   190  	ContentLength      int64  `h:"Content-Length"`
   191  	ContentType        string `h:"Content-Type"`
   192  	CopyFrom           string `h:"X-Copy-From"`
   193  	DeleteAfter        int64  `h:"X-Delete-After"`
   194  	DeleteAt           int64  `h:"X-Delete-At"`
   195  	DetectContentType  string `h:"X-Detect-Content-Type"`
   196  	ETag               string `h:"ETag"`
   197  	IfNoneMatch        string `h:"If-None-Match"`
   198  	ObjectManifest     string `h:"X-Object-Manifest"`
   199  	TransferEncoding   string `h:"Transfer-Encoding"`
   200  	Expires            string `q:"expires"`
   201  	MultipartManifest  string `q:"multipart-manifest"`
   202  	Signature          string `q:"signature"`
   203  }
   204  
   205  // ToObjectCreateParams formats a CreateOpts into a query string and map of
   206  // headers.
   207  func (opts CreateOpts) ToObjectCreateParams() (io.Reader, map[string]string, string, error) {
   208  	q, err := gophercloud.BuildQueryString(opts)
   209  	if err != nil {
   210  		return nil, nil, "", err
   211  	}
   212  	h, err := gophercloud.BuildHeaders(opts)
   213  	if err != nil {
   214  		return nil, nil, "", err
   215  	}
   216  
   217  	for k, v := range opts.Metadata {
   218  		h["X-Object-Meta-"+k] = v
   219  	}
   220  
   221  	if opts.NoETag {
   222  		delete(h, "etag")
   223  		return opts.Content, h, q.String(), nil
   224  	}
   225  
   226  	if h["ETag"] != "" {
   227  		return opts.Content, h, q.String(), nil
   228  	}
   229  
   230  	// When we're dealing with big files an io.ReadSeeker allows us to efficiently calculate
   231  	// the md5 sum. An io.Reader is only readable once which means we have to copy the entire
   232  	// file content into memory first.
   233  	readSeeker, isReadSeeker := opts.Content.(io.ReadSeeker)
   234  	if !isReadSeeker {
   235  		data, err := ioutil.ReadAll(opts.Content)
   236  		if err != nil {
   237  			return nil, nil, "", err
   238  		}
   239  		readSeeker = bytes.NewReader(data)
   240  	}
   241  
   242  	hash := md5.New()
   243  	// io.Copy into md5 is very efficient as it's done in small chunks.
   244  	if _, err := io.Copy(hash, readSeeker); err != nil {
   245  		return nil, nil, "", err
   246  	}
   247  	readSeeker.Seek(0, io.SeekStart)
   248  
   249  	h["ETag"] = fmt.Sprintf("%x", hash.Sum(nil))
   250  
   251  	return readSeeker, h, q.String(), nil
   252  }
   253  
   254  // Create is a function that creates a new object or replaces an existing
   255  // object. If the returned response's ETag header fails to match the local
   256  // checksum, the failed request will automatically be retried up to a maximum
   257  // of 3 times.
   258  func Create(c *gophercloud.ServiceClient, containerName, objectName string, opts CreateOptsBuilder) (r CreateResult) {
   259  	url, err := createURL(c, containerName, objectName)
   260  	if err != nil {
   261  		r.Err = err
   262  		return
   263  	}
   264  	h := make(map[string]string)
   265  	var b io.Reader
   266  	if opts != nil {
   267  		tmpB, headers, query, err := opts.ToObjectCreateParams()
   268  		if err != nil {
   269  			r.Err = err
   270  			return
   271  		}
   272  		for k, v := range headers {
   273  			h[k] = v
   274  		}
   275  		url += query
   276  		b = tmpB
   277  	}
   278  
   279  	resp, err := c.Put(url, b, nil, &gophercloud.RequestOpts{
   280  		MoreHeaders: h,
   281  	})
   282  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   283  	return
   284  }
   285  
   286  // CopyOptsBuilder allows extensions to add additional parameters to the
   287  // Copy request.
   288  type CopyOptsBuilder interface {
   289  	ToObjectCopyMap() (map[string]string, error)
   290  }
   291  
   292  // CopyOptsQueryBuilder allows extensions to add additional query parameters to
   293  // the Copy request.
   294  type CopyOptsQueryBuilder interface {
   295  	ToObjectCopyQuery() (string, error)
   296  }
   297  
   298  // CopyOpts is a structure that holds parameters for copying one object to
   299  // another.
   300  type CopyOpts struct {
   301  	Metadata           map[string]string
   302  	ContentDisposition string `h:"Content-Disposition"`
   303  	ContentEncoding    string `h:"Content-Encoding"`
   304  	ContentType        string `h:"Content-Type"`
   305  	Destination        string `h:"Destination" required:"true"`
   306  	ObjectVersionID    string `q:"version-id"`
   307  }
   308  
   309  // ToObjectCopyMap formats a CopyOpts into a map of headers.
   310  func (opts CopyOpts) ToObjectCopyMap() (map[string]string, error) {
   311  	h, err := gophercloud.BuildHeaders(opts)
   312  	if err != nil {
   313  		return nil, err
   314  	}
   315  	for k, v := range opts.Metadata {
   316  		h["X-Object-Meta-"+k] = v
   317  	}
   318  	return h, nil
   319  }
   320  
   321  // ToObjectCopyQuery formats a CopyOpts into a query.
   322  func (opts CopyOpts) ToObjectCopyQuery() (string, error) {
   323  	q, err := gophercloud.BuildQueryString(opts)
   324  	if err != nil {
   325  		return "", err
   326  	}
   327  	return q.String(), nil
   328  }
   329  
   330  // Copy is a function that copies one object to another.
   331  func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts CopyOptsBuilder) (r CopyResult) {
   332  	url, err := copyURL(c, containerName, objectName)
   333  	if err != nil {
   334  		r.Err = err
   335  		return
   336  	}
   337  
   338  	h := make(map[string]string)
   339  	headers, err := opts.ToObjectCopyMap()
   340  	if err != nil {
   341  		r.Err = err
   342  		return
   343  	}
   344  	for k, v := range headers {
   345  		h[k] = v
   346  	}
   347  
   348  	if opts, ok := opts.(CopyOptsQueryBuilder); ok {
   349  		query, err := opts.ToObjectCopyQuery()
   350  		if err != nil {
   351  			r.Err = err
   352  			return
   353  		}
   354  		url += query
   355  	}
   356  
   357  	resp, err := c.Request("COPY", url, &gophercloud.RequestOpts{
   358  		MoreHeaders: h,
   359  		OkCodes:     []int{201},
   360  	})
   361  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   362  	return
   363  }
   364  
   365  // DeleteOptsBuilder allows extensions to add additional parameters to the
   366  // Delete request.
   367  type DeleteOptsBuilder interface {
   368  	ToObjectDeleteQuery() (string, error)
   369  }
   370  
   371  // DeleteOpts is a structure that holds parameters for deleting an object.
   372  type DeleteOpts struct {
   373  	MultipartManifest string `q:"multipart-manifest"`
   374  	ObjectVersionID   string `q:"version-id"`
   375  }
   376  
   377  // ToObjectDeleteQuery formats a DeleteOpts into a query string.
   378  func (opts DeleteOpts) ToObjectDeleteQuery() (string, error) {
   379  	q, err := gophercloud.BuildQueryString(opts)
   380  	return q.String(), err
   381  }
   382  
   383  // Delete is a function that deletes an object.
   384  func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts DeleteOptsBuilder) (r DeleteResult) {
   385  	url, err := deleteURL(c, containerName, objectName)
   386  	if err != nil {
   387  		r.Err = err
   388  		return
   389  	}
   390  	if opts != nil {
   391  		query, err := opts.ToObjectDeleteQuery()
   392  		if err != nil {
   393  			r.Err = err
   394  			return
   395  		}
   396  		url += query
   397  	}
   398  	resp, err := c.Delete(url, nil)
   399  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   400  	return
   401  }
   402  
   403  // GetOptsBuilder allows extensions to add additional parameters to the
   404  // Get request.
   405  type GetOptsBuilder interface {
   406  	ToObjectGetParams() (map[string]string, string, error)
   407  }
   408  
   409  // GetOpts is a structure that holds parameters for getting an object's
   410  // metadata.
   411  type GetOpts struct {
   412  	Newest          bool   `h:"X-Newest"`
   413  	Expires         string `q:"expires"`
   414  	Signature       string `q:"signature"`
   415  	ObjectVersionID string `q:"version-id"`
   416  }
   417  
   418  // ToObjectGetParams formats a GetOpts into a query string and a map of headers.
   419  func (opts GetOpts) ToObjectGetParams() (map[string]string, string, error) {
   420  	q, err := gophercloud.BuildQueryString(opts)
   421  	if err != nil {
   422  		return nil, "", err
   423  	}
   424  	h, err := gophercloud.BuildHeaders(opts)
   425  	if err != nil {
   426  		return nil, q.String(), err
   427  	}
   428  	return h, q.String(), nil
   429  }
   430  
   431  // Get is a function that retrieves the metadata of an object. To extract just
   432  // the custom metadata, pass the GetResult response to the ExtractMetadata
   433  // function.
   434  func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts GetOptsBuilder) (r GetResult) {
   435  	url, err := getURL(c, containerName, objectName)
   436  	if err != nil {
   437  		r.Err = err
   438  		return
   439  	}
   440  	h := make(map[string]string)
   441  	if opts != nil {
   442  		headers, query, err := opts.ToObjectGetParams()
   443  		if err != nil {
   444  			r.Err = err
   445  			return
   446  		}
   447  		for k, v := range headers {
   448  			h[k] = v
   449  		}
   450  		url += query
   451  	}
   452  
   453  	resp, err := c.Head(url, &gophercloud.RequestOpts{
   454  		MoreHeaders: h,
   455  		OkCodes:     []int{200, 204},
   456  	})
   457  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   458  	return
   459  }
   460  
   461  // UpdateOptsBuilder allows extensions to add additional parameters to the
   462  // Update request.
   463  type UpdateOptsBuilder interface {
   464  	ToObjectUpdateMap() (map[string]string, error)
   465  }
   466  
   467  // UpdateOpts is a structure that holds parameters for updating, creating, or
   468  // deleting an object's metadata.
   469  type UpdateOpts struct {
   470  	Metadata           map[string]string
   471  	RemoveMetadata     []string
   472  	ContentDisposition *string `h:"Content-Disposition"`
   473  	ContentEncoding    *string `h:"Content-Encoding"`
   474  	ContentType        *string `h:"Content-Type"`
   475  	DeleteAfter        *int64  `h:"X-Delete-After"`
   476  	DeleteAt           *int64  `h:"X-Delete-At"`
   477  	DetectContentType  *bool   `h:"X-Detect-Content-Type"`
   478  }
   479  
   480  // ToObjectUpdateMap formats a UpdateOpts into a map of headers.
   481  func (opts UpdateOpts) ToObjectUpdateMap() (map[string]string, error) {
   482  	h, err := gophercloud.BuildHeaders(opts)
   483  	if err != nil {
   484  		return nil, err
   485  	}
   486  
   487  	for k, v := range opts.Metadata {
   488  		h["X-Object-Meta-"+k] = v
   489  	}
   490  
   491  	for _, k := range opts.RemoveMetadata {
   492  		h["X-Remove-Object-Meta-"+k] = "remove"
   493  	}
   494  	return h, nil
   495  }
   496  
   497  // Update is a function that creates, updates, or deletes an object's metadata.
   498  func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts UpdateOptsBuilder) (r UpdateResult) {
   499  	url, err := updateURL(c, containerName, objectName)
   500  	if err != nil {
   501  		r.Err = err
   502  		return
   503  	}
   504  	h := make(map[string]string)
   505  	if opts != nil {
   506  		headers, err := opts.ToObjectUpdateMap()
   507  		if err != nil {
   508  			r.Err = err
   509  			return
   510  		}
   511  
   512  		for k, v := range headers {
   513  			h[k] = v
   514  		}
   515  	}
   516  	resp, err := c.Post(url, nil, nil, &gophercloud.RequestOpts{
   517  		MoreHeaders: h,
   518  	})
   519  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   520  	return
   521  }
   522  
   523  // HTTPMethod represents an HTTP method string (e.g. "GET").
   524  type HTTPMethod string
   525  
   526  var (
   527  	// GET represents an HTTP "GET" method.
   528  	GET HTTPMethod = "GET"
   529  	// HEAD represents an HTTP "HEAD" method.
   530  	HEAD HTTPMethod = "HEAD"
   531  	// PUT represents an HTTP "PUT" method.
   532  	PUT HTTPMethod = "PUT"
   533  	// POST represents an HTTP "POST" method.
   534  	POST HTTPMethod = "POST"
   535  	// DELETE represents an HTTP "DELETE" method.
   536  	DELETE HTTPMethod = "DELETE"
   537  )
   538  
   539  // CreateTempURLOpts are options for creating a temporary URL for an object.
   540  type CreateTempURLOpts struct {
   541  	// (REQUIRED) Method is the HTTP method to allow for users of the temp URL.
   542  	// Valid values are "GET", "HEAD", "PUT", "POST" and "DELETE".
   543  	Method HTTPMethod
   544  
   545  	// (REQUIRED) TTL is the number of seconds the temp URL should be active.
   546  	TTL int
   547  
   548  	// (Optional) Split is the string on which to split the object URL. Since only
   549  	// the object path is used in the hash, the object URL needs to be parsed. If
   550  	// empty, the default OpenStack URL split point will be used ("/v1/").
   551  	Split string
   552  
   553  	// (Optional) Timestamp is the current timestamp used to calculate the Temp URL
   554  	// signature. If not specified, the current UNIX timestamp is used as the base
   555  	// timestamp.
   556  	Timestamp time.Time
   557  
   558  	// (Optional) TempURLKey overrides the Swift container or account Temp URL key.
   559  	// TempURLKey must correspond to a target container/account key, otherwise the
   560  	// generated link will be invalid. If not specified, the key is obtained from
   561  	// a Swift container or account.
   562  	TempURLKey string
   563  
   564  	// (Optional) Digest specifies the cryptographic hash function used to
   565  	// calculate the signature. Valid values include sha1, sha256, and
   566  	// sha512. If not specified, the default hash function is sha1.
   567  	Digest string
   568  }
   569  
   570  // CreateTempURL is a function for creating a temporary URL for an object. It
   571  // allows users to have "GET" or "POST" access to a particular tenant's object
   572  // for a limited amount of time.
   573  func CreateTempURL(c *gophercloud.ServiceClient, containerName, objectName string, opts CreateTempURLOpts) (string, error) {
   574  	url, err := getURL(c, containerName, objectName)
   575  	if err != nil {
   576  		return "", err
   577  	}
   578  
   579  	if opts.Split == "" {
   580  		opts.Split = "/v1/"
   581  	}
   582  
   583  	// Initialize time if it was not passed as opts
   584  	date := opts.Timestamp
   585  	if date.IsZero() {
   586  		date = time.Now()
   587  	}
   588  	duration := time.Duration(opts.TTL) * time.Second
   589  	// UNIX time is always UTC
   590  	expiry := date.Add(duration).Unix()
   591  
   592  	// Initialize the tempURLKey to calculate a signature
   593  	tempURLKey := opts.TempURLKey
   594  	if tempURLKey == "" {
   595  		// fallback to a container TempURL key
   596  		getHeader, err := containers.Get(c, containerName, nil).Extract()
   597  		if err != nil {
   598  			return "", err
   599  		}
   600  		tempURLKey = getHeader.TempURLKey
   601  		if tempURLKey == "" {
   602  			// fallback to an account TempURL key
   603  			getHeader, err := accounts.Get(c, nil).Extract()
   604  			if err != nil {
   605  				return "", err
   606  			}
   607  			tempURLKey = getHeader.TempURLKey
   608  		}
   609  		if tempURLKey == "" {
   610  			return "", ErrTempURLKeyNotFound{}
   611  		}
   612  	}
   613  
   614  	secretKey := []byte(tempURLKey)
   615  	splitPath := strings.SplitN(url, opts.Split, 2)
   616  	baseURL, objectPath := splitPath[0], splitPath[1]
   617  	objectPath = opts.Split + objectPath
   618  	body := fmt.Sprintf("%s\n%d\n%s", opts.Method, expiry, objectPath)
   619  	var hash hash.Hash
   620  	switch opts.Digest {
   621  	case "", "sha1":
   622  		hash = hmac.New(sha1.New, secretKey)
   623  	case "sha256":
   624  		hash = hmac.New(sha256.New, secretKey)
   625  	case "sha512":
   626  		hash = hmac.New(sha512.New, secretKey)
   627  	default:
   628  		return "", ErrTempURLDigestNotValid{Digest: opts.Digest}
   629  	}
   630  	hash.Write([]byte(body))
   631  	hexsum := fmt.Sprintf("%x", hash.Sum(nil))
   632  	return fmt.Sprintf("%s%s?temp_url_sig=%s&temp_url_expires=%d", baseURL, objectPath, hexsum, expiry), nil
   633  }
   634  
   635  // BulkDelete is a function that bulk deletes objects.
   636  // In Swift, the maximum number of deletes per request is set by default to 10000.
   637  //
   638  // See:
   639  // * https://github.com/openstack/swift/blob/6d3d4197151f44bf28b51257c1a4c5d33411dcae/etc/proxy-server.conf-sample#L1029-L1034
   640  // * https://github.com/openstack/swift/blob/e8cecf7fcc1630ee83b08f9a73e1e59c07f8d372/swift/common/middleware/bulk.py#L309
   641  func BulkDelete(c *gophercloud.ServiceClient, container string, objects []string) (r BulkDeleteResult) {
   642  	err := containers.CheckContainerName(container)
   643  	if err != nil {
   644  		r.Err = err
   645  		return
   646  	}
   647  
   648  	var body bytes.Buffer
   649  	for i := range objects {
   650  		if objects[i] == "" {
   651  			r.Err = fmt.Errorf("object names must not be the empty string")
   652  			return
   653  		}
   654  		body.WriteString(container)
   655  		body.WriteRune('/')
   656  		body.WriteString(objects[i])
   657  		body.WriteRune('\n')
   658  	}
   659  
   660  	resp, err := c.Post(bulkDeleteURL(c), &body, &r.Body, &gophercloud.RequestOpts{
   661  		MoreHeaders: map[string]string{
   662  			"Accept":       "application/json",
   663  			"Content-Type": "text/plain",
   664  		},
   665  		OkCodes: []int{200},
   666  	})
   667  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   668  	return
   669  }