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