google-apis-core 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,153 @@
1
+ # Copyright 2020 Google LLC
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
+ require 'representable/json'
16
+ require 'representable/json/hash'
17
+ require 'base64'
18
+ require 'date'
19
+
20
+ module Google
21
+ module Apis
22
+ module Core
23
+ # Support for serializing hashes + property value/nil/unset tracking
24
+ # To be included in representers as a feature.
25
+ # @private
26
+ module JsonRepresentationSupport
27
+ def self.included(base)
28
+ base.extend(JsonSupport)
29
+ end
30
+
31
+ # @private
32
+ module JsonSupport
33
+ # Returns a customized getter function for Representable. Allows
34
+ # indifferent hash/attribute access.
35
+ #
36
+ # @param [String] name Property name
37
+ # @return [Proc]
38
+ def getter_fn(name)
39
+ ivar_name = "@#{name}".to_sym
40
+ lambda do |_|
41
+ if respond_to?(:fetch)
42
+ fetch(name, instance_variable_get(ivar_name))
43
+ else
44
+ instance_variable_get(ivar_name)
45
+ end
46
+ end
47
+ end
48
+
49
+ # Returns a customized function for Representable that checks whether or not
50
+ # an attribute should be serialized. Allows proper patch semantics by distinguishing
51
+ # between nil & unset values
52
+ #
53
+ # @param [String] name Property name
54
+ # @return [Proc]
55
+ def if_fn(name)
56
+ ivar_name = "@#{name}".to_sym
57
+ lambda do |opts|
58
+ if opts[:user_options] && opts[:user_options][:skip_undefined]
59
+ if respond_to?(:key?)
60
+ self.key?(name) || instance_variable_defined?(ivar_name)
61
+ else
62
+ instance_variable_defined?(ivar_name)
63
+ end
64
+ else
65
+ true
66
+ end
67
+ end
68
+ end
69
+
70
+ def set_default_options(name, options)
71
+ if options[:base64]
72
+ options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : Base64.urlsafe_encode64(value) }
73
+ options[:parse_filter] = ->(fragment, _doc, *_args) { Base64.urlsafe_decode64(fragment) }
74
+ end
75
+ if options[:numeric_string]
76
+ options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : value.to_s}
77
+ options[:parse_filter] = ->(fragment, _doc, *_args) { fragment.to_i }
78
+ end
79
+ if options[:type] == DateTime
80
+ options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : value.is_a?(DateTime) ? value.rfc3339(3) : value.to_s }
81
+ options[:parse_filter] = ->(fragment, _doc, *_args) { DateTime.parse(fragment) }
82
+ end
83
+ if options[:type] == Date
84
+ options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : value.to_s}
85
+ options[:parse_filter] = ->(fragment, _doc, *_args) { Date.parse(fragment) }
86
+ end
87
+
88
+ options[:render_nil] = true
89
+ options[:getter] = getter_fn(name)
90
+ options[:if] = if_fn(name)
91
+ end
92
+
93
+ # Define a single value property
94
+ #
95
+ # @param [String] name
96
+ # Property name
97
+ # @param [Hash] options
98
+ def property(name, options = {})
99
+ set_default_options(name, options)
100
+ super(name, options)
101
+ end
102
+
103
+ # Define a collection property
104
+ #
105
+ # @param [String] name
106
+ # Property name
107
+ # @param [Hash] options
108
+ def collection(name, options = {})
109
+ set_default_options(name, options)
110
+ super(name, options)
111
+ end
112
+
113
+ # Define a hash property
114
+ #
115
+ # @param [String] name
116
+ # Property name
117
+ # @param [Hash] options
118
+ def hash(name = nil, options = nil)
119
+ return super() unless name # Allow Object.hash
120
+ set_default_options(name, options)
121
+ super(name, options)
122
+ end
123
+ end
124
+ end
125
+
126
+ # Base decorator for JSON representers
127
+ #
128
+ # @see https://github.com/apotonick/representable
129
+ class JsonRepresentation < Representable::Decorator
130
+ include Representable::JSON
131
+ feature JsonRepresentationSupport
132
+ end
133
+
134
+ module JsonObjectSupport
135
+ def self.included(base)
136
+ base.extend(ClassMethods)
137
+ end
138
+
139
+ module ClassMethods
140
+ def from_json(json)
141
+ representation = self.const_get(:Representation)
142
+ representation.new(self.new).from_json(json, unwrap: self)
143
+ end
144
+ end
145
+
146
+ def to_json(*a)
147
+ representation = self.class.const_get(:Representation)
148
+ representation.new(self).to_hash(user_options: { skip_undefined: true }).to_json(*a)
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,30 @@
1
+ # Copyright 2020 Google LLC
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
+ require 'google/apis'
16
+
17
+ module Google
18
+ module Apis
19
+ module Core
20
+ # Logging support
21
+ module Logging
22
+ # Get the logger instance
23
+ # @return [Logger]
24
+ def logger
25
+ Google::Apis.logger
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,135 @@
1
+ # Copyright 2020 Google LLC
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
+ module Google
17
+ module Apis
18
+ module Core
19
+ # Part of a multipart request for holding JSON data
20
+ #
21
+ # @private
22
+ class JsonPart
23
+
24
+ # @param [String] value
25
+ # JSON content
26
+ # @param [Hash] header
27
+ # Additional headers
28
+ def initialize(value, header = {})
29
+ @value = value
30
+ @header = header
31
+ end
32
+
33
+ def to_io(boundary)
34
+ part = ''
35
+ part << "--#{boundary}\r\n"
36
+ part << "Content-Type: application/json\r\n"
37
+ @header.each do |(k, v)|
38
+ part << "#{k}: #{v}\r\n"
39
+ end
40
+ part << "\r\n"
41
+ part << "#{@value}\r\n"
42
+ StringIO.new(part)
43
+ end
44
+
45
+ end
46
+
47
+ # Part of a multipart request for holding arbitrary content.
48
+ #
49
+ # @private
50
+ class FilePart
51
+ # @param [IO] io
52
+ # IO stream
53
+ # @param [Hash] header
54
+ # Additional headers
55
+ def initialize(io, header = {})
56
+ @io = io
57
+ @header = header
58
+ @length = io.respond_to?(:size) ? io.size : nil
59
+ end
60
+
61
+ def to_io(boundary)
62
+ head = ''
63
+ head << "--#{boundary}\r\n"
64
+ @header.each do |(k, v)|
65
+ head << "#{k}: #{v}\r\n"
66
+ end
67
+ head << "Content-Length: #{@length}\r\n" unless @length.nil?
68
+ head << "Content-Transfer-Encoding: binary\r\n"
69
+ head << "\r\n"
70
+ Google::Apis::Core::CompositeIO.new(StringIO.new(head), @io, StringIO.new("\r\n"))
71
+ end
72
+ end
73
+
74
+ # Helper for building multipart requests
75
+ class Multipart
76
+ MULTIPART_RELATED = 'multipart/related'
77
+
78
+ # @return [String]
79
+ # Content type header
80
+ attr_reader :content_type
81
+
82
+ # @param [String] content_type
83
+ # Content type for the multipart request
84
+ # @param [String] boundary
85
+ # Part delimiter
86
+
87
+ def initialize(content_type: MULTIPART_RELATED, boundary: nil)
88
+ @parts = []
89
+ @boundary = boundary || Digest::SHA1.hexdigest(SecureRandom.random_bytes(8))
90
+ @content_type = "#{content_type}; boundary=#{@boundary}"
91
+ end
92
+
93
+ # Append JSON data part
94
+ #
95
+ # @param [String] body
96
+ # JSON text
97
+ # @param [String] content_id
98
+ # Optional unique ID of this part
99
+ # @return [self]
100
+ def add_json(body, content_id: nil)
101
+ header = {}
102
+ header['Content-ID'] = content_id unless content_id.nil?
103
+ @parts << Google::Apis::Core::JsonPart.new(body, header).to_io(@boundary)
104
+ self
105
+ end
106
+
107
+ # Append arbitrary data as a part
108
+ #
109
+ # @param [IO] upload_io
110
+ # IO stream
111
+ # @param [String] content_id
112
+ # Optional unique ID of this part
113
+ # @return [self]
114
+ def add_upload(upload_io, content_type: nil, content_id: nil)
115
+ header = {
116
+ 'Content-Type' => content_type || 'application/octet-stream'
117
+ }
118
+ header['Content-Id'] = content_id unless content_id.nil?
119
+ @parts << Google::Apis::Core::FilePart.new(upload_io,
120
+ header).to_io(@boundary)
121
+ self
122
+ end
123
+
124
+ # Assemble the multipart requests
125
+ #
126
+ # @return [IO]
127
+ # IO stream
128
+ def assemble
129
+ @parts << StringIO.new("--#{@boundary}--\r\n\r\n")
130
+ Google::Apis::Core::CompositeIO.new(*@parts)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,273 @@
1
+ # Copyright 2020 Google LLC
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
+ require 'google/apis/core/multipart'
16
+ require 'google/apis/core/http_command'
17
+ require 'google/apis/core/api_command'
18
+ require 'google/apis/errors'
19
+ require 'addressable/uri'
20
+ require 'tempfile'
21
+ require 'mini_mime'
22
+
23
+ module Google
24
+ module Apis
25
+ module Core
26
+ # Base upload command. Not intended to be used directly
27
+ # @private
28
+ class BaseUploadCommand < ApiCommand
29
+ UPLOAD_PROTOCOL_HEADER = 'X-Goog-Upload-Protocol'
30
+ UPLOAD_CONTENT_TYPE_HEADER = 'X-Goog-Upload-Header-Content-Type'
31
+ UPLOAD_CONTENT_LENGTH = 'X-Goog-Upload-Header-Content-Length'
32
+ CONTENT_TYPE_HEADER = 'Content-Type'
33
+
34
+ # File name or IO containing the content to upload
35
+ # @return [String, File, #read]
36
+ attr_accessor :upload_source
37
+
38
+ # Content type of the upload material
39
+ # @return [String]
40
+ attr_accessor :upload_content_type
41
+
42
+ # Content, as UploadIO
43
+ # @return [Google::Apis::Core::UploadIO]
44
+ attr_accessor :upload_io
45
+
46
+ # Ensure the content is readable and wrapped in an IO instance.
47
+ #
48
+ # @return [void]
49
+ # @raise [Google::Apis::ClientError] if upload source is invalid
50
+ def prepare!
51
+ super
52
+ if streamable?(upload_source)
53
+ self.upload_io = upload_source
54
+ @close_io_on_finish = false
55
+ elsif self.upload_source.is_a?(String)
56
+ self.upload_io = File.new(upload_source, 'r')
57
+ if self.upload_content_type.nil?
58
+ type = MiniMime.lookup_by_filename(upload_source)
59
+ self.upload_content_type = type && type.content_type
60
+ end
61
+ @close_io_on_finish = true
62
+ else
63
+ fail Google::Apis::ClientError, 'Invalid upload source'
64
+ end
65
+ if self.upload_content_type.nil? || self.upload_content_type.empty?
66
+ self.upload_content_type = 'application/octet-stream'
67
+ end
68
+ end
69
+
70
+ # Close IO stream when command done. Only closes the stream if it was opened by the command.
71
+ def release!
72
+ upload_io.close if @close_io_on_finish
73
+ end
74
+
75
+ private
76
+
77
+ def streamable?(upload_source)
78
+ upload_source.is_a?(IO) || upload_source.is_a?(StringIO) || upload_source.is_a?(Tempfile)
79
+ end
80
+ end
81
+
82
+ # Implementation of the raw upload protocol
83
+ class RawUploadCommand < BaseUploadCommand
84
+ RAW_PROTOCOL = 'raw'
85
+
86
+ # Ensure the content is readable and wrapped in an {{Google::Apis::Core::UploadIO}} instance.
87
+ #
88
+ # @return [void]
89
+ # @raise [Google::Apis::ClientError] if upload source is invalid
90
+ def prepare!
91
+ super
92
+ self.body = upload_io
93
+ header[UPLOAD_PROTOCOL_HEADER] = RAW_PROTOCOL
94
+ header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type
95
+ end
96
+ end
97
+
98
+ # Implementation of the multipart upload protocol
99
+ class MultipartUploadCommand < BaseUploadCommand
100
+ MULTIPART_PROTOCOL = 'multipart'
101
+ MULTIPART_RELATED = 'multipart/related'
102
+
103
+ # Encode the multipart request
104
+ #
105
+ # @return [void]
106
+ # @raise [Google::Apis::ClientError] if upload source is invalid
107
+ def prepare!
108
+ super
109
+ multipart = Multipart.new
110
+ multipart.add_json(body)
111
+ multipart.add_upload(upload_io, content_type: upload_content_type)
112
+ self.body = multipart.assemble
113
+ header['Content-Type'] = multipart.content_type
114
+ header[UPLOAD_PROTOCOL_HEADER] = MULTIPART_PROTOCOL
115
+ end
116
+ end
117
+
118
+ # Implementation of the resumable upload protocol
119
+ class ResumableUploadCommand < BaseUploadCommand
120
+ UPLOAD_COMMAND_HEADER = 'X-Goog-Upload-Command'
121
+ UPLOAD_OFFSET_HEADER = 'X-Goog-Upload-Offset'
122
+ BYTES_RECEIVED_HEADER = 'X-Goog-Upload-Size-Received'
123
+ UPLOAD_URL_HEADER = 'X-Goog-Upload-URL'
124
+ UPLOAD_STATUS_HEADER = 'X-Goog-Upload-Status'
125
+ STATUS_ACTIVE = 'active'
126
+ STATUS_FINAL = 'final'
127
+ STATUS_CANCELLED = 'cancelled'
128
+ RESUMABLE = 'resumable'
129
+ START_COMMAND = 'start'
130
+ QUERY_COMMAND = 'query'
131
+ UPLOAD_COMMAND = 'upload, finalize'
132
+
133
+ # Reset upload to initial state.
134
+ #
135
+ # @return [void]
136
+ # @raise [Google::Apis::ClientError] if upload source is invalid
137
+ def prepare!
138
+ @state = :start
139
+ @upload_url = nil
140
+ @offset = 0
141
+ # Prevent the command from populating the body with form encoding, by
142
+ # asserting that it already has a body. Form encoding is never used
143
+ # by upload requests.
144
+ self.body = '' unless self.body
145
+ super
146
+ end
147
+
148
+ # Check the to see if the upload is complete or needs to be resumed.
149
+ #
150
+ # @param [Fixnum] status
151
+ # HTTP status code of response
152
+ # @param [HTTP::Message::Headers] header
153
+ # Response headers
154
+ # @param [String, #read] body
155
+ # Response body
156
+ # @return [Object]
157
+ # Response object
158
+ # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
159
+ # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
160
+ # @raise [Google::Apis::AuthorizationError] Authorization is required
161
+ def process_response(status, header, body)
162
+ @offset = Integer(header[BYTES_RECEIVED_HEADER].first) unless header[BYTES_RECEIVED_HEADER].empty?
163
+ @upload_url = header[UPLOAD_URL_HEADER].first unless header[UPLOAD_URL_HEADER].empty?
164
+ upload_status = header[UPLOAD_STATUS_HEADER].first
165
+ logger.debug { sprintf('Upload status %s', upload_status) }
166
+ if upload_status == STATUS_ACTIVE
167
+ @state = :active
168
+ elsif upload_status == STATUS_FINAL
169
+ @state = :final
170
+ elsif upload_status == STATUS_CANCELLED
171
+ @state = :cancelled
172
+ fail Google::Apis::ClientError, body
173
+ end
174
+ super(status, header, body)
175
+ end
176
+
177
+ def send_start_command(client)
178
+ logger.debug { sprintf('Sending upload start command to %s', url) }
179
+
180
+ request_header = header.dup
181
+ apply_request_options(request_header)
182
+ request_header[UPLOAD_PROTOCOL_HEADER] = RESUMABLE
183
+ request_header[UPLOAD_COMMAND_HEADER] = START_COMMAND
184
+ request_header[UPLOAD_CONTENT_LENGTH] = upload_io.size.to_s
185
+ request_header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type
186
+
187
+ client.request(method.to_s.upcase,
188
+ url.to_s, query: nil,
189
+ body: body,
190
+ header: request_header,
191
+ follow_redirect: true)
192
+ rescue => e
193
+ raise Google::Apis::ServerError, e.message
194
+ end
195
+
196
+ # Query for the status of an incomplete upload
197
+ #
198
+ # @param [HTTPClient] client
199
+ # HTTP client
200
+ # @return [HTTP::Message]
201
+ # @raise [Google::Apis::ServerError] Unable to send the request
202
+ def send_query_command(client)
203
+ logger.debug { sprintf('Sending upload query command to %s', @upload_url) }
204
+
205
+ request_header = header.dup
206
+ apply_request_options(request_header)
207
+ request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
208
+
209
+ client.post(@upload_url, body: '', header: request_header, follow_redirect: true)
210
+ end
211
+
212
+
213
+ # Send the actual content
214
+ #
215
+ # @param [HTTPClient] client
216
+ # HTTP client
217
+ # @return [HTTP::Message]
218
+ # @raise [Google::Apis::ServerError] Unable to send the request
219
+ def send_upload_command(client)
220
+ logger.debug { sprintf('Sending upload command to %s', @upload_url) }
221
+
222
+ content = upload_io
223
+ content.pos = @offset
224
+
225
+ request_header = header.dup
226
+ apply_request_options(request_header)
227
+ request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
228
+ request_header[UPLOAD_COMMAND_HEADER] = UPLOAD_COMMAND
229
+ request_header[UPLOAD_OFFSET_HEADER] = @offset.to_s
230
+ request_header[CONTENT_TYPE_HEADER] = upload_content_type
231
+
232
+ client.post(@upload_url, body: content, header: request_header, follow_redirect: true)
233
+ end
234
+
235
+ # Execute the upload request once. This will typically perform two HTTP requests -- one to initiate or query
236
+ # for the status of the upload, the second to send the (remaining) content.
237
+ #
238
+ # @private
239
+ # @param [HTTPClient] client
240
+ # HTTP client
241
+ # @yield [result, err] Result or error if block supplied
242
+ # @return [Object]
243
+ # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
244
+ # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
245
+ # @raise [Google::Apis::AuthorizationError] Authorization is required
246
+ def execute_once(client, &block)
247
+ case @state
248
+ when :start
249
+ response = send_start_command(client)
250
+ result = process_response(response.status_code, response.header, response.body)
251
+ when :active
252
+ response = send_query_command(client)
253
+ result = process_response(response.status_code, response.header, response.body)
254
+ when :cancelled, :final
255
+ error(@last_error, rethrow: true, &block)
256
+ end
257
+ if @state == :active
258
+ response = send_upload_command(client)
259
+ result = process_response(response.status_code, response.header, response.body)
260
+ end
261
+
262
+ success(result, &block) if @state == :final
263
+ rescue => e
264
+ # Some APIs like Youtube generate non-retriable 401 errors and mark
265
+ # the upload as finalized. Save the error just in case we get
266
+ # retried.
267
+ @last_error = e
268
+ error(e, rethrow: true, &block)
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end