gcs_put 0.1.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.
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+
5
+ # Keeps one connection per host open for the duration of the upload
6
+ class GCSPut::Transport::NetHTTP
7
+ # What Net::HTTP raises when the connection, rather than the request, is at fault
8
+ TRANSIENT_ERRORS = [
9
+ IOError, EOFError, SocketError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout,
10
+ Net::WriteTimeout, OpenSSL::SSL::SSLError
11
+ ].freeze
12
+
13
+ # @param http_options[Hash] forwarded to `Net::HTTP.start` (`open_timeout:`, `read_timeout:`...)
14
+ def initialize(**http_options)
15
+ @http_options = http_options
16
+ @connections = {}
17
+ end
18
+
19
+ # @param uri[URI::Generic]
20
+ # @param body[String]
21
+ # @param headers[Hash{String => String}]
22
+ # @return [GCSPut::Transport::Response]
23
+ # @raise [GCSPut::TransientError] on connection errors and timeouts
24
+ def put(uri, body, headers)
25
+ request(Net::HTTP::Put, uri, body, headers)
26
+ end
27
+
28
+ # @param uri[URI::Generic]
29
+ # @param body[String]
30
+ # @param headers[Hash{String => String}]
31
+ # @return [GCSPut::Transport::Response]
32
+ # @raise [GCSPut::TransientError] on connection errors and timeouts
33
+ def post(uri, body, headers)
34
+ request(Net::HTTP::Post, uri, body, headers)
35
+ end
36
+
37
+ # Closes the kept connections. Safe to call repeatedly, the next request reopens as needed
38
+ #
39
+ # @return [void]
40
+ def close
41
+ @connections.each_value do |connection|
42
+ connection.finish if connection.started?
43
+ rescue IOError
44
+ # Already gone, which is what we wanted
45
+ end
46
+ @connections.clear
47
+ end
48
+
49
+ private
50
+
51
+ def request(request_class, uri, body, headers)
52
+ request = request_class.new(uri, headers)
53
+ request.body = body
54
+ response = connection_for(uri).request(request)
55
+ GCSPut::Transport::Response.new(response.code, response.each_header.to_h, response.body)
56
+ rescue *TRANSIENT_ERRORS => e
57
+ close
58
+ raise GCSPut::TransientError, "#{e.class}: #{e.message}"
59
+ end
60
+
61
+ def connection_for(uri)
62
+ @connections[[uri.host, uri.port]] ||= Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", **@http_options)
63
+ end
64
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The uploader needs two HTTP verbs and does not care what performs them. A transport is any
4
+ # object with `put(uri, body, headers)`, `post(uri, body, headers)` and `close`, where the verbs
5
+ # return a `Transport::Response`. Failures worth retrying (connection resets, timeouts and the
6
+ # like) must surface as `GCSPut::TransientError` so the uploader knows it may query
7
+ # the session and carry on. Anything else is allowed to propagate.
8
+ module GCSPut::Transport
9
+ autoload :NetHTTP, "gcs_put/transport/net_http"
10
+ autoload :Faraday, "gcs_put/transport/faraday"
11
+
12
+ # What a transport hands back from `put` and `post`
13
+ class Response
14
+ # @return [Integer] the HTTP status code
15
+ attr_reader :status
16
+
17
+ # @return [Hash{String => String}] the headers, with lowercased names
18
+ attr_reader :headers
19
+
20
+ # @return [String] the body, empty if there was none
21
+ attr_reader :body
22
+
23
+ # @param status[#to_i]
24
+ # @param headers[#to_h] header names in any case
25
+ # @param body[#to_s]
26
+ def initialize(status, headers, body)
27
+ @status = status.to_i
28
+ @headers = headers.to_h.transform_keys { |name| name.to_s.downcase }
29
+ @body = body.to_s
30
+ end
31
+
32
+ # @param name[String] header name in any case
33
+ # @return [String, nil]
34
+ def [](name)
35
+ @headers[name.to_s.downcase]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GCSPut
4
+ # The gem version
5
+ VERSION = "0.1.0"
6
+ end
data/lib/gcs_put.rb ADDED
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "digest/md5"
5
+ require "forwardable"
6
+
7
+ # The Ruby GCP SDK can only upload things it can measure in advance. GCS itself has supported
8
+ # resumable uploads of unknown size for ages though. This gem gives you a writable object
9
+ # which chops what you write to it into correctly sized chunks and PUTs them into a
10
+ # resumable upload session, so you never need to know the size up front.
11
+ module GCSPut
12
+ # Base class for everything the gem raises
13
+ class Error < StandardError
14
+ end
15
+
16
+ # Raised by transports for failures worth retrying - connection resets, timeouts and the like
17
+ class TransientError < Error
18
+ end
19
+
20
+ # Raised when GCS answers a chunk PUT with something we cannot recover from
21
+ class UploadFailed < Error
22
+ # @return [GCSPut::Transport::Response, nil] the response which caused the failure, if there was one
23
+ attr_reader :response
24
+
25
+ # @param message[String]
26
+ # @param response[GCSPut::Transport::Response, nil]
27
+ def initialize(message, response: nil)
28
+ super(message)
29
+ @response = response
30
+ end
31
+ end
32
+
33
+ # GCS insists that all chunks except the last are sized in multiples of this
34
+ CHUNK_SIZE_UNIT = 256 * 1024
35
+
36
+ # AWS recommend 5MB as the default part size for multipart uploads, and GCP recommend
37
+ # doing "less requests" in general. Since we have to hold a buffer of this size anyway,
38
+ # 5MB seems like a reasonable number for GCP too
39
+ DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024
40
+
41
+ autoload :ByteChunker, "gcs_put/byte_chunker"
42
+ autoload :ResumableUpload, "gcs_put/resumable_upload"
43
+ autoload :Signer, "gcs_put/signer"
44
+ autoload :Transport, "gcs_put/transport"
45
+
46
+ # @!method self.with_gcs_file(gcs_file, content_type: "binary/octet-stream", transport: GCSPut::Transport::NetHTTP.new, signed_url_options: {}, **options, &blk)
47
+ # Shorthand for {GCSPut::ResumableUpload.with_gcs_file}
48
+ # @return [GCSPut::ResumableUpload, Integer]
49
+ # @!method self.with_signed_post_url(signed_post_url, content_type: "binary/octet-stream", transport: GCSPut::Transport::NetHTTP.new, **options, &blk)
50
+ # Shorthand for {GCSPut::ResumableUpload.with_signed_post_url}
51
+ # @return [GCSPut::ResumableUpload, Integer]
52
+ # @!method self.with_session_url(session_url, **options, &blk)
53
+ # Shorthand for {GCSPut::ResumableUpload.with_session_url}
54
+ # @return [GCSPut::ResumableUpload, Integer]
55
+ class << self
56
+ extend Forwardable
57
+
58
+ def_delegators :"GCSPut::ResumableUpload", :with_gcs_file, :with_signed_post_url, :with_session_url
59
+ end
60
+ end
61
+
62
+ require "gcs_put/version"
data/rbi/gcs_put.rbi ADDED
@@ -0,0 +1,399 @@
1
+ # typed: strong
2
+ # The Ruby GCP SDK can only upload things it can measure in advance. GCS itself has supported
3
+ # resumable uploads of unknown size for ages though. This gem gives you a writable object
4
+ # which chops what you write to it into correctly sized chunks and PUTs them into a
5
+ # resumable upload session, so you never need to know the size up front.
6
+ module GCSPut
7
+ extend Forwardable
8
+ CHUNK_SIZE_UNIT = T.let(256 * 1024, T.untyped)
9
+ DEFAULT_CHUNK_SIZE = T.let(5 * 1024 * 1024, T.untyped)
10
+ VERSION = T.let("0.1.0", T.untyped)
11
+
12
+ # Base class for everything the gem raises
13
+ class Error < StandardError
14
+ end
15
+
16
+ # Raised by transports for failures worth retrying - connection resets, timeouts and the like
17
+ class TransientError < GCSPut::Error
18
+ end
19
+
20
+ # Raised when GCS answers a chunk PUT with something we cannot recover from
21
+ class UploadFailed < GCSPut::Error
22
+ # _@param_ `message`
23
+ #
24
+ # _@param_ `response`
25
+ sig { params(message: String, response: T.nilable(GCSPut::Transport::Response)).void }
26
+ def initialize(message, response: nil); end
27
+
28
+ # _@return_ — the response which caused the failure, if there was one
29
+ sig { returns(T.nilable(GCSPut::Transport::Response)) }
30
+ attr_reader :response
31
+ end
32
+
33
+ # Supplies the `issuer` and `signer` that `Google::Cloud::Storage::File#signed_url` needs
34
+ # when there is no private key around to sign with
35
+ module Signer
36
+ # When running on GCE, GKE, Cloud Run etc. under a service account there is no private key
37
+ # on the box to sign the URL with. The SDK then needs an `issuer` (the account email) and a
38
+ # `signer` lambda which asks the IAM credentials API to sign for us. For that to be allowed the
39
+ # service account must have `roles/iam.serviceAccountTokenCreator` on itself.
40
+ # Lifted from https://github.com/googleapis/google-cloud-ruby/issues/13307#issuecomment-1894546343
41
+ #
42
+ # _@return_ — either `{issuer:, signer:}` or an empty hash when not on compute engine
43
+ sig { returns(T::Hash[T.untyped, T.untyped]) }
44
+ def self.url_issuer_and_signer; end
45
+
46
+ # _@param_ `service_account_email`
47
+ #
48
+ # _@return_ — a lambda which takes the string to sign and returns the signature
49
+ sig { params(service_account_email: String).returns(Proc) }
50
+ def self.iam_signer_for(service_account_email); end
51
+ end
52
+
53
+ # The uploader needs two HTTP verbs and does not care what performs them. A transport is any
54
+ # object with `put(uri, body, headers)`, `post(uri, body, headers)` and `close`, where the verbs
55
+ # return a `Transport::Response`. Failures worth retrying (connection resets, timeouts and the
56
+ # like) must surface as `GCSPut::TransientError` so the uploader knows it may query
57
+ # the session and carry on. Anything else is allowed to propagate.
58
+ module Transport
59
+ # What a transport hands back from `put` and `post`
60
+ class Response
61
+ # sord duck - #to_i looks like a duck type, replacing with untyped
62
+ # sord duck - #to_h looks like a duck type, replacing with untyped
63
+ # sord duck - #to_s looks like a duck type, replacing with untyped
64
+ # _@param_ `status`
65
+ #
66
+ # _@param_ `headers` — header names in any case
67
+ #
68
+ # _@param_ `body`
69
+ sig { params(status: T.untyped, headers: T.untyped, body: T.untyped).void }
70
+ def initialize(status, headers, body); end
71
+
72
+ # _@param_ `name` — header name in any case
73
+ sig { params(name: String).returns(T.nilable(String)) }
74
+ def [](name); end
75
+
76
+ # _@return_ — the HTTP status code
77
+ sig { returns(Integer) }
78
+ attr_reader :status
79
+
80
+ # _@return_ — the headers, with lowercased names
81
+ sig { returns(T::Hash[String, String]) }
82
+ attr_reader :headers
83
+
84
+ # _@return_ — the body, empty if there was none
85
+ sig { returns(String) }
86
+ attr_reader :body
87
+ end
88
+
89
+ # Uses a Faraday connection, so you can pick the adapter, timeouts and instrumentation yourself.
90
+ # Check the bytes arrive intact with whatever adapter you choose - we send a `Content-MD5` with
91
+ # every chunk precisely because httpx 1.4.0 used to mangle request bodies,
92
+ # see https://gitlab.com/os85/httpx/-/issues/338
93
+ class Faraday
94
+ TRANSIENT_ERRORS = T.let([::Faraday::ConnectionFailed, ::Faraday::TimeoutError, ::Faraday::SSLError].freeze, T.untyped)
95
+
96
+ # sord warn - Faraday::Connection wasn't able to be resolved to a constant in this project
97
+ # _@param_ `connection` — a connection of your own, or `nil` to get a default one
98
+ sig { params(connection: T.nilable(Faraday::Connection)).void }
99
+ def initialize(connection = nil); end
100
+
101
+ # sord warn - URI::Generic wasn't able to be resolved to a constant in this project
102
+ # _@param_ `uri`
103
+ #
104
+ # _@param_ `body`
105
+ #
106
+ # _@param_ `headers`
107
+ sig { params(uri: URI::Generic, body: String, headers: T::Hash[String, String]).returns(GCSPut::Transport::Response) }
108
+ def put(uri, body, headers); end
109
+
110
+ # sord warn - URI::Generic wasn't able to be resolved to a constant in this project
111
+ # _@param_ `uri`
112
+ #
113
+ # _@param_ `body`
114
+ #
115
+ # _@param_ `headers`
116
+ sig { params(uri: URI::Generic, body: String, headers: T::Hash[String, String]).returns(GCSPut::Transport::Response) }
117
+ def post(uri, body, headers); end
118
+
119
+ # A connection you passed in is yours to close, we only close the one we made
120
+ sig { void }
121
+ def close; end
122
+
123
+ # sord omit - no YARD type given for "verb", using untyped
124
+ # sord omit - no YARD type given for "uri", using untyped
125
+ # sord omit - no YARD type given for "body", using untyped
126
+ # sord omit - no YARD type given for "headers", using untyped
127
+ # sord omit - no YARD return type given, using untyped
128
+ sig do
129
+ params(
130
+ verb: T.untyped,
131
+ uri: T.untyped,
132
+ body: T.untyped,
133
+ headers: T.untyped
134
+ ).returns(T.untyped)
135
+ end
136
+ def request(verb, uri, body, headers); end
137
+ end
138
+
139
+ # Keeps one connection per host open for the duration of the upload
140
+ class NetHTTP
141
+ TRANSIENT_ERRORS = T.let([
142
+ IOError, EOFError, SocketError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout,
143
+ Net::WriteTimeout, OpenSSL::SSL::SSLError
144
+ ].freeze, T.untyped)
145
+
146
+ # _@param_ `http_options` — forwarded to `Net::HTTP.start` (`open_timeout:`, `read_timeout:`...)
147
+ sig { params(http_options: T::Hash[T.untyped, T.untyped]).void }
148
+ def initialize(**http_options); end
149
+
150
+ # sord warn - URI::Generic wasn't able to be resolved to a constant in this project
151
+ # _@param_ `uri`
152
+ #
153
+ # _@param_ `body`
154
+ #
155
+ # _@param_ `headers`
156
+ sig { params(uri: URI::Generic, body: String, headers: T::Hash[String, String]).returns(GCSPut::Transport::Response) }
157
+ def put(uri, body, headers); end
158
+
159
+ # sord warn - URI::Generic wasn't able to be resolved to a constant in this project
160
+ # _@param_ `uri`
161
+ #
162
+ # _@param_ `body`
163
+ #
164
+ # _@param_ `headers`
165
+ sig { params(uri: URI::Generic, body: String, headers: T::Hash[String, String]).returns(GCSPut::Transport::Response) }
166
+ def post(uri, body, headers); end
167
+
168
+ # Closes the kept connections. Safe to call repeatedly, the next request reopens as needed
169
+ sig { void }
170
+ def close; end
171
+
172
+ # sord omit - no YARD type given for "request_class", using untyped
173
+ # sord omit - no YARD type given for "uri", using untyped
174
+ # sord omit - no YARD type given for "body", using untyped
175
+ # sord omit - no YARD type given for "headers", using untyped
176
+ # sord omit - no YARD return type given, using untyped
177
+ sig do
178
+ params(
179
+ request_class: T.untyped,
180
+ uri: T.untyped,
181
+ body: T.untyped,
182
+ headers: T.untyped
183
+ ).returns(T.untyped)
184
+ end
185
+ def request(request_class, uri, body, headers); end
186
+
187
+ # sord omit - no YARD type given for "uri", using untyped
188
+ # sord omit - no YARD return type given, using untyped
189
+ sig { params(uri: T.untyped).returns(T.untyped) }
190
+ def connection_for(uri); end
191
+ end
192
+ end
193
+
194
+ # Chops an arbitrary stream of writes into evenly sized chunks. Every chunk except the last
195
+ # will be exactly `chunk_size` bytes, and the last one can be anything from 0 bytes up to and
196
+ # including `chunk_size`. A chunk which fills up exactly is held back until `finish` so that
197
+ # a stream ending on a chunk boundary still delivers that chunk flagged as the last one.
198
+ #
199
+ # chunker = ByteChunker.new(chunk_size: 3) { |bytes, is_last| puts [bytes, is_last].inspect }
200
+ # chunker << "ab" << "cdefg" # => ["abc", false], ["def", false]
201
+ # chunker.finish # => ["g", true]
202
+ class ByteChunker
203
+ # _@param_ `chunk_size` — the size that every chunk except the last must have
204
+ sig { params(chunk_size: Integer, delivery_proc: T.untyped).void }
205
+ def initialize(chunk_size:, &delivery_proc); end
206
+
207
+ # _@param_ `bin_str` — the bytes to append
208
+ sig { params(bin_str: String).returns(T.self_type) }
209
+ def <<(bin_str); end
210
+
211
+ # _@param_ `bin_str` — the bytes to append
212
+ #
213
+ # _@return_ — the number of bytes appended, like `IO#write`
214
+ sig { params(bin_str: String).returns(Integer) }
215
+ def write(bin_str); end
216
+
217
+ # Delivers whatever is left in the buffer as the last chunk. The last chunk
218
+ # gets delivered even when empty - it is what closes the upload
219
+ sig { void }
220
+ def finish; end
221
+
222
+ sig { void }
223
+ def deliver_full_chunks; end
224
+ end
225
+
226
+ # A writable object which streams what you write into a GCS resumable upload session using
227
+ # ranged PUTs, see https://cloud.google.com/storage/docs/performing-resumable-uploads#chunked-upload
228
+ # You do not need to know the size of the output in advance. You do need to `finish` the upload,
229
+ # since it is the last PUT (with the total size filled in) which closes the GCS object.
230
+ #
231
+ # gcs_file = bucket.file("upload.bin", skip_lookup: true)
232
+ # GCSPut.with_gcs_file(gcs_file) do |io|
233
+ # io.write("Hello resumable")
234
+ # 20.times { io.write(Random.bytes(1024 * 1024)) }
235
+ # end
236
+ #
237
+ # Every chunk is retried from the byte offset GCS reports as persisted, so a chunk which
238
+ # only partially made it over the wire gets topped up rather than resent from the start.
239
+ #
240
+ # Starting the session needs a signed POST URL, and signing needs a private key. Under
241
+ # workload identity there is no key, so we fall back to the IAM signBlob API - which means
242
+ # the service account must hold `roles/iam.serviceAccountTokenCreator` on itself. See
243
+ # https://github.com/googleapis/google-cloud-ruby/issues/13307
244
+ class ResumableUpload
245
+ extend Forwardable
246
+
247
+ # Appends bytes to the upload, sending out a chunk whenever one fills up
248
+ #
249
+ # _@param_ `bin_str`
250
+ #
251
+ # _@return_ — the number of bytes appended, like `IO#write`
252
+ sig { params(bin_str: String).returns(Integer) }
253
+ def write(bin_str); end
254
+
255
+ # Appends bytes to the upload, sending out a chunk whenever one fills up
256
+ #
257
+ # _@param_ `bin_str`
258
+ sig { params(bin_str: String).returns(T.self_type) }
259
+ def <<(bin_str); end
260
+
261
+ # sord warn - Google::Cloud::Storage::File wasn't able to be resolved to a constant in this project
262
+ # sord duck - #put looks like a duck type, replacing with untyped
263
+ # sord duck - #post looks like a duck type, replacing with untyped
264
+ # sord duck - #close looks like a duck type, replacing with untyped
265
+ # Signs a session start URL for the object, starts the session and returns the upload.
266
+ # With a block, yields the upload, finishes it once the block returns and returns the total size.
267
+ #
268
+ # _@param_ `gcs_file` — the object to upload into, does not need to exist yet
269
+ #
270
+ # _@param_ `content_type` — the content type of the resulting object
271
+ #
272
+ # _@param_ `transport` — see `GCSPut::Transport`
273
+ #
274
+ # _@param_ `signed_url_options` — passed to `gcs_file.signed_url`, see `Signer.url_issuer_and_signer`
275
+ #
276
+ # _@param_ `options` — see {#initialize}
277
+ #
278
+ # _@return_ — the upload, or the total size when given a block
279
+ sig do
280
+ params(
281
+ gcs_file: Google::Cloud::Storage::File,
282
+ content_type: String,
283
+ transport: T.untyped,
284
+ signed_url_options: T::Hash[T.untyped, T.untyped],
285
+ options: T::Hash[T.untyped, T.untyped],
286
+ blk: T.untyped
287
+ ).returns(T.any(GCSPut::ResumableUpload, Integer))
288
+ end
289
+ def self.with_gcs_file(gcs_file, content_type: "binary/octet-stream", transport: GCSPut::Transport::NetHTTP.new, signed_url_options: {}, **options, &blk); end
290
+
291
+ # sord duck - #put looks like a duck type, replacing with untyped
292
+ # sord duck - #post looks like a duck type, replacing with untyped
293
+ # sord duck - #close looks like a duck type, replacing with untyped
294
+ # Starts a session from a signed POST URL (one with `x-goog-resumable: start` among its signed headers)
295
+ # and returns the upload, see https://cloud.google.com/storage/docs/performing-resumable-uploads#initiate-session
296
+ # With a block, yields the upload, finishes it once the block returns and returns the total size.
297
+ #
298
+ # _@param_ `signed_post_url`
299
+ #
300
+ # _@param_ `content_type` — must match the content type the URL was signed with
301
+ #
302
+ # _@param_ `transport` — see `GCSPut::Transport`
303
+ #
304
+ # _@param_ `options` — see {#initialize}
305
+ #
306
+ # _@return_ — the upload, or the total size when given a block
307
+ sig do
308
+ params(
309
+ signed_post_url: String,
310
+ content_type: String,
311
+ transport: T.untyped,
312
+ options: T::Hash[T.untyped, T.untyped],
313
+ blk: T.untyped
314
+ ).returns(T.any(GCSPut::ResumableUpload, Integer))
315
+ end
316
+ def self.with_signed_post_url(signed_post_url, content_type: "binary/octet-stream", transport: GCSPut::Transport::NetHTTP.new, **options, &blk); end
317
+
318
+ # Wraps an already started session. With a block, yields the upload, finishes it once
319
+ # the block returns and returns the total size.
320
+ #
321
+ # _@param_ `session_url` — the `Location` returned by the session start
322
+ #
323
+ # _@param_ `options` — see {#initialize}
324
+ #
325
+ # _@return_ — the upload, or the total size when given a block
326
+ sig { params(session_url: String, options: T::Hash[T.untyped, T.untyped]).returns(T.any(GCSPut::ResumableUpload, Integer)) }
327
+ def self.with_session_url(session_url, **options); end
328
+
329
+ # sord duck - #put looks like a duck type, replacing with untyped
330
+ # sord duck - #post looks like a duck type, replacing with untyped
331
+ # sord duck - #close looks like a duck type, replacing with untyped
332
+ # Prefer the `with_*` factories. This does no HTTP by itself, the first request goes out
333
+ # once a chunk fills up or `finish` gets called
334
+ #
335
+ # _@param_ `session_url` — the `Location` returned by the session start
336
+ #
337
+ # _@param_ `chunk_size` — must be a multiple of 256 KiB
338
+ #
339
+ # _@param_ `content_type` — must match the content type the session was started with
340
+ #
341
+ # _@param_ `max_attempts` — how many times a single chunk may be sent before giving up
342
+ #
343
+ # _@param_ `transport` — see `GCSPut::Transport`
344
+ sig do
345
+ params(
346
+ session_url: String,
347
+ chunk_size: Integer,
348
+ content_type: String,
349
+ max_attempts: Integer,
350
+ transport: T.untyped
351
+ ).void
352
+ end
353
+ def initialize(session_url, chunk_size: GCSPut::DEFAULT_CHUNK_SIZE, content_type: "binary/octet-stream", max_attempts: 5, transport: GCSPut::Transport::NetHTTP.new); end
354
+
355
+ # Sends the remaining buffered bytes as the final chunk and closes the GCS object.
356
+ # Also available as `close` so that writers which close their underlying IO, like
357
+ # `Zlib::GzipWriter`, finish the upload for you
358
+ #
359
+ # _@return_ — the total number of bytes uploaded
360
+ sig { returns(Integer) }
361
+ def finish; end
362
+
363
+ # _@param_ `chunk`
364
+ #
365
+ # _@param_ `is_last`
366
+ sig { params(chunk: String, is_last: T::Boolean).void }
367
+ def upload_chunk(chunk, is_last); end
368
+
369
+ # _@param_ `body`
370
+ #
371
+ # _@param_ `from`
372
+ #
373
+ # _@param_ `total` — the total size, or "*" while still unknown
374
+ sig { params(body: String, from: Integer, total: T.any(Integer, String)).returns(GCSPut::Transport::Response) }
375
+ def put_bytes(body, from:, total:); end
376
+
377
+ # Asks GCS how much of the upload it has and updates `bytes_persisted` accordingly, see
378
+ # https://cloud.google.com/storage/docs/performing-resumable-uploads#status-check
379
+ # A status check which fails itself just leaves us with what we already knew
380
+ #
381
+ # _@return_ — whether the session turned out to be finalized already
382
+ sig { returns(T::Boolean) }
383
+ def sync_with_session_finds_it_finalized?; end
384
+
385
+ # The `Range` header is "bytes=0-N" with N being the last persisted byte, and absent if nothing persisted yet
386
+ #
387
+ # _@param_ `response`
388
+ sig { params(response: GCSPut::Transport::Response).returns(Integer) }
389
+ def persisted_offset_from(response); end
390
+
391
+ # _@return_ — the session URL, valid for a week and usable from any process
392
+ sig { returns(String) }
393
+ attr_reader :session_url
394
+
395
+ # _@return_ — the number of bytes GCS has confirmed as persisted so far
396
+ sig { returns(Integer) }
397
+ attr_reader :bytes_persisted
398
+ end
399
+ end