google-cloud-storage 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,157 @@
1
+ # Copyright 2015 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 "delegate"
17
+
18
+ module Google
19
+ module Cloud
20
+ module Storage
21
+ class Bucket
22
+ ##
23
+ # # Bucket Cors
24
+ #
25
+ # A special-case Array for managing the website CORS rules for a bucket.
26
+ # Accessed via {Bucket#cors}.
27
+ #
28
+ # @see https://cloud.google.com/storage/docs/cross-origin Cross-Origin
29
+ # Resource Sharing (CORS)
30
+ #
31
+ # @example
32
+ # require "google/cloud"
33
+ #
34
+ # gcloud = Google::Cloud.new
35
+ # storage = gcloud.storage
36
+ # bucket = storage.bucket "my-todo-app"
37
+ #
38
+ # bucket = storage.bucket "my-bucket"
39
+ # bucket.cors do |c|
40
+ # # Remove the last CORS rule from the array
41
+ # c.pop
42
+ # # Remove all existing rules with the https protocol
43
+ # c.delete_if { |r| r.origin.include? "http://example.com" }
44
+ # c.add_rule ["http://example.org", "https://example.org"],
45
+ # ["GET", "POST", "DELETE"],
46
+ # response_headers: ["X-My-Custom-Header"],
47
+ # max_age: 3600
48
+ # end
49
+ #
50
+ class Cors < DelegateClass(::Array)
51
+ ##
52
+ # @private Initialize a new CORS rules builder with existing CORS
53
+ # rules, if any.
54
+ def initialize rules = []
55
+ super rules
56
+ @original = to_gapi.map(&:to_json)
57
+ end
58
+
59
+ # @private
60
+ def changed?
61
+ @original != to_gapi.map(&:to_json)
62
+ end
63
+
64
+ ##
65
+ # Add a CORS rule to the CORS rules for a bucket. Accepts options for
66
+ # setting preflight response headers. Preflight requests and responses
67
+ # are required if the request method and headers are not both [simple
68
+ # methods](http://www.w3.org/TR/cors/#simple-method) and [simple
69
+ # headers](http://www.w3.org/TR/cors/#simple-header).
70
+ #
71
+ # @param [String, Array<String>] origin The
72
+ # [origin](http://tools.ietf.org/html/rfc6454) or origins permitted
73
+ # for cross origin resource sharing with the bucket. Note: "*" is
74
+ # permitted in the list of origins, and means "any Origin".
75
+ # @param [String, Array<String>] methods The list of HTTP methods
76
+ # permitted in cross origin resource sharing with the bucket. (GET,
77
+ # OPTIONS, POST, etc) Note: "*" is permitted in the list of methods,
78
+ # and means "any method".
79
+ # @param [String, Array<String>] headers The list of header field
80
+ # names to send in the Access-Control-Allow-Headers header in the
81
+ # preflight response. Indicates the custom request headers that may
82
+ # be used in the actual request.
83
+ # @param [Integer] max_age The value to send in the
84
+ # Access-Control-Max-Age header in the preflight response. Indicates
85
+ # how many seconds the results of a preflight request can be cached
86
+ # in a preflight result cache. The default value is `1800` (30
87
+ # minutes.)
88
+ #
89
+ # @example
90
+ # require "google/cloud"
91
+ #
92
+ # gcloud = Google::Cloud.new
93
+ # storage = gcloud.storage
94
+ #
95
+ # bucket = storage.create_bucket "my-bucket" do |c|
96
+ # c.add_rule ["http://example.org", "https://example.org"],
97
+ # "*",
98
+ # response_headers: ["X-My-Custom-Header"],
99
+ # max_age: 300
100
+ # end
101
+ #
102
+ def add_rule origin, methods, headers: nil, max_age: nil
103
+ push Rule.new(origin, methods, headers: headers, max_age: max_age)
104
+ end
105
+
106
+ # @private
107
+ def to_gapi
108
+ map(&:to_gapi)
109
+ end
110
+
111
+ # @private
112
+ def self.from_gapi gapi_list
113
+ rules = Array(gapi_list).map { |gapi| Rule.from_gapi gapi }
114
+ new rules
115
+ end
116
+
117
+ # @private
118
+ def freeze
119
+ each(&:freeze)
120
+ super
121
+ end
122
+
123
+ class Rule
124
+ attr_accessor :origin, :methods, :headers, :max_age
125
+
126
+ def initialize origin, methods, headers: nil, max_age: nil
127
+ @origin = Array(origin)
128
+ @methods = Array(methods)
129
+ @headers = Array(headers)
130
+ @max_age = (max_age||1800)
131
+ end
132
+
133
+ def to_gapi
134
+ Google::Apis::StorageV1::Bucket::CorsConfiguration.new(
135
+ origin: Array(origin).dup, http_method: Array(methods).dup,
136
+ response_header: Array(headers).dup, max_age_seconds: max_age
137
+ )
138
+ end
139
+
140
+ def self.from_gapi gapi
141
+ new gapi.origin.dup, gapi.http_method.dup, \
142
+ headers: gapi.response_header.dup,
143
+ max_age: gapi.max_age_seconds
144
+ end
145
+
146
+ def freeze
147
+ @origin.freeze
148
+ @methods.freeze
149
+ @headers.freeze
150
+ super
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,174 @@
1
+ # Copyright 2015 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 "delegate"
17
+
18
+ module Google
19
+ module Cloud
20
+ module Storage
21
+ class Bucket
22
+ ##
23
+ # Bucket::List is a special case Array with additional values.
24
+ class List < DelegateClass(::Array)
25
+ ##
26
+ # If not empty, indicates that there are more buckets
27
+ # that match the request and this value should be passed to
28
+ # the next {Google::Cloud::Storage::Project#buckets} to continue.
29
+ attr_accessor :token
30
+
31
+ ##
32
+ # @private Create a new Bucket::List with an array of values.
33
+ def initialize arr = []
34
+ super arr
35
+ end
36
+
37
+ ##
38
+ # Whether there is a next page of buckets.
39
+ #
40
+ # @return [Boolean]
41
+ #
42
+ # @example
43
+ # require "google/cloud"
44
+ #
45
+ # gcloud = Google::Cloud.new
46
+ # storage = gcloud.storage
47
+ #
48
+ # buckets = storage.buckets
49
+ # if buckets.next?
50
+ # next_buckets = buckets.next
51
+ # end
52
+ #
53
+ def next?
54
+ !token.nil?
55
+ end
56
+
57
+ ##
58
+ # Retrieve the next page of buckets.
59
+ #
60
+ # @return [Bucket::List]
61
+ #
62
+ # @example
63
+ # require "google/cloud"
64
+ #
65
+ # gcloud = Google::Cloud.new
66
+ # storage = gcloud.storage
67
+ #
68
+ # buckets = storage.buckets
69
+ # if buckets.next?
70
+ # next_buckets = buckets.next
71
+ # end
72
+ #
73
+ def next
74
+ return nil unless next?
75
+ ensure_service!
76
+ options = { prefix: @prefix, token: @token, max: @max }
77
+ gapi = @service.list_buckets options
78
+ Bucket::List.from_gapi gapi, @service, @prefix, @max
79
+ end
80
+
81
+ ##
82
+ # Retrieves all buckets by repeatedly loading {#next} until {#next?}
83
+ # returns `false`. Calls the given block once for each bucket, which
84
+ # is passed as the parameter.
85
+ #
86
+ # An Enumerator is returned if no block is given.
87
+ #
88
+ # This method may make several API calls until all buckets are
89
+ # retrieved. Be sure to use as narrow a search criteria as possible.
90
+ # Please use with caution.
91
+ #
92
+ # @param [Integer] request_limit The upper limit of API requests to
93
+ # make to load all buckets. Default is no limit.
94
+ # @yield [bucket] The block for accessing each bucket.
95
+ # @yieldparam [Bucket] bucket The bucket object.
96
+ #
97
+ # @return [Enumerator]
98
+ #
99
+ # @example Iterating each bucket by passing a block:
100
+ # require "google/cloud"
101
+ #
102
+ # gcloud = Google::Cloud.new
103
+ # storage = gcloud.storage
104
+ #
105
+ # buckets = storage.buckets
106
+ # buckets.all do |bucket|
107
+ # puts bucket.name
108
+ # end
109
+ #
110
+ # @example Using the enumerator by not passing a block:
111
+ # require "google/cloud"
112
+ #
113
+ # gcloud = Google::Cloud.new
114
+ # storage = gcloud.storage
115
+ #
116
+ # buckets = storage.buckets
117
+ # all_names = buckets.all.map do |bucket|
118
+ # bucket.name
119
+ # end
120
+ #
121
+ # @example Limit the number of API calls made:
122
+ # require "google/cloud"
123
+ #
124
+ # gcloud = Google::Cloud.new
125
+ # storage = gcloud.storage
126
+ #
127
+ # buckets = storage.buckets
128
+ # buckets.all(request_limit: 10) do |bucket|
129
+ # puts bucket.name
130
+ # end
131
+ #
132
+ def all request_limit: nil
133
+ request_limit = request_limit.to_i if request_limit
134
+ unless block_given?
135
+ return enum_for(:all, request_limit: request_limit)
136
+ end
137
+ results = self
138
+ loop do
139
+ results.each { |r| yield r }
140
+ if request_limit
141
+ request_limit -= 1
142
+ break if request_limit < 0
143
+ end
144
+ break unless results.next?
145
+ results = results.next
146
+ end
147
+ end
148
+
149
+ ##
150
+ # @private New Bucket::List from a Google API Client
151
+ # Google::Apis::StorageV1::Buckets object.
152
+ def self.from_gapi gapi_list, service, prefix = nil, max = nil
153
+ buckets = new(Array(gapi_list.items).map do |gapi_object|
154
+ Bucket.from_gapi gapi_object, service
155
+ end)
156
+ buckets.instance_variable_set :@token, gapi_list.next_page_token
157
+ buckets.instance_variable_set :@service, service
158
+ buckets.instance_variable_set :@prefix, prefix
159
+ buckets.instance_variable_set :@max, max
160
+ buckets
161
+ end
162
+
163
+ protected
164
+
165
+ ##
166
+ # Raise an error unless an active connection is available.
167
+ def ensure_service!
168
+ fail "Must have active connection" unless @service
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,31 @@
1
+ # Copyright 2014 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 "google/cloud/credentials"
17
+
18
+ module Google
19
+ module Cloud
20
+ module Storage
21
+ ##
22
+ # @private Represents the OAuth 2.0 signing logic for Storage.
23
+ class Credentials < Google::Cloud::Credentials
24
+ SCOPE = ["https://www.googleapis.com/auth/devstorage.full_control"]
25
+ PATH_ENV_VARS = %w(STORAGE_KEYFILE GOOGLE_CLOUD_KEYFILE GCLOUD_KEYFILE)
26
+ JSON_ENV_VARS = %w(STORAGE_KEYFILE_JSON GOOGLE_CLOUD_KEYFILE_JSON
27
+ GCLOUD_KEYFILE_JSON)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,67 @@
1
+ # Copyright 2014 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 "google/cloud/errors"
17
+
18
+ module Google
19
+ module Cloud
20
+ module Storage
21
+ ##
22
+ # # FileVerificationError
23
+ #
24
+ # Raised when a File download fails the verification.
25
+ class FileVerificationError < Google::Cloud::Error
26
+ ##
27
+ # The type of digest that failed verification,
28
+ # :md5 or :crc32c.
29
+ attr_accessor :type
30
+
31
+ ##
32
+ # The value of the digest on the google-cloud file.
33
+ attr_accessor :gcloud_digest
34
+
35
+ ##
36
+ # The value of the digest on the downloaded file.
37
+ attr_accessor :local_digest
38
+
39
+ # @private
40
+ def self.for_md5 gcloud_digest, local_digest
41
+ new("The downloaded file failed MD5 verification.").tap do |e|
42
+ e.type = :md5
43
+ e.gcloud_digest = gcloud_digest
44
+ e.local_digest = local_digest
45
+ end
46
+ end
47
+
48
+ # @private
49
+ def self.for_crc32c gcloud_digest, local_digest
50
+ new("The downloaded file failed CRC32c verification.").tap do |e|
51
+ e.type = :crc32c
52
+ e.gcloud_digest = gcloud_digest
53
+ e.local_digest = local_digest
54
+ end
55
+ end
56
+ end
57
+
58
+ ##
59
+ # # SignedUrlUnavailable Error
60
+ #
61
+ # This is raised when File#signed_url is unable to generate a URL due to
62
+ # missing credentials needed to create the URL.
63
+ class SignedUrlUnavailable < Google::Cloud::Error
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,849 @@
1
+ # Copyright 2014 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 "google/cloud/storage/file/acl"
17
+ require "google/cloud/storage/file/list"
18
+ require "google/cloud/storage/file/verifier"
19
+
20
+ module Google
21
+ module Cloud
22
+ module Storage
23
+ ##
24
+ # # File
25
+ #
26
+ # Represents a File
27
+ # ([Object](https://cloud.google.com/storage/docs/json_api/v1/objects))
28
+ # that belongs to a {Bucket}. Files (Objects) are the individual pieces of
29
+ # data that you store in Google Cloud Storage. A file can be up to 5 TB in
30
+ # size. Files have two components: data and metadata. The data component
31
+ # is the data from an external file or other data source that you want to
32
+ # store in Google Cloud Storage. The metadata component is a collection of
33
+ # name-value pairs that describe various qualities of the data.
34
+ #
35
+ # @see https://cloud.google.com/storage/docs/concepts-techniques Concepts
36
+ # and Techniques
37
+ #
38
+ # @example
39
+ # require "google/cloud"
40
+ #
41
+ # gcloud = Google::Cloud.new
42
+ # storage = gcloud.storage
43
+ #
44
+ # bucket = storage.bucket "my-bucket"
45
+ #
46
+ # file = bucket.file "path/to/my-file.ext"
47
+ # file.download "path/to/downloaded/file.ext"
48
+ #
49
+ class File
50
+ ##
51
+ # @private The Connection object.
52
+ attr_accessor :service
53
+
54
+ ##
55
+ # @private The Google API Client object.
56
+ attr_accessor :gapi
57
+
58
+ ##
59
+ # @private Create an empty File object.
60
+ def initialize
61
+ @service = nil
62
+ @gapi = Google::Apis::StorageV1::Object.new
63
+ end
64
+
65
+ ##
66
+ # The kind of item this is.
67
+ # For files, this is always storage#object.
68
+ def kind
69
+ @gapi.kind
70
+ end
71
+
72
+ ##
73
+ # The ID of the file.
74
+ def id
75
+ @gapi.id
76
+ end
77
+
78
+ ##
79
+ # The name of this file.
80
+ def name
81
+ @gapi.name
82
+ end
83
+
84
+ ##
85
+ # The name of the {Bucket} containing this file.
86
+ def bucket
87
+ @gapi.bucket
88
+ end
89
+
90
+ ##
91
+ # The content generation of this file.
92
+ # Used for object versioning.
93
+ def generation
94
+ @gapi.generation
95
+ end
96
+
97
+ ##
98
+ # The version of the metadata for this file at this generation.
99
+ # Used for preconditions and for detecting changes in metadata.
100
+ # A metageneration number is only meaningful in the context of a
101
+ # particular generation of a particular file.
102
+ def metageneration
103
+ @gapi.metageneration
104
+ end
105
+
106
+ ##
107
+ # A URL that can be used to access the file using the REST API.
108
+ def api_url
109
+ @gapi.self_link
110
+ end
111
+
112
+ ##
113
+ # A URL that can be used to download the file using the REST API.
114
+ def media_url
115
+ @gapi.media_link
116
+ end
117
+
118
+ ##
119
+ # Content-Length of the data in bytes.
120
+ def size
121
+ @gapi.size.to_i if @gapi.size
122
+ end
123
+
124
+ ##
125
+ # Creation time of the file.
126
+ def created_at
127
+ @gapi.time_created
128
+ end
129
+
130
+ ##
131
+ # The creation or modification time of the file.
132
+ # For buckets with versioning enabled, changing an object's
133
+ # metadata does not change this property.
134
+ def updated_at
135
+ @gapi.updated
136
+ end
137
+
138
+ ##
139
+ # MD5 hash of the data; encoded using base64.
140
+ def md5
141
+ @gapi.md5_hash
142
+ end
143
+
144
+ ##
145
+ # The CRC32c checksum of the data, as described in
146
+ # [RFC 4960, Appendix B](http://tools.ietf.org/html/rfc4960#appendix-B).
147
+ # Encoded using base64 in big-endian byte order.
148
+ def crc32c
149
+ @gapi.crc32c
150
+ end
151
+
152
+ ##
153
+ # HTTP 1.1 Entity tag for the file.
154
+ def etag
155
+ @gapi.etag
156
+ end
157
+
158
+ ##
159
+ # The [Cache-Control](https://tools.ietf.org/html/rfc7234#section-5.2)
160
+ # directive for the file data.
161
+ def cache_control
162
+ @gapi.cache_control
163
+ end
164
+
165
+ ##
166
+ # Updates the
167
+ # [Cache-Control](https://tools.ietf.org/html/rfc7234#section-5.2)
168
+ # directive for the file data.
169
+ def cache_control= cache_control
170
+ @gapi.cache_control = cache_control
171
+ patch_gapi! :cache_control
172
+ end
173
+
174
+ ##
175
+ # The [Content-Disposition](https://tools.ietf.org/html/rfc6266) of the
176
+ # file data.
177
+ def content_disposition
178
+ @gapi.content_disposition
179
+ end
180
+
181
+ ##
182
+ # Updates the [Content-Disposition](https://tools.ietf.org/html/rfc6266)
183
+ # of the file data.
184
+ def content_disposition= content_disposition
185
+ @gapi.content_disposition = content_disposition
186
+ patch_gapi! :content_disposition
187
+ end
188
+
189
+ ##
190
+ # The [Content-Encoding
191
+ # ](https://tools.ietf.org/html/rfc7231#section-3.1.2.2) of the file
192
+ # data.
193
+ def content_encoding
194
+ @gapi.content_encoding
195
+ end
196
+
197
+ ##
198
+ # Updates the [Content-Encoding
199
+ # ](https://tools.ietf.org/html/rfc7231#section-3.1.2.2) of the file
200
+ # data.
201
+ def content_encoding= content_encoding
202
+ @gapi.content_encoding = content_encoding
203
+ patch_gapi! :content_encoding
204
+ end
205
+
206
+ ##
207
+ # The [Content-Language](http://tools.ietf.org/html/bcp47) of the file
208
+ # data.
209
+ def content_language
210
+ @gapi.content_language
211
+ end
212
+
213
+ ##
214
+ # Updates the [Content-Language](http://tools.ietf.org/html/bcp47) of
215
+ # the file data.
216
+ def content_language= content_language
217
+ @gapi.content_language = content_language
218
+ patch_gapi! :content_language
219
+ end
220
+
221
+ ##
222
+ # The [Content-Type](https://tools.ietf.org/html/rfc2616#section-14.17)
223
+ # of the file data.
224
+ def content_type
225
+ @gapi.content_type
226
+ end
227
+
228
+ ##
229
+ # Updates the
230
+ # [Content-Type](https://tools.ietf.org/html/rfc2616#section-14.17) of
231
+ # the file data.
232
+ def content_type= content_type
233
+ @gapi.content_type = content_type
234
+ patch_gapi! :content_type
235
+ end
236
+
237
+ ##
238
+ # A hash of custom, user-provided web-safe keys and arbitrary string
239
+ # values that will returned with requests for the file as "x-goog-meta-"
240
+ # response headers.
241
+ def metadata
242
+ m = @gapi.metadata
243
+ m = m.to_h if m.respond_to? :to_h
244
+ m.dup.freeze
245
+ end
246
+
247
+ ##
248
+ # Updates the hash of custom, user-provided web-safe keys and arbitrary
249
+ # string values that will returned with requests for the file as
250
+ # "x-goog-meta-" response headers.
251
+ def metadata= metadata
252
+ @gapi.metadata = metadata
253
+ patch_gapi! :metadata
254
+ end
255
+
256
+ ##
257
+ # An [RFC 4648](https://tools.ietf.org/html/rfc4648#section-4)
258
+ # Base64-encoded string of the SHA256 hash of the [customer-supplied
259
+ # encryption
260
+ # key](https://cloud.google.com/storage/docs/encryption#customer-supplied).
261
+ # You can use this SHA256 hash to uniquely identify the AES-256
262
+ # encryption key required to decrypt this file.
263
+ def encryption_key_sha256
264
+ return nil unless @gapi.customer_encryption
265
+ Base64.decode64 @gapi.customer_encryption.key_sha256
266
+ end
267
+
268
+ ##
269
+ # Updates the file with changes made in the given block in a single
270
+ # PATCH request. The following attributes may be set: {#cache_control=},
271
+ # {#content_disposition=}, {#content_encoding=}, {#content_language=},
272
+ # {#content_type=}, and {#metadata=}. The {#metadata} hash accessible in
273
+ # the block is completely mutable and will be included in the request.
274
+ #
275
+ # @yield [file] a block yielding a delegate object for updating the file
276
+ #
277
+ # @example
278
+ # require "google/cloud"
279
+ #
280
+ # gcloud = Google::Cloud.new
281
+ # storage = gcloud.storage
282
+ #
283
+ # bucket = storage.bucket "my-bucket"
284
+ #
285
+ # file = bucket.file "path/to/my-file.ext"
286
+ #
287
+ # file.update do |f|
288
+ # f.cache_control = "private, max-age=0, no-cache"
289
+ # f.content_disposition = "inline; filename=filename.ext"
290
+ # f.content_encoding = "deflate"
291
+ # f.content_language = "de"
292
+ # f.content_type = "application/json"
293
+ # f.metadata["player"] = "Bob"
294
+ # f.metadata["score"] = "10"
295
+ # end
296
+ #
297
+ def update
298
+ updater = Updater.new gapi
299
+ yield updater
300
+ updater.check_for_changed_metadata!
301
+ patch_gapi! updater.updates unless updater.updates.empty?
302
+ end
303
+
304
+ ##
305
+ # Download the file's contents to a local file.
306
+ #
307
+ # By default, the download is verified by calculating the MD5 digest.
308
+ #
309
+ # If a [customer-supplied encryption
310
+ # key](https://cloud.google.com/storage/docs/encryption#customer-supplied)
311
+ # was used with {Bucket#create_file}, the `encryption_key` and
312
+ # `encryption_key_sha256` options must be provided.
313
+ #
314
+ # @param [String] path The path on the local file system to write the
315
+ # data to. The path provided must be writable.
316
+ # @param [Symbol] verify The verification algoruthm used to ensure the
317
+ # downloaded file contents are correct. Default is `:md5`.
318
+ #
319
+ # Acceptable values are:
320
+ #
321
+ # * `md5` - Verify file content match using the MD5 hash.
322
+ # * `crc32c` - Verify file content match using the CRC32c hash.
323
+ # * `all` - Perform all available file content verification.
324
+ # * `none` - Don't perform file content verification.
325
+ #
326
+ # @param [String] encryption_key Optional. The customer-supplied,
327
+ # AES-256 encryption key used to encrypt the file, if one was provided
328
+ # to {Bucket#create_file}. Must be provided if `encryption_key_sha256`
329
+ # is provided.
330
+ # @param [String] encryption_key_sha256 Optional. The SHA256 hash of the
331
+ # customer-supplied, AES-256 encryption key used to encrypt the file,
332
+ # if one was provided to {Bucket#create_file}. Must be provided if
333
+ # `encryption_key` is provided.
334
+ #
335
+ # @return [File] Returns a `::File` object on the local file system
336
+ #
337
+ # @example
338
+ # require "google/cloud"
339
+ #
340
+ # gcloud = Google::Cloud.new
341
+ # storage = gcloud.storage
342
+ #
343
+ # bucket = storage.bucket "my-bucket"
344
+ #
345
+ # file = bucket.file "path/to/my-file.ext"
346
+ # file.download "path/to/downloaded/file.ext"
347
+ #
348
+ # @example Use the CRC32c digest by passing :crc32c.
349
+ # require "google/cloud"
350
+ #
351
+ # gcloud = Google::Cloud.new
352
+ # storage = gcloud.storage
353
+ #
354
+ # bucket = storage.bucket "my-bucket"
355
+ #
356
+ # file = bucket.file "path/to/my-file.ext"
357
+ # file.download "path/to/downloaded/file.ext", verify: :crc32c
358
+ #
359
+ # @example Use the MD5 and CRC32c digests by passing :all.
360
+ # require "google/cloud"
361
+ #
362
+ # gcloud = Google::Cloud.new
363
+ # storage = gcloud.storage
364
+ #
365
+ # bucket = storage.bucket "my-bucket"
366
+ #
367
+ # file = bucket.file "path/to/my-file.ext"
368
+ # file.download "path/to/downloaded/file.ext", verify: :all
369
+ #
370
+ # @example Disable the download verification by passing :none.
371
+ # require "google/cloud"
372
+ #
373
+ # gcloud = Google::Cloud.new
374
+ # storage = gcloud.storage
375
+ #
376
+ # bucket = storage.bucket "my-bucket"
377
+ #
378
+ # file = bucket.file "path/to/my-file.ext"
379
+ # file.download "path/to/downloaded/file.ext", verify: :none
380
+ #
381
+ def download path, verify: :md5, encryption_key: nil,
382
+ encryption_key_sha256: nil
383
+ ensure_service!
384
+ service.download_file \
385
+ bucket, name, path,
386
+ key: encryption_key, key_sha256: encryption_key_sha256
387
+ verify_file! ::File.new(path), verify
388
+ end
389
+
390
+ ##
391
+ # Copy the file to a new location.
392
+ #
393
+ # If a [customer-supplied encryption
394
+ # key](https://cloud.google.com/storage/docs/encryption#customer-supplied)
395
+ # was used with {Bucket#create_file}, the `encryption_key` and
396
+ # `encryption_key_sha256` options must be provided.
397
+ #
398
+ # @param [String] dest_bucket_or_path Either the bucket to copy the file
399
+ # to, or the path to copy the file to in the current bucket.
400
+ # @param [String] dest_path If a bucket was provided in the first
401
+ # parameter, this contains the path to copy the file to in the given
402
+ # bucket.
403
+ # @param [String] acl A predefined set of access controls to apply to
404
+ # new file.
405
+ #
406
+ # Acceptable values are:
407
+ #
408
+ # * `auth`, `auth_read`, `authenticated`, `authenticated_read`,
409
+ # `authenticatedRead` - File owner gets OWNER access, and
410
+ # allAuthenticatedUsers get READER access.
411
+ # * `owner_full`, `bucketOwnerFullControl` - File owner gets OWNER
412
+ # access, and project team owners get OWNER access.
413
+ # * `owner_read`, `bucketOwnerRead` - File owner gets OWNER access,
414
+ # and project team owners get READER access.
415
+ # * `private` - File owner gets OWNER access.
416
+ # * `project_private`, `projectPrivate` - File owner gets OWNER
417
+ # access, and project team members get access according to their
418
+ # roles.
419
+ # * `public`, `public_read`, `publicRead` - File owner gets OWNER
420
+ # access, and allUsers get READER access.
421
+ # @param [Integer] generation Select a specific revision of the file to
422
+ # copy. The default is the latest version.
423
+ # @param [String] encryption_key Optional. The customer-supplied,
424
+ # AES-256 encryption key used to encrypt the file, if one was provided
425
+ # to {Bucket#create_file}. Must be provided if `encryption_key_sha256`
426
+ # is provided.
427
+ # @param [String] encryption_key_sha256 Optional. The SHA256 hash of the
428
+ # customer-supplied, AES-256 encryption key used to encrypt the file,
429
+ # if one was provided to {Bucket#create_file}. Must be provided if
430
+ # `encryption_key` is provided.
431
+ #
432
+ # @return [Google::Cloud::Storage::File]
433
+ #
434
+ # @example The file can be copied to a new path in the current bucket:
435
+ # require "google/cloud"
436
+ #
437
+ # gcloud = Google::Cloud.new
438
+ # storage = gcloud.storage
439
+ #
440
+ # bucket = storage.bucket "my-bucket"
441
+ #
442
+ # file = bucket.file "path/to/my-file.ext"
443
+ # file.copy "path/to/destination/file.ext"
444
+ #
445
+ # @example The file can also be copied to a different bucket:
446
+ # require "google/cloud"
447
+ #
448
+ # gcloud = Google::Cloud.new
449
+ # storage = gcloud.storage
450
+ #
451
+ # bucket = storage.bucket "my-bucket"
452
+ #
453
+ # file = bucket.file "path/to/my-file.ext"
454
+ # file.copy "new-destination-bucket",
455
+ # "path/to/destination/file.ext"
456
+ #
457
+ # @example The file can also be copied by specifying a generation:
458
+ # file.copy "copy/of/previous/generation/file.ext",
459
+ # generation: 123456
460
+ #
461
+ def copy dest_bucket_or_path, dest_path = nil, acl: nil,
462
+ generation: nil, encryption_key: nil,
463
+ encryption_key_sha256: nil
464
+ ensure_service!
465
+ options = { acl: acl, generation: generation,
466
+ key: encryption_key, key_sha256: encryption_key_sha256 }
467
+ dest_bucket, dest_path, options = fix_copy_args dest_bucket_or_path,
468
+ dest_path, options
469
+
470
+ gapi = service.copy_file bucket, name,
471
+ dest_bucket, dest_path, options
472
+ File.from_gapi gapi, service
473
+ end
474
+
475
+ ##
476
+ # Permanently deletes the file.
477
+ #
478
+ # @return [Boolean] Returns `true` if the file was deleted.
479
+ #
480
+ # @example
481
+ # require "google/cloud"
482
+ #
483
+ # gcloud = Google::Cloud.new
484
+ # storage = gcloud.storage
485
+ #
486
+ # bucket = storage.bucket "my-bucket"
487
+ #
488
+ # file = bucket.file "path/to/my-file.ext"
489
+ # file.delete
490
+ #
491
+ def delete
492
+ ensure_service!
493
+ service.delete_file bucket, name
494
+ true
495
+ end
496
+
497
+ ##
498
+ # Public URL to access the file. If the file is not public, requests to
499
+ # the URL will return an error. (See {File::Acl#public!} and
500
+ # {Bucket::DefaultAcl#public!}) To share a file that is not public see
501
+ # {#signed_url}.
502
+ #
503
+ # @see https://cloud.google.com/storage/docs/access-public-data
504
+ # Accessing Public Data
505
+ #
506
+ # @param [String] protocol The protocol to use for the URL. Default is
507
+ # `HTTPS`.
508
+ #
509
+ # @example
510
+ # require "google/cloud"
511
+ #
512
+ # gcloud = Google::Cloud.new
513
+ # storage = gcloud.storage
514
+ #
515
+ # bucket = storage.bucket "my-todo-app"
516
+ # file = bucket.file "avatars/heidi/400x400.png"
517
+ # public_url = file.public_url
518
+ #
519
+ # @example Generate the URL with a protocol other than HTTPS:
520
+ # require "google/cloud"
521
+ #
522
+ # gcloud = Google::Cloud.new
523
+ # storage = gcloud.storage
524
+ #
525
+ # bucket = storage.bucket "my-todo-app"
526
+ # file = bucket.file "avatars/heidi/400x400.png"
527
+ # public_url = file.public_url protocol: "http"
528
+ #
529
+ def public_url protocol: :https
530
+ "#{protocol}://storage.googleapis.com/#{bucket}/#{name}"
531
+ end
532
+ alias_method :url, :public_url
533
+
534
+ ##
535
+ # Access without authentication can be granted to a File for a specified
536
+ # period of time. This URL uses a cryptographic signature of your
537
+ # credentials to access the file.
538
+ #
539
+ # Generating a URL requires service account credentials, either by
540
+ # connecting with a service account when calling
541
+ # {Google::Cloud.storage}, or by passing in the service account `issuer`
542
+ # and `signing_key` values. Although the private key can be passed as a
543
+ # string for convenience, creating and storing an instance of
544
+ # `OpenSSL::PKey::RSA` is more efficient when making multiple calls to
545
+ # `signed_url`.
546
+ #
547
+ # A {SignedUrlUnavailable} is raised if the service account credentials
548
+ # are missing. Service account credentials are acquired by following the
549
+ # steps in [Service Account Authentication](
550
+ # https://cloud.google.com/storage/docs/authentication#service_accounts).
551
+ #
552
+ # @see https://cloud.google.com/storage/docs/access-control#Signed-URLs
553
+ # Access Control Signed URLs guide
554
+ #
555
+ # @param [String] method The HTTP verb to be used with the signed URL.
556
+ # Signed URLs can be used
557
+ # with `GET`, `HEAD`, `PUT`, and `DELETE` requests. Default is `GET`.
558
+ # @param [Integer] expires The number of seconds until the URL expires.
559
+ # Default is 300/5 minutes.
560
+ # @param [String] content_type When provided, the client (browser) must
561
+ # send this value in the HTTP header. e.g. `text/plain`
562
+ # @param [String] content_md5 The MD5 digest value in base64. If you
563
+ # provide this in the string, the client (usually a browser) must
564
+ # provide this HTTP header with this same value in its request.
565
+ # @param [String] issuer Service Account's Client Email.
566
+ # @param [String] client_email Service Account's Client Email.
567
+ # @param [OpenSSL::PKey::RSA, String] signing_key Service Account's
568
+ # Private Key.
569
+ # @param [OpenSSL::PKey::RSA, String] private_key Service Account's
570
+ # Private Key.
571
+ #
572
+ # @example
573
+ # require "google/cloud"
574
+ #
575
+ # gcloud = Google::Cloud.new
576
+ # storage = gcloud.storage
577
+ #
578
+ # bucket = storage.bucket "my-todo-app"
579
+ # file = bucket.file "avatars/heidi/400x400.png"
580
+ # shared_url = file.signed_url
581
+ #
582
+ # @example Any of the option parameters may be specified:
583
+ # require "google/cloud"
584
+ #
585
+ # gcloud = Google::Cloud.new
586
+ # storage = gcloud.storage
587
+ #
588
+ # bucket = storage.bucket "my-todo-app"
589
+ # file = bucket.file "avatars/heidi/400x400.png"
590
+ # shared_url = file.signed_url method: "GET",
591
+ # expires: 300 # 5 minutes from now
592
+ #
593
+ # @example Using the `issuer` and `signing_key` options:
594
+ # require "google/cloud/storage"
595
+ #
596
+ # storage = Google::Cloud.storage
597
+ #
598
+ # bucket = storage.bucket "my-todo-app"
599
+ # file = bucket.file "avatars/heidi/400x400.png"
600
+ # key = OpenSSL::PKey::RSA.new "-----BEGIN PRIVATE KEY-----\n..."
601
+ # shared_url = file.signed_url issuer: "service-account@gcloud.com",
602
+ # signing_key: key
603
+ #
604
+ def signed_url method: nil, expires: nil, content_type: nil,
605
+ content_md5: nil, issuer: nil, client_email: nil,
606
+ signing_key: nil, private_key: nil
607
+ ensure_service!
608
+ options = { method: method, expires: expires,
609
+ content_type: content_type, content_md5: content_md5,
610
+ issuer: issuer, client_email: client_email,
611
+ signing_key: signing_key, private_key: private_key }
612
+ signer = File::Signer.new self
613
+ signer.signed_url options
614
+ end
615
+
616
+ ##
617
+ # The {File::Acl} instance used to control access to the file.
618
+ #
619
+ # A file has owners, writers, and readers. Permissions can be granted to
620
+ # an individual user's email address, a group's email address, as well
621
+ # as many predefined lists.
622
+ #
623
+ # @see https://cloud.google.com/storage/docs/access-control Access
624
+ # Control guide
625
+ #
626
+ # @example Grant access to a user by prepending `"user-"` to an email:
627
+ # require "google/cloud"
628
+ #
629
+ # gcloud = Google::Cloud.new
630
+ # storage = gcloud.storage
631
+ #
632
+ # bucket = storage.bucket "my-todo-app"
633
+ # file = bucket.file "avatars/heidi/400x400.png"
634
+ #
635
+ # email = "heidi@example.net"
636
+ # file.acl.add_reader "user-#{email}"
637
+ #
638
+ # @example Grant access to a group by prepending `"group-"` to an email:
639
+ # require "google/cloud"
640
+ #
641
+ # gcloud = Google::Cloud.new
642
+ # storage = gcloud.storage
643
+ #
644
+ # bucket = storage.bucket "my-todo-app"
645
+ # file = bucket.file "avatars/heidi/400x400.png"
646
+ #
647
+ # email = "authors@example.net"
648
+ # file.acl.add_reader "group-#{email}"
649
+ #
650
+ # @example Or, grant access via a predefined permissions list:
651
+ # require "google/cloud"
652
+ #
653
+ # gcloud = Google::Cloud.new
654
+ # storage = gcloud.storage
655
+ #
656
+ # bucket = storage.bucket "my-todo-app"
657
+ # file = bucket.file "avatars/heidi/400x400.png"
658
+ #
659
+ # file.acl.public!
660
+ #
661
+ def acl
662
+ @acl ||= File::Acl.new self
663
+ end
664
+
665
+ ##
666
+ # Reloads the file with current data from the Storage service.
667
+ def reload!
668
+ ensure_service!
669
+ @gapi = service.get_file bucket, name
670
+ end
671
+ alias_method :refresh!, :reload!
672
+
673
+ ##
674
+ # @private URI of the location and file name in the format of
675
+ # <code>gs://my-bucket/file-name.json</code>.
676
+ def to_gs_url
677
+ "gs://#{bucket}/#{name}"
678
+ end
679
+
680
+ ##
681
+ # @private New File from a Google API Client object.
682
+ def self.from_gapi gapi, service
683
+ new.tap do |f|
684
+ f.gapi = gapi
685
+ f.service = service
686
+ end
687
+ end
688
+
689
+ protected
690
+
691
+ ##
692
+ # Raise an error unless an active service is available.
693
+ def ensure_service!
694
+ fail "Must have active connection" unless service
695
+ end
696
+
697
+ def patch_gapi! *attributes
698
+ attributes.flatten!
699
+ return if attributes.empty?
700
+ ensure_service!
701
+ patch_args = Hash[attributes.map do |attr|
702
+ [attr, @gapi.send(attr)]
703
+ end]
704
+ patch_gapi = Google::Apis::StorageV1::Object.new patch_args
705
+ @gapi = service.patch_file bucket, name, patch_gapi
706
+ end
707
+
708
+ def fix_copy_args dest_bucket, dest_path, options = {}
709
+ if dest_path.respond_to?(:to_hash) && options.empty?
710
+ options = dest_path
711
+ dest_path = nil
712
+ end
713
+ if dest_path.nil?
714
+ dest_path = dest_bucket
715
+ dest_bucket = bucket
716
+ end
717
+ dest_bucket = dest_bucket.name if dest_bucket.respond_to? :name
718
+ options[:acl] = File::Acl.predefined_rule_for options[:acl]
719
+ [dest_bucket, dest_path, options]
720
+ end
721
+
722
+ def verify_file! file, verify = :md5
723
+ verify_md5 = verify == :md5 || verify == :all
724
+ verify_crc32c = verify == :crc32c || verify == :all
725
+ Verifier.verify_md5! self, file if verify_md5
726
+ Verifier.verify_crc32c! self, file if verify_crc32c
727
+ file
728
+ end
729
+
730
+ ##
731
+ # @private Create a signed_url for a file.
732
+ class Signer
733
+ def initialize file
734
+ @file = file
735
+ end
736
+
737
+ ##
738
+ # The external path to the file.
739
+ def ext_path
740
+ "/#{@file.bucket}/#{@file.name}"
741
+ end
742
+
743
+ ##
744
+ # The external url to the file.
745
+ def ext_url
746
+ "https://storage.googleapis.com#{ext_path}"
747
+ end
748
+
749
+ def apply_option_defaults options
750
+ adjusted_expires = (Time.now.utc + (options[:expires] || 300)).to_i
751
+ options[:expires] = adjusted_expires
752
+ options[:method] ||= "GET"
753
+ options
754
+ end
755
+
756
+ def signature_str options
757
+ [options[:method], options[:content_md5],
758
+ options[:content_type], options[:expires],
759
+ ext_path].join "\n"
760
+ end
761
+
762
+ def determine_signing_key options = {}
763
+ options[:signing_key] || options[:private_key] ||
764
+ @file.service.credentials.signing_key
765
+ end
766
+
767
+ def determine_issuer options = {}
768
+ options[:issuer] || options[:client_email] ||
769
+ @file.service.credentials.issuer
770
+ end
771
+
772
+ def signed_url options
773
+ options = apply_option_defaults options
774
+
775
+ i = determine_issuer options
776
+ s = determine_signing_key options
777
+
778
+ fail SignedUrlUnavailable unless i && s
779
+
780
+ sig = generate_signature s, options
781
+ generate_signed_url i, sig, options[:expires]
782
+ end
783
+
784
+ def generate_signature signing_key, options = {}
785
+ unless signing_key.respond_to? :sign
786
+ signing_key = OpenSSL::PKey::RSA.new signing_key
787
+ end
788
+ signing_key.sign OpenSSL::Digest::SHA256.new, signature_str(options)
789
+ end
790
+
791
+ def generate_signed_url issuer, signed_string, expires
792
+ signature = Base64.strict_encode64(signed_string).delete("\n")
793
+ "#{ext_url}?GoogleAccessId=#{CGI.escape issuer}" \
794
+ "&Expires=#{expires}" \
795
+ "&Signature=#{CGI.escape signature}"
796
+ end
797
+ end
798
+
799
+ ##
800
+ # Yielded to a block to accumulate changes for a patch request.
801
+ class Updater < File
802
+ attr_reader :updates
803
+ ##
804
+ # Create an Updater object.
805
+ def initialize gapi
806
+ @updates = []
807
+ @gapi = gapi
808
+ end
809
+
810
+ ##
811
+ # A hash of custom, user-provided web-safe keys and arbitrary string
812
+ # values that will returned with requests for the file as
813
+ # "x-goog-meta-" response headers.
814
+ def metadata
815
+ # do not freeze metadata
816
+ @metadata ||= @gapi.metadata.to_h.dup
817
+ end
818
+
819
+ ##
820
+ # Updates the hash of custom, user-provided web-safe keys and
821
+ # arbitrary string values that will returned with requests for the
822
+ # file as "x-goog-meta-" response headers.
823
+ def metadata= metadata
824
+ @metadata = metadata
825
+ @gapi.metadata = @metadata
826
+ patch_gapi! :metadata
827
+ end
828
+
829
+ ##
830
+ # @private Make sure any metadata changes are saved
831
+ def check_for_changed_metadata!
832
+ return if @metadata == @gapi.metadata
833
+ @gapi.metadata = @metadata
834
+ patch_gapi! :metadata
835
+ end
836
+
837
+ protected
838
+
839
+ ##
840
+ # Queue up all the updates instead of making them.
841
+ def patch_gapi! attribute
842
+ @updates << attribute
843
+ @updates.uniq!
844
+ end
845
+ end
846
+ end
847
+ end
848
+ end
849
+ end