baza.rb 0.7.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: f6ee64c99f7aa6a6254d2a98ffd0a49f33cb5c33992094afedf2d926905b7a24
4
- data.tar.gz: 76722a8fdafbc741e314213ae3912ff8e8c6d7368cbde7edbe3ab89c87448916
3
+ metadata.gz: 82a8cd1767f0f915a8345f5aa9ad79699b8a5a1d5f102cae9c77dd822d2a4e77
4
+ data.tar.gz: 15c9601e95620ccc4904c94e6e813cac13ea3b46aa398797433d03d9c5ccf219
5
5
  SHA512:
6
- metadata.gz: 351ef08d92a9d637b51b163db95e26b8927abed703a51a1362e4fe51a10714c97afa7b16700bc348cfa4de168788929f5abb89c62e59a4ecf53ce2cd588b3cec
7
- data.tar.gz: 6560d77ff5939235f2db005c6151e3cf83ea0d8b40f4c0b7e84c6dde97caaa25daad25beb4cd3dbb936e837601783454b9eefad8bc0a56320d4a4c44d6e1b555
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.7.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,36 +281,33 @@ 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?
371
291
  raise "The file '#{file}' is absent" unless File.exist?(file)
372
292
  id = nil
373
- elapsed(@loog) do
374
- ret =
375
- retry_it do
376
- checked(
377
- Typhoeus::Request.post(
378
- home.append('durables').append('place').to_s,
379
- body: {
380
- '_csrf' => csrf,
381
- 'jname' => jname,
382
- 'file' => File.basename(file),
383
- 'zip' => File.open(file, 'rb')
384
- },
385
- headers:,
386
- connecttimeout: @timeout,
387
- timeout: @timeout
388
- ),
389
- 302
390
- )
391
- end
392
- id = ret.headers['X-Zerocracy-DurableId'].to_i
393
- throw :"Durable ##{id} (#{file}) placed for job \"#{jname}\" at #{@host}"
293
+ Tempfile.open do |f|
294
+ File.write(f.path, 'placeholder')
295
+ elapsed(@loog) do
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
+ )
304
+ id = ret.headers['X-Zerocracy-DurableId'].to_i
305
+ throw :"Durable ##{id} (#{file}, #{File.size(file)} bytes) placed for job \"#{jname}\" at #{@host}"
306
+ end
394
307
  end
308
+ durable_lock(id, user_agent)
309
+ durable_save(id, file, chunk_size:)
310
+ durable_unlock(id, user_agent)
395
311
  id
396
312
  end
397
313
 
@@ -399,23 +315,15 @@ class BazaRb
399
315
  #
400
316
  # @param [Integer] id The ID of the durable
401
317
  # @param [String] file The file to upload
402
- 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)
403
321
  raise 'The ID of the durable is nil' if id.nil?
404
322
  raise 'The ID of the durable must be a positive integer' unless id.positive?
405
323
  raise 'The "file" of the durable is nil' if file.nil?
406
324
  raise "The file '#{file}' is absent" unless File.exist?(file)
407
325
  elapsed(@loog) do
408
- retry_it do
409
- checked(
410
- Typhoeus::Request.put(
411
- home.append('durables').append(id).to_s,
412
- body: File.binread(file),
413
- headers:,
414
- connecttimeout: @timeout,
415
- timeout: @timeout
416
- )
417
- )
418
- end
326
+ upload(home.append('durables').append(id), file, chunk_size:)
419
327
  throw :"Durable ##{id} saved #{File.size(file)} bytes to #{@host}"
420
328
  end
421
329
  end
@@ -423,13 +331,14 @@ class BazaRb
423
331
  # Load a single durable from server to local file.
424
332
  #
425
333
  # @param [Integer] id The ID of the durable
426
- # @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
427
336
  def durable_load(id, file)
428
337
  raise 'The ID of the durable is nil' if id.nil?
429
338
  raise 'The ID of the durable must be a positive integer' unless id.positive?
430
339
  raise 'The "file" of the durable is nil' if file.nil?
431
340
  elapsed(@loog) do
432
- download(home.append('durables').append(id).to_s, file)
341
+ download(home.append('durables').append(id), file)
433
342
  throw :"Durable ##{id} loaded #{File.size(file)} bytes from #{@host}"
434
343
  end
435
344
  end
@@ -438,21 +347,17 @@ class BazaRb
438
347
  #
439
348
  # @param [Integer] id The ID of the durable
440
349
  # @param [String] owner The owner of the lock
350
+ # @raise [ServerFailure] If the lock operation fails
441
351
  def durable_lock(id, owner)
442
352
  raise 'The ID of the durable is nil' if id.nil?
443
353
  raise 'The ID of the durable must be a positive integer' unless id.positive?
444
354
  raise 'The "owner" of the lock is nil' if owner.nil?
445
355
  raise 'The "owner" of the lock may not be empty' if owner.empty?
446
356
  elapsed(@loog) do
447
- retry_it do
448
- checked(
449
- Typhoeus::Request.get(
450
- home.append('durables').append(id).append('lock').add(owner:).to_s,
451
- headers:
452
- ),
453
- 302
454
- )
455
- end
357
+ post(
358
+ home.append('durables').append(id).append('lock'),
359
+ { 'owner' => owner }
360
+ )
456
361
  throw :"Durable ##{id} locked at #{@host}"
457
362
  end
458
363
  end
@@ -461,21 +366,17 @@ class BazaRb
461
366
  #
462
367
  # @param [Integer] id The ID of the durable
463
368
  # @param [String] owner The owner of the lock
369
+ # @raise [ServerFailure] If the unlock operation fails
464
370
  def durable_unlock(id, owner)
465
371
  raise 'The ID of the durable is nil' if id.nil?
466
372
  raise 'The ID of the durable must be a positive integer' unless id.positive?
467
373
  raise 'The "owner" of the lock is nil' if owner.nil?
468
374
  raise 'The "owner" of the lock may not be empty' if owner.empty?
469
375
  elapsed(@loog) do
470
- retry_it do
471
- checked(
472
- Typhoeus::Request.get(
473
- home.append('durables').append(id).append('unlock').add(owner:).to_s,
474
- headers:
475
- ),
476
- 302
477
- )
478
- end
376
+ post(
377
+ home.append('durables').append(id).append('unlock'),
378
+ { 'owner' => owner }
379
+ )
479
380
  throw :"Durable ##{id} unlocked at #{@host}"
480
381
  end
481
382
  end
@@ -492,16 +393,7 @@ class BazaRb
492
393
  raise 'The "file" may not be empty' if file.empty?
493
394
  id = nil
494
395
  elapsed(@loog) do
495
- ret =
496
- retry_it do
497
- checked(
498
- Typhoeus::Request.get(
499
- home.append('durables').append('find').add(jname:, file:).to_s,
500
- headers:
501
- ),
502
- [200, 404]
503
- )
504
- end
396
+ ret = get(home.append('durables').append('find').add(jname:, file:), [200, 404])
505
397
  if ret.code == 200
506
398
  id = ret.body.to_i
507
399
  throw :"Found durable ##{id} for job \"#{jname}\" file \"#{file}\" at #{@host}"
@@ -527,26 +419,16 @@ class BazaRb
527
419
  raise 'The "summary" is nil' if summary.nil?
528
420
  id = nil
529
421
  body = {
530
- '_csrf' => csrf,
531
422
  'human' => recipient,
532
423
  'amount' => format('%0.6f', amount),
533
424
  'summary' => summary
534
425
  }
535
426
  body['job'] = job unless job.nil?
536
427
  elapsed(@loog) do
537
- ret =
538
- retry_it do
539
- checked(
540
- Typhoeus::Request.post(
541
- home.append('account').append('transfer').to_s,
542
- body:,
543
- headers:,
544
- connecttimeout: @timeout,
545
- timeout: @timeout
546
- ),
547
- 302
548
- )
549
- end
428
+ ret = post(
429
+ home.append('account').append('transfer'),
430
+ body
431
+ )
550
432
  id = ret.headers['X-Zerocracy-ReceiptId'].to_i
551
433
  throw :"Transferred Ƶ#{format('%0.6f', amount)} to @#{recipient} at #{@host}"
552
434
  end
@@ -569,27 +451,16 @@ class BazaRb
569
451
  raise 'The "job" must be Integer' unless job.is_a?(Integer)
570
452
  raise 'The "summary" is nil' if summary.nil?
571
453
  id = nil
572
- body = {
573
- '_csrf' => csrf,
574
- 'tab' => tab,
575
- 'amount' => format('%0.6f', amount),
576
- 'summary' => summary,
577
- 'job' => job.to_s
578
- }
579
454
  elapsed(@loog) do
580
- ret =
581
- retry_it do
582
- checked(
583
- Typhoeus::Request.post(
584
- home.append('account').append('fee').to_s,
585
- body:,
586
- headers:,
587
- connecttimeout: @timeout,
588
- timeout: @timeout
589
- ),
590
- 302
591
- )
592
- 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
+ )
593
464
  id = ret.headers['X-Zerocracy-ReceiptId'].to_i
594
465
  throw :"Fee Ƶ#{format('%0.6f', amount)} paid at #{@host}"
595
466
  end
@@ -606,16 +477,7 @@ class BazaRb
606
477
  success = false
607
478
  elapsed(@loog) do
608
479
  uri = home.append('pop').add(owner:)
609
- ret =
610
- retry_it do
611
- checked(
612
- Typhoeus::Request.get(
613
- uri.to_s,
614
- headers:
615
- ),
616
- [200, 204, 206]
617
- )
618
- end
480
+ ret = get(uri, [204, 302])
619
481
  if ret.code == 204
620
482
  FileUtils.rm_f(zip)
621
483
  throw :"Nothing to pop at #{uri}"
@@ -641,20 +503,7 @@ class BazaRb
641
503
  raise 'The "zip" of the job is nil' if zip.nil?
642
504
  raise "The 'zip' file is absent: #{zip}" unless File.exist?(zip)
643
505
  elapsed(@loog) do
644
- retry_it do
645
- checked(
646
- Typhoeus::Request.put(
647
- home.append('finish').add(id:).to_s,
648
- connecttimeout: @timeout,
649
- timeout: @timeout,
650
- body: File.binread(zip),
651
- headers: headers.merge(
652
- 'Content-Type' => 'application/octet-stream',
653
- 'Content-Length' => File.size(zip)
654
- )
655
- )
656
- )
657
- end
506
+ upload(home.append('finish').add(id:), zip)
658
507
  throw :"Pushed #{File.size(zip)} bytes to #{@host}, finished job ##{id}"
659
508
  end
660
509
  end
@@ -675,30 +524,19 @@ class BazaRb
675
524
  def enter(name, badge, why, job)
676
525
  elapsed(@loog, intro: "Entered valve #{badge} to #{name}") do
677
526
  retry_it do
678
- ret = checked(
679
- Typhoeus::Request.get(
680
- home.append('valves').append('result').add(badge:).to_s,
681
- headers:
682
- ),
683
- [200, 204]
684
- )
527
+ ret = get(home.append('valves').append('result').add(badge:), [200, 204])
685
528
  return ret.body if ret.code == 200
686
529
  r = yield
687
530
  uri = home.append('valves').append('add')
688
531
  uri = uri.add(job:) unless job.nil?
689
- checked(
690
- Typhoeus::Request.post(
691
- uri.to_s,
692
- body: {
693
- '_csrf' => csrf,
694
- 'name' => name,
695
- 'badge' => badge,
696
- 'why' => why,
697
- 'result' => r.to_s
698
- },
699
- headers:
700
- ),
701
- 302
532
+ post(
533
+ uri,
534
+ {
535
+ 'name' => name,
536
+ 'badge' => badge,
537
+ 'why' => why,
538
+ 'result' => r.to_s
539
+ }
702
540
  )
703
541
  r
704
542
  end
@@ -715,15 +553,7 @@ class BazaRb
715
553
  def csrf
716
554
  token = nil
717
555
  elapsed(@loog) do
718
- retry_it do
719
- token = checked(
720
- Typhoeus::Request.get(
721
- home.append('csrf').to_s,
722
- headers:
723
- ),
724
- 200
725
- ).body
726
- end
556
+ token = get(home.append('csrf')).body
727
557
  throw :"CSRF token retrieved (#{token.length} chars)"
728
558
  end
729
559
  token
@@ -731,16 +561,44 @@ class BazaRb
731
561
 
732
562
  private
733
563
 
564
+ # Get the user agent string for HTTP requests.
565
+ #
566
+ # @return [String] The user agent string
567
+ def user_agent
568
+ "baza.rb #{BazaRb::VERSION}"
569
+ end
570
+
571
+ # Get default headers for HTTP requests.
572
+ #
573
+ # @return [Hash] The default headers including User-Agent, Connection, and authentication token
734
574
  def headers
735
575
  {
736
- 'User-Agent' => "baza.rb #{BazaRb::VERSION}",
576
+ 'User-Agent' => user_agent,
737
577
  'Connection' => 'close',
738
578
  'X-Zerocracy-Token' => @token
739
579
  }
740
580
  end
741
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
742
596
  def zipped(params)
743
- 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
744
602
  headers = params
745
603
  .fetch(:headers)
746
604
  .merge(
@@ -753,15 +611,9 @@ class BazaRb
753
611
  params.merge(body:, headers:)
754
612
  end
755
613
 
756
- def gzip(data)
757
- (+'').tap do |result|
758
- io = StringIO.new(result)
759
- gz = Zlib::GzipWriter.new(io)
760
- gz.write(data)
761
- gz.close
762
- end
763
- end
764
-
614
+ # Build the base URI for API requests.
615
+ #
616
+ # @return [Iri] The base URI object
765
617
  def home
766
618
  Iri.new('')
767
619
  .host(@host)
@@ -769,6 +621,10 @@ class BazaRb
769
621
  .scheme(@ssl ? 'https' : 'http')
770
622
  end
771
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
772
628
  def retry_it(&)
773
629
  with_retries(max_tries: @retries, rescue: TimedOut, &)
774
630
  end
@@ -821,48 +677,168 @@ class BazaRb
821
677
  raise ServerFailure, msg
822
678
  end
823
679
 
824
- # Download file via GET, in ranges.
825
- # @param [String] uri The URI
826
- # @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
827
727
  def download(uri, file)
828
- raise 'The "file" is nil' if file.nil?
829
728
  FileUtils.mkdir_p(File.dirname(file))
729
+ FileUtils.rm_f(file)
730
+ chunk = 0
731
+ elapsed(@loog) do
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)
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?
780
+ end
781
+ throw :"Downloaded #{File.size(file)} bytes in #{chunk + 1} chunks from #{uri}"
782
+ end
783
+ end
784
+
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)
793
+ params = {
794
+ connecttimeout: @timeout,
795
+ timeout: @timeout,
796
+ headers: headers.merge(extra).merge(
797
+ 'Content-Type' => 'application/octet-stream'
798
+ )
799
+ }
800
+ total = File.size(file)
801
+ chunk = 0
802
+ sent = 0
830
803
  elapsed(@loog) do
831
- File.open(file, 'wb+') do |f|
832
- loop do
833
- request = Typhoeus::Request.new(
834
- uri.to_s,
835
- method: :get,
836
- headers: headers.merge(
837
- 'Accept' => 'application/octet-stream',
838
- 'Range' => "bytes=#{f.size}-"
839
- ),
840
- connecttimeout: @timeout,
841
- timeout: @timeout
842
- )
843
- request.on_body do |chunk|
844
- f.write(chunk)
804
+ loop do
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)
845
814
  end
815
+ params[:body] = slice
816
+ params[:headers]['Content-Length'] = slice.bytesize
817
+ params = zipped(params) if @compress
818
+ ret =
846
819
  retry_it do
847
- request.run
848
- end
849
- ret = request.response
850
- checked(ret, [200, 206])
851
- break if ret.code == 200
852
- _, v = ret.headers['Content-Range'].split
853
- range, total = v.split('/')
854
- raise "Total size is not valid (#{total.inspect})" unless total.match?(/^\*|[0-9]+$/)
855
- b, e = range.split('-')
856
- raise "Range is not valid (#{range.inspect})" unless e.match?(/^[0-9]+$/)
857
- len = ret.headers['Content-Length'].to_i
858
- unless len.zero?
859
- raise "Range size (#{range.inspect}) is not equal to Content-Length" unless len - 1 == e.to_i - b.to_i
860
- raise "Range end (#{range.inspect}) is not equal to #{f.size}" if e.to_i != f.size - 1
820
+ checked(
821
+ Typhoeus::Request.put(
822
+ uri.to_s,
823
+ params
824
+ )
825
+ )
861
826
  end
862
- break if e.to_i == total.to_i - 1
863
- 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
839
+ chunk += 1
864
840
  end
865
- throw :"Downloaded #{File.size(file)} bytes from #{uri}"
841
+ throw :"Uploaded #{sent} bytes to #{uri}#{" in #{chunk + 1} chunks" if chunk.positive?}"
866
842
  end
867
843
  end
868
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')
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,20 +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
- Dir.mktmpdir do |dir|
183
- file = File.join(dir, 'test.bin')
184
- File.binwrite(file, 'hello')
185
- 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
186
208
  end
187
209
  end
188
210
 
@@ -191,10 +213,7 @@ class TestBazaRb < Minitest::Test
191
213
  stub_request(:put, 'https://example.org/push/simple').to_return(
192
214
  status: 200, body: '42'
193
215
  )
194
- assert_equal(
195
- 42,
196
- fake_baza.push('simple', 'hello, world!', [])
197
- )
216
+ fake_baza.push('simple', 'hello, world!', [])
198
217
  end
199
218
 
200
219
  def test_simple_pop_with_no_job_found
@@ -212,9 +231,16 @@ class TestBazaRb < Minitest::Test
212
231
  job = 4242
213
232
  stub_request(:get, 'https://example.org/pop')
214
233
  .with(query: { owner: })
234
+ .to_return(
235
+ status: 302,
236
+ headers: { 'X-Zerocracy-JobId' => job },
237
+ body: ''
238
+ )
239
+ stub_request(:get, 'https://example.org/pop')
240
+ .with(query: { job: })
215
241
  .to_return(
216
242
  status: 206,
217
- headers: { 'Content-Range' => 'bytes 0-0/*', 'X-Zerocracy-JobId' => job, 'Content-Length' => 0 },
243
+ headers: { 'Content-Range' => 'bytes 0-0/*', 'Content-Length' => 0 },
218
244
  body: ''
219
245
  )
220
246
  bin = nil
@@ -228,7 +254,6 @@ class TestBazaRb < Minitest::Test
228
254
  status: 206,
229
255
  headers: {
230
256
  'Content-Range' => "bytes 0-7/#{bin.size}",
231
- 'X-Zerocracy-JobId' => job,
232
257
  'Content-Length' => 8
233
258
  },
234
259
  body: bin[0..7]
@@ -240,7 +265,6 @@ class TestBazaRb < Minitest::Test
240
265
  status: 206,
241
266
  headers: {
242
267
  'Content-Range' => "bytes 8-#{bin.size - 1}/#{bin.size}",
243
- 'X-Zerocracy-JobId' => job,
244
268
  'Content-Length' => bin.size - 8
245
269
  },
246
270
  body: bin[8..]
@@ -305,7 +329,7 @@ class TestBazaRb < Minitest::Test
305
329
  def test_simple_pull
306
330
  WebMock.disable_net_connect!
307
331
  stub_request(:get, 'https://example.org/pull/333.fb').to_return(
308
- status: 200, body: 'hello, world!'
332
+ status: 200, body: 'hello, world!', headers: {}
309
333
  )
310
334
  assert(
311
335
  fake_baza.pull(333).start_with?('hello')
@@ -314,13 +338,15 @@ class TestBazaRb < Minitest::Test
314
338
 
315
339
  def test_simple_lock_success
316
340
  WebMock.disable_net_connect!
317
- 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)
318
343
  fake_baza.lock('name', 'owner')
319
344
  end
320
345
 
321
346
  def test_simple_lock_failure
322
347
  WebMock.disable_net_connect!
323
- 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)
324
350
  assert_raises(StandardError) do
325
351
  fake_baza.lock('name', 'owner')
326
352
  end
@@ -434,7 +460,7 @@ class TestBazaRb < Minitest::Test
434
460
  file = File.join(dir, 'test.txt')
435
461
  File.write(file, 'test content')
436
462
  stub_request(:put, 'https://example.org:443/durables/42')
437
- .with(headers: { 'X-Zerocracy-Token' => '000' }, body: 'test content')
463
+ .with(headers: { 'X-Zerocracy-Token' => '000' })
438
464
  .to_return(status: 200)
439
465
  fake_baza.durable_save(42, file)
440
466
  end
@@ -446,7 +472,7 @@ class TestBazaRb < Minitest::Test
446
472
  file = File.join(dir, 'loaded.txt')
447
473
  stub_request(:get, 'https://example.org:443/durables/42')
448
474
  .with(headers: { 'X-Zerocracy-Token' => '000' })
449
- .to_return(status: 200, body: 'loaded content')
475
+ .to_return(status: 200, body: 'loaded content', headers: {})
450
476
  fake_baza.durable_load(42, file)
451
477
  assert_equal('loaded content', File.read(file))
452
478
  end
@@ -454,7 +480,8 @@ class TestBazaRb < Minitest::Test
454
480
 
455
481
  def test_durable_lock
456
482
  WebMock.disable_net_connect!
457
- 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})
458
485
  .with(headers: { 'X-Zerocracy-Token' => '000' })
459
486
  .to_return(status: 302)
460
487
  fake_baza.durable_lock(42, 'test-owner')
@@ -462,7 +489,8 @@ class TestBazaRb < Minitest::Test
462
489
 
463
490
  def test_durable_unlock
464
491
  WebMock.disable_net_connect!
465
- 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})
466
494
  .with(headers: { 'X-Zerocracy-Token' => '000' })
467
495
  .to_return(status: 302)
468
496
  fake_baza.durable_unlock(42, 'test-owner')
@@ -618,8 +646,7 @@ class TestBazaRb < Minitest::Test
618
646
  body: 'data'
619
647
  )
620
648
  .to_return(status: 200, body: '123')
621
- id = baza.push('test', 'data', [])
622
- assert_equal(123, id)
649
+ baza.push('test', 'data', [])
623
650
  end
624
651
 
625
652
  private
@@ -651,8 +678,8 @@ class TestBazaRb < Minitest::Test
651
678
  req
652
679
  end
653
680
 
654
- def fake_baza
655
- 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:)
656
683
  end
657
684
 
658
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.7.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko