gapic-common 1.4.0 → 1.5.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 +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +2 -5
- data/lib/gapic/common/retry_policy.rb +34 -0
- data/lib/gapic/common/version.rb +1 -1
- data/lib/gapic/logging_concerns.rb +4 -0
- data/lib/gapic/rest/client_stub.rb +13 -3
- data/lib/gapic/rest/error.rb +4 -1
- data/lib/gapic/rest/grpc_transcoder.rb +39 -0
- data/lib/gapic/rest/resumable_upload/core.rb +77 -0
- data/lib/gapic/rest/resumable_upload/data_types.rb +428 -0
- data/lib/gapic/rest/resumable_upload/driver/abridge.rb +168 -0
- data/lib/gapic/rest/resumable_upload/driver/retry_decider.rb +190 -0
- data/lib/gapic/rest/resumable_upload/driver/upload_log.rb +343 -0
- data/lib/gapic/rest/resumable_upload/driver.rb +855 -0
- data/lib/gapic/rest/resumable_upload/errors.rb +581 -0
- data/lib/gapic/rest/resumable_upload/events.rb +129 -0
- data/lib/gapic/rest/resumable_upload/instructions.rb +273 -0
- data/lib/gapic/rest/resumable_upload/retry_policies.rb +116 -0
- data/lib/gapic/rest/resumable_upload/rules.rb +1210 -0
- data/lib/gapic/rest/resumable_upload.rb +110 -0
- data/lib/gapic/rest.rb +2 -0
- data/lib/gapic/resumable_upload.rb +508 -0
- metadata +14 -1
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 Google LLC
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
# rubocop:disable Metrics/ModuleLength
|
|
18
|
+
|
|
19
|
+
module Gapic
|
|
20
|
+
module Rest
|
|
21
|
+
module ResumableUpload
|
|
22
|
+
##
|
|
23
|
+
# @private
|
|
24
|
+
# Configuration members shared by {StartUploadConfig} and {ResumeUploadConfig}, in the order both
|
|
25
|
+
# definitions splat them.
|
|
26
|
+
#
|
|
27
|
+
# The two config types are deliberately *flat*: {Core}, {Rules} and {Driver} read every member
|
|
28
|
+
# straight off `config`. Nesting the shared members inside a common object would turn every
|
|
29
|
+
# `config.upload_size` into `config.common.upload_size` at some thirty call sites for no behavioural
|
|
30
|
+
# gain, so they are spliced into each `Data.define` instead.
|
|
31
|
+
#
|
|
32
|
+
# * `stream` [IO] Binary input stream to upload. Required.
|
|
33
|
+
# * `upload_size` [Integer, nil] Total upload bytes if known upfront.
|
|
34
|
+
# * `content_type` [String, nil] MIME type of uploaded media.
|
|
35
|
+
# * `timeout` [Numeric, nil] Total upload timeout in seconds (zero or negative is treated as nil).
|
|
36
|
+
# * `control_plane_retry_policy` [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for control
|
|
37
|
+
# commands (query, cancel).
|
|
38
|
+
# * `data_plane_retry_policy` [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for data commands
|
|
39
|
+
# (upload, finalize).
|
|
40
|
+
# * `on_progress` [Proc, nil] Callback invoked as `->(progress)` with a {Progress} instance.
|
|
41
|
+
#
|
|
42
|
+
# Every retry policy member, here and in the per-run configs, follows the same convention: a
|
|
43
|
+
# {Gapic::Common::RetryPolicy} replaces the default policy outright, while a Hash overrides only the
|
|
44
|
+
# settings it names and leaves the remaining defaults — including retry codes and predicates — in place.
|
|
45
|
+
#
|
|
46
|
+
COMMON_MEMBERS = [
|
|
47
|
+
:stream,
|
|
48
|
+
:upload_size,
|
|
49
|
+
:content_type,
|
|
50
|
+
:timeout,
|
|
51
|
+
:control_plane_retry_policy,
|
|
52
|
+
:data_plane_retry_policy,
|
|
53
|
+
:on_progress
|
|
54
|
+
].freeze
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# @private
|
|
58
|
+
# Header names a caller may not use in `initial_headers`, lowercased for comparison.
|
|
59
|
+
#
|
|
60
|
+
# These five headers are protocol machinery the driver owns: the protocol identifier, the command
|
|
61
|
+
# verb, the byte offset, and the content descriptors derived from `content_type` and `upload_size`.
|
|
62
|
+
# `x-goog-upload-offset` is included for completeness even though initiation never sets an offset:
|
|
63
|
+
# supplying an offset at initiation is meaningless and indicates a confused caller. Pass-through
|
|
64
|
+
# headers such as `X-Goog-Upload-Header-Content-Disposition` remain permitted.
|
|
65
|
+
#
|
|
66
|
+
# See `Driver#start_headers`, which builds the initiation headers this list protects.
|
|
67
|
+
#
|
|
68
|
+
# @return [Array<String>]
|
|
69
|
+
RESERVED_INITIAL_HEADERS = [
|
|
70
|
+
"x-goog-upload-protocol",
|
|
71
|
+
"x-goog-upload-command",
|
|
72
|
+
"x-goog-upload-offset",
|
|
73
|
+
"x-goog-upload-header-content-type",
|
|
74
|
+
"x-goog-upload-header-content-length"
|
|
75
|
+
].freeze
|
|
76
|
+
|
|
77
|
+
##
|
|
78
|
+
# @private
|
|
79
|
+
# Immutable configuration for a run that initiates a new upload session, i.e.
|
|
80
|
+
# {Gapic::ResumableUpload#start}.
|
|
81
|
+
#
|
|
82
|
+
# Carries {COMMON_MEMBERS} plus the members only an initiating run uses.
|
|
83
|
+
#
|
|
84
|
+
# @!attribute [r] initial_url
|
|
85
|
+
# @return [String] Initial endpoint URI for session initiation
|
|
86
|
+
# @!attribute [r] initial_body
|
|
87
|
+
# @return [String, nil] Request payload for session initiation
|
|
88
|
+
# @!attribute [r] initial_headers
|
|
89
|
+
# @return [Hash<String, String>] Additional headers for initiation, merged over the driver's
|
|
90
|
+
# own headers. Keys in {RESERVED_INITIAL_HEADERS} are rejected in any casing; use
|
|
91
|
+
# `content_type` and `upload_size` to shape those.
|
|
92
|
+
# @!attribute [r] chunk_size
|
|
93
|
+
# @return [Integer, nil] Requested chunk size in bytes, aligned to the granularity the server
|
|
94
|
+
# reports during initiation. A resumed run takes its chunk size from {ResumeUploadConfig}.
|
|
95
|
+
# @!attribute [r] start_retry_policy
|
|
96
|
+
# @return [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for session initiation
|
|
97
|
+
#
|
|
98
|
+
StartUploadConfig = Data.define(
|
|
99
|
+
*COMMON_MEMBERS,
|
|
100
|
+
:initial_url,
|
|
101
|
+
:initial_body,
|
|
102
|
+
:initial_headers,
|
|
103
|
+
:chunk_size,
|
|
104
|
+
:start_retry_policy
|
|
105
|
+
) do
|
|
106
|
+
##
|
|
107
|
+
# @private
|
|
108
|
+
# Initializes a new upload configuration.
|
|
109
|
+
#
|
|
110
|
+
# @param initial_url [String] Initial endpoint URI for session initiation
|
|
111
|
+
# @param stream [IO] Binary input stream to upload
|
|
112
|
+
# @param initial_body [String, nil] Request payload for session initiation
|
|
113
|
+
# @param initial_headers [Hash<String, String>] Additional headers for initiation. Keys in
|
|
114
|
+
# {RESERVED_INITIAL_HEADERS} are rejected in any casing; use `content_type` and `upload_size`
|
|
115
|
+
# to shape those.
|
|
116
|
+
# @param upload_size [Integer, nil] Total upload bytes if known upfront
|
|
117
|
+
# @param chunk_size [Integer, nil] Requested chunk size in bytes
|
|
118
|
+
# @param content_type [String, nil] MIME type of uploaded media
|
|
119
|
+
# @param timeout [Numeric, nil] Total upload timeout in seconds (zero/negative values treated as nil)
|
|
120
|
+
# @param start_retry_policy [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for session initiation
|
|
121
|
+
# @param control_plane_retry_policy [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for control commands
|
|
122
|
+
# @param data_plane_retry_policy [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for data commands
|
|
123
|
+
# @param on_progress [Proc, nil] Callback invoked as `->(progress)` with a {Progress} instance
|
|
124
|
+
# @raise [ArgumentError] If required arguments are missing or invalid
|
|
125
|
+
#
|
|
126
|
+
def initialize initial_url:,
|
|
127
|
+
stream:,
|
|
128
|
+
initial_body: nil,
|
|
129
|
+
initial_headers: {},
|
|
130
|
+
upload_size: nil,
|
|
131
|
+
chunk_size: nil,
|
|
132
|
+
content_type: nil,
|
|
133
|
+
timeout: nil,
|
|
134
|
+
start_retry_policy: nil,
|
|
135
|
+
control_plane_retry_policy: nil,
|
|
136
|
+
data_plane_retry_policy: nil,
|
|
137
|
+
on_progress: nil
|
|
138
|
+
raise ArgumentError, "initial_url is required" if initial_url.nil? || initial_url.to_s.strip.empty?
|
|
139
|
+
raise ArgumentError, "stream is required" if stream.nil?
|
|
140
|
+
reserved = (initial_headers || {}).keys.find do |key|
|
|
141
|
+
RESERVED_INITIAL_HEADERS.include? key.to_s.downcase
|
|
142
|
+
end
|
|
143
|
+
if reserved
|
|
144
|
+
raise ArgumentError,
|
|
145
|
+
"initial_headers must not set protocol header #{reserved.inspect}; " \
|
|
146
|
+
"use content_type and upload_size instead"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
super(
|
|
150
|
+
initial_url: initial_url,
|
|
151
|
+
initial_body: initial_body,
|
|
152
|
+
initial_headers: initial_headers || {},
|
|
153
|
+
stream: stream,
|
|
154
|
+
upload_size: upload_size,
|
|
155
|
+
chunk_size: chunk_size,
|
|
156
|
+
content_type: content_type,
|
|
157
|
+
timeout: timeout,
|
|
158
|
+
start_retry_policy: start_retry_policy,
|
|
159
|
+
control_plane_retry_policy: control_plane_retry_policy,
|
|
160
|
+
data_plane_retry_policy: data_plane_retry_policy,
|
|
161
|
+
on_progress: on_progress
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
##
|
|
167
|
+
# @private
|
|
168
|
+
# Immutable configuration for a run that resumes an existing upload session, i.e.
|
|
169
|
+
# {Gapic::ResumableUpload#resume}.
|
|
170
|
+
#
|
|
171
|
+
# Carries {COMMON_MEMBERS} plus the upload URL and chunk size the earlier run established. There is
|
|
172
|
+
# no `start_retry_policy` here: a resumed run issues no initiation request, so the member would
|
|
173
|
+
# always be dead.
|
|
174
|
+
#
|
|
175
|
+
# @!attribute [r] upload_url
|
|
176
|
+
# @return [String] Session upload URL returned by the upload backend
|
|
177
|
+
# @!attribute [r] chunk_size
|
|
178
|
+
# @return [Integer] Explicit chunk size in bytes (must be a positive integer). Server granularity is
|
|
179
|
+
# reported only during initiation, which a resumed run skips, so the size is carried forward from
|
|
180
|
+
# the earlier run rather than re-negotiated.
|
|
181
|
+
#
|
|
182
|
+
ResumeUploadConfig = Data.define(
|
|
183
|
+
*COMMON_MEMBERS,
|
|
184
|
+
:upload_url,
|
|
185
|
+
:chunk_size
|
|
186
|
+
) do
|
|
187
|
+
##
|
|
188
|
+
# @private
|
|
189
|
+
# Initializes a new upload resume configuration.
|
|
190
|
+
#
|
|
191
|
+
# @param upload_url [String] Session upload URL
|
|
192
|
+
# @param chunk_size [Integer] Explicit chunk size in bytes (must be a positive integer)
|
|
193
|
+
# @param stream [IO] Binary input stream to upload
|
|
194
|
+
# @param upload_size [Integer, nil] Total upload bytes if known upfront
|
|
195
|
+
# @param content_type [String, nil] MIME type of uploaded media
|
|
196
|
+
# @param timeout [Numeric, nil] Total upload timeout in seconds (zero/negative values treated as nil)
|
|
197
|
+
# @param control_plane_retry_policy [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for control commands
|
|
198
|
+
# @param data_plane_retry_policy [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for data commands
|
|
199
|
+
# @param on_progress [Proc, nil] Callback invoked as `->(progress)` with a {Progress} instance
|
|
200
|
+
# @raise [ArgumentError] If required arguments are missing or invalid
|
|
201
|
+
#
|
|
202
|
+
def initialize upload_url:,
|
|
203
|
+
chunk_size:,
|
|
204
|
+
stream:,
|
|
205
|
+
upload_size: nil,
|
|
206
|
+
content_type: nil,
|
|
207
|
+
timeout: nil,
|
|
208
|
+
control_plane_retry_policy: nil,
|
|
209
|
+
data_plane_retry_policy: nil,
|
|
210
|
+
on_progress: nil
|
|
211
|
+
raise ArgumentError, "upload_url is required" if upload_url.nil? || upload_url.to_s.strip.empty?
|
|
212
|
+
unless chunk_size.is_a?(Integer) && chunk_size.positive?
|
|
213
|
+
raise ArgumentError, "chunk_size must be a positive integer"
|
|
214
|
+
end
|
|
215
|
+
raise ArgumentError, "stream is required" if stream.nil?
|
|
216
|
+
|
|
217
|
+
super(
|
|
218
|
+
upload_url: upload_url,
|
|
219
|
+
chunk_size: chunk_size,
|
|
220
|
+
stream: stream,
|
|
221
|
+
upload_size: upload_size,
|
|
222
|
+
content_type: content_type,
|
|
223
|
+
timeout: timeout,
|
|
224
|
+
control_plane_retry_policy: control_plane_retry_policy,
|
|
225
|
+
data_plane_retry_policy: data_plane_retry_policy,
|
|
226
|
+
on_progress: on_progress
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
##
|
|
232
|
+
# Immutable progress snapshot passed to the `on_progress` callback.
|
|
233
|
+
#
|
|
234
|
+
# The `on_progress` callback runs synchronously on the same thread as the upload protocol
|
|
235
|
+
# and must not block. Any exception raised inside the callback aborts the upload session
|
|
236
|
+
# and propagates out of {Gapic::ResumableUpload#start} or {Gapic::ResumableUpload#resume}.
|
|
237
|
+
#
|
|
238
|
+
# @!attribute [r] phase
|
|
239
|
+
# @return [Symbol] Current upload phase, one of {Progress::PHASES}
|
|
240
|
+
# @!attribute [r] bytes_uploaded
|
|
241
|
+
# @return [Integer] Cumulative bytes acknowledged by the server. Note that this is the
|
|
242
|
+
# server-confirmed offset and is not guaranteed to be monotonic — a server rewind during
|
|
243
|
+
# recovery can decrease this value.
|
|
244
|
+
# @!attribute [r] total_bytes
|
|
245
|
+
# @return [Integer, nil] Total upload size in bytes if known, or `nil`. Always set on the
|
|
246
|
+
# `:completed` phase — the total is known once the transfer finishes, even when `upload_size`
|
|
247
|
+
# was not supplied upfront.
|
|
248
|
+
#
|
|
249
|
+
Progress = Data.define(
|
|
250
|
+
:phase,
|
|
251
|
+
:bytes_uploaded,
|
|
252
|
+
:total_bytes
|
|
253
|
+
) do
|
|
254
|
+
##
|
|
255
|
+
# Initializes a new progress snapshot.
|
|
256
|
+
#
|
|
257
|
+
# @param phase [Symbol] Current upload phase, one of {Progress::PHASES}
|
|
258
|
+
# @param bytes_uploaded [Integer] Cumulative bytes acknowledged by the server
|
|
259
|
+
# @param total_bytes [Integer, nil] Total upload size in bytes if known, or nil
|
|
260
|
+
# @raise [ArgumentError] If the phase is not one of {Progress::PHASES}
|
|
261
|
+
#
|
|
262
|
+
def initialize phase:, bytes_uploaded:, total_bytes: nil
|
|
263
|
+
# Must use `self.class::` to access constants from the class scope
|
|
264
|
+
unless self.class::PHASES.include? phase
|
|
265
|
+
raise ArgumentError, "Invalid phase: #{phase.inspect}. Expected one of #{self.class::PHASES.inspect}"
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
super(
|
|
269
|
+
phase: phase,
|
|
270
|
+
bytes_uploaded: bytes_uploaded,
|
|
271
|
+
total_bytes: total_bytes
|
|
272
|
+
)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
##
|
|
277
|
+
# Allowed lifecycle phases for an upload session.
|
|
278
|
+
#
|
|
279
|
+
# A callback observes `:initiating`, `:uploading`, `:recovering`, `:finalizing` and `:completed`.
|
|
280
|
+
# `:cancelling` is reserved: cancellation is not exposed on {Gapic::ResumableUpload}, so no phase
|
|
281
|
+
# with that value is currently emitted.
|
|
282
|
+
#
|
|
283
|
+
# @return [Array<Symbol>]
|
|
284
|
+
Progress::PHASES = [:initiating, :uploading, :recovering, :finalizing, :cancelling, :completed].freeze
|
|
285
|
+
|
|
286
|
+
##
|
|
287
|
+
# Immutable handle containing parameters necessary to resume an in-progress upload session.
|
|
288
|
+
# These parameters are provided by the server and can be persisted to resume the upload
|
|
289
|
+
# at a later time.
|
|
290
|
+
#
|
|
291
|
+
# @!attribute [r] upload_url
|
|
292
|
+
# @return [String] Upload session URL provided by the server
|
|
293
|
+
# @!attribute [r] chunk_size
|
|
294
|
+
# @return [Integer] Effective chunk size in bytes
|
|
295
|
+
#
|
|
296
|
+
ResumeHandle = Data.define(
|
|
297
|
+
:upload_url,
|
|
298
|
+
:chunk_size
|
|
299
|
+
) do
|
|
300
|
+
##
|
|
301
|
+
# Initializes a new resume handle.
|
|
302
|
+
#
|
|
303
|
+
# @param upload_url [String] Upload session URL provided by the server
|
|
304
|
+
# @param chunk_size [Integer] Effective chunk size in bytes
|
|
305
|
+
#
|
|
306
|
+
def initialize upload_url:, chunk_size:
|
|
307
|
+
super(
|
|
308
|
+
upload_url: upload_url,
|
|
309
|
+
chunk_size: chunk_size
|
|
310
|
+
)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
##
|
|
315
|
+
# @private
|
|
316
|
+
# Immutable state snapshot representing the current protocol progression.
|
|
317
|
+
#
|
|
318
|
+
# @!attribute [r] status
|
|
319
|
+
# @return [Symbol] Protocol lifecycle status, one of {Rules::STATUSES}
|
|
320
|
+
# @!attribute [r] upload_url
|
|
321
|
+
# @return [String, nil] Session upload URL returned by the upload backend
|
|
322
|
+
# @!attribute [r] offset
|
|
323
|
+
# @return [Integer] Contiguous bytes acknowledged by server
|
|
324
|
+
# @!attribute [r] chunk_size
|
|
325
|
+
# @return [Integer] Resolved effective chunk size in bytes
|
|
326
|
+
# @!attribute [r] chunk_granularity
|
|
327
|
+
# @return [Integer, nil] Alignment modulus returned by server
|
|
328
|
+
# @!attribute [r] in_flight_length
|
|
329
|
+
# @return [Integer] Byte length of in-flight chunk currently being transmitted
|
|
330
|
+
# @!attribute [r] last_error
|
|
331
|
+
# @return [StandardError, nil] Terminal exception if in an error or rejected status
|
|
332
|
+
# @!attribute [r] recovery_offset
|
|
333
|
+
# @return [Integer, nil] Server-confirmed offset when the open recovery episode began, or `nil` when no
|
|
334
|
+
# episode is open. A recovery episode is the run of consecutive recovery attempts during which the
|
|
335
|
+
# confirmed offset does not advance; every query in it after the first waits for the next backoff delay.
|
|
336
|
+
# See `design/resumable_upload/implementation-guide.md` section 6.2.1.
|
|
337
|
+
#
|
|
338
|
+
State = Data.define(
|
|
339
|
+
:status,
|
|
340
|
+
:upload_url,
|
|
341
|
+
:offset,
|
|
342
|
+
:chunk_size,
|
|
343
|
+
:chunk_granularity,
|
|
344
|
+
:in_flight_length,
|
|
345
|
+
:last_error,
|
|
346
|
+
:recovery_offset
|
|
347
|
+
) do
|
|
348
|
+
##
|
|
349
|
+
# @private
|
|
350
|
+
# Initializes a protocol state snapshot.
|
|
351
|
+
#
|
|
352
|
+
# @param status [Symbol] Protocol lifecycle status, one of {Rules::STATUSES}
|
|
353
|
+
# @param upload_url [String, nil] Session upload URL
|
|
354
|
+
# @param offset [Integer] Contiguous bytes acknowledged by server
|
|
355
|
+
# @param chunk_size [Integer] Resolved effective chunk size in bytes
|
|
356
|
+
# @param chunk_granularity [Integer, nil] Alignment modulus returned by server
|
|
357
|
+
# @param in_flight_length [Integer] Byte length of in-flight chunk
|
|
358
|
+
# @param last_error [StandardError, nil] Terminal exception
|
|
359
|
+
# @param recovery_offset [Integer, nil] Offset the open recovery episode began at; `nil` if none is open
|
|
360
|
+
#
|
|
361
|
+
def initialize status: :initializing,
|
|
362
|
+
upload_url: nil,
|
|
363
|
+
offset: 0,
|
|
364
|
+
chunk_size: Rules::DEFAULT_CHUNK_SIZE,
|
|
365
|
+
chunk_granularity: nil,
|
|
366
|
+
in_flight_length: 0,
|
|
367
|
+
last_error: nil,
|
|
368
|
+
recovery_offset: nil
|
|
369
|
+
super(
|
|
370
|
+
status: status,
|
|
371
|
+
upload_url: upload_url,
|
|
372
|
+
offset: offset,
|
|
373
|
+
chunk_size: chunk_size,
|
|
374
|
+
chunk_granularity: chunk_granularity,
|
|
375
|
+
in_flight_length: in_flight_length,
|
|
376
|
+
last_error: last_error,
|
|
377
|
+
recovery_offset: recovery_offset
|
|
378
|
+
)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
##
|
|
383
|
+
# @private
|
|
384
|
+
# Immutable decision snapshot emitted by Rules.decide.
|
|
385
|
+
#
|
|
386
|
+
# @!attribute [r] from_status
|
|
387
|
+
# @return [Symbol] The protocol status before the transition
|
|
388
|
+
# @!attribute [r] shape
|
|
389
|
+
# @return [Symbol] The canonical event shape
|
|
390
|
+
# @!attribute [r] recipe
|
|
391
|
+
# @return [Symbol] Selected transition recipe method name
|
|
392
|
+
# @!attribute [r] next_state
|
|
393
|
+
# @return [State] The new protocol state snapshot after transition
|
|
394
|
+
# @!attribute [r] instructions
|
|
395
|
+
# @return [Array<Object>] Emitted instructions for the Driver
|
|
396
|
+
#
|
|
397
|
+
Decision = Data.define(
|
|
398
|
+
:from_status,
|
|
399
|
+
:shape,
|
|
400
|
+
:recipe,
|
|
401
|
+
:next_state,
|
|
402
|
+
:instructions
|
|
403
|
+
) do
|
|
404
|
+
##
|
|
405
|
+
# @private
|
|
406
|
+
# Initializes a decision snapshot.
|
|
407
|
+
#
|
|
408
|
+
# @param from_status [Symbol] The protocol status before the transition
|
|
409
|
+
# @param shape [Symbol] The canonical event shape
|
|
410
|
+
# @param recipe [Symbol] Selected transition recipe method name
|
|
411
|
+
# @param next_state [State] Resulting protocol state snapshot
|
|
412
|
+
# @param instructions [Array<Object>] Emitted instructions for the Driver
|
|
413
|
+
#
|
|
414
|
+
def initialize from_status:, shape:, recipe:, next_state:, instructions: []
|
|
415
|
+
super(
|
|
416
|
+
from_status: from_status,
|
|
417
|
+
shape: shape,
|
|
418
|
+
recipe: recipe,
|
|
419
|
+
next_state: next_state,
|
|
420
|
+
instructions: instructions
|
|
421
|
+
)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# rubocop:enable Metrics/ModuleLength
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 Google LLC
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require "uri"
|
|
18
|
+
|
|
19
|
+
module Gapic
|
|
20
|
+
module Rest
|
|
21
|
+
module ResumableUpload
|
|
22
|
+
class Driver
|
|
23
|
+
##
|
|
24
|
+
# @private
|
|
25
|
+
# Pure functions for redacting and abridging sensitive data and large payloads in logs.
|
|
26
|
+
#
|
|
27
|
+
module Abridge
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# @private
|
|
32
|
+
# Formats binary payload into truncated hex representation.
|
|
33
|
+
#
|
|
34
|
+
# @param data [Object, nil] Binary or string payload
|
|
35
|
+
# @return [String, nil] Truncated hex representation or nil
|
|
36
|
+
#
|
|
37
|
+
def bytes data
|
|
38
|
+
return nil if data.nil?
|
|
39
|
+
|
|
40
|
+
str = data.to_s
|
|
41
|
+
if str.bytesize >= 64
|
|
42
|
+
"#{str.byteslice(0, 32).unpack1('H*')}... <#{str.bytesize} bytes>"
|
|
43
|
+
else
|
|
44
|
+
str.unpack1 "H*"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# @private
|
|
50
|
+
# Truncates error body to a safe log length.
|
|
51
|
+
#
|
|
52
|
+
# @param data [Object, nil] Error body payload
|
|
53
|
+
# @return [String, nil] UTF-8 scrubbed and truncated string
|
|
54
|
+
#
|
|
55
|
+
def error_body data
|
|
56
|
+
return nil if data.nil?
|
|
57
|
+
|
|
58
|
+
data.to_s.dup.force_encoding(Encoding::UTF_8).scrub[0, 512]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
# @private
|
|
63
|
+
# Redacts query parameter values in URLs for safe logging.
|
|
64
|
+
#
|
|
65
|
+
# @param url [Object, nil] URL string or URI
|
|
66
|
+
# @return [String, nil] URL with query values elided
|
|
67
|
+
#
|
|
68
|
+
def url url
|
|
69
|
+
return nil if url.nil?
|
|
70
|
+
|
|
71
|
+
uri = URI.parse url.to_s
|
|
72
|
+
if uri.query && !uri.query.empty?
|
|
73
|
+
elided = uri.query.split("&").map do |pair|
|
|
74
|
+
key, _val = pair.split "=", 2
|
|
75
|
+
"#{key}=<...>"
|
|
76
|
+
end.join "&"
|
|
77
|
+
uri.query = nil
|
|
78
|
+
return "#{uri}?#{elided}"
|
|
79
|
+
end
|
|
80
|
+
uri.to_s
|
|
81
|
+
rescue URI::InvalidURIError
|
|
82
|
+
url.to_s
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# @private
|
|
87
|
+
# Redacts non-protocol headers for safe logging.
|
|
88
|
+
#
|
|
89
|
+
# @param headers [Object] Headers hash
|
|
90
|
+
# @return [Hash<String, String>] Redacted headers
|
|
91
|
+
#
|
|
92
|
+
def headers headers
|
|
93
|
+
return {} unless headers.is_a? Hash
|
|
94
|
+
|
|
95
|
+
headers.each_with_object({}) do |(k, v), acc|
|
|
96
|
+
key_str = k.to_s
|
|
97
|
+
acc[key_str] = if key_str.downcase == "x-goog-upload-url"
|
|
98
|
+
url v
|
|
99
|
+
elsif key_str.downcase.start_with? "x-goog-upload-"
|
|
100
|
+
v
|
|
101
|
+
else
|
|
102
|
+
"<...>"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
##
|
|
108
|
+
# @private
|
|
109
|
+
# Converts a list of instructions into log-safe representation hashes.
|
|
110
|
+
#
|
|
111
|
+
# @param instructions [Array<Object>] List of instructions
|
|
112
|
+
# @return [Array<Hash>] Log-safe instruction summaries
|
|
113
|
+
#
|
|
114
|
+
def instructions instructions
|
|
115
|
+
instructions.map { |i| instruction i }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# rubocop:disable Metrics/MethodLength
|
|
119
|
+
##
|
|
120
|
+
# @private
|
|
121
|
+
# Converts an instruction into a log-safe representation hash.
|
|
122
|
+
#
|
|
123
|
+
# @param instruction [Object] Instruction object
|
|
124
|
+
# @return [Hash] Log-safe instruction summary
|
|
125
|
+
#
|
|
126
|
+
def instruction instruction
|
|
127
|
+
case instruction
|
|
128
|
+
when Instruction::SendStart
|
|
129
|
+
{ "type" => "SendStart", "url" => url(instruction.url) }
|
|
130
|
+
when Instruction::SendChunk
|
|
131
|
+
{
|
|
132
|
+
"type" => "SendChunk",
|
|
133
|
+
"url" => url(instruction.url),
|
|
134
|
+
"offset" => instruction.offset,
|
|
135
|
+
"length" => instruction.length,
|
|
136
|
+
"finalize" => instruction.finalize
|
|
137
|
+
}
|
|
138
|
+
when Instruction::SendFinalize
|
|
139
|
+
{ "type" => "SendFinalize", "url" => url(instruction.url) }
|
|
140
|
+
when Instruction::SendQuery
|
|
141
|
+
{ "type" => "SendQuery", "url" => url(instruction.url), "backoff" => instruction.backoff }
|
|
142
|
+
when Instruction::SendCancel
|
|
143
|
+
{ "type" => "SendCancel", "url" => url(instruction.url) }
|
|
144
|
+
when Instruction::RealignBuffer
|
|
145
|
+
{ "type" => "RealignBuffer", "serverOffset" => instruction.server_offset }
|
|
146
|
+
when Instruction::FillBuffer
|
|
147
|
+
{ "type" => "FillBuffer", "targetBytesize" => instruction.target_bytesize }
|
|
148
|
+
when Instruction::NotifyProgress
|
|
149
|
+
{
|
|
150
|
+
"type" => "NotifyProgress",
|
|
151
|
+
"phase" => instruction.progress.phase.to_s,
|
|
152
|
+
"bytesUploaded" => instruction.progress.bytes_uploaded,
|
|
153
|
+
"totalBytes" => instruction.progress.total_bytes
|
|
154
|
+
}
|
|
155
|
+
when Instruction::TerminateSuccess
|
|
156
|
+
{ "type" => "TerminateSuccess" }
|
|
157
|
+
when Instruction::TerminateFailure
|
|
158
|
+
{ "type" => "TerminateFailure", "error" => instruction.error.to_s }
|
|
159
|
+
else
|
|
160
|
+
{ "type" => instruction.class.name }
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
# rubocop:enable Metrics/MethodLength
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|