google-cloud-storage 0.24.0 → 0.25.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e64f8e7e313f4c8f5dc1bd0229e04436c7594281
4
- data.tar.gz: 96638f6c5921a7be66cffac75e36d88abbee9337
3
+ metadata.gz: 4450abcfeebceca2abde8444b448c4361cd66bba
4
+ data.tar.gz: 9f9d2b7ec0dfcece945d361f3cc9828b8560d189
5
5
  SHA512:
6
- metadata.gz: 984d4b6c132149b11cf697b341d5b6e88cb2e7d729597c2e2e93af21801771ac3486da2aa620b007e605bc1dca397a5b3ed2f9af999953d5bdd3d1679b08b1cd
7
- data.tar.gz: 2cc2fbcaf2be7e8a5381f0791211a44cc6655428786c45dd4076833b10712313f7ee81e693073a9d0e0caf5e268e128557c130e79360f48fb3f8b1f2e6e8b62d
6
+ metadata.gz: 21241923c3f1b341eb21f495ae3e747ee2508df599c7ac1cf27ecccab85960eda2e7cdac878580d6def3343d551ff9de48cba5b2bda3fb73374be9eb77ada937
7
+ data.tar.gz: a685308ed10e4cece3b05d15c48c9a4f1c04bdba60587332db912646b9fd998e1e4c479f158472ac07a0b4079dc8bbe6c843880fef959f3000d17ee9d078fc85
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [Google Cloud Storage](https://cloud.google.com/storage/) ([docs](https://cloud.google.com/storage/docs/json_api/)) allows you to store data on Google infrastructure with very high reliability, performance and availability, and can be used to distribute large data objects to users via direct download.
4
4
 
5
- - [google-cloud-storage API documentation](http://googlecloudplatform.github.io/google-cloud-ruby/#/docs/google-cloud-storage/master/google/cloud/storage)
5
+ - [google-cloud-storage API documentation](http://googlecloudplatform.github.io/google-cloud-ruby/#/docs/google-cloud-storage/latest)
6
6
  - [google-cloud-storage on RubyGems](https://rubygems.org/gems/google-cloud-storage)
7
7
  - [Google Cloud Storage documentation](https://cloud.google.com/storage/docs)
8
8
 
@@ -55,11 +55,14 @@ module Google
55
55
  #
56
56
  # ## Retrieving Buckets
57
57
  #
58
- # A Bucket is the container for your data. There is no limit on the number
59
- # of buckets that you can create in a project. You can use buckets to
60
- # organize and control access to your data. Each bucket has a unique name,
61
- # which is how they are retrieved: (See
62
- # {Google::Cloud::Storage::Project#bucket})
58
+ # A {Google::Cloud::Storage::Bucket} instance is a container for your data.
59
+ # There is no limit on the number of buckets that you can create in a
60
+ # project. You can use buckets to organize and control access to your data.
61
+ # For more information, see [Working with
62
+ # Buckets](https://cloud.google.com/storage/docs/creating-buckets).
63
+ #
64
+ # Each bucket has a globally unique name, which is how they are retrieved:
65
+ # (See {Google::Cloud::Storage::Project#bucket})
63
66
  #
64
67
  # ```ruby
65
68
  # require "google/cloud/storage"
@@ -80,27 +83,38 @@ module Google
80
83
  # all_buckets = storage.buckets
81
84
  # ```
82
85
  #
83
- # If you have a significant number of buckets, you may need to paginate
84
- # through them: (See {Google::Cloud::Storage::Bucket::List#token})
86
+ # If you have a significant number of buckets, you may need to fetch them
87
+ # in multiple service requests.
88
+ #
89
+ # Iterating over each bucket, potentially with multiple API calls, by
90
+ # invoking `all` with a block:
91
+ #
92
+ # ```ruby
93
+ # require "google/cloud/storage"
94
+ #
95
+ # storage = Google::Cloud::Storage.new
96
+ #
97
+ # buckets = storage.buckets
98
+ # buckets.all do |bucket|
99
+ # puts bucket.name
100
+ # end
101
+ # ```
102
+ #
103
+ # Limiting the number of API calls made:
85
104
  #
86
105
  # ```ruby
87
106
  # require "google/cloud/storage"
88
107
  #
89
108
  # storage = Google::Cloud::Storage.new
90
109
  #
91
- # all_buckets = []
92
- # tmp_buckets = storage.buckets
93
- # while tmp_buckets.any? do
94
- # tmp_buckets.each do |bucket|
95
- # all_buckets << bucket
96
- # end
97
- # # break loop if no more buckets available
98
- # break if tmp_buckets.token.nil?
99
- # # get the next group of buckets
100
- # tmp_buckets = storage.buckets token: tmp_buckets.token
110
+ # buckets = storage.buckets
111
+ # buckets.all(request_limit: 10) do |bucket|
112
+ # puts bucket.name
101
113
  # end
102
114
  # ```
103
115
  #
116
+ # See {Google::Cloud::Storage::Bucket::List} for details.
117
+ #
104
118
  # ## Creating a Bucket
105
119
  #
106
120
  # A unique name is all that is needed to create a new bucket: (See
@@ -116,10 +130,12 @@ module Google
116
130
  #
117
131
  # ## Retrieving Files
118
132
  #
119
- # A File is an individual pieces of data that you store in Google Cloud
120
- # Storage. Files contain the data stored as well as metadata describing the
121
- # data. Files belong to a bucket and cannot be shared among buckets. There
122
- # is no limit on the number of objects that you can create in a bucket.
133
+ # A {Google::Cloud::Storage::File} instance is an individual data object
134
+ # that you store in Google Cloud Storage. Files contain the data stored as
135
+ # well as metadata describing the data. Files belong to a bucket and cannot
136
+ # be shared among buckets. There is no limit on the number of files that
137
+ # you can create in a bucket. For more information, see [Working with
138
+ # Objects](https://cloud.google.com/storage/docs/object-basics).
123
139
  #
124
140
  # Files are retrieved by their name, which is the path of the file in the
125
141
  # bucket: (See {Google::Cloud::Storage::Bucket#file})
@@ -155,32 +171,42 @@ module Google
155
171
  # avatar_files = bucket.files prefix: "avatars/"
156
172
  # ```
157
173
  #
158
- # If you have a significant number of files, you may need to paginate
159
- # through them: (See {Google::Cloud::Storage::File::List#token})
174
+ # If you have a significant number of files, you may need to fetch them
175
+ # in multiple service requests.
176
+ #
177
+ # Iterating over each file, potentially with multiple API calls, by
178
+ # invoking `all` with a block:
160
179
  #
161
180
  # ```ruby
162
181
  # require "google/cloud/storage"
163
182
  #
164
183
  # storage = Google::Cloud::Storage.new
165
- #
166
184
  # bucket = storage.bucket "my-todo-app"
167
185
  #
168
- # all_files = []
169
- # tmp_files = bucket.files
170
- # while tmp_files.any? do
171
- # tmp_files.each do |file|
172
- # all_files << file
173
- # end
174
- # # break loop if no more files available
175
- # break if tmp_files.token.nil?
176
- # # get the next group of files
177
- # tmp_files = bucket.files token: tmp_files.token
186
+ # files = storage.files
187
+ # files.all do |file|
188
+ # puts file.name
178
189
  # end
179
190
  # ```
180
191
  #
192
+ # Limiting the number of API calls made:
193
+ #
194
+ # ```ruby
195
+ # require "google/cloud/storage"
196
+ #
197
+ # storage = Google::Cloud::Storage.new
198
+ #
199
+ # files = storage.files
200
+ # files.all(request_limit: 10) do |file|
201
+ # puts bucket.name
202
+ # end
203
+ # ```
204
+ #
205
+ # See {Google::Cloud::Storage::File::List} for details.
206
+ #
181
207
  # ## Creating a File
182
208
  #
183
- # A new File can be uploaded by specifying the location of a file on the
209
+ # A new file can be uploaded by specifying the location of a file on the
184
210
  # local file system, and the name/path that the file should be stored in the
185
211
  # bucket. (See {Google::Cloud::Storage::Bucket#create_file})
186
212
  #
@@ -194,6 +220,17 @@ module Google
194
220
  # "avatars/heidi/400x400.png"
195
221
  # ```
196
222
  #
223
+ # Files can also be created from an in-memory StringIO object:
224
+ #
225
+ # ```ruby
226
+ # require "google/cloud/storage"
227
+ #
228
+ # storage = Google::Cloud::Storage.new
229
+ #
230
+ # bucket = storage.bucket "my-todo-app"
231
+ # bucket.create_file StringIO.new("Hello world!"), "hello-world.txt"
232
+ # ```
233
+ #
197
234
  # ### Customer-supplied encryption keys
198
235
  #
199
236
  # By default, Google Cloud Storage manages server-side encryption keys on
@@ -265,9 +302,24 @@ module Google
265
302
  # file.download "/var/todo-app/avatars/heidi/400x400.png"
266
303
  # ```
267
304
  #
305
+ # Files can also be downloaded to an in-memory StringIO object:
306
+ #
307
+ # ```ruby
308
+ # require "google/cloud/storage"
309
+ #
310
+ # storage = Google::Cloud::Storage.new
311
+ #
312
+ # bucket = storage.bucket "my-todo-app"
313
+ # file = bucket.file "hello-world.txt"
314
+ #
315
+ # downloaded = file.download
316
+ # downloaded.rewind
317
+ # downloaded.read #=> "Hello world!"
318
+ # ```
319
+ #
268
320
  # ## Using Signed URLs
269
321
  #
270
- # Access without authentication can be granted to a File for a specified
322
+ # Access without authentication can be granted to a file for a specified
271
323
  # period of time. This URL uses a cryptographic signature of your
272
324
  # credentials to access the file. (See
273
325
  # {Google::Cloud::Storage::File#signed_url})
@@ -449,8 +449,9 @@ module Google
449
449
  alias_method :find_file, :file
450
450
 
451
451
  ##
452
- # Creates a new {File} object by providing a path to a local file to
453
- # upload and the path to store it with in the bucket.
452
+ # Creates a new {File} object by providing a path to a local file (or
453
+ # any IO or IO-ish object) to upload, along with the path at which to
454
+ # store it in the bucket.
454
455
  #
455
456
  # #### Customer-supplied encryption keys
456
457
  #
@@ -465,7 +466,10 @@ module Google
465
466
  # and you can read or update the metadata of an encrypted file without
466
467
  # providing the encryption key.
467
468
  #
468
- # @param [String] file Path of the file on the filesystem to upload.
469
+ # @param [String, IO] file Path of the file on the filesystem to
470
+ # upload. Can be an IO object, or IO-ish object like StringIO. (If the
471
+ # IO object does not have path, a `path` argument must be also be
472
+ # provided.)
469
473
  # @param [String] path Path to store the file in Google Cloud Storage.
470
474
  # @param [String] acl A predefined set of access controls to apply to
471
475
  # this file.
@@ -580,9 +584,11 @@ module Google
580
584
  content_encoding: content_encoding, metadata: metadata,
581
585
  content_language: content_language, key: encryption_key,
582
586
  storage_class: storage_class_for(storage_class) }
583
- ensure_file_exists! file
584
- # TODO: Handle file as an IO and path is missing more gracefully
585
- path ||= Pathname(file).to_path
587
+ ensure_io_or_file_exists! file
588
+ path ||= file.path if file.respond_to? :path
589
+ path ||= file if file.is_a? String
590
+ fail ArgumentError, "must provide path" if path.nil?
591
+
586
592
  gapi = service.insert_file name, file, path, options
587
593
  File.from_gapi gapi, service
588
594
  end
@@ -612,7 +618,7 @@ module Google
612
618
  # @see https://cloud.google.com/storage/docs/access-control#Signed-URLs
613
619
  # Access Control Signed URLs guide
614
620
  #
615
- # @param [String] path Path to of the file in Google Cloud Storage.
621
+ # @param [String] path Path to the file in Google Cloud Storage.
616
622
  # @param [String] method The HTTP verb to be used with the signed URL.
617
623
  # Signed URLs can be used
618
624
  # with `GET`, `HEAD`, `PUT`, and `DELETE` requests. Default is `GET`.
@@ -649,6 +655,7 @@ module Google
649
655
  # bucket = storage.bucket "my-todo-app"
650
656
  # shared_url = bucket.signed_url "avatars/heidi/400x400.png",
651
657
  # method: "PUT",
658
+ # content_type: "image/png",
652
659
  # expires: 300 # 5 minutes from now
653
660
  #
654
661
  # @example Using the issuer and signing_key options:
@@ -705,7 +712,7 @@ module Google
705
712
  #
706
713
  # @see https://cloud.google.com/storage/docs/xml-api/post-object
707
714
  #
708
- # @param [String] path Path to of the file in Google Cloud Storage.
715
+ # @param [String] path Path to the file in Google Cloud Storage.
709
716
  # @param [Hash] policy The security policy that describes what
710
717
  # can and cannot be uploaded in the form. When provided,
711
718
  # the PostObject fields will include a Signature based on the JSON
@@ -919,7 +926,8 @@ module Google
919
926
 
920
927
  ##
921
928
  # Raise an error if the file is not found.
922
- def ensure_file_exists! file
929
+ def ensure_io_or_file_exists! file
930
+ return if file.respond_to?(:read) && file.respond_to?(:rewind)
923
931
  return if ::File.file? file
924
932
  fail ArgumentError, "cannot find file #{file}"
925
933
  end
@@ -77,15 +77,15 @@ module Google
77
77
  end
78
78
 
79
79
  ##
80
- # Retrieves all buckets by repeatedly loading {#next} until {#next?}
81
- # returns `false`. Calls the given block once for each bucket, which
82
- # is passed as the parameter.
80
+ # Retrieves remaining results by repeatedly invoking {#next} until
81
+ # {#next?} returns `false`. Calls the given block once for each
82
+ # result, which is passed as the argument to the block.
83
83
  #
84
84
  # An Enumerator is returned if no block is given.
85
85
  #
86
- # This method may make several API calls until all buckets are
87
- # retrieved. Be sure to use as narrow a search criteria as possible.
88
- # Please use with caution.
86
+ # This method will make repeated API calls until all remaining results
87
+ # are retrieved. (Unlike `#each`, for example, which merely iterates
88
+ # over the results returned by a single API call.) Use with caution.
89
89
  #
90
90
  # @param [Integer] request_limit The upper limit of API requests to
91
91
  # make to load all buckets. Default is no limit.
@@ -17,6 +17,7 @@ require "uri"
17
17
  require "google/cloud/storage/file/acl"
18
18
  require "google/cloud/storage/file/list"
19
19
  require "google/cloud/storage/file/verifier"
20
+ require "google/cloud/storage/file/signer"
20
21
 
21
22
  module Google
22
23
  module Cloud
@@ -332,7 +333,7 @@ module Google
332
333
  end
333
334
 
334
335
  ##
335
- # Download the file's contents to a local file.
336
+ # Download the file's contents to a local file or an IO instance.
336
337
  #
337
338
  # By default, the download is verified by calculating the MD5 digest.
338
339
  #
@@ -341,9 +342,12 @@ module Google
341
342
  # was used with {Bucket#create_file}, the `encryption_key` option must
342
343
  # be provided.
343
344
  #
344
- # @param [String] path The path on the local file system to write the
345
- # data to. The path provided must be writable.
346
- # @param [Symbol] verify The verification algoruthm used to ensure the
345
+ # @param [String, IO] path The path on the local file system to write
346
+ # the data to. The path provided must be writable. Can also be an IO
347
+ # object, or IO-ish object like StringIO. If an IO object, the object
348
+ # will be written to, not the filesystem. If omitted, a new StringIO
349
+ # instance will be written to and returned. Optional.
350
+ # @param [Symbol] verify The verification algorithm used to ensure the
347
351
  # downloaded file contents are correct. Default is `:md5`.
348
352
  #
349
353
  # Acceptable values are:
@@ -357,7 +361,11 @@ module Google
357
361
  # AES-256 encryption key used to encrypt the file, if one was provided
358
362
  # to {Bucket#create_file}.
359
363
  #
360
- # @return [File] Returns a `::File` object on the local file system
364
+ # @return [IO] Returns an IO object representing the file data. This
365
+ # will ordinarily be a `::File` object referencing the local file
366
+ # system. However, if the argument to `path` is `nil`, a StringIO
367
+ # instance will be returned. If the argument to `path` is an IO
368
+ # object, then that object will be returned.
361
369
  #
362
370
  # @example
363
371
  # require "google/cloud/storage"
@@ -399,12 +407,30 @@ module Google
399
407
  # file = bucket.file "path/to/my-file.ext"
400
408
  # file.download "path/to/downloaded/file.ext", verify: :none
401
409
  #
402
- def download path, verify: :md5, encryption_key: nil
410
+ # @example Download to an in-memory StringIO object.
411
+ # require "google/cloud/storage"
412
+ #
413
+ # storage = Google::Cloud::Storage.new
414
+ #
415
+ # bucket = storage.bucket "my-bucket"
416
+ #
417
+ # file = bucket.file "path/to/my-file.ext"
418
+ # downloaded = file.download
419
+ # downloaded.rewind
420
+ # downloaded.read #=> "Hello world!"
421
+ #
422
+ def download path = nil, verify: :md5, encryption_key: nil
403
423
  ensure_service!
404
- service.download_file \
424
+ if path.nil?
425
+ path = StringIO.new
426
+ path.set_encoding "ASCII-8BIT"
427
+ end
428
+ file = service.download_file \
405
429
  bucket, name, path,
406
430
  key: encryption_key
407
- verify_file! ::File.new(path), verify
431
+ # FIX: downloading with encryption key will return nil
432
+ file ||= ::File.new(path)
433
+ verify_file! file, verify
408
434
  end
409
435
 
410
436
  ##
@@ -661,7 +687,8 @@ module Google
661
687
  #
662
688
  # bucket = storage.bucket "my-todo-app"
663
689
  # file = bucket.file "avatars/heidi/400x400.png"
664
- # shared_url = file.signed_url method: "GET",
690
+ # shared_url = file.signed_url method: "PUT",
691
+ # content_type: "image/png",
665
692
  # expires: 300 # 5 minutes from now
666
693
  #
667
694
  # @example Using the `issuer` and `signing_key` options:
@@ -685,7 +712,7 @@ module Google
685
712
  # shared_url = file.signed_url method: "GET",
686
713
  # headers: {
687
714
  # "x-goog-acl" => "public-read",
688
- # "x-goog-meta-foo" => bar,baz"
715
+ # "x-goog-meta-foo" => "bar,baz"
689
716
  # }
690
717
  #
691
718
  def signed_url method: nil, expires: nil, content_type: nil,
@@ -823,122 +850,6 @@ module Google
823
850
  "standard" => "STANDARD" }[str.to_s.downcase] || str.to_s
824
851
  end
825
852
 
826
- ##
827
- # @private Create a signed_url for a file.
828
- class Signer
829
- def initialize bucket, path, service
830
- @bucket = bucket
831
- @path = path
832
- @service = service
833
- end
834
-
835
- def self.from_file file
836
- new file.bucket, file.name, file.service
837
- end
838
-
839
- def self.from_bucket bucket, path
840
- new bucket.name, path, bucket.service
841
- end
842
-
843
- ##
844
- # The external path to the file.
845
- def ext_path
846
- URI.escape "/#{@bucket}/#{@path}"
847
- end
848
-
849
- ##
850
- # The external url to the file.
851
- def ext_url
852
- "#{GOOGLEAPIS_URL}#{ext_path}"
853
- end
854
-
855
- def apply_option_defaults options
856
- adjusted_expires = (Time.now.utc + (options[:expires] || 300)).to_i
857
- options[:expires] = adjusted_expires
858
- options[:method] ||= "GET"
859
- options
860
- end
861
-
862
- def signature_str options
863
- [options[:method], options[:content_md5],
864
- options[:content_type], options[:expires],
865
- format_extension_headers(options[:headers]) + ext_path].join "\n"
866
- end
867
-
868
- def determine_signing_key options = {}
869
- options[:signing_key] || options[:private_key] ||
870
- @service.credentials.signing_key
871
- end
872
-
873
- def determine_issuer options = {}
874
- options[:issuer] || options[:client_email] ||
875
- @service.credentials.issuer
876
- end
877
-
878
- def post_object options
879
- options = apply_option_defaults options
880
-
881
- fields = {
882
- key: ext_path.sub("/", "")
883
- }
884
-
885
- p = options[:policy] || {}
886
- fail "Policy must be given in a Hash" unless p.is_a? Hash
887
-
888
- i = determine_issuer options
889
- s = determine_signing_key options
890
-
891
- fail SignedUrlUnavailable unless i && s
892
-
893
- policy_str = p.to_json
894
- policy = Base64.strict_encode64(policy_str).delete("\n")
895
-
896
- signature = generate_signature s, policy
897
-
898
- fields[:GoogleAccessId] = i
899
- fields[:signature] = signature
900
- fields[:policy] = policy
901
-
902
- Google::Cloud::Storage::PostObject.new GOOGLEAPIS_URL, fields
903
- end
904
-
905
- def signed_url options
906
- options = apply_option_defaults options
907
-
908
- i = determine_issuer options
909
- s = determine_signing_key options
910
-
911
- fail SignedUrlUnavailable unless i && s
912
-
913
- sig = generate_signature s, signature_str(options)
914
- generate_signed_url i, sig, options[:expires]
915
- end
916
-
917
- def generate_signature signing_key, secret
918
- unless signing_key.respond_to? :sign
919
- signing_key = OpenSSL::PKey::RSA.new signing_key
920
- end
921
- signature = signing_key.sign OpenSSL::Digest::SHA256.new, secret
922
- Base64.strict_encode64(signature).delete("\n")
923
- end
924
-
925
- def generate_signed_url issuer, signed_string, expires
926
- "#{ext_url}?GoogleAccessId=#{CGI.escape issuer}" \
927
- "&Expires=#{expires}" \
928
- "&Signature=#{CGI.escape signed_string}"
929
- end
930
-
931
- def format_extension_headers headers
932
- return "" if headers.nil?
933
- fail "Headers must be given in a Hash" unless headers.is_a? Hash
934
- flatten = headers.map do |key, value|
935
- "#{key.to_s.downcase}:#{value.gsub(/\s+/, ' ')}\n"
936
- end
937
- flatten.reject! { |h| h.start_with? "x-goog-encryption-key" }
938
- flatten.sort.join
939
- end
940
- end
941
-
942
853
  ##
943
854
  # Yielded to a block to accumulate changes for a patch request.
944
855
  class Updater < File
@@ -76,11 +76,7 @@ module Google
76
76
  #
77
77
  def reload!
78
78
  gapi = @service.list_file_acls @bucket, @file
79
- acls = Array(gapi.items).map do |acl|
80
- next acl if acl.is_a? Google::Apis::StorageV1::ObjectAccessControl
81
- fail "Unknown ACL format: #{acl.class}" unless acl.is_a? Hash
82
- Google::Apis::StorageV1::ObjectAccessControl.from_json acl.to_json
83
- end
79
+ acls = Array(gapi.items)
84
80
  @owners = entities_from_acls acls, "OWNER"
85
81
  @readers = entities_from_acls acls, "READER"
86
82
  end
@@ -87,15 +87,15 @@ module Google
87
87
  end
88
88
 
89
89
  ##
90
- # Retrieves all files by repeatedly loading {#next} until {#next?}
91
- # returns `false`. Calls the given block once for each file, which is
92
- # passed as the parameter.
90
+ # Retrieves remaining results by repeatedly invoking {#next} until
91
+ # {#next?} returns `false`. Calls the given block once for each
92
+ # result, which is passed as the argument to the block.
93
93
  #
94
94
  # An Enumerator is returned if no block is given.
95
95
  #
96
- # This method may make several API calls until all files are
97
- # retrieved. Be sure to use as narrow a search criteria as possible.
98
- # Please use with caution.
96
+ # This method will make repeated API calls until all remaining results
97
+ # are retrieved. (Unlike `#each`, for example, which merely iterates
98
+ # over the results returned by a single API call.) Use with caution.
99
99
  #
100
100
  # @param [Integer] request_limit The upper limit of API requests to
101
101
  # make to load all files. Default is no limit.
@@ -0,0 +1,142 @@
1
+ # Copyright 2017 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "base64"
17
+ require "openssl"
18
+ require "google/cloud/storage/errors"
19
+
20
+ module Google
21
+ module Cloud
22
+ module Storage
23
+ class File
24
+ ##
25
+ # @private Create a signed_url for a file.
26
+ class Signer
27
+ def initialize bucket, path, service
28
+ @bucket = bucket
29
+ @path = path
30
+ @service = service
31
+ end
32
+
33
+ def self.from_file file
34
+ new file.bucket, file.name, file.service
35
+ end
36
+
37
+ def self.from_bucket bucket, path
38
+ new bucket.name, path, bucket.service
39
+ end
40
+
41
+ ##
42
+ # The external path to the file.
43
+ def ext_path
44
+ URI.escape "/#{@bucket}/#{@path}"
45
+ end
46
+
47
+ ##
48
+ # The external url to the file.
49
+ def ext_url
50
+ "#{GOOGLEAPIS_URL}#{ext_path}"
51
+ end
52
+
53
+ def apply_option_defaults options
54
+ adjusted_expires = (Time.now.utc + (options[:expires] || 300)).to_i
55
+ options[:expires] = adjusted_expires
56
+ options[:method] ||= "GET"
57
+ options
58
+ end
59
+
60
+ def signature_str options
61
+ [options[:method], options[:content_md5],
62
+ options[:content_type], options[:expires],
63
+ format_extension_headers(options[:headers]) + ext_path].join "\n"
64
+ end
65
+
66
+ def determine_signing_key options = {}
67
+ options[:signing_key] || options[:private_key] ||
68
+ @service.credentials.signing_key
69
+ end
70
+
71
+ def determine_issuer options = {}
72
+ options[:issuer] || options[:client_email] ||
73
+ @service.credentials.issuer
74
+ end
75
+
76
+ def post_object options
77
+ options = apply_option_defaults options
78
+
79
+ fields = {
80
+ key: ext_path.sub("/", "")
81
+ }
82
+
83
+ p = options[:policy] || {}
84
+ fail "Policy must be given in a Hash" unless p.is_a? Hash
85
+
86
+ i = determine_issuer options
87
+ s = determine_signing_key options
88
+
89
+ fail SignedUrlUnavailable unless i && s
90
+
91
+ policy_str = p.to_json
92
+ policy = Base64.strict_encode64(policy_str).delete("\n")
93
+
94
+ signature = generate_signature s, policy
95
+
96
+ fields[:GoogleAccessId] = i
97
+ fields[:signature] = signature
98
+ fields[:policy] = policy
99
+
100
+ Google::Cloud::Storage::PostObject.new GOOGLEAPIS_URL, fields
101
+ end
102
+
103
+ def signed_url options
104
+ options = apply_option_defaults options
105
+
106
+ i = determine_issuer options
107
+ s = determine_signing_key options
108
+
109
+ fail SignedUrlUnavailable unless i && s
110
+
111
+ sig = generate_signature s, signature_str(options)
112
+ generate_signed_url i, sig, options[:expires]
113
+ end
114
+
115
+ def generate_signature signing_key, secret
116
+ unless signing_key.respond_to? :sign
117
+ signing_key = OpenSSL::PKey::RSA.new signing_key
118
+ end
119
+ signature = signing_key.sign OpenSSL::Digest::SHA256.new, secret
120
+ Base64.strict_encode64(signature).delete("\n")
121
+ end
122
+
123
+ def generate_signed_url issuer, signed_string, expires
124
+ "#{ext_url}?GoogleAccessId=#{CGI.escape issuer}" \
125
+ "&Expires=#{expires}" \
126
+ "&Signature=#{CGI.escape signed_string}"
127
+ end
128
+
129
+ def format_extension_headers headers
130
+ return "" if headers.nil?
131
+ fail "Headers must be given in a Hash" unless headers.is_a? Hash
132
+ flatten = headers.map do |key, value|
133
+ "#{key.to_s.downcase}:#{value.gsub(/\s+/, ' ')}\n"
134
+ end
135
+ flatten.reject! { |h| h.start_with? "x-goog-encryption-key" }
136
+ flatten.sort.join
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -52,14 +52,24 @@ module Google
52
52
  end
53
53
 
54
54
  def self.md5_for local_file
55
- ::File.open(Pathname(local_file).to_path, "rb") do |f|
56
- ::Digest::MD5.file(f).base64digest
55
+ if local_file.respond_to? :path
56
+ ::File.open(Pathname(local_file).to_path, "rb") do |f|
57
+ ::Digest::MD5.file(f).base64digest
58
+ end
59
+ else # StringIO
60
+ local_file.rewind
61
+ ::Digest::MD5.base64digest local_file.read
57
62
  end
58
63
  end
59
64
 
60
65
  def self.crc32c_for local_file
61
- ::File.open(Pathname(local_file).to_path, "rb") do |f|
62
- ::Digest::CRC32c.file(f).base64digest
66
+ if local_file.respond_to? :path
67
+ ::File.open(Pathname(local_file).to_path, "rb") do |f|
68
+ ::Digest::CRC32c.file(f).base64digest
69
+ end
70
+ else # StringIO
71
+ local_file.rewind
72
+ ::Digest::CRC32c.base64digest local_file.read
63
73
  end
64
74
  end
65
75
  end
@@ -13,7 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
 
16
- require "google/cloud/core/environment"
16
+ require "google/cloud/env"
17
17
  require "google/cloud/storage/errors"
18
18
  require "google/cloud/storage/service"
19
19
  require "google/cloud/storage/credentials"
@@ -82,7 +82,7 @@ module Google
82
82
  ENV["STORAGE_PROJECT"] ||
83
83
  ENV["GOOGLE_CLOUD_PROJECT"] ||
84
84
  ENV["GCLOUD_PROJECT"] ||
85
- Google::Cloud::Core::Environment.project_id
85
+ Google::Cloud.env.project_id
86
86
  end
87
87
 
88
88
  ##
@@ -299,6 +299,110 @@ module Google
299
299
  Bucket.from_gapi gapi, service
300
300
  end
301
301
 
302
+ ##
303
+ # Access without authentication can be granted to a File for a specified
304
+ # period of time. This URL uses a cryptographic signature of your
305
+ # credentials to access the file identified by `path`. A URL can be
306
+ # created for paths that do not yet exist. For instance, a URL can be
307
+ # created to `PUT` file contents to.
308
+ #
309
+ # Generating a URL requires service account credentials, either by
310
+ # connecting with a service account when calling
311
+ # {Google::Cloud.storage}, or by passing in the service account `issuer`
312
+ # and `signing_key` values. Although the private key can be passed as a
313
+ # string for convenience, creating and storing an instance of
314
+ # `OpenSSL::PKey::RSA` is more efficient when making multiple calls to
315
+ # `signed_url`.
316
+ #
317
+ # A {SignedUrlUnavailable} is raised if the service account credentials
318
+ # are missing. Service account credentials are acquired by following the
319
+ # steps in [Service Account Authentication](
320
+ # https://cloud.google.com/storage/docs/authentication#service_accounts).
321
+ #
322
+ # @see https://cloud.google.com/storage/docs/access-control#Signed-URLs
323
+ # Access Control Signed URLs guide
324
+ #
325
+ # @param [String] bucket Name of the bucket.
326
+ # @param [String] path Path to the file in Google Cloud Storage.
327
+ # @param [String] method The HTTP verb to be used with the signed URL.
328
+ # Signed URLs can be used
329
+ # with `GET`, `HEAD`, `PUT`, and `DELETE` requests. Default is `GET`.
330
+ # @param [Integer] expires The number of seconds until the URL expires.
331
+ # Default is 300/5 minutes.
332
+ # @param [String] content_type When provided, the client (browser) must
333
+ # send this value in the HTTP header. e.g. `text/plain`
334
+ # @param [String] content_md5 The MD5 digest value in base64. If you
335
+ # provide this in the string, the client (usually a browser) must
336
+ # provide this HTTP header with this same value in its request.
337
+ # @param [Hash] headers Google extension headers (custom HTTP headers
338
+ # that begin with `x-goog-`) that must be included in requests that
339
+ # use the signed URL.
340
+ # @param [String] issuer Service Account's Client Email.
341
+ # @param [String] client_email Service Account's Client Email.
342
+ # @param [OpenSSL::PKey::RSA, String] signing_key Service Account's
343
+ # Private Key.
344
+ # @param [OpenSSL::PKey::RSA, String] private_key Service Account's
345
+ # Private Key.
346
+ #
347
+ # @example
348
+ # require "google/cloud/storage"
349
+ #
350
+ # storage = Google::Cloud::Storage.new
351
+ #
352
+ # bucket_name = "my-todo-app"
353
+ # file_path = "avatars/heidi/400x400.png"
354
+ # shared_url = storage.signed_url bucket_name, file_path
355
+ #
356
+ # @example Any of the option parameters may be specified:
357
+ # require "google/cloud/storage"
358
+ #
359
+ # storage = Google::Cloud::Storage.new
360
+ #
361
+ # bucket_name = "my-todo-app"
362
+ # file_path = "avatars/heidi/400x400.png"
363
+ # shared_url = storage.signed_url bucket_name, file_path,
364
+ # method: "PUT",
365
+ # content_type: "image/png",
366
+ # expires: 300 # 5 minutes from now
367
+ #
368
+ # @example Using the issuer and signing_key options:
369
+ # require "google/cloud/storage"
370
+ #
371
+ # storage = Google::Cloud.storage
372
+ #
373
+ # bucket_name = "my-todo-app"
374
+ # file_path = "avatars/heidi/400x400.png"
375
+ # issuer_email = "service-account@gcloud.com"
376
+ # key = OpenSSL::PKey::RSA.new "-----BEGIN PRIVATE KEY-----\n..."
377
+ # shared_url = storage.signed_url bucket_name, file_path,
378
+ # issuer: issuer_email,
379
+ # signing_key: key
380
+ #
381
+ # @example Using the headers option:
382
+ # require "google/cloud/storage"
383
+ #
384
+ # storage = Google::Cloud.storage
385
+ #
386
+ # bucket_name = "my-todo-app"
387
+ # file_path = "avatars/heidi/400x400.png"
388
+ # shared_url = storage.signed_url bucket_name, file_path,
389
+ # headers: {
390
+ # "x-goog-acl" => "private",
391
+ # "x-goog-meta-foo" => "bar,baz"
392
+ # }
393
+ #
394
+ def signed_url bucket, path, method: nil, expires: nil,
395
+ content_type: nil, content_md5: nil, headers: nil,
396
+ issuer: nil, client_email: nil, signing_key: nil,
397
+ private_key: nil
398
+ options = { method: method, expires: expires, headers: headers,
399
+ content_type: content_type, content_md5: content_md5,
400
+ issuer: issuer, client_email: client_email,
401
+ signing_key: signing_key, private_key: private_key }
402
+ signer = File::Signer.new bucket, path, service
403
+ signer.signed_url options
404
+ end
405
+
302
406
  protected
303
407
 
304
408
  def acl_rule option_name
@@ -49,6 +49,9 @@ module Google
49
49
  @service.request_options.retries = retries || 3
50
50
  @service.request_options.timeout_sec = timeout
51
51
  @service.request_options.open_timeout_sec = timeout
52
+ @service.request_options.header ||= {}
53
+ @service.request_options.header["x-goog-api-client"] = \
54
+ "gl-ruby/#{RUBY_VERSION} gccl/#{Google::Cloud::Storage::VERSION}"
52
55
  @service.authorization = @credentials.client
53
56
  end
54
57
 
@@ -178,7 +181,8 @@ module Google
178
181
  content_encoding: content_encoding, crc32c: crc32c,
179
182
  content_language: content_language, metadata: metadata,
180
183
  storage_class: storage_class }.delete_if { |_k, v| v.nil? })
181
- content_type ||= mime_type_for(Pathname(source).to_path)
184
+ content_type ||= mime_type_for(path || Pathname(source).to_path)
185
+
182
186
  execute do
183
187
  service.insert_object \
184
188
  bucket_name, file_obj,
@@ -16,7 +16,7 @@
16
16
  module Google
17
17
  module Cloud
18
18
  module Storage
19
- VERSION = "0.24.0"
19
+ VERSION = "0.25.0"
20
20
  end
21
21
  end
22
22
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: google-cloud-storage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.24.0
4
+ version: 0.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Moore
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-03-04 00:00:00.000000000 Z
12
+ date: 2017-04-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: google-cloud-core
@@ -17,14 +17,14 @@ dependencies:
17
17
  requirements:
18
18
  - - "~>"
19
19
  - !ruby/object:Gem::Version
20
- version: 0.21.0
20
+ version: '1.0'
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
- version: 0.21.0
27
+ version: '1.0'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: google-api-client
30
30
  requirement: !ruby/object:Gem::Requirement
@@ -201,6 +201,7 @@ files:
201
201
  - lib/google/cloud/storage/file.rb
202
202
  - lib/google/cloud/storage/file/acl.rb
203
203
  - lib/google/cloud/storage/file/list.rb
204
+ - lib/google/cloud/storage/file/signer.rb
204
205
  - lib/google/cloud/storage/file/verifier.rb
205
206
  - lib/google/cloud/storage/post_object.rb
206
207
  - lib/google/cloud/storage/project.rb
@@ -226,7 +227,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
226
227
  version: '0'
227
228
  requirements: []
228
229
  rubyforge_project:
229
- rubygems_version: 2.6.10
230
+ rubygems_version: 2.6.11
230
231
  signing_key:
231
232
  specification_version: 4
232
233
  summary: API Client library for Google Cloud Storage