google-apis-core 0.17.0 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b72f0bd125a4b9a5bbefb344ca6906f60c2da97299f477a411f4174a5a1e125
4
- data.tar.gz: f60c015250f7df95b17c06a13e30a038f02f63d964940fb35b1be3596b455226
3
+ metadata.gz: 2e4864fdbdd96f55fa077d183ac6c25f87d16509b48bd270803c73f81d4fa47b
4
+ data.tar.gz: 869598753c71741fcd2a4f474b464a609d6fcb2b6513c9c87d75ff1365192d07
5
5
  SHA512:
6
- metadata.gz: 7a08ed2a45ee09dd772fe046fa7b245663d9c39c12f26aab8a15d836d78a5bd1b98d6d9b97d4b97fc86dfba57e068074c7f484555ff1acea1b9ecba7bf699a34
7
- data.tar.gz: b7125709ef96626805cf00bd31638b628e1425964a73021c0efd0a62d488a01037a9055371dde53ebf54c64905526de682474d006ae68072e081f423b3954618
6
+ metadata.gz: 4bfce12bdbf8dd48e4856b77902d7fef1343ae940400e56090b1d946ed26e786b1f39ae17f3aa60dfc63e3594411c48eb5711957ad7a0f55c136fab03e45edb8
7
+ data.tar.gz: 21c7758ca4994eef7b077b19b10affcc112a909834f3b7dc5b346df42712ea3d68cccabf8131c869410ce3091f679509f39f834d5f05fbb31ea8c14ec40d8a45
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Release History
2
2
 
3
+ ### 1.0.0 (2025-08-03)
4
+
5
+ This is a major release that replaces the underlying httpclient library with Faraday ([#23524](https://github.com/googleapis/google-api-ruby-client/issues/23524)). This will ensure the client libraries are on a more stable and better maintained foundation moving forward.
6
+
7
+ For most users, this change should be transparent. However, if your application depends on the httpclient interfaces, you can retain compatibility with httpclient by pinning the `google-apis-core` gem to `~> 0.18` in your Gemfile. Httpclient-based versions of this gem will remain on the 0.x release train, while Faraday-based versions will occupy the 1.x release train. We will push critical fixes and security updates to both branches for one year until August 2026, but new feature work will take place only on the 1.x branch.
8
+
9
+ ### 0.18.0 (2025-05-22)
10
+
11
+ #### Features
12
+
13
+ * Restart & delete resumable upload ([#21896](https://github.com/googleapis/google-api-ruby-client/issues/21896))
14
+
3
15
  ### 0.17.0 (2025-04-30)
4
16
 
5
17
  #### Features
data/OVERVIEW.md CHANGED
@@ -4,6 +4,24 @@ This library includes common base classes and dependencies used by legacy REST
4
4
  clients for Google APIs. It is used by client libraries, but you should not
5
5
  need to install it by itself.
6
6
 
7
+ ## Usage
8
+
9
+ In most cases, this library is installed automatically as a dependency of
10
+ another library. For example, if you install the
11
+ [google-apis-drive_v3](https://rubygems.org/gems/google-apis-drive_v3) client
12
+ library, it will bring in the latest `google-apis-core` as a dependency. Thus,
13
+ in most cases, you do not need to add `google-apis-core` to your Gemfile
14
+ directly.
15
+
16
+ Earlier (0.x) versions of this library utilized the legacy
17
+ [httpclient](https://rubygems.org/gems/httpclient) gem and made some of its
18
+ interfaces available for advanced use cases. Version 1.0 and later of this
19
+ library replaced httpclient with [faraday](https://rubygems.org/gems/faraday).
20
+ If your application makes use of the httpclient interfaces (this is rare), you
21
+ should pin `google-apis-core` to a 0.x version in your Gemfile. For example:
22
+
23
+ gem "google-apis-core", "~> 0.18"
24
+
7
25
  ## Documentation
8
26
 
9
27
  More detailed descriptions of the Google legacy REST clients are available in two documents.
@@ -18,13 +18,13 @@ require 'google/apis'
18
18
  require 'google/apis/core/version'
19
19
  require 'google/apis/core/api_command'
20
20
  require 'google/apis/core/batch'
21
+ require 'google/apis/core/faraday_integration'
21
22
  require 'google/apis/core/upload'
22
23
  require 'google/apis/core/storage_upload'
23
24
  require 'google/apis/core/download'
24
25
  require 'google/apis/core/storage_download'
25
26
  require 'google/apis/options'
26
27
  require 'googleauth'
27
- require 'httpclient'
28
28
 
29
29
  module Google
30
30
  module Apis
@@ -149,8 +149,8 @@ module Google
149
149
  # @return [Addressable::URI]
150
150
  attr_accessor :batch_path
151
151
 
152
- # HTTP client
153
- # @return [HTTPClient]
152
+ # Faraday HTTP connection
153
+ # @return [Faraday::Connection]
154
154
  attr_writer :client
155
155
 
156
156
  # General settings
@@ -263,8 +263,8 @@ module Google
263
263
  batch_command.execute(client)
264
264
  end
265
265
 
266
- # Get the current HTTP client
267
- # @return [HTTPClient]
266
+ # Get the current HTTP connection
267
+ # @return [Faraday::Connection]
268
268
  def client
269
269
  @client ||= new_client
270
270
  end
@@ -350,6 +350,36 @@ module Google
350
350
  true
351
351
  end
352
352
 
353
+ # Restarts An interrupted Resumable upload
354
+ # @param [String] bucket
355
+ # Name of the bucket where the upload is being performed.
356
+ # @param [IO, String] upload_source
357
+ # IO stream or filename containing content to upload
358
+ # @param [IO, String] upload_id
359
+ # unique id generated for an ongoing upload
360
+
361
+ def restart_resumable_upload(bucket, upload_source, upload_id, options: nil)
362
+ command = make_storage_upload_command(:put, 'b/{bucket}/o', options)
363
+ command.upload_source = upload_source
364
+ command.upload_id = upload_id
365
+ command.params['bucket'] = bucket unless bucket.nil?
366
+ execute_or_queue_command(command)
367
+ end
368
+
369
+ # Deletes An interrupted Resumable upload
370
+ # @param [String] bucket
371
+ # Name of the bucket where the upload is being performed.
372
+ # @param [IO, String] upload_id
373
+ # unique id generated for an ongoing upload
374
+
375
+ def delete_resumable_upload(bucket, upload_id, options: nil)
376
+ command = make_storage_upload_command(:delete, 'b/{bucket}/o', options)
377
+ command.upload_id = upload_id
378
+ command.params['bucket'] = bucket unless bucket.nil?
379
+ command.delete_upload = true
380
+ execute_or_queue_command(command)
381
+ end
382
+
353
383
  protected
354
384
 
355
385
  # Create a new upload command.
@@ -512,39 +542,22 @@ module Google
512
542
  Thread.current[:google_api_batch_service] = nil
513
543
  end
514
544
 
515
- # Create a new HTTP client
516
- # @return [HTTPClient]
545
+ # Create a new HTTP connection
546
+ # @return [Faraday::Connection]
517
547
  def new_client
518
- client = ::HTTPClient.new
519
-
520
- if client_options.transparent_gzip_decompression
521
- client.transparent_gzip_decompression = client_options.transparent_gzip_decompression
522
- end
523
-
524
- client.proxy = client_options.proxy_url if client_options.proxy_url
525
-
526
- if client_options.open_timeout_sec
527
- client.connect_timeout = client_options.open_timeout_sec
548
+ options = {}
549
+ request_options = {params_encoder: Faraday::FlatParamsEncoder}
550
+ options[:proxy] = {uri: client_options.proxy_url} if client_options.proxy_url
551
+ request_options[:open_timeout] = client_options.open_timeout_sec if client_options.open_timeout_sec
552
+ request_options[:read_timeout] = client_options.read_timeout_sec if client_options.read_timeout_sec
553
+ request_options[:write_timeout] = client_options.send_timeout_sec if client_options.send_timeout_sec
554
+ options[:request] = request_options unless request_options.empty?
555
+ options[:headers] = { 'User-Agent' => user_agent }
556
+
557
+ Faraday.new options do |faraday|
558
+ faraday.response :logger, Google::Apis.logger if client_options.log_http_requests
559
+ faraday.response :follow_redirects_google_apis_core, limit: 5
528
560
  end
529
-
530
- if client_options.read_timeout_sec
531
- client.receive_timeout = client_options.read_timeout_sec
532
- end
533
-
534
- if client_options.send_timeout_sec
535
- client.send_timeout = client_options.send_timeout_sec
536
- end
537
-
538
- client.follow_redirect_count = 5
539
- client.default_header = { 'User-Agent' => user_agent }
540
-
541
- client.debug_dev = logger if client_options.log_http_requests
542
-
543
- # Make HttpClient use system default root CA path
544
- # https://github.com/nahi/httpclient/issues/445
545
- client.ssl_config.clear_cert_store
546
- client.ssl_config.cert_store.set_default_paths
547
- client
548
561
  end
549
562
 
550
563
 
@@ -82,7 +82,7 @@ module Google
82
82
  parts.each_index do |index|
83
83
  response = deserializer.to_http_response(parts[index])
84
84
  outer_header = response.shift
85
- call_id = header_to_id(outer_header['Content-ID'].first) || index
85
+ call_id = header_to_id(Array(outer_header['Content-ID']).first) || index
86
86
  call, callback = @calls[call_id]
87
87
  begin
88
88
  result = call.process_response(*response) unless call.nil?
@@ -211,14 +211,14 @@ module Google
211
211
 
212
212
  protected
213
213
 
214
- # Auxiliary method to split the header from the body in an HTTP response.
214
+ # Auxiliary method to split the headers from the body in an HTTP response.
215
215
  #
216
216
  # @param [String] response
217
217
  # the response to parse.
218
- # @return [Array<(HTTP::Message::Headers, String)>]
219
- # the header and the body, separately.
218
+ # @return [Array<(Hash{String=>Array<String>}, String)>]
219
+ # the headers and the body, separately.
220
220
  def split_header_and_body(response)
221
- header = HTTP::Message::Headers.new
221
+ headers = {}
222
222
  payload = response.lstrip
223
223
  while payload
224
224
  line, payload = payload.split(/\n/, 2)
@@ -226,9 +226,9 @@ module Google
226
226
  break if line.empty?
227
227
  match = /\A([^:]+):\s*/.match(line)
228
228
  fail BatchError, sprintf('Invalid header line in response: %s', line) if match.nil?
229
- header[match[1]] = match.post_match
229
+ (headers[match[1]] ||= []) << match.post_match
230
230
  end
231
- [header, payload]
231
+ [headers, payload]
232
232
  end
233
233
  end
234
234
  end
@@ -23,6 +23,8 @@ module Google
23
23
  # Streaming/resumable media download support
24
24
  class DownloadCommand < ApiCommand
25
25
  RANGE_HEADER = 'Range'
26
+
27
+ # @deprecated No longer used
26
28
  OK_STATUS = [200, 201, 206]
27
29
 
28
30
  # File or IO to write content to
@@ -61,8 +63,7 @@ module Google
61
63
  # of file content.
62
64
  #
63
65
  # @private
64
- # @param [HTTPClient] client
65
- # HTTP client
66
+ # @param [Faraday::Connection] client Faraday connection
66
67
  # @yield [result, err] Result or error if block supplied
67
68
  # @return [Object]
68
69
  # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
@@ -78,39 +79,39 @@ module Google
78
79
  request_header[RANGE_HEADER] = sprintf('bytes=%d-', @offset)
79
80
  end
80
81
 
81
- http_res = client.get(url.to_s,
82
- query: query,
83
- header: request_header,
84
- follow_redirect: true) do |res, chunk|
85
- status = res.http_header.status_code.to_i
86
- next unless OK_STATUS.include?(status)
82
+ http_res = client.get(url.to_s, query, request_header) do |request|
83
+ request.options.on_data = proc do |chunk, _size, res|
84
+ status = res.status.to_i
85
+ next if chunk.nil? || (status >= 300 && status < 400)
87
86
 
88
- download_offset ||= (status == 206 ? @offset : 0)
89
- download_offset += chunk.bytesize
87
+ # HTTP 206 is Partial Content
88
+ download_offset ||= (status == 206 ? @offset : 0)
89
+ download_offset += chunk.bytesize
90
90
 
91
- if download_offset - chunk.bytesize == @offset
92
- next_chunk = chunk
93
- else
94
- # Oh no! Requested a chunk, but received the entire content
95
- chunk_index = @offset - (download_offset - chunk.bytesize)
96
- next_chunk = chunk.byteslice(chunk_index..-1)
97
- next if next_chunk.nil?
98
- end
91
+ if download_offset - chunk.bytesize == @offset
92
+ next_chunk = chunk
93
+ else
94
+ # Oh no! Requested a chunk, but received the entire content
95
+ chunk_index = @offset - (download_offset - chunk.bytesize)
96
+ next_chunk = chunk.byteslice(chunk_index..-1)
97
+ next if next_chunk.nil?
98
+ end
99
99
 
100
- # logger.debug { sprintf('Writing chunk (%d bytes, %d total)', chunk.length, bytes_read) }
101
- @download_io.write(next_chunk)
100
+ # logger.debug { sprintf('Writing chunk (%d bytes, %d total)', chunk.length, bytes_read) }
101
+ @download_io.write(next_chunk)
102
102
 
103
- @offset += next_chunk.bytesize
103
+ @offset += next_chunk.bytesize
104
+ end
104
105
  end
105
106
 
106
- @download_io.flush if @download_io.respond_to?(:flush)
107
+ @download_io.flush if @download_io.respond_to?(:flush)
107
108
 
108
109
  if @close_io_on_finish
109
110
  result = nil
110
111
  else
111
112
  result = @download_io
112
113
  end
113
- check_status(http_res.status.to_i, http_res.header, http_res.body)
114
+ check_status(http_res.status.to_i, http_res.headers, http_res.body)
114
115
  success(result, &block)
115
116
  rescue => e
116
117
  @download_io.flush if @download_io.respond_to?(:flush)
@@ -0,0 +1,47 @@
1
+ # Copyright 2025 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 "faraday"
16
+ require "faraday/follow_redirects"
17
+
18
+ module Google
19
+ module Apis
20
+ module Core
21
+ # Customized version of the FollowRedirects middleware that does not
22
+ # trigger on 308. HttpCommand wants to handle 308 itself for resumable
23
+ # uploads.
24
+ class FollowRedirectsMiddleware < Faraday::FollowRedirects::Middleware
25
+ def follow_redirect?(env, response)
26
+ super && response.status != 308
27
+ end
28
+ end
29
+
30
+ Faraday::Response.register_middleware(follow_redirects_google_apis_core: FollowRedirectsMiddleware)
31
+
32
+ # Customized subclass of Faraday::Response with additional capabilities
33
+ # needed by older versions of some downstream dependencies.
34
+ class Response < Faraday::Response
35
+ # Compatibility alias.
36
+ # Earlier versions based on the old `httpclient` gem used `HTTP::Message`,
37
+ # which defined the `header` field that some clients, notably
38
+ # google-cloud-storage, depend on.
39
+ # Faraday's `headers` isn't an exact replacement because its values are
40
+ # single strings whereas `HTTP::Message` values are arrays, but
41
+ # google-cloud-storage already passes the result through `Array()` so this
42
+ # should work sufficiently.
43
+ alias header headers
44
+ end
45
+ end
46
+ end
47
+ end
@@ -17,6 +17,7 @@ require 'addressable/template'
17
17
  require 'google/apis/options'
18
18
  require 'google/apis/errors'
19
19
  require 'retriable'
20
+ require 'google/apis/core/faraday_integration'
20
21
  require 'google/apis/core/logging'
21
22
  require 'pp'
22
23
 
@@ -59,8 +60,8 @@ module Google
59
60
  # @return [symbol]
60
61
  attr_accessor :method
61
62
 
62
- # HTTP Client
63
- # @return [HTTPClient]
63
+ # Faraday connection
64
+ # @return [Faraday::Connection]
64
65
  attr_accessor :connection
65
66
 
66
67
  # Query params
@@ -96,8 +97,8 @@ module Google
96
97
 
97
98
  # Execute the command, retrying as necessary
98
99
  #
99
- # @param [HTTPClient] client
100
- # HTTP client
100
+ # @param [Faraday::Connection] client
101
+ # Faraday connection
101
102
  # @yield [result, err] Result or error if block supplied
102
103
  # @return [Object]
103
104
  # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
@@ -181,7 +182,7 @@ module Google
181
182
  @form_encoded = false
182
183
  end
183
184
 
184
- self.body = '' unless self.body
185
+ self.body = '' if self.body.nil? && [:post, :put, :patch].include?(method)
185
186
  end
186
187
 
187
188
  # Release any resources used by this command
@@ -205,7 +206,7 @@ module Google
205
206
  # @raise [Google::Apis::AuthorizationError] Authorization is required
206
207
  def process_response(status, header, body)
207
208
  check_status(status, header, body)
208
- decode_response_body(header['Content-Type'].first, body)
209
+ decode_response_body(Array(header['Content-Type']).first, body)
209
210
  end
210
211
 
211
212
  # Check the response and raise error if needed
@@ -284,17 +285,10 @@ module Google
284
285
  # @raise [StandardError] if no block
285
286
  def error(err, rethrow: false, &block)
286
287
  logger.debug { sprintf('Error - %s', PP.pp(err, +'')) }
287
- if err.is_a?(HTTPClient::BadResponseError)
288
- begin
289
- res = err.res
290
- raise Google::Apis::TransmissionError.new(err) if res.nil?
291
- check_status(res.status.to_i, res.header, res.body)
292
- rescue Google::Apis::Error => e
293
- err = e
294
- end
295
- elsif err.is_a?(HTTPClient::TimeoutError) ||
288
+ if err.is_a?(Faraday::FollowRedirects::RedirectLimitReached)
289
+ err = Google::Apis::RedirectError.new(err)
290
+ elsif err.is_a?(Faraday::Error) ||
296
291
  err.is_a?(SocketError) ||
297
- err.is_a?(HTTPClient::KeepAliveDisconnected) ||
298
292
  err.is_a?(Errno::ECONNREFUSED) ||
299
293
  err.is_a?(Errno::ETIMEDOUT) ||
300
294
  err.is_a?(Errno::ECONNRESET)
@@ -307,8 +301,8 @@ module Google
307
301
  # Execute the command once.
308
302
  #
309
303
  # @private
310
- # @param [HTTPClient] client
311
- # HTTP client
304
+ # @param [Faraday::Connection] client
305
+ # Faraday connection
312
306
  # @return [Object]
313
307
  # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
314
308
  # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
@@ -320,15 +314,11 @@ module Google
320
314
  request_header = header.dup
321
315
  apply_request_options(request_header)
322
316
 
323
- @http_res = client.request(method.to_s.upcase,
324
- url.to_s,
325
- query: nil,
326
- body: body,
327
- header: request_header,
328
- follow_redirect: true)
317
+ @http_res = client.run_request(method, url.to_s, body, request_header)
318
+
329
319
  logger.debug { @http_res.status }
330
320
  logger.debug { safe_single_line_representation @http_res }
331
- response = process_response(@http_res.status.to_i, @http_res.header, @http_res.body)
321
+ response = process_response(@http_res.status.to_i, @http_res.headers, @http_res.body)
332
322
  success(response)
333
323
  rescue => e
334
324
  logger.debug { sprintf('Caught error %s', e) }
@@ -410,9 +400,14 @@ module Google
410
400
  @opencensus_span.put_attribute "http.host", url.host.to_s
411
401
  @opencensus_span.put_attribute "http.method", method.to_s.upcase
412
402
  @opencensus_span.put_attribute "http.path", url.path.to_s
413
- if body.respond_to? :bytesize
414
- @opencensus_span.put_message_event \
415
- OpenCensus::Trace::SpanBuilder::SENT, 1, body.bytesize
403
+ sent_size =
404
+ if body.respond_to? :bytesize
405
+ body.bytesize
406
+ elsif body.nil?
407
+ 0
408
+ end
409
+ if sent_size
410
+ @opencensus_span.put_message_event OpenCensus::Trace::SpanBuilder::SENT, 1, sent_size
416
411
  end
417
412
 
418
413
  formatter = OpenCensus::Trace.config.http_formatter
@@ -30,8 +30,7 @@ module Google
30
30
  # here too.
31
31
  #
32
32
  # @private
33
- # @param [HTTPClient] client
34
- # HTTP client
33
+ # @param [Faraday::Connection] client Faraday connection
35
34
  # @yield [result, err] Result or error if block supplied
36
35
  # @return [Object]
37
36
  # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
@@ -47,41 +46,45 @@ module Google
47
46
  request_header[RANGE_HEADER] = sprintf('bytes=%d-', @offset)
48
47
  end
49
48
 
50
- http_res = client.get(url.to_s,
51
- query: query,
52
- header: request_header,
53
- follow_redirect: true) do |res, chunk|
54
- status = res.http_header.status_code.to_i
55
- next unless OK_STATUS.include?(status)
49
+ http_res = client.get(url.to_s, query, request_header) do |request|
50
+ request.options.on_data = proc do |chunk, _size, res|
51
+ status = res.status.to_i
52
+ next if chunk.nil? || (status >= 300 && status < 400)
56
53
 
57
- download_offset ||= (status == 206 ? @offset : 0)
58
- download_offset += chunk.bytesize
54
+ download_offset ||= (status == 206 ? @offset : 0)
55
+ download_offset += chunk.bytesize
59
56
 
60
- if download_offset - chunk.bytesize == @offset
61
- next_chunk = chunk
62
- else
63
- # Oh no! Requested a chunk, but received the entire content
64
- chunk_index = @offset - (download_offset - chunk.bytesize)
65
- next_chunk = chunk.byteslice(chunk_index..-1)
66
- next if next_chunk.nil?
67
- end
57
+ if download_offset - chunk.bytesize == @offset
58
+ next_chunk = chunk
59
+ else
60
+ # Oh no! Requested a chunk, but received the entire content
61
+ chunk_index = @offset - (download_offset - chunk.bytesize)
62
+ next_chunk = chunk.byteslice(chunk_index..-1)
63
+ next if next_chunk.nil?
64
+ end
68
65
 
69
- # logger.debug { sprintf('Writing chunk (%d bytes, %d total)', chunk.length, bytes_read) }
70
- @download_io.write(next_chunk)
66
+ # logger.debug { sprintf('Writing chunk (%d bytes, %d total)', chunk.length, bytes_read) }
67
+ @download_io.write(next_chunk)
71
68
 
72
- @offset += next_chunk.bytesize
69
+ @offset += next_chunk.bytesize
70
+ end
73
71
  end
74
72
 
75
- @download_io.flush if @download_io.respond_to?(:flush)
73
+ @download_io.flush if @download_io.respond_to?(:flush)
76
74
 
77
75
  if @close_io_on_finish
78
76
  result = nil
79
77
  else
80
78
  result = @download_io
81
79
  end
82
- check_status(http_res.status.to_i, http_res.header, http_res.body)
83
- # In case of file download in storage, we need to respond back with http
84
- # header along with the actual object.
80
+ check_status(http_res.status.to_i, http_res.headers, http_res.body)
81
+ # In case of file download in storage, we need to respond back with
82
+ # the http response object along with the result IO object, because
83
+ # google-cloud-storage uses the HTTP info.
84
+ # Also, older versions of google-cloud-storage assume this object
85
+ # conforms to the old httpclient response API instead of the Faraday
86
+ # response API. Return a subclass that provides the needed methods.
87
+ http_res = Core::Response.new http_res.env
85
88
  success([result, http_res], &block)
86
89
  rescue => e
87
90
  @download_io.flush if @download_io.respond_to?(:flush)
@@ -49,6 +49,14 @@ module Google
49
49
  # @return [Integer]
50
50
  attr_accessor :upload_chunk_size
51
51
 
52
+ # Unique upload_id of a resumable upload
53
+ # @return [String]
54
+ attr_accessor :upload_id
55
+
56
+ # Boolean Value to specify is a resumable upload is to be deleted or not
57
+ # @return [Boolean]
58
+ attr_accessor :delete_upload
59
+
52
60
  # Ensure the content is readable and wrapped in an IO instance.
53
61
  #
54
62
  # @return [void]
@@ -61,7 +69,6 @@ module Google
61
69
  # asserting that it already has a body. Form encoding is never used
62
70
  # by upload requests.
63
71
  self.body = '' unless self.body
64
-
65
72
  super
66
73
  if streamable?(upload_source)
67
74
  self.upload_io = upload_source
@@ -73,6 +80,8 @@ module Google
73
80
  self.upload_content_type = type&.content_type
74
81
  end
75
82
  @close_io_on_finish = true
83
+ elsif !upload_id.nil? && delete_upload
84
+ @close_io_on_finish = false
76
85
  else
77
86
  fail Google::Apis::ClientError, 'Invalid upload source'
78
87
  end
@@ -80,13 +89,12 @@ module Google
80
89
 
81
90
  # Close IO stream when command done. Only closes the stream if it was opened by the command.
82
91
  def release!
83
- upload_io.close if @close_io_on_finish
92
+ upload_io.close if @close_io_on_finish && !upload_io.nil?
84
93
  end
85
94
 
86
95
  # Execute the command, retrying as necessary
87
96
  #
88
- # @param [HTTPClient] client
89
- # HTTP client
97
+ # @param [Faraday::Connection] client Faraday connection
90
98
  # @yield [result, err] Result or error if block supplied
91
99
  # @return [Object]
92
100
  # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
@@ -96,8 +104,16 @@ module Google
96
104
  prepare!
97
105
  opencensus_begin_span
98
106
  @upload_chunk_size = options.upload_chunk_size
107
+ if upload_id.nil?
108
+ res = do_retry :initiate_resumable_upload, client
109
+ elsif delete_upload && !upload_id.nil?
110
+ construct_resumable_upload_url upload_id
111
+ res = do_retry :cancel_resumable_upload, client
112
+ else
113
+ construct_resumable_upload_url upload_id
114
+ res = do_retry :reinitiate_resumable_upload, client
115
+ end
99
116
 
100
- do_retry :initiate_resumable_upload, client
101
117
  while @upload_incomplete
102
118
  res = do_retry :send_upload_command, client
103
119
  end
@@ -121,20 +137,34 @@ module Google
121
137
  request_header[CONTENT_TYPE_HEADER] = JSON_CONTENT_TYPE
122
138
  request_header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type unless upload_content_type.nil?
123
139
 
124
- response = client.post(url.to_s, query: request_query,
125
- body: body,
126
- header: request_header,
127
- follow_redirect: true)
128
- result = process_response(response.status_code, response.header, response.body)
140
+ response = client.post(url.to_s, body, request_header) do |request|
141
+ request.params.replace(request_query)
142
+ end
143
+ result = process_response(response.status.to_i, response.headers, response.body)
129
144
  success(result)
130
145
  rescue => e
131
146
  error(e, rethrow: true)
132
147
  end
133
148
 
149
+ # Reinitiating resumable upload
150
+ def reinitiate_resumable_upload(client)
151
+ logger.debug { sprintf('Restarting resumable upload command to %s', url) }
152
+ check_resumable_upload client
153
+ upload_io.pos = @offset
154
+ end
155
+
156
+ # Making resumable upload url from upload_id
157
+ def construct_resumable_upload_url(upload_id)
158
+ query_params = query.dup
159
+ query_params['uploadType'] = RESUMABLE
160
+ query_params['upload_id'] = upload_id
161
+ resumable_upload_params = query_params.map { |key, value| "#{key}=#{value}" }.join('&')
162
+ @upload_url = "#{url}&#{resumable_upload_params}"
163
+ end
164
+
134
165
  # Send the actual content
135
166
  #
136
- # @param [HTTPClient] client
137
- # HTTP client
167
+ # @param [Faraday::Connection] client Faraday connection
138
168
  # @return [HTTP::Message]
139
169
  # @raise [Google::Apis::ServerError] Unable to send the request
140
170
  def send_upload_command(client)
@@ -145,7 +175,7 @@ module Google
145
175
 
146
176
  request_header = header.dup
147
177
  request_header[CONTENT_RANGE_HEADER] = get_content_range_header current_chunk_size
148
- request_header[CONTENT_LENGTH_HEADER] = current_chunk_size
178
+ request_header[CONTENT_LENGTH_HEADER] = current_chunk_size.to_s
149
179
  chunk_body =
150
180
  if @upload_chunk_size == 0
151
181
  upload_io
@@ -153,13 +183,16 @@ module Google
153
183
  StringIO.new(upload_io.read(current_chunk_size))
154
184
  end
155
185
 
156
- response = client.put(@upload_url, body: chunk_body, header: request_header, follow_redirect: true)
186
+ response = client.put(@upload_url, chunk_body, request_header)
157
187
 
158
- result = process_response(response.status_code, response.header, response.body)
159
- @upload_incomplete = false if response.status_code.eql? OK_STATUS
188
+ result = process_response(response.status.to_i, response.headers, response.body)
189
+ @upload_incomplete = false if response.status.to_i.eql? OK_STATUS
160
190
  @offset += current_chunk_size if @upload_incomplete
161
191
  success(result)
162
192
  rescue => e
193
+ logger.warn {
194
+ "error occured please use uploadId-#{response.headers['X-GUploader-UploadID']} to resume your upload"
195
+ } unless response.nil?
163
196
  upload_io.pos = @offset
164
197
  error(e, rethrow: true)
165
198
  end
@@ -178,10 +211,64 @@ module Google
178
211
  # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
179
212
  # @raise [Google::Apis::AuthorizationError] Authorization is required
180
213
  def process_response(status, header, body)
181
- @upload_url = header[LOCATION_HEADER].first unless header[LOCATION_HEADER].empty?
214
+ location_header = Array(header[LOCATION_HEADER])
215
+ @upload_url = location_header.first unless location_header.empty?
182
216
  super(status, header, body)
183
217
  end
184
218
 
219
+ def check_resumable_upload(client)
220
+ # Setting up request header
221
+ request_header = header.dup
222
+ request_header[CONTENT_RANGE_HEADER] = "bytes */#{upload_io.size}"
223
+ request_header[CONTENT_LENGTH_HEADER] = '0'
224
+ # Initiating call
225
+ response = client.put(@upload_url, "", request_header)
226
+ handle_resumable_upload_http_response_codes(response)
227
+ end
228
+
229
+ # Cancel resumable upload
230
+ def cancel_resumable_upload(client)
231
+ # Setting up request header
232
+ request_header = header.dup
233
+ request_header[CONTENT_LENGTH_HEADER] = '0'
234
+ # Initiating call
235
+ response = client.delete(@upload_url, nil, request_header)
236
+ handle_resumable_upload_http_response_codes(response)
237
+
238
+ if !@upload_incomplete && (400..499).include?(response.status.to_i)
239
+ @close_io_on_finish = true
240
+ true # method returns true if upload is successfully cancelled
241
+ else
242
+ logger.debug { sprintf("Failed to cancel upload session. Response: #{response.status.to_i} - #{response.body}") }
243
+ end
244
+
245
+ end
246
+
247
+ def handle_resumable_upload_http_response_codes(response)
248
+ code = response.status.to_i
249
+
250
+ case code
251
+ when 308
252
+ if response.headers['Range']
253
+ range = Array(response.headers['Range']).first
254
+ @offset = range.split('-').last.to_i + 1
255
+ logger.debug { sprintf("Upload is incomplete. Bytes uploaded so far: #{range}") }
256
+ else
257
+ logger.debug { sprintf('No bytes uploaded yet.') }
258
+ end
259
+ @upload_incomplete = true
260
+ when 400..499
261
+ # Upload is canceled
262
+ @upload_incomplete = false
263
+ when 200, 201
264
+ # Upload is complete.
265
+ @upload_incomplete = false
266
+ else
267
+ logger.debug { sprintf("Unexpected response: #{response.status.to_i} - #{response.body}") }
268
+ @upload_incomplete = true
269
+ end
270
+ end
271
+
185
272
  def streamable?(upload_source)
186
273
  upload_source.is_a?(IO) || upload_source.is_a?(StringIO) || upload_source.is_a?(Tempfile)
187
274
  end
@@ -30,6 +30,7 @@ module Google
30
30
  UPLOAD_CONTENT_TYPE_HEADER = 'X-Goog-Upload-Header-Content-Type'
31
31
  UPLOAD_CONTENT_LENGTH = 'X-Goog-Upload-Header-Content-Length'
32
32
  CONTENT_TYPE_HEADER = 'Content-Type'
33
+ CONTENT_LENGTH_HEADER = "Content-Length"
33
34
 
34
35
  # File name or IO containing the content to upload
35
36
  # @return [String, File, #read]
@@ -159,9 +160,11 @@ module Google
159
160
  # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
160
161
  # @raise [Google::Apis::AuthorizationError] Authorization is required
161
162
  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
163
+ bytes_received_header = Array(header[BYTES_RECEIVED_HEADER])
164
+ @offset = Integer(bytes_received_header.first) unless bytes_received_header.empty?
165
+ upload_url_header = Array(header[UPLOAD_URL_HEADER])
166
+ @upload_url = upload_url_header.first unless upload_url_header.empty?
167
+ upload_status = Array(header[UPLOAD_STATUS_HEADER]).first
165
168
  logger.debug { sprintf('Upload status %s', upload_status) }
166
169
  if upload_status == STATUS_ACTIVE
167
170
  @state = :active
@@ -184,19 +187,14 @@ module Google
184
187
  request_header[UPLOAD_CONTENT_LENGTH] = upload_io.size.to_s
185
188
  request_header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type
186
189
 
187
- client.request(method.to_s.upcase,
188
- url.to_s, query: nil,
189
- body: body,
190
- header: request_header,
191
- follow_redirect: true)
190
+ client.run_request(method, url.to_s, body, request_header)
192
191
  rescue => e
193
192
  raise Google::Apis::ServerError, e.message
194
193
  end
195
194
 
196
195
  # Query for the status of an incomplete upload
197
196
  #
198
- # @param [HTTPClient] client
199
- # HTTP client
197
+ # @param [Faraday::Connection] client Faraday connection
200
198
  # @return [HTTP::Message]
201
199
  # @raise [Google::Apis::ServerError] Unable to send the request
202
200
  def send_query_command(client)
@@ -206,14 +204,13 @@ module Google
206
204
  apply_request_options(request_header)
207
205
  request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
208
206
 
209
- client.post(@upload_url, body: '', header: request_header, follow_redirect: true)
207
+ client.post(@upload_url, '', request_header)
210
208
  end
211
209
 
212
210
 
213
211
  # Send the actual content
214
212
  #
215
- # @param [HTTPClient] client
216
- # HTTP client
213
+ # @param [Faraday::Connection] client Faraday connection
217
214
  # @return [HTTP::Message]
218
215
  # @raise [Google::Apis::ServerError] Unable to send the request
219
216
  def send_upload_command(client)
@@ -228,16 +225,16 @@ module Google
228
225
  request_header[UPLOAD_COMMAND_HEADER] = UPLOAD_COMMAND
229
226
  request_header[UPLOAD_OFFSET_HEADER] = @offset.to_s
230
227
  request_header[CONTENT_TYPE_HEADER] = upload_content_type
228
+ request_header[CONTENT_LENGTH_HEADER] = (upload_io.size - @offset).to_s
231
229
 
232
- client.post(@upload_url, body: content, header: request_header, follow_redirect: true)
230
+ client.post(@upload_url, content, request_header)
233
231
  end
234
232
 
235
233
  # Execute the upload request once. This will typically perform two HTTP requests -- one to initiate or query
236
234
  # for the status of the upload, the second to send the (remaining) content.
237
235
  #
238
236
  # @private
239
- # @param [HTTPClient] client
240
- # HTTP client
237
+ # @param [Faraday::Connection] client Faraday connection
241
238
  # @yield [result, err] Result or error if block supplied
242
239
  # @return [Object]
243
240
  # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
@@ -247,16 +244,16 @@ module Google
247
244
  case @state
248
245
  when :start
249
246
  response = send_start_command(client)
250
- result = process_response(response.status_code, response.header, response.body)
247
+ result = process_response(response.status.to_i, response.headers, response.body)
251
248
  when :active
252
249
  response = send_query_command(client)
253
- result = process_response(response.status_code, response.header, response.body)
250
+ result = process_response(response.status.to_i, response.headers, response.body)
254
251
  when :cancelled, :final
255
252
  error(@last_error, rethrow: true, &block)
256
253
  end
257
254
  if @state == :active
258
255
  response = send_upload_command(client)
259
- result = process_response(response.status_code, response.header, response.body)
256
+ result = process_response(response.status.to_i, response.headers, response.body)
260
257
  end
261
258
 
262
259
  success(result, &block) if @state == :final
@@ -16,7 +16,7 @@ module Google
16
16
  module Apis
17
17
  module Core
18
18
  # Core version
19
- VERSION = "0.17.0".freeze
19
+ VERSION = "1.0.0".freeze
20
20
  end
21
21
  end
22
22
  end
@@ -41,7 +41,8 @@ module Google
41
41
  :quota_project,
42
42
  :query,
43
43
  :add_invocation_id_header,
44
- :upload_chunk_size)
44
+ :upload_chunk_size
45
+ )
45
46
 
46
47
  # General client options
47
48
  class ClientOptions
@@ -60,7 +61,7 @@ module Google
60
61
  # @!attribute [rw] read_timeout_sec
61
62
  # @return [Integer] How long, in seconds, before receiving data times out
62
63
  # @!attribute [rw] transparent_gzip_decompression
63
- # @return [Boolean] True if gzip compression needs to be enabled
64
+ # @return [Boolean] DEPRECATED. Gzip decompression is now always on.
64
65
  # Get the default options
65
66
  # @return [Google::Apis::ClientOptions]
66
67
  def self.default
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: google-apis-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Google LLC
@@ -27,104 +27,92 @@ dependencies:
27
27
  name: retriable
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: '2.0'
33
- - - "<"
30
+ - - "~>"
34
31
  - !ruby/object:Gem::Version
35
- version: 4.a
32
+ version: '3.1'
36
33
  type: :runtime
37
34
  prerelease: false
38
35
  version_requirements: !ruby/object:Gem::Requirement
39
36
  requirements:
40
- - - ">="
41
- - !ruby/object:Gem::Version
42
- version: '2.0'
43
- - - "<"
37
+ - - "~>"
44
38
  - !ruby/object:Gem::Version
45
- version: 4.a
39
+ version: '3.1'
46
40
  - !ruby/object:Gem::Dependency
47
41
  name: addressable
48
42
  requirement: !ruby/object:Gem::Requirement
49
43
  requirements:
50
44
  - - "~>"
51
45
  - !ruby/object:Gem::Version
52
- version: '2.5'
46
+ version: '2.8'
53
47
  - - ">="
54
48
  - !ruby/object:Gem::Version
55
- version: 2.5.1
49
+ version: 2.8.7
56
50
  type: :runtime
57
51
  prerelease: false
58
52
  version_requirements: !ruby/object:Gem::Requirement
59
53
  requirements:
60
54
  - - "~>"
61
55
  - !ruby/object:Gem::Version
62
- version: '2.5'
56
+ version: '2.8'
63
57
  - - ">="
64
58
  - !ruby/object:Gem::Version
65
- version: 2.5.1
59
+ version: 2.8.7
66
60
  - !ruby/object:Gem::Dependency
67
61
  name: mini_mime
68
62
  requirement: !ruby/object:Gem::Requirement
69
63
  requirements:
70
64
  - - "~>"
71
65
  - !ruby/object:Gem::Version
72
- version: '1.0'
66
+ version: '1.1'
73
67
  type: :runtime
74
68
  prerelease: false
75
69
  version_requirements: !ruby/object:Gem::Requirement
76
70
  requirements:
77
71
  - - "~>"
78
72
  - !ruby/object:Gem::Version
79
- version: '1.0'
73
+ version: '1.1'
80
74
  - !ruby/object:Gem::Dependency
81
75
  name: googleauth
82
76
  requirement: !ruby/object:Gem::Requirement
83
77
  requirements:
84
78
  - - "~>"
85
79
  - !ruby/object:Gem::Version
86
- version: '1.9'
80
+ version: '1.14'
87
81
  type: :runtime
88
82
  prerelease: false
89
83
  version_requirements: !ruby/object:Gem::Requirement
90
84
  requirements:
91
85
  - - "~>"
92
86
  - !ruby/object:Gem::Version
93
- version: '1.9'
87
+ version: '1.14'
94
88
  - !ruby/object:Gem::Dependency
95
- name: httpclient
89
+ name: faraday
96
90
  requirement: !ruby/object:Gem::Requirement
97
91
  requirements:
98
- - - ">="
99
- - !ruby/object:Gem::Version
100
- version: 2.8.3
101
- - - "<"
92
+ - - "~>"
102
93
  - !ruby/object:Gem::Version
103
- version: 3.a
94
+ version: '2.13'
104
95
  type: :runtime
105
96
  prerelease: false
106
97
  version_requirements: !ruby/object:Gem::Requirement
107
98
  requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: 2.8.3
111
- - - "<"
99
+ - - "~>"
112
100
  - !ruby/object:Gem::Version
113
- version: 3.a
101
+ version: '2.13'
114
102
  - !ruby/object:Gem::Dependency
115
- name: mutex_m
103
+ name: faraday-follow_redirects
116
104
  requirement: !ruby/object:Gem::Requirement
117
105
  requirements:
118
- - - ">="
106
+ - - "~>"
119
107
  - !ruby/object:Gem::Version
120
- version: '0'
108
+ version: '0.3'
121
109
  type: :runtime
122
110
  prerelease: false
123
111
  version_requirements: !ruby/object:Gem::Requirement
124
112
  requirements:
125
- - - ">="
113
+ - - "~>"
126
114
  - !ruby/object:Gem::Version
127
- version: '0'
115
+ version: '0.3'
128
116
  email: googleapis-packages@google.com
129
117
  executables: []
130
118
  extensions: []
@@ -147,6 +135,7 @@ files:
147
135
  - lib/google/apis/core/batch.rb
148
136
  - lib/google/apis/core/composite_io.rb
149
137
  - lib/google/apis/core/download.rb
138
+ - lib/google/apis/core/faraday_integration.rb
150
139
  - lib/google/apis/core/hashable.rb
151
140
  - lib/google/apis/core/http_command.rb
152
141
  - lib/google/apis/core/json_representation.rb
@@ -164,7 +153,7 @@ licenses:
164
153
  metadata:
165
154
  bug_tracker_uri: https://github.com/googleapis/google-api-ruby-client/issues
166
155
  changelog_uri: https://github.com/googleapis/google-api-ruby-client/tree/main/google-apis-core/CHANGELOG.md
167
- documentation_uri: https://googleapis.dev/ruby/google-apis-core/v0.17.0
156
+ documentation_uri: https://googleapis.dev/ruby/google-apis-core/v1.0.0
168
157
  source_code_uri: https://github.com/googleapis/google-api-ruby-client/tree/main/google-apis-core
169
158
  rdoc_options: []
170
159
  require_paths:
@@ -180,7 +169,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
180
169
  - !ruby/object:Gem::Version
181
170
  version: '0'
182
171
  requirements: []
183
- rubygems_version: 3.6.8
172
+ rubygems_version: 3.6.9
184
173
  specification_version: 4
185
174
  summary: Common utility and base classes for legacy Google REST clients
186
175
  test_files: []