File: batch.rb

package info (click to toggle)
ruby-google-apis-core 0.11.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 260 kB
  • sloc: ruby: 1,964; makefile: 4
file content (236 lines) | stat: -rw-r--r-- 8,334 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'google/apis/core/multipart'
require 'google/apis/core/http_command'
require 'google/apis/core/upload'
require 'google/apis/core/storage_upload'
require 'google/apis/core/download'
require 'google/apis/core/composite_io'
require 'addressable/uri'
require 'securerandom'

module Google
  module Apis
    module Core
      # Wrapper request for batching multiple calls in a single server request
      class BatchCommand < HttpCommand
        MULTIPART_MIXED = 'multipart/mixed'

        # @param [symbol] method
        #   HTTP method
        # @param [String,Addressable::URI, Addressable::Template] url
        #   HTTP URL or template
        def initialize(method, url)
          super(method, url)
          @calls = []
          @base_id = SecureRandom.uuid
        end

        ##
        # Add a new call to the batch request.
        #
        # @param [Google::Apis::Core::HttpCommand] call API Request to add
        # @yield [result, err] Result & error when response available
        # @return [Google::Apis::Core::BatchCommand] self
        def add(call, &block)
          ensure_valid_command(call)
          @calls << [call, block]
          self
        end

        protected

        ##
        # Deconstruct the batch response and process the individual results
        #
        # @param [String] content_type
        #  Content type of body
        # @param [String, #read] body
        #  Response body
        # @return [Object]
        #   Response object
        def decode_response_body(content_type, body)
          m = /.*boundary=(.+)/.match(content_type)
          if m
            parts = split_parts(body, m[1])
            deserializer = CallDeserializer.new
            parts.each_index do |index|
              response = deserializer.to_http_response(parts[index])
              outer_header = response.shift
              call_id = header_to_id(outer_header['Content-ID'].first) || index
              call, callback = @calls[call_id]
              begin
                result = call.process_response(*response) unless call.nil?
                success(result, &callback)
              rescue => e
                error(e, &callback)
              end
            end
          end
          nil
        end

        def split_parts(body, boundary)
          parts = body.split(/\r?\n?--#{Regexp.escape(boundary)}/)
          parts[1...-1]
        end

        # Encode the batch request
        # @return [void]
        # @raise [Google::Apis::BatchError] if batch is empty
        def prepare!
          fail BatchError, 'Cannot make an empty batch request' if @calls.empty?

          serializer = CallSerializer.new
          multipart = Multipart.new(content_type: MULTIPART_MIXED)
          @calls.each_index do |index|
            call, _ = @calls[index]
            content_id = id_to_header(index)
            io = serializer.to_part(call)
            multipart.add_upload(io, content_type: 'application/http', content_id: content_id)
          end
          self.body = multipart.assemble

          header['Content-Type'] = multipart.content_type
          super
        end

        def ensure_valid_command(command)
          if command.is_a?(Google::Apis::Core::BaseUploadCommand) || command.is_a?(Google::Apis::Core::DownloadCommand) || command.is_a?(Google::Apis::Core::StorageDownloadCommand) || command.is_a?(Google::Apis::Core::StorageUploadCommand)
            fail Google::Apis::ClientError, 'Can not include media requests in batch'
          end
          fail Google::Apis::ClientError, 'Invalid command object' unless command.is_a?(HttpCommand)
        end

        def id_to_header(call_id)
          return sprintf('<%s+%i>', @base_id, call_id)
        end

        def header_to_id(content_id)
          match = /<response-.*\+(\d+)>/.match(content_id)
          return match[1].to_i if match
          return nil
        end

      end

      # Wrapper request for batching multiple uploads in a single server request
      class BatchUploadCommand < BatchCommand
        def ensure_valid_command(command)
          fail Google::Apis::ClientError, 'Can only include upload commands in batch' \
            unless command.is_a?(Google::Apis::Core::BaseUploadCommand)
        end

        def prepare!
          header['X-Goog-Upload-Protocol'] = 'batch'
          super
        end
      end

      # Serializes a command for embedding in a multipart batch request
      # @private
      class CallSerializer
        ##
        # Serialize a single batched call for assembling the multipart message
        #
        # @param [Google::Apis::Core::HttpCommand] call
        #   the call to serialize.
        # @return [IO]
        #   the serialized request
        def to_part(call)
          call.prepare!
          # This will add the Authorization header if needed.
          call.apply_request_options(call.header)
          parts = []
          parts << build_head(call)
          parts << build_body(call) unless call.body.nil?
          Google::Apis::Core::CompositeIO.new(*parts)
        end

        protected

        def build_head(call)
          request_head = "#{call.method.to_s.upcase} #{Addressable::URI.parse(call.url).request_uri} HTTP/1.1"
          call.header.each do |key, value|
            request_head << sprintf("\r\n%s: %s", key, value)
          end
          request_head << sprintf("\r\nHost: %s", call.url.host)
          request_head << "\r\n\r\n"
          StringIO.new(request_head)
        end

        def build_body(call)
          return nil if call.body.nil?
          return call.body if call.body.respond_to?(:read)
          StringIO.new(call.body)
        end
      end

      # Deconstructs a raw HTTP response part
      # @private
      class CallDeserializer
        # Parse a batched response.
        #
        # @param [String] call_response
        #   the response to parse.
        # @return [Array<(Fixnum, Hash, String)>]
        #   Status, header, and response body.
        def to_http_response(call_response)
          outer_header, outer_body = split_header_and_body(call_response)
          status_line, payload = outer_body.split(/\n/, 2)
          _, status = status_line.split(' ', 3)

          header, body = split_header_and_body(payload)
          [outer_header, status.to_i, header, body]
        end

        protected

        # Auxiliary method to split the header from the body in an HTTP response.
        #
        # @param [String] response
        #   the response to parse.
        # @return [Array<(HTTP::Message::Headers, String)>]
        #   the header and the body, separately.
        def split_header_and_body(response)
          header = HTTP::Message::Headers.new
          payload = response.lstrip
          while payload
            line, payload = payload.split(/\n/, 2)
            line.sub!(/\s+\z/, '')
            break if line.empty?
            match = /\A([^:]+):\s*/.match(line)
            fail BatchError, sprintf('Invalid header line in response: %s', line) if match.nil?
            header[match[1]] = match.post_match
          end
          [header, payload]
        end
      end
    end
  end
end