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 }