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,236 @@
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
+ # Copyright 2015 Google Inc.
15
+ #
16
+ # Licensed under the Apache License, Version 2.0 (the "License");
17
+ # you may not use this file except in compliance with the License.
18
+ # You may obtain a copy of the License at
19
+ #
20
+ # http://www.apache.org/licenses/LICENSE-2.0
21
+ #
22
+ # Unless required by applicable law or agreed to in writing, software
23
+ # distributed under the License is distributed on an "AS IS" BASIS,
24
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25
+ # See the License for the specific language governing permissions and
26
+ # limitations under the License.
27
+
28
+ require 'google/apis/core/multipart'
29
+ require 'google/apis/core/http_command'
30
+ require 'google/apis/core/upload'
31
+ require 'google/apis/core/download'
32
+ require 'google/apis/core/composite_io'
33
+ require 'addressable/uri'
34
+ require 'securerandom'
35
+
36
+ module Google
37
+ module Apis
38
+ module Core
39
+ # Wrapper request for batching multiple calls in a single server request
40
+ class BatchCommand < HttpCommand
41
+ MULTIPART_MIXED = 'multipart/mixed'
42
+
43
+ # @param [symbol] method
44
+ # HTTP method
45
+ # @param [String,Addressable::URI, Addressable::Template] url
46
+ # HTTP URL or template
47
+ def initialize(method, url)
48
+ super(method, url)
49
+ @calls = []
50
+ @base_id = SecureRandom.uuid
51
+ end
52
+
53
+ ##
54
+ # Add a new call to the batch request.
55
+ #
56
+ # @param [Google::Apis::Core::HttpCommand] call API Request to add
57
+ # @yield [result, err] Result & error when response available
58
+ # @return [Google::Apis::Core::BatchCommand] self
59
+ def add(call, &block)
60
+ ensure_valid_command(call)
61
+ @calls << [call, block]
62
+ self
63
+ end
64
+
65
+ protected
66
+
67
+ ##
68
+ # Deconstruct the batch response and process the individual results
69
+ #
70
+ # @param [String] content_type
71
+ # Content type of body
72
+ # @param [String, #read] body
73
+ # Response body
74
+ # @return [Object]
75
+ # Response object
76
+ def decode_response_body(content_type, body)
77
+ m = /.*boundary=(.+)/.match(content_type)
78
+ if m
79
+ parts = split_parts(body, m[1])
80
+ deserializer = CallDeserializer.new
81
+ parts.each_index do |index|
82
+ response = deserializer.to_http_response(parts[index])
83
+ outer_header = response.shift
84
+ call_id = header_to_id(outer_header['Content-ID'].first) || index
85
+ call, callback = @calls[call_id]
86
+ begin
87
+ result = call.process_response(*response) unless call.nil?
88
+ success(result, &callback)
89
+ rescue => e
90
+ error(e, &callback)
91
+ end
92
+ end
93
+ end
94
+ nil
95
+ end
96
+
97
+ def split_parts(body, boundary)
98
+ parts = body.split(/\r?\n?--#{Regexp.escape(boundary)}/)
99
+ parts[1...-1]
100
+ end
101
+
102
+ # Encode the batch request
103
+ # @return [void]
104
+ # @raise [Google::Apis::BatchError] if batch is empty
105
+ def prepare!
106
+ fail BatchError, 'Cannot make an empty batch request' if @calls.empty?
107
+
108
+ serializer = CallSerializer.new
109
+ multipart = Multipart.new(content_type: MULTIPART_MIXED)
110
+ @calls.each_index do |index|
111
+ call, _ = @calls[index]
112
+ content_id = id_to_header(index)
113
+ io = serializer.to_part(call)
114
+ multipart.add_upload(io, content_type: 'application/http', content_id: content_id)
115
+ end
116
+ self.body = multipart.assemble
117
+
118
+ header['Content-Type'] = multipart.content_type
119
+ super
120
+ end
121
+
122
+ def ensure_valid_command(command)
123
+ if command.is_a?(Google::Apis::Core::BaseUploadCommand) || command.is_a?(Google::Apis::Core::DownloadCommand)
124
+ fail Google::Apis::ClientError, 'Can not include media requests in batch'
125
+ end
126
+ fail Google::Apis::ClientError, 'Invalid command object' unless command.is_a?(HttpCommand)
127
+ end
128
+
129
+ def id_to_header(call_id)
130
+ return sprintf('<%s+%i>', @base_id, call_id)
131
+ end
132
+
133
+ def header_to_id(content_id)
134
+ match = /<response-.*\+(\d+)>/.match(content_id)
135
+ return match[1].to_i if match
136
+ return nil
137
+ end
138
+
139
+ end
140
+
141
+ # Wrapper request for batching multiple uploads in a single server request
142
+ class BatchUploadCommand < BatchCommand
143
+ def ensure_valid_command(command)
144
+ fail Google::Apis::ClientError, 'Can only include upload commands in batch' \
145
+ unless command.is_a?(Google::Apis::Core::BaseUploadCommand)
146
+ end
147
+
148
+ def prepare!
149
+ header['X-Goog-Upload-Protocol'] = 'batch'
150
+ super
151
+ end
152
+ end
153
+
154
+ # Serializes a command for embedding in a multipart batch request
155
+ # @private
156
+ class CallSerializer
157
+ ##
158
+ # Serialize a single batched call for assembling the multipart message
159
+ #
160
+ # @param [Google::Apis::Core::HttpCommand] call
161
+ # the call to serialize.
162
+ # @return [IO]
163
+ # the serialized request
164
+ def to_part(call)
165
+ call.prepare!
166
+ # This will add the Authorization header if needed.
167
+ call.apply_request_options(call.header)
168
+ parts = []
169
+ parts << build_head(call)
170
+ parts << build_body(call) unless call.body.nil?
171
+ length = parts.inject(0) { |a, e| a + e.length }
172
+ Google::Apis::Core::CompositeIO.new(*parts)
173
+ end
174
+
175
+ protected
176
+
177
+ def build_head(call)
178
+ request_head = "#{call.method.to_s.upcase} #{Addressable::URI.parse(call.url).request_uri} HTTP/1.1"
179
+ call.header.each do |key, value|
180
+ request_head << sprintf("\r\n%s: %s", key, value)
181
+ end
182
+ request_head << sprintf("\r\nHost: %s", call.url.host)
183
+ request_head << "\r\n\r\n"
184
+ StringIO.new(request_head)
185
+ end
186
+
187
+ def build_body(call)
188
+ return nil if call.body.nil?
189
+ return call.body if call.body.respond_to?(:read)
190
+ StringIO.new(call.body)
191
+ end
192
+ end
193
+
194
+ # Deconstructs a raw HTTP response part
195
+ # @private
196
+ class CallDeserializer
197
+ # Parse a batched response.
198
+ #
199
+ # @param [String] call_response
200
+ # the response to parse.
201
+ # @return [Array<(Fixnum, Hash, String)>]
202
+ # Status, header, and response body.
203
+ def to_http_response(call_response)
204
+ outer_header, outer_body = split_header_and_body(call_response)
205
+ status_line, payload = outer_body.split(/\n/, 2)
206
+ _, status = status_line.split(' ', 3)
207
+
208
+ header, body = split_header_and_body(payload)
209
+ [outer_header, status.to_i, header, body]
210
+ end
211
+
212
+ protected
213
+
214
+ # Auxiliary method to split the header from the body in an HTTP response.
215
+ #
216
+ # @param [String] response
217
+ # the response to parse.
218
+ # @return [Array<(HTTP::Message::Headers, String)>]
219
+ # the header and the body, separately.
220
+ def split_header_and_body(response)
221
+ header = HTTP::Message::Headers.new
222
+ payload = response.lstrip
223
+ while payload
224
+ line, payload = payload.split(/\n/, 2)
225
+ line.sub!(/\s+\z/, '')
226
+ break if line.empty?
227
+ match = /\A([^:]+):\s*/.match(line)
228
+ fail BatchError, sprintf('Invalid header line in response: %s', line) if match.nil?
229
+ header[match[1]] = match.post_match
230
+ end
231
+ [header, payload]
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,97 @@
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
+ # Copyright 2015 Google Inc.
15
+ #
16
+ # Licensed under the Apache License, Version 2.0 (the "License");
17
+ # you may not use this file except in compliance with the License.
18
+ # You may obtain a copy of the License at
19
+ #
20
+ # http://www.apache.org/licenses/LICENSE-2.0
21
+ #
22
+ # Unless required by applicable law or agreed to in writing, software
23
+ # distributed under the License is distributed on an "AS IS" BASIS,
24
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25
+ # See the License for the specific language governing permissions and
26
+ # limitations under the License.
27
+
28
+ require 'google/apis/core/http_command'
29
+ require 'google/apis/core/upload'
30
+ require 'google/apis/core/download'
31
+ require 'addressable/uri'
32
+ require 'securerandom'
33
+ module Google
34
+ module Apis
35
+ module Core
36
+ class CompositeIO
37
+ def initialize(*ios)
38
+ @ios = ios.flatten
39
+ @pos = 0
40
+ @index = 0
41
+ @sizes = @ios.map(&:size)
42
+ end
43
+
44
+ def read(length = nil, buf = nil)
45
+ buf = buf ? buf.replace('') : ''
46
+
47
+ begin
48
+ io = @ios[@index]
49
+ break if io.nil?
50
+ result = io.read(length)
51
+ if result
52
+ buf << result
53
+ if length
54
+ length -= result.length
55
+ break if length == 0
56
+ end
57
+ end
58
+ @index += 1
59
+ end while @index < @ios.length
60
+ buf.length > 0 ? buf : nil
61
+ end
62
+
63
+ def size
64
+ @sizes.reduce(:+)
65
+ end
66
+
67
+ alias_method :length, :size
68
+
69
+ def pos
70
+ @pos
71
+ end
72
+
73
+ def pos=(pos)
74
+ fail ArgumentError, "Position can not be negative" if pos < 0
75
+ @pos = pos
76
+ new_index = nil
77
+ @ios.each_with_index do |io,idx|
78
+ size = io.size
79
+ if pos <= size
80
+ new_index ||= idx
81
+ io.pos = pos
82
+ pos = 0
83
+ else
84
+ io.pos = size
85
+ pos -= size
86
+ end
87
+ end
88
+ @index = new_index unless new_index.nil?
89
+ end
90
+
91
+ def rewind
92
+ self.pos = 0
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,118 @@
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/api_command'
16
+ require 'google/apis/errors'
17
+ require 'addressable/uri'
18
+
19
+ module Google
20
+ module Apis
21
+ module Core
22
+ # Streaming/resumable media download support
23
+ class DownloadCommand < ApiCommand
24
+ RANGE_HEADER = 'Range'
25
+ OK_STATUS = [200, 201, 206]
26
+
27
+ # File or IO to write content to
28
+ # @return [String, File, #write]
29
+ attr_accessor :download_dest
30
+
31
+ # Ensure the download destination is a writable stream.
32
+ #
33
+ # @return [void]
34
+ def prepare!
35
+ @state = :start
36
+ @download_url = nil
37
+ @offset = 0
38
+ if download_dest.respond_to?(:write)
39
+ @download_io = download_dest
40
+ @close_io_on_finish = false
41
+ elsif download_dest.is_a?(String)
42
+ @download_io = File.open(download_dest, 'wb')
43
+ @close_io_on_finish = true
44
+ else
45
+ @download_io = StringIO.new('', 'wb')
46
+ @close_io_on_finish = false
47
+ end
48
+ super
49
+ end
50
+
51
+ # Close IO stream when command done. Only closes the stream if it was opened by the command.
52
+ def release!
53
+ @download_io.close if @close_io_on_finish
54
+ end
55
+
56
+ # Execute the upload request once. Overrides the default implementation to handle streaming/chunking
57
+ # of file content.
58
+ #
59
+ # @private
60
+ # @param [HTTPClient] client
61
+ # HTTP client
62
+ # @yield [result, err] Result or error if block supplied
63
+ # @return [Object]
64
+ # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
65
+ # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
66
+ # @raise [Google::Apis::AuthorizationError] Authorization is required
67
+ def execute_once(client, &block)
68
+ request_header = header.dup
69
+ apply_request_options(request_header)
70
+ download_offset = nil
71
+
72
+ if @offset > 0
73
+ logger.debug { sprintf('Resuming download from offset %d', @offset) }
74
+ request_header[RANGE_HEADER] = sprintf('bytes=%d-', @offset)
75
+ end
76
+
77
+ http_res = client.get(url.to_s,
78
+ query: query,
79
+ header: request_header,
80
+ follow_redirect: true) do |res, chunk|
81
+ status = res.http_header.status_code.to_i
82
+ next unless OK_STATUS.include?(status)
83
+
84
+ download_offset ||= (status == 206 ? @offset : 0)
85
+ download_offset += chunk.bytesize
86
+
87
+ if download_offset - chunk.bytesize == @offset
88
+ next_chunk = chunk
89
+ else
90
+ # Oh no! Requested a chunk, but received the entire content
91
+ chunk_index = @offset - (download_offset - chunk.bytesize)
92
+ next_chunk = chunk.byteslice(chunk_index..-1)
93
+ next if next_chunk.nil?
94
+ end
95
+
96
+ # logger.debug { sprintf('Writing chunk (%d bytes, %d total)', chunk.length, bytes_read) }
97
+ @download_io.write(next_chunk)
98
+
99
+ @offset += next_chunk.bytesize
100
+ end
101
+
102
+ @download_io.flush if @download_io.respond_to?(:flush)
103
+
104
+ if @close_io_on_finish
105
+ result = nil
106
+ else
107
+ result = @download_io
108
+ end
109
+ check_status(http_res.status.to_i, http_res.header, http_res.body)
110
+ success(result, &block)
111
+ rescue => e
112
+ @download_io.flush if @download_io.respond_to?(:flush)
113
+ error(e, rethrow: true, &block)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end