google-cloud-storage 0.20.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.
@@ -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