google-cloud-storage 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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