baza.rb 0.8.0 → 0.9.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: ebcb57db3953278c8d402cc38ae954e982062cfc4498e301bc0495a7b3cba95a
4
- data.tar.gz: 0a6acf7107e149f8515911542460266b95b825df586576713734648751f2de56
3
+ metadata.gz: 82a8cd1767f0f915a8345f5aa9ad79699b8a5a1d5f102cae9c77dd822d2a4e77
4
+ data.tar.gz: 15c9601e95620ccc4904c94e6e813cac13ea3b46aa398797433d03d9c5ccf219
5
5
  SHA512:
6
- metadata.gz: a40b4c3655a48f0449cb0247f1dd1045344b95868330121b6ce462d874aec6117b1490d12819ceac9afc29217e0352d4d30d2c6ff78c1031761946bb3cf89654
7
- data.tar.gz: 7a479de17acf3e82f60327fa7c131605bebf2a0ae2dbfd1fde80ac08a3bcfafd2d40b252ef4d23845de083867a5955244d7079ce5526574d94c1fb9814e62cf8
6
+ metadata.gz: 92e9caded9b39f0194c79920b12a5d81ee3dbc0e21bace3ce8b7368ecea63ab9c7af739e02114f1cdff72337d09926ec3a7b8bbf5b2c60ebc65e88e7a17dd6da
7
+ data.tar.gz: e492fc6856487d474b58d869844e51b240f629572d5d4118df20926ba2cbab5ec116e3af4ddfa7b7586a3ba82d03667d240adcc66f319c3b0106d7698ea81c7c
@@ -13,5 +13,5 @@
13
13
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
14
14
  # License:: MIT
15
15
  class BazaRb
16
- VERSION = '0.8.0'
16
+ VERSION = '0.9.0'
17
17
  end
data/lib/baza-rb.rb CHANGED
@@ -68,15 +68,7 @@ class BazaRb
68
68
  def whoami
69
69
  nick = nil
70
70
  elapsed(@loog) do
71
- ret =
72
- retry_it do
73
- checked(
74
- Typhoeus::Request.get(
75
- home.append('whoami').to_s,
76
- headers:
77
- )
78
- )
79
- end
71
+ ret = get(home.append('whoami'))
80
72
  nick = ret.body
81
73
  throw :"I know that I am @#{nick}, at #{@host}"
82
74
  end
@@ -90,15 +82,7 @@ class BazaRb
90
82
  def balance
91
83
  z = nil
92
84
  elapsed(@loog) do
93
- ret =
94
- retry_it do
95
- checked(
96
- Typhoeus::Request.get(
97
- home.append('account').append('balance').to_s,
98
- headers:
99
- )
100
- )
101
- end
85
+ ret = get(home.append('account').append('balance'))
102
86
  z = ret.body.to_f
103
87
  throw :"The balance is Ƶ#{z}, at #{@host}"
104
88
  end
@@ -110,41 +94,27 @@ class BazaRb
110
94
  # @param [String] name The unique name of the job on the server
111
95
  # @param [String] data The binary data to push to the server (factbase content)
112
96
  # @param [Array<String>] meta List of metadata strings to attach to the job
113
- # @return [Integer] Job ID assigned by the server
97
+ # @param [Integer] chunk_size Maximum size of one chunk
114
98
  # @raise [ServerFailure] If the push operation fails
115
- def push(name, data, meta)
99
+ def push(name, data, meta, chunk_size: 1_000_000)
116
100
  raise 'The "name" of the job is nil' if name.nil?
117
101
  raise 'The "name" of the job may not be empty' if name.empty?
118
102
  raise 'The "data" of the job is nil' if data.nil?
119
103
  raise 'The "meta" of the job is nil' if meta.nil?
120
- id = 0
121
- hdrs = headers.merge(
122
- 'Content-Type' => 'application/octet-stream',
123
- 'Content-Length' => data.bytesize
124
- )
125
- unless meta.empty?
126
- hdrs = hdrs.merge('X-Zerocracy-Meta' => meta.map { |v| Base64.encode64(v).delete("\n") }.join(' '))
127
- end
128
- params = {
129
- connecttimeout: @timeout,
130
- timeout: @timeout,
131
- body: data,
132
- headers: hdrs
133
- }
134
104
  elapsed(@loog) do
135
- ret =
136
- retry_it do
137
- checked(
138
- Typhoeus::Request.put(
139
- home.append('push').append(name).to_s,
140
- @compress ? zipped(params) : params
141
- )
142
- )
143
- end
144
- id = ret.body.to_i
145
- throw :"Pushed #{data.bytesize} bytes to #{@host}, job ID is ##{id}"
105
+ Tempfile.open do |file|
106
+ File.binwrite(file.path, data)
107
+ upload(
108
+ home.append('push').append(name),
109
+ file.path,
110
+ headers.merge(
111
+ 'X-Zerocracy-Meta' => meta.map { |v| Base64.encode64(v).delete("\n") }.join(' ')
112
+ ),
113
+ chunk_size:
114
+ )
115
+ end
116
+ throw :"Pushed #{data.bytesize} bytes to #{@host}"
146
117
  end
147
- id
148
118
  end
149
119
 
150
120
  # Pull factbase from the server for a specific job.
@@ -158,7 +128,7 @@ class BazaRb
158
128
  data = ''
159
129
  elapsed(@loog) do
160
130
  Tempfile.open do |file|
161
- download(home.append('pull').append("#{id}.fb").to_s, file.path)
131
+ download(home.append('pull').append("#{id}.fb"), file.path)
162
132
  data = File.binread(file)
163
133
  throw :"Pulled #{data.bytesize} bytes of job ##{id} factbase at #{@host}"
164
134
  end
@@ -176,15 +146,7 @@ class BazaRb
176
146
  raise 'The ID of the job must be a positive integer' unless id.positive?
177
147
  fin = false
178
148
  elapsed(@loog) do
179
- ret =
180
- retry_it do
181
- checked(
182
- Typhoeus::Request.get(
183
- home.append('finished').append(id).to_s,
184
- headers:
185
- )
186
- )
187
- end
149
+ ret = get(home.append('finished').append(id))
188
150
  fin = ret.body == 'yes'
189
151
  throw :"The job ##{id} is #{'not yet ' unless fin}finished at #{@host}#{" (#{ret.body.inspect})" unless fin}"
190
152
  end
@@ -195,20 +157,13 @@ class BazaRb
195
157
  #
196
158
  # @param [Integer] id The ID of the job on the server
197
159
  # @return [String] The stdout, as a text
160
+ # @raise [ServerFailure] If the job doesn't exist or retrieval fails
198
161
  def stdout(id)
199
162
  raise 'The ID of the job is nil' if id.nil?
200
163
  raise 'The ID of the job must be a positive integer' unless id.positive?
201
164
  stdout = ''
202
165
  elapsed(@loog) do
203
- ret =
204
- retry_it do
205
- checked(
206
- Typhoeus::Request.get(
207
- home.append('stdout').append("#{id}.txt").to_s,
208
- headers:
209
- )
210
- )
211
- end
166
+ ret = get(home.append('stdout').append("#{id}.txt"))
212
167
  stdout = ret.body
213
168
  throw :"The stdout of the job ##{id} has #{stdout.split("\n").count} lines"
214
169
  end
@@ -219,20 +174,13 @@ class BazaRb
219
174
  #
220
175
  # @param [Integer] id The ID of the job on the server
221
176
  # @return [Integer] The exit code
177
+ # @raise [ServerFailure] If the job doesn't exist or retrieval fails
222
178
  def exit_code(id)
223
179
  raise 'The ID of the job is nil' if id.nil?
224
180
  raise 'The ID of the job must be a positive integer' unless id.positive?
225
181
  code = 0
226
182
  elapsed(@loog) do
227
- ret =
228
- retry_it do
229
- checked(
230
- Typhoeus::Request.get(
231
- home.append('exit').append("#{id}.txt").to_s,
232
- headers:
233
- )
234
- )
235
- end
183
+ ret = get(home.append('exit').append("#{id}.txt"))
236
184
  code = ret.body.to_i
237
185
  throw :"The exit code of the job ##{id} is #{code}"
238
186
  end
@@ -243,20 +191,13 @@ class BazaRb
243
191
  #
244
192
  # @param [Integer] id The ID of the job on the server
245
193
  # @return [String] The verdict
194
+ # @raise [ServerFailure] If the job doesn't exist or retrieval fails
246
195
  def verified(id)
247
196
  raise 'The ID of the job is nil' if id.nil?
248
197
  raise 'The ID of the job must be a positive integer' unless id.positive?
249
198
  verdict = ''
250
199
  elapsed(@loog) do
251
- ret =
252
- retry_it do
253
- checked(
254
- Typhoeus::Request.get(
255
- home.append('jobs').append(id).append('verified.txt').to_s,
256
- headers:
257
- )
258
- )
259
- end
200
+ ret = get(home.append('jobs').append(id).append('verified.txt'))
260
201
  verdict = ret.body
261
202
  throw :"The verdict of the job ##{id} is #{verdict.inspect}"
262
203
  end
@@ -267,21 +208,18 @@ class BazaRb
267
208
  #
268
209
  # @param [String] name The name of the job on the server
269
210
  # @param [String] owner The owner of the lock (any string)
211
+ # @raise [RuntimeError] If the name is already locked
212
+ # @raise [ServerFailure] If the lock operation fails
270
213
  def lock(name, owner)
271
214
  raise 'The "name" of the job is nil' if name.nil?
272
215
  raise 'The "name" of the job may not be empty' if name.empty?
273
216
  raise 'The "owner" of the lock is nil' if owner.nil?
274
217
  elapsed(@loog) do
275
- ret =
276
- retry_it do
277
- checked(
278
- Typhoeus::Request.get(
279
- home.append('lock').append(name).add(owner:).to_s,
280
- headers:
281
- ),
282
- [302, 409]
283
- )
284
- end
218
+ ret = post(
219
+ home.append('lock').append(name),
220
+ { 'owner' => owner },
221
+ [302, 409]
222
+ )
285
223
  throw :"Job name '#{name}' locked at #{@host}" if ret.code == 302
286
224
  raise "Failed to lock '#{name}' job at #{@host}, it's already locked"
287
225
  end
@@ -291,21 +229,17 @@ class BazaRb
291
229
  #
292
230
  # @param [String] name The name of the job on the server
293
231
  # @param [String] owner The owner of the lock (any string)
232
+ # @raise [ServerFailure] If the unlock operation fails
294
233
  def unlock(name, owner)
295
234
  raise 'The "name" of the job is nil' if name.nil?
296
235
  raise 'The "name" of the job may not be empty' if name.empty?
297
236
  raise 'The "owner" of the lock is nil' if owner.nil?
298
237
  raise 'The "owner" of the lock may not be empty' if owner.empty?
299
238
  elapsed(@loog) do
300
- retry_it do
301
- checked(
302
- Typhoeus::Request.get(
303
- home.append('unlock').append(name).add(owner:).to_s,
304
- headers:
305
- ),
306
- 302
307
- )
308
- end
239
+ post(
240
+ home.append('unlock').append(name),
241
+ { 'owner' => owner }
242
+ )
309
243
  throw :"Job name '#{name}' unlocked at #{@host}"
310
244
  end
311
245
  end
@@ -314,20 +248,13 @@ class BazaRb
314
248
  #
315
249
  # @param [String] name The name of the job on the server
316
250
  # @return [Integer] The ID of the job on the server
251
+ # @raise [ServerFailure] If the job doesn't exist or retrieval fails
317
252
  def recent(name)
318
253
  raise 'The "name" of the job is nil' if name.nil?
319
254
  raise 'The "name" of the job may not be empty' if name.empty?
320
255
  job = nil
321
256
  elapsed(@loog) do
322
- ret =
323
- retry_it do
324
- checked(
325
- Typhoeus::Request.get(
326
- home.append('recent').append("#{name}.txt").to_s,
327
- headers:
328
- )
329
- )
330
- end
257
+ ret = get(home.append('recent').append("#{name}.txt"))
331
258
  job = ret.body.to_i
332
259
  throw :"The recent \"#{name}\" job's ID is ##{job} at #{@host}"
333
260
  end
@@ -343,15 +270,7 @@ class BazaRb
343
270
  raise 'The "name" of the job may not be empty' if name.empty?
344
271
  exists = false
345
272
  elapsed(@loog) do
346
- ret =
347
- retry_it do
348
- checked(
349
- Typhoeus::Request.get(
350
- home.append('exists').append(name).to_s,
351
- headers:
352
- )
353
- )
354
- end
273
+ ret = get(home.append('exists').append(name))
355
274
  exists = ret.body == 'yes'
356
275
  throw :"The name \"#{name}\" #{exists ? 'exists' : "doesn't exist"} at #{@host}"
357
276
  end
@@ -362,9 +281,10 @@ class BazaRb
362
281
  #
363
282
  # @param [String] jname The name of the job on the server
364
283
  # @param [String] file The path to the file to upload
284
+ # @param [Integer] chunk_size Maximum size of one chunk
365
285
  # @return [Integer] The ID of the created durable
366
286
  # @raise [ServerFailure] If the upload fails
367
- def durable_place(jname, file)
287
+ def durable_place(jname, file, chunk_size: 1_000_000)
368
288
  raise 'The "jname" of the durable is nil' if jname.nil?
369
289
  raise 'The "jname" of the durable may not be empty' if jname.empty?
370
290
  raise 'The "file" of the durable is nil' if file.nil?
@@ -373,30 +293,20 @@ class BazaRb
373
293
  Tempfile.open do |f|
374
294
  File.write(f.path, 'placeholder')
375
295
  elapsed(@loog) do
376
- ret =
377
- retry_it do
378
- checked(
379
- Typhoeus::Request.post(
380
- home.append('durables').append('place').to_s,
381
- body: {
382
- '_csrf' => csrf,
383
- 'jname' => jname,
384
- 'file' => File.basename(file),
385
- 'zip' => File.open(f, 'rb')
386
- },
387
- headers:,
388
- connecttimeout: @timeout,
389
- timeout: @timeout
390
- ),
391
- 302
392
- )
393
- end
296
+ ret = post(
297
+ home.append('durables').append('place'),
298
+ {
299
+ 'jname' => jname,
300
+ 'file' => File.basename(file),
301
+ 'zip' => File.open(f, 'rb')
302
+ }
303
+ )
394
304
  id = ret.headers['X-Zerocracy-DurableId'].to_i
395
- throw :"Durable ##{id} (#{file}) placed for job \"#{jname}\" at #{@host}"
305
+ throw :"Durable ##{id} (#{file}, #{File.size(file)} bytes) placed for job \"#{jname}\" at #{@host}"
396
306
  end
397
307
  end
398
308
  durable_lock(id, user_agent)
399
- durable_save(id, file)
309
+ durable_save(id, file, chunk_size:)
400
310
  durable_unlock(id, user_agent)
401
311
  id
402
312
  end
@@ -405,23 +315,15 @@ class BazaRb
405
315
  #
406
316
  # @param [Integer] id The ID of the durable
407
317
  # @param [String] file The file to upload
408
- def durable_save(id, file)
318
+ # @param [Integer] chunk_size Maximum size of one chunk
319
+ # @raise [ServerFailure] If the save operation fails
320
+ def durable_save(id, file, chunk_size: 1_000_000)
409
321
  raise 'The ID of the durable is nil' if id.nil?
410
322
  raise 'The ID of the durable must be a positive integer' unless id.positive?
411
323
  raise 'The "file" of the durable is nil' if file.nil?
412
324
  raise "The file '#{file}' is absent" unless File.exist?(file)
413
325
  elapsed(@loog) do
414
- retry_it do
415
- checked(
416
- Typhoeus::Request.put(
417
- home.append('durables').append(id).to_s,
418
- body: File.binread(file),
419
- headers:,
420
- connecttimeout: @timeout,
421
- timeout: @timeout
422
- )
423
- )
424
- end
326
+ upload(home.append('durables').append(id), file, chunk_size:)
425
327
  throw :"Durable ##{id} saved #{File.size(file)} bytes to #{@host}"
426
328
  end
427
329
  end
@@ -429,13 +331,14 @@ class BazaRb
429
331
  # Load a single durable from server to local file.
430
332
  #
431
333
  # @param [Integer] id The ID of the durable
432
- # @param [String] file The file to upload
334
+ # @param [String] file The local file path to save the downloaded durable
335
+ # @raise [ServerFailure] If the load operation fails
433
336
  def durable_load(id, file)
434
337
  raise 'The ID of the durable is nil' if id.nil?
435
338
  raise 'The ID of the durable must be a positive integer' unless id.positive?
436
339
  raise 'The "file" of the durable is nil' if file.nil?
437
340
  elapsed(@loog) do
438
- download(home.append('durables').append(id).to_s, file)
341
+ download(home.append('durables').append(id), file)
439
342
  throw :"Durable ##{id} loaded #{File.size(file)} bytes from #{@host}"
440
343
  end
441
344
  end
@@ -444,21 +347,17 @@ class BazaRb
444
347
  #
445
348
  # @param [Integer] id The ID of the durable
446
349
  # @param [String] owner The owner of the lock
350
+ # @raise [ServerFailure] If the lock operation fails
447
351
  def durable_lock(id, owner)
448
352
  raise 'The ID of the durable is nil' if id.nil?
449
353
  raise 'The ID of the durable must be a positive integer' unless id.positive?
450
354
  raise 'The "owner" of the lock is nil' if owner.nil?
451
355
  raise 'The "owner" of the lock may not be empty' if owner.empty?
452
356
  elapsed(@loog) do
453
- retry_it do
454
- checked(
455
- Typhoeus::Request.get(
456
- home.append('durables').append(id).append('lock').add(owner:).to_s,
457
- headers:
458
- ),
459
- 302
460
- )
461
- end
357
+ post(
358
+ home.append('durables').append(id).append('lock'),
359
+ { 'owner' => owner }
360
+ )
462
361
  throw :"Durable ##{id} locked at #{@host}"
463
362
  end
464
363
  end
@@ -467,21 +366,17 @@ class BazaRb
467
366
  #
468
367
  # @param [Integer] id The ID of the durable
469
368
  # @param [String] owner The owner of the lock
369
+ # @raise [ServerFailure] If the unlock operation fails
470
370
  def durable_unlock(id, owner)
471
371
  raise 'The ID of the durable is nil' if id.nil?
472
372
  raise 'The ID of the durable must be a positive integer' unless id.positive?
473
373
  raise 'The "owner" of the lock is nil' if owner.nil?
474
374
  raise 'The "owner" of the lock may not be empty' if owner.empty?
475
375
  elapsed(@loog) do
476
- retry_it do
477
- checked(
478
- Typhoeus::Request.get(
479
- home.append('durables').append(id).append('unlock').add(owner:).to_s,
480
- headers:
481
- ),
482
- 302
483
- )
484
- end
376
+ post(
377
+ home.append('durables').append(id).append('unlock'),
378
+ { 'owner' => owner }
379
+ )
485
380
  throw :"Durable ##{id} unlocked at #{@host}"
486
381
  end
487
382
  end
@@ -498,16 +393,7 @@ class BazaRb
498
393
  raise 'The "file" may not be empty' if file.empty?
499
394
  id = nil
500
395
  elapsed(@loog) do
501
- ret =
502
- retry_it do
503
- checked(
504
- Typhoeus::Request.get(
505
- home.append('durables').append('find').add(jname:, file:).to_s,
506
- headers:
507
- ),
508
- [200, 404]
509
- )
510
- end
396
+ ret = get(home.append('durables').append('find').add(jname:, file:), [200, 404])
511
397
  if ret.code == 200
512
398
  id = ret.body.to_i
513
399
  throw :"Found durable ##{id} for job \"#{jname}\" file \"#{file}\" at #{@host}"
@@ -533,26 +419,16 @@ class BazaRb
533
419
  raise 'The "summary" is nil' if summary.nil?
534
420
  id = nil
535
421
  body = {
536
- '_csrf' => csrf,
537
422
  'human' => recipient,
538
423
  'amount' => format('%0.6f', amount),
539
424
  'summary' => summary
540
425
  }
541
426
  body['job'] = job unless job.nil?
542
427
  elapsed(@loog) do
543
- ret =
544
- retry_it do
545
- checked(
546
- Typhoeus::Request.post(
547
- home.append('account').append('transfer').to_s,
548
- body:,
549
- headers:,
550
- connecttimeout: @timeout,
551
- timeout: @timeout
552
- ),
553
- 302
554
- )
555
- end
428
+ ret = post(
429
+ home.append('account').append('transfer'),
430
+ body
431
+ )
556
432
  id = ret.headers['X-Zerocracy-ReceiptId'].to_i
557
433
  throw :"Transferred Ƶ#{format('%0.6f', amount)} to @#{recipient} at #{@host}"
558
434
  end
@@ -575,27 +451,16 @@ class BazaRb
575
451
  raise 'The "job" must be Integer' unless job.is_a?(Integer)
576
452
  raise 'The "summary" is nil' if summary.nil?
577
453
  id = nil
578
- body = {
579
- '_csrf' => csrf,
580
- 'tab' => tab,
581
- 'amount' => format('%0.6f', amount),
582
- 'summary' => summary,
583
- 'job' => job.to_s
584
- }
585
454
  elapsed(@loog) do
586
- ret =
587
- retry_it do
588
- checked(
589
- Typhoeus::Request.post(
590
- home.append('account').append('fee').to_s,
591
- body:,
592
- headers:,
593
- connecttimeout: @timeout,
594
- timeout: @timeout
595
- ),
596
- 302
597
- )
598
- end
455
+ ret = post(
456
+ home.append('account').append('fee'),
457
+ {
458
+ 'tab' => tab,
459
+ 'amount' => format('%0.6f', amount),
460
+ 'summary' => summary,
461
+ 'job' => job.to_s
462
+ }
463
+ )
599
464
  id = ret.headers['X-Zerocracy-ReceiptId'].to_i
600
465
  throw :"Fee Ƶ#{format('%0.6f', amount)} paid at #{@host}"
601
466
  end
@@ -612,16 +477,7 @@ class BazaRb
612
477
  success = false
613
478
  elapsed(@loog) do
614
479
  uri = home.append('pop').add(owner:)
615
- ret =
616
- retry_it do
617
- checked(
618
- Typhoeus::Request.get(
619
- uri.to_s,
620
- headers:
621
- ),
622
- [204, 302]
623
- )
624
- end
480
+ ret = get(uri, [204, 302])
625
481
  if ret.code == 204
626
482
  FileUtils.rm_f(zip)
627
483
  throw :"Nothing to pop at #{uri}"
@@ -647,20 +503,7 @@ class BazaRb
647
503
  raise 'The "zip" of the job is nil' if zip.nil?
648
504
  raise "The 'zip' file is absent: #{zip}" unless File.exist?(zip)
649
505
  elapsed(@loog) do
650
- retry_it do
651
- checked(
652
- Typhoeus::Request.put(
653
- home.append('finish').add(id:).to_s,
654
- connecttimeout: @timeout,
655
- timeout: @timeout,
656
- body: File.binread(zip),
657
- headers: headers.merge(
658
- 'Content-Type' => 'application/octet-stream',
659
- 'Content-Length' => File.size(zip)
660
- )
661
- )
662
- )
663
- end
506
+ upload(home.append('finish').add(id:), zip)
664
507
  throw :"Pushed #{File.size(zip)} bytes to #{@host}, finished job ##{id}"
665
508
  end
666
509
  end
@@ -681,30 +524,19 @@ class BazaRb
681
524
  def enter(name, badge, why, job)
682
525
  elapsed(@loog, intro: "Entered valve #{badge} to #{name}") do
683
526
  retry_it do
684
- ret = checked(
685
- Typhoeus::Request.get(
686
- home.append('valves').append('result').add(badge:).to_s,
687
- headers:
688
- ),
689
- [200, 204]
690
- )
527
+ ret = get(home.append('valves').append('result').add(badge:), [200, 204])
691
528
  return ret.body if ret.code == 200
692
529
  r = yield
693
530
  uri = home.append('valves').append('add')
694
531
  uri = uri.add(job:) unless job.nil?
695
- checked(
696
- Typhoeus::Request.post(
697
- uri.to_s,
698
- body: {
699
- '_csrf' => csrf,
700
- 'name' => name,
701
- 'badge' => badge,
702
- 'why' => why,
703
- 'result' => r.to_s
704
- },
705
- headers:
706
- ),
707
- 302
532
+ post(
533
+ uri,
534
+ {
535
+ 'name' => name,
536
+ 'badge' => badge,
537
+ 'why' => why,
538
+ 'result' => r.to_s
539
+ }
708
540
  )
709
541
  r
710
542
  end
@@ -721,15 +553,7 @@ class BazaRb
721
553
  def csrf
722
554
  token = nil
723
555
  elapsed(@loog) do
724
- retry_it do
725
- token = checked(
726
- Typhoeus::Request.get(
727
- home.append('csrf').to_s,
728
- headers:
729
- ),
730
- 200
731
- ).body
732
- end
556
+ token = get(home.append('csrf')).body
733
557
  throw :"CSRF token retrieved (#{token.length} chars)"
734
558
  end
735
559
  token
@@ -737,10 +561,16 @@ class BazaRb
737
561
 
738
562
  private
739
563
 
564
+ # Get the user agent string for HTTP requests.
565
+ #
566
+ # @return [String] The user agent string
740
567
  def user_agent
741
568
  "baza.rb #{BazaRb::VERSION}"
742
569
  end
743
570
 
571
+ # Get default headers for HTTP requests.
572
+ #
573
+ # @return [Hash] The default headers including User-Agent, Connection, and authentication token
744
574
  def headers
745
575
  {
746
576
  'User-Agent' => user_agent,
@@ -749,8 +579,26 @@ class BazaRb
749
579
  }
750
580
  end
751
581
 
582
+ # Decompress gzipped data.
583
+ #
584
+ # @param [String] data The gzipped data to decompress
585
+ # @return [String] The decompressed data
586
+ def unzip(data)
587
+ io = StringIO.new(data)
588
+ gz = Zlib::GzipReader.new(io)
589
+ gz.read
590
+ end
591
+
592
+ # Compress request parameters with gzip.
593
+ #
594
+ # @param [Hash] params The request parameters with :body and :headers keys
595
+ # @return [Hash] The modified parameters with compressed body and updated headers
752
596
  def zipped(params)
753
- body = gzip(params.fetch(:body))
597
+ io = StringIO.new
598
+ gz = Zlib::GzipWriter.new(io)
599
+ gz.write(params.fetch(:body))
600
+ gz.close
601
+ body = io.string
754
602
  headers = params
755
603
  .fetch(:headers)
756
604
  .merge(
@@ -763,15 +611,9 @@ class BazaRb
763
611
  params.merge(body:, headers:)
764
612
  end
765
613
 
766
- def gzip(data)
767
- (+'').tap do |result|
768
- io = StringIO.new(result)
769
- gz = Zlib::GzipWriter.new(io)
770
- gz.write(data)
771
- gz.close
772
- end
773
- end
774
-
614
+ # Build the base URI for API requests.
615
+ #
616
+ # @return [Iri] The base URI object
775
617
  def home
776
618
  Iri.new('')
777
619
  .host(@host)
@@ -779,6 +621,10 @@ class BazaRb
779
621
  .scheme(@ssl ? 'https' : 'http')
780
622
  end
781
623
 
624
+ # Execute a block with retries on timeout.
625
+ #
626
+ # @yield The block to execute with retries
627
+ # @return [Object] The result of the block execution
782
628
  def retry_it(&)
783
629
  with_retries(max_tries: @retries, rescue: TimedOut, &)
784
630
  end
@@ -831,89 +677,168 @@ class BazaRb
831
677
  raise ServerFailure, msg
832
678
  end
833
679
 
834
- # Download file via GET, in ranges.
835
- # @param [String] uri The URI
836
- # @param [String] file The path to save to
680
+ # Make a GET request.
681
+ #
682
+ # @param [Iri] uri The URI to send the request to
683
+ # @param [Array<Integer>] allowed List of allowed HTTP response codes
684
+ # @return [Typhoeus::Response] The HTTP response
685
+ # @raise [ServerFailure] If the response code is not in the allowed list
686
+ def get(uri, allowed = [200])
687
+ retry_it do
688
+ checked(
689
+ Typhoeus::Request.get(
690
+ uri.to_s,
691
+ headers:,
692
+ connecttimeout: @timeout,
693
+ timeout: @timeout
694
+ ),
695
+ allowed
696
+ )
697
+ end
698
+ end
699
+
700
+ # Make a POST request.
701
+ #
702
+ # @param [Iri] uri The URI to send the request to
703
+ # @param [Hash] params The request parameters to send in the body
704
+ # @param [Array<Integer>] allowed List of allowed HTTP response codes
705
+ # @return [Typhoeus::Response] The HTTP response
706
+ # @raise [ServerFailure] If the response code is not in the allowed list
707
+ def post(uri, params, allowed = [302])
708
+ retry_it do
709
+ checked(
710
+ Typhoeus::Request.post(
711
+ uri.to_s,
712
+ body: params.merge('_csrf' => csrf),
713
+ headers:,
714
+ connecttimeout: @timeout,
715
+ timeout: @timeout
716
+ ),
717
+ allowed
718
+ )
719
+ end
720
+ end
721
+
722
+ # Download file via GET, using range requests for large files.
723
+ #
724
+ # @param [Iri] uri The URI to download from
725
+ # @param [String] file The local file path to save to
726
+ # @raise [ServerFailure] If the download fails
837
727
  def download(uri, file)
838
- raise 'The "file" is nil' if file.nil?
839
728
  FileUtils.mkdir_p(File.dirname(file))
729
+ FileUtils.rm_f(file)
730
+ chunk = 0
840
731
  elapsed(@loog) do
841
- File.open(file, 'wb+') do |f|
842
- loop do
843
- request = Typhoeus::Request.new(
844
- uri.to_s,
845
- method: :get,
846
- headers: headers.merge(
847
- 'Accept' => 'application/octet-stream',
848
- 'Range' => "bytes=#{f.size}-"
849
- ),
850
- connecttimeout: @timeout,
851
- timeout: @timeout
852
- )
853
- request.on_body do |chunk|
854
- f.write(chunk)
855
- end
856
- retry_it do
857
- request.run
858
- end
859
- ret = request.response
860
- checked(ret, [200, 206])
861
- break if ret.code == 200
862
- _, v = ret.headers['Content-Range'].split
863
- range, total = v.split('/')
864
- raise "Total size is not valid (#{total.inspect})" unless total.match?(/^\*|[0-9]+$/)
865
- b, e = range.split('-')
866
- raise "Range is not valid (#{range.inspect})" unless e.match?(/^[0-9]+$/)
867
- len = ret.headers['Content-Length'].to_i
868
- unless len.zero?
869
- raise "Range size (#{range.inspect}) is not equal to Content-Length" unless len - 1 == e.to_i - b.to_i
870
- raise "Range end (#{range.inspect}) is not equal to #{f.size}" if e.to_i != f.size - 1
871
- end
872
- break if e.to_i == total.to_i - 1
732
+ pos = 0
733
+ loop do
734
+ request = Typhoeus::Request.new(
735
+ uri.to_s,
736
+ method: :get,
737
+ headers: headers.merge(
738
+ 'Accept' => 'application/octet-stream',
739
+ 'Accept-Encoding' => 'gzip',
740
+ 'Range' => "bytes=#{pos}-"
741
+ ),
742
+ connecttimeout: @timeout,
743
+ timeout: @timeout
744
+ )
745
+ slice = ''
746
+ request.on_body do |data|
747
+ slice = data
748
+ end
749
+ retry_it do
750
+ request.run
751
+ end
752
+ ret = request.response
753
+ msg = [
754
+ "GET #{uri.to_uri.path} #{ret.code}",
755
+ "#{slice.bytesize} bytes",
756
+ ('in gzip' if ret.headers['Content-Encoding'] == 'gzip'),
757
+ ("ranged as #{ret.headers['Content-Range'].inspect}" if ret.headers['Content-Range'])
758
+ ]
759
+ checked(ret, [200, 206])
760
+ if ret.headers['Content-Encoding'] == 'gzip'
761
+ slice = unzip(slice)
762
+ msg << "unzipped to #{slice.bytesize} bytes"
763
+ end
764
+ File.open(file, 'ab') do |f|
765
+ f.write(slice)
873
766
  end
767
+ @loog.debug(msg.compact.join(', '))
768
+ break if ret.code == 200
769
+ _, v = ret.headers['Content-Range'].split
770
+ range, total = v.split('/')
771
+ raise "Total size is not valid (#{total.inspect})" unless total.match?(/^\*|[0-9]+$/)
772
+ _b, e = range.split('-')
773
+ raise "Range is not valid (#{range.inspect})" unless e.match?(/^[0-9]+$/)
774
+ len = ret.headers['Content-Length'].to_i
775
+ pos = e.to_i
776
+ pos += 1 unless len.zero?
777
+ break if e.to_i == total.to_i - 1
778
+ chunk += 1
779
+ sleep(1) if len.zero?
874
780
  end
875
- throw :"Downloaded #{File.size(file)} bytes from #{uri}"
781
+ throw :"Downloaded #{File.size(file)} bytes in #{chunk + 1} chunks from #{uri}"
876
782
  end
877
783
  end
878
784
 
879
- # Upload file via PUT, in ranges.
880
- # @param [String] uri The URI
881
- # @param [String] file The path to save to
882
- # @param [Hash] headers Hash of HTTP headers
883
- def upload(uri, file, headers = {})
884
- raise 'The "file" is nil' if file.nil?
885
- raise 'The "file" does not exist' unless File.exist?(file)
785
+ # Upload file via PUT, using chunked uploads for large files.
786
+ #
787
+ # @param [Iri] uri The URI to upload to
788
+ # @param [String] file The local file path to upload from
789
+ # @param [Hash] extra Hash of extra HTTP headers to include
790
+ # @param [Integer] chunk_size Maximum size of each chunk in bytes
791
+ # @raise [ServerFailure] If the upload fails
792
+ def upload(uri, file, extra = {}, chunk_size: 1_000_000)
886
793
  params = {
887
794
  connecttimeout: @timeout,
888
795
  timeout: @timeout,
889
- body: data,
890
- headers: headers.merge(
796
+ headers: headers.merge(extra).merge(
891
797
  'Content-Type' => 'application/octet-stream'
892
798
  )
893
799
  }
894
- max = 1_000_000
800
+ total = File.size(file)
895
801
  chunk = 0
802
+ sent = 0
896
803
  elapsed(@loog) do
897
- params[:headers]['X-Zerocracy-Chunk'] = chunk.to_s
898
804
  loop do
899
- File.open(file, 'rb') do |f|
900
- f.seek(max * chunk)
901
- data = f.read(max)
902
- data = '' if data.nil?
903
- params[:headers]['Content-Length'] = data.bytesize
805
+ slice =
806
+ if total > chunk_size
807
+ File.open(file, 'rb') do |f|
808
+ params[:headers]['X-Zerocracy-Chunk'] = chunk.to_s
809
+ f.seek(chunk_size * chunk)
810
+ f.read(chunk_size) || ''
811
+ end
812
+ else
813
+ File.binread(file)
814
+ end
815
+ params[:body] = slice
816
+ params[:headers]['Content-Length'] = slice.bytesize
817
+ params = zipped(params) if @compress
818
+ ret =
904
819
  retry_it do
905
820
  checked(
906
821
  Typhoeus::Request.put(
907
822
  uri.to_s,
908
- @compress ? zipped(params) : params
823
+ params
909
824
  )
910
825
  )
911
826
  end
912
- break if data.empty?
913
- end
827
+ sent += params[:body].bytesize
828
+ @loog.debug(
829
+ [
830
+ "PUT #{uri.to_uri.path} #{ret.code}",
831
+ ("gzipped #{slice.bytesize} bytes" if params[:headers]['Content-Encoding'] == 'gzip'),
832
+ "sent #{params[:body].bytesize} bytes",
833
+ ("chunk ##{chunk}" if params[:headers]['X-Zerocracy-Chunk']),
834
+ ('no chunks' unless params[:headers]['X-Zerocracy-Chunk'])
835
+ ].compact.join(', ')
836
+ )
837
+ break if slice.empty?
838
+ break if total <= chunk_size
914
839
  chunk += 1
915
840
  end
916
- throw :"Uploaded #{File.size(file)} bytes to #{uri} in #{chunk + 1} chunk(s)"
841
+ throw :"Uploaded #{sent} bytes to #{uri}#{" in #{chunk + 1} chunks" if chunk.positive?}"
917
842
  end
918
843
  end
919
844
  end
data/test/test_baza-rb.rb CHANGED
@@ -40,7 +40,7 @@ class TestBazaRb < Minitest::Test
40
40
  fb.insert.foo = 'test-' * 10_000
41
41
  fb.insert
42
42
  n = fake_name
43
- assert_predicate(LIVE.push(n, fb.export, []), :positive?)
43
+ LIVE.push(n, fb.export, [])
44
44
  assert(LIVE.name_exists?(n))
45
45
  assert_predicate(LIVE.recent(n), :positive?)
46
46
  id = LIVE.recent(n)
@@ -86,15 +86,16 @@ class TestBazaRb < Minitest::Test
86
86
  fb.insert.foo = 'test-' * 10_000
87
87
  fb.insert
88
88
  baza = BazaRb.new(HOST, PORT, TOKEN, compress: false)
89
- assert_predicate(baza.push(fake_name, fb.export, []), :positive?)
89
+ baza.push(fake_name, fb.export, [])
90
90
  end
91
91
 
92
92
  def test_live_durable_lock_unlock
93
93
  WebMock.enable_net_connect!
94
94
  skip('We are offline') unless we_are_online
95
95
  Dir.mktmpdir do |dir|
96
- file = File.join(dir, "#{fake_name}.bin")
97
- File.binwrite(file, 'hello, world!' * 100_000)
96
+ file = File.join(dir, 'before.bin')
97
+ before = 'hello, Джеф!' * 10
98
+ File.binwrite(file, before)
98
99
  jname = fake_name
99
100
  refute(LIVE.durable_find(jname, File.basename(file)))
100
101
  id = LIVE.durable_place(jname, file)
@@ -102,7 +103,12 @@ class TestBazaRb < Minitest::Test
102
103
  owner = fake_name
103
104
  LIVE.durable_lock(id, owner)
104
105
  LIVE.durable_load(id, file)
106
+ assert_equal(before, File.binread(file).force_encoding('UTF-8'))
107
+ after = 'привет, друг!'
108
+ File.binwrite(file, after)
105
109
  LIVE.durable_save(id, file)
110
+ LIVE.durable_load(id, file)
111
+ assert_equal(after, File.binread(file).force_encoding('UTF-8'))
106
112
  LIVE.durable_unlock(id, owner)
107
113
  end
108
114
  end
@@ -169,25 +175,36 @@ class TestBazaRb < Minitest::Test
169
175
 
170
176
  def test_unlocks_job_by_name
171
177
  WebMock.disable_net_connect!
172
- stub_request(:get, 'https://example.org/unlock/foo?owner=x').to_return(status: 302)
178
+ stub_request(:get, 'https://example.org/csrf').to_return(body: 'token')
179
+ stub_request(:post, %r{https://example.org/unlock/foo}).to_return(status: 302)
173
180
  assert(fake_baza.unlock('foo', 'x'))
174
181
  end
175
182
 
176
183
  def test_durable_place
177
184
  WebMock.disable_net_connect!
178
- stub_request(:get, 'https://example.org/csrf').to_return(body: 'token')
179
- stub_request(:post, 'https://example.org/durables/place').to_return(
180
- status: 302, headers: { 'X-Zerocracy-DurableId' => '42' }
181
- )
182
- stub_request(:get, %r{https://example\.org/durables/42/lock})
183
- .to_return(status: 302)
184
- stub_request(:get, %r{https://example\.org/durables/42/unlock})
185
- .to_return(status: 302)
186
- stub_request(:put, 'https://example.org/durables/42').to_return(status: 200)
187
- Dir.mktmpdir do |dir|
188
- file = File.join(dir, 'test.bin')
189
- File.binwrite(file, 'hello')
190
- assert_equal(42, fake_baza.durable_place('simple', file))
185
+ [fake_baza(compress: true), fake_baza(compress: false)].each do |baza|
186
+ stub_request(:get, 'https://example.org/csrf').to_return(body: 'token')
187
+ stub_request(:post, 'https://example.org/durables/place').to_return(
188
+ status: 302, headers: { 'X-Zerocracy-DurableId' => '42' }
189
+ )
190
+ stub_request(:post, %r{https://example\.org/durables/42/lock})
191
+ .to_return(status: 302)
192
+ stub_request(:post, %r{https://example\.org/durables/42/unlock})
193
+ .to_return(status: 302)
194
+ stub_request(:put, 'https://example.org/durables/42')
195
+ .with(headers: { 'X-Zerocracy-Chunk' => '0' })
196
+ .to_return(status: 200)
197
+ stub_request(:put, 'https://example.org/durables/42')
198
+ .with(headers: { 'X-Zerocracy-Chunk' => '1' })
199
+ .to_return(status: 200)
200
+ stub_request(:put, 'https://example.org/durables/42')
201
+ .with(headers: { 'X-Zerocracy-Chunk' => '2' })
202
+ .to_return(status: 200)
203
+ Dir.mktmpdir do |dir|
204
+ file = File.join(dir, 'test.bin')
205
+ File.binwrite(file, 'hello, world!')
206
+ assert_equal(42, baza.durable_place('simple', file, chunk_size: 8))
207
+ end
191
208
  end
192
209
  end
193
210
 
@@ -196,10 +213,7 @@ class TestBazaRb < Minitest::Test
196
213
  stub_request(:put, 'https://example.org/push/simple').to_return(
197
214
  status: 200, body: '42'
198
215
  )
199
- assert_equal(
200
- 42,
201
- fake_baza.push('simple', 'hello, world!', [])
202
- )
216
+ fake_baza.push('simple', 'hello, world!', [])
203
217
  end
204
218
 
205
219
  def test_simple_pop_with_no_job_found
@@ -226,7 +240,7 @@ class TestBazaRb < Minitest::Test
226
240
  .with(query: { job: })
227
241
  .to_return(
228
242
  status: 206,
229
- headers: { 'Content-Range' => 'bytes 0-0/*', 'X-Zerocracy-JobId' => job, 'Content-Length' => 0 },
243
+ headers: { 'Content-Range' => 'bytes 0-0/*', 'Content-Length' => 0 },
230
244
  body: ''
231
245
  )
232
246
  bin = nil
@@ -240,7 +254,6 @@ class TestBazaRb < Minitest::Test
240
254
  status: 206,
241
255
  headers: {
242
256
  'Content-Range' => "bytes 0-7/#{bin.size}",
243
- 'X-Zerocracy-JobId' => job,
244
257
  'Content-Length' => 8
245
258
  },
246
259
  body: bin[0..7]
@@ -252,7 +265,6 @@ class TestBazaRb < Minitest::Test
252
265
  status: 206,
253
266
  headers: {
254
267
  'Content-Range' => "bytes 8-#{bin.size - 1}/#{bin.size}",
255
- 'X-Zerocracy-JobId' => job,
256
268
  'Content-Length' => bin.size - 8
257
269
  },
258
270
  body: bin[8..]
@@ -317,7 +329,7 @@ class TestBazaRb < Minitest::Test
317
329
  def test_simple_pull
318
330
  WebMock.disable_net_connect!
319
331
  stub_request(:get, 'https://example.org/pull/333.fb').to_return(
320
- status: 200, body: 'hello, world!'
332
+ status: 200, body: 'hello, world!', headers: {}
321
333
  )
322
334
  assert(
323
335
  fake_baza.pull(333).start_with?('hello')
@@ -326,13 +338,15 @@ class TestBazaRb < Minitest::Test
326
338
 
327
339
  def test_simple_lock_success
328
340
  WebMock.disable_net_connect!
329
- stub_request(:get, 'https://example.org/lock/name?owner=owner').to_return(status: 302)
341
+ stub_request(:get, 'https://example.org/csrf').to_return(body: 'token')
342
+ stub_request(:post, %r{https://example.org/lock/name}).to_return(status: 302)
330
343
  fake_baza.lock('name', 'owner')
331
344
  end
332
345
 
333
346
  def test_simple_lock_failure
334
347
  WebMock.disable_net_connect!
335
- stub_request(:get, 'https://example.org/lock/name?owner=owner').to_return(status: 409)
348
+ stub_request(:get, 'https://example.org/csrf').to_return(body: 'token')
349
+ stub_request(:post, %r{https://example.org/lock/name}).to_return(status: 409)
336
350
  assert_raises(StandardError) do
337
351
  fake_baza.lock('name', 'owner')
338
352
  end
@@ -446,7 +460,7 @@ class TestBazaRb < Minitest::Test
446
460
  file = File.join(dir, 'test.txt')
447
461
  File.write(file, 'test content')
448
462
  stub_request(:put, 'https://example.org:443/durables/42')
449
- .with(headers: { 'X-Zerocracy-Token' => '000' }, body: 'test content')
463
+ .with(headers: { 'X-Zerocracy-Token' => '000' })
450
464
  .to_return(status: 200)
451
465
  fake_baza.durable_save(42, file)
452
466
  end
@@ -458,7 +472,7 @@ class TestBazaRb < Minitest::Test
458
472
  file = File.join(dir, 'loaded.txt')
459
473
  stub_request(:get, 'https://example.org:443/durables/42')
460
474
  .with(headers: { 'X-Zerocracy-Token' => '000' })
461
- .to_return(status: 200, body: 'loaded content')
475
+ .to_return(status: 200, body: 'loaded content', headers: {})
462
476
  fake_baza.durable_load(42, file)
463
477
  assert_equal('loaded content', File.read(file))
464
478
  end
@@ -466,7 +480,8 @@ class TestBazaRb < Minitest::Test
466
480
 
467
481
  def test_durable_lock
468
482
  WebMock.disable_net_connect!
469
- stub_request(:get, 'https://example.org:443/durables/42/lock?owner=test-owner')
483
+ stub_request(:get, 'https://example.org/csrf').to_return(body: 'token')
484
+ stub_request(:post, %r{https://example.org:443/durables/42/lock})
470
485
  .with(headers: { 'X-Zerocracy-Token' => '000' })
471
486
  .to_return(status: 302)
472
487
  fake_baza.durable_lock(42, 'test-owner')
@@ -474,7 +489,8 @@ class TestBazaRb < Minitest::Test
474
489
 
475
490
  def test_durable_unlock
476
491
  WebMock.disable_net_connect!
477
- stub_request(:get, 'https://example.org:443/durables/42/unlock?owner=test-owner')
492
+ stub_request(:get, 'https://example.org/csrf').to_return(body: 'token')
493
+ stub_request(:post, %r{https://example.org:443/durables/42/unlock})
478
494
  .with(headers: { 'X-Zerocracy-Token' => '000' })
479
495
  .to_return(status: 302)
480
496
  fake_baza.durable_unlock(42, 'test-owner')
@@ -630,8 +646,7 @@ class TestBazaRb < Minitest::Test
630
646
  body: 'data'
631
647
  )
632
648
  .to_return(status: 200, body: '123')
633
- id = baza.push('test', 'data', [])
634
- assert_equal(123, id)
649
+ baza.push('test', 'data', [])
635
650
  end
636
651
 
637
652
  private
@@ -663,8 +678,8 @@ class TestBazaRb < Minitest::Test
663
678
  req
664
679
  end
665
680
 
666
- def fake_baza
667
- BazaRb.new('example.org', 443, '000', loog: Loog::NULL)
681
+ def fake_baza(compress: true)
682
+ BazaRb.new('example.org', 443, '000', loog: Loog::NULL, compress:)
668
683
  end
669
684
 
670
685
  def fake_name
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: baza.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko