github.com/vnpaycloud-console/gophercloud/v2@v2.0.5/openstack/objectstorage/v1/objects/requests.go (about)

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