baza.rb 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/baza.rb ADDED
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2024 Zerocracy
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require 'typhoeus'
24
+ require 'retries'
25
+ require 'iri'
26
+ require 'loog'
27
+ require 'base64'
28
+ require_relative 'baza/elapsed'
29
+
30
+ # Interface to the API of zerocracy.com.
31
+ #
32
+ # You make an instance of this class and then call one of its methods.
33
+ # The object will make HTTP request to api.zerocracy.com and interpret the
34
+ # results returned.
35
+ #
36
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
37
+ # Copyright:: Copyright (c) 2024 Yegor Bugayenko
38
+ # License:: MIT
39
+ class Baza
40
+ VERSION = '0.0.0'
41
+
42
+ def initialize(host, port, token, ssl: true, timeout: 30, retries: 3, loog: Loog::NULL, compression: true)
43
+ @host = host
44
+ @port = port
45
+ @ssl = ssl
46
+ @token = token
47
+ @timeout = timeout
48
+ @loog = loog
49
+ @retries = retries
50
+ @compression = compression
51
+ end
52
+
53
+ # Push factbase to the server.
54
+ # @param [String] name The name of the job on the server
55
+ # @param [Bytes] data The data to push to the server (binary)
56
+ # @param [Array<String>] meta List of metas, possibly empty
57
+ # @return [Integer] Job ID on the server
58
+ def push(name, data, meta)
59
+ id = 0
60
+ hdrs = headers.merge(
61
+ 'Content-Type' => 'application/octet-stream',
62
+ 'Content-Length' => data.size
63
+ )
64
+ unless meta.empty?
65
+ hdrs = hdrs.merge('X-Zerocracy-Meta' => meta.map { |v| Base64.encode64(v).gsub("\n", '') }.join(' '))
66
+ end
67
+ params = {
68
+ connecttimeout: @timeout,
69
+ timeout: @timeout,
70
+ body: data,
71
+ headers: hdrs
72
+ }
73
+ elapsed(@loog) do
74
+ ret =
75
+ with_retries(max_tries: @retries) do
76
+ checked(
77
+ Typhoeus::Request.put(
78
+ home.append('push').append(name).to_s,
79
+ @compression ? zipped(params) : params
80
+ )
81
+ )
82
+ end
83
+ id = ret.body.to_i
84
+ throw :"Pushed #{data.size} bytes to #{@host}, job ID is ##{id}"
85
+ end
86
+ id
87
+ end
88
+
89
+ # Pull factbase from the server.
90
+ # @param [Integer] id The ID of the job on the server
91
+ # @return [Bytes] Binary data pulled
92
+ def pull(id)
93
+ data = 0
94
+ elapsed(@loog) do
95
+ Tempfile.open do |file|
96
+ File.open(file, 'wb') do |f|
97
+ request = Typhoeus::Request.new(
98
+ home.append('pull').append("#{id}.fb").to_s,
99
+ headers: headers.merge(
100
+ 'Accept' => 'application/octet-stream'
101
+ ),
102
+ connecttimeout: @timeout,
103
+ timeout: @timeout
104
+ )
105
+ request.on_body do |chunk|
106
+ f.write(chunk)
107
+ end
108
+ with_retries(max_tries: @retries) do
109
+ request.run
110
+ end
111
+ checked(request.response)
112
+ end
113
+ data = File.binread(file)
114
+ throw :"Pulled #{data.size} bytes of job ##{id} factbase at #{@host}"
115
+ end
116
+ end
117
+ data
118
+ end
119
+
120
+ # The job with this ID is finished already?
121
+ # @param [Integer] id The ID of the job on the server
122
+ # @return [Boolean] TRUE if the job is already finished
123
+ def finished?(id)
124
+ finished = false
125
+ elapsed(@loog) do
126
+ ret =
127
+ with_retries(max_tries: @retries) do
128
+ checked(
129
+ Typhoeus::Request.get(
130
+ home.append('finished').append(id).to_s,
131
+ headers:
132
+ )
133
+ )
134
+ end
135
+ finished = ret.body == 'yes'
136
+ throw :"The job ##{id} is #{finished ? '' : 'not yet '}finished at #{@host}"
137
+ end
138
+ finished
139
+ end
140
+
141
+ # Read and return the stdout of the job.
142
+ # @param [Integer] id The ID of the job on the server
143
+ # @return [String] The stdout, as a text
144
+ def stdout(id)
145
+ stdout = ''
146
+ elapsed(@loog) do
147
+ ret =
148
+ with_retries(max_tries: @retries) do
149
+ checked(
150
+ Typhoeus::Request.get(
151
+ home.append('stdout').append("#{id}.txt").to_s,
152
+ headers:
153
+ )
154
+ )
155
+ end
156
+ stdout = ret.body
157
+ throw :"The stdout of the job ##{id} has #{stdout.split("\n")} lines"
158
+ end
159
+ stdout
160
+ end
161
+
162
+ # Read and return the exit code of the job.
163
+ # @param [Integer] id The ID of the job on the server
164
+ # @return [Integer] The exit code
165
+ def exit_code(id)
166
+ code = 0
167
+ elapsed(@loog) do
168
+ ret =
169
+ with_retries(max_tries: @retries) do
170
+ checked(
171
+ Typhoeus::Request.get(
172
+ home.append('exit').append("#{id}.txt").to_s,
173
+ headers:
174
+ )
175
+ )
176
+ end
177
+ code = ret.body.to_i
178
+ throw :"The exit code of the job ##{id} is #{code}"
179
+ end
180
+ code
181
+ end
182
+
183
+ # Lock the name.
184
+ # @param [String] name The name of the job on the server
185
+ # @param [String] owner The owner of the lock (any string)
186
+ def lock(name, owner)
187
+ elapsed(@loog) do
188
+ with_retries(max_tries: @retries) do
189
+ checked(
190
+ Typhoeus::Request.get(
191
+ home.append('lock').append(name).add(owner:).to_s,
192
+ headers:
193
+ ),
194
+ 302
195
+ )
196
+ end
197
+ end
198
+ end
199
+
200
+ # Unlock the name.
201
+ # @param [String] name The name of the job on the server
202
+ # @param [String] owner The owner of the lock (any string)
203
+ def unlock(name, owner)
204
+ elapsed(@loog) do
205
+ with_retries(max_tries: @retries) do
206
+ checked(
207
+ Typhoeus::Request.get(
208
+ home.append('unlock').append(name).add(owner:).to_s,
209
+ headers:
210
+ ),
211
+ 302
212
+ )
213
+ end
214
+ end
215
+ end
216
+
217
+ # Get the ID of the job by the name.
218
+ # @param [String] name The name of the job on the server
219
+ # @return [Integer] The ID of the job on the server
220
+ def recent(name)
221
+ job = 0
222
+ elapsed(@loog) do
223
+ ret =
224
+ with_retries(max_tries: @retries) do
225
+ checked(
226
+ Typhoeus::Request.get(
227
+ home.append('recent').append("#{name}.txt").to_s,
228
+ headers:
229
+ )
230
+ )
231
+ end
232
+ job = ret.body.to_i
233
+ throw :"The recent \"#{name}\" job's ID is ##{job} at #{@host}"
234
+ end
235
+ job
236
+ end
237
+
238
+ # Check whether the name of the job exists on the server.
239
+ # @param [String] name The name of the job on the server
240
+ # @return [Boolean] TRUE if such name exists
241
+ def name_exists?(name)
242
+ exists = 0
243
+ elapsed(@loog) do
244
+ ret =
245
+ with_retries(max_tries: @retries) do
246
+ checked(
247
+ Typhoeus::Request.get(
248
+ home.append('exists').append(name).to_s,
249
+ headers:
250
+ )
251
+ )
252
+ end
253
+ exists = ret.body == 'yes'
254
+ throw :"The name \"#{name}\" #{exists ? 'exists' : "doesn't exist"} at #{@host}"
255
+ end
256
+ exists
257
+ end
258
+
259
+ private
260
+
261
+ def headers
262
+ {
263
+ 'User-Agent' => "baza.rb #{Baza::VERSION}",
264
+ 'Connection' => 'close',
265
+ 'X-Zerocracy-Token' => @token
266
+ }
267
+ end
268
+
269
+ def zipped(params)
270
+ body = gzip(params.fetch(:body))
271
+ headers = params
272
+ .fetch(:headers)
273
+ .merge(
274
+ {
275
+ 'Content-Type' => 'application/zip',
276
+ 'Content-Encoding' => 'gzip',
277
+ 'Content-Length' => body.size
278
+ }
279
+ )
280
+ params.merge(body:, headers:)
281
+ end
282
+
283
+ def gzip(data)
284
+ ''.dup.tap do |result|
285
+ io = StringIO.new(result)
286
+ gz = Zlib::GzipWriter.new(io)
287
+ gz.write(data)
288
+ gz.close
289
+ end
290
+ end
291
+
292
+ def home
293
+ Iri.new('')
294
+ .host(@host)
295
+ .port(@port)
296
+ .scheme(@ssl ? 'https' : 'http')
297
+ end
298
+
299
+ def checked(ret, allowed = [200])
300
+ allowed = [allowed] unless allowed.is_a?(Array)
301
+ mtd = (ret.request.original_options[:method] || '???').upcase
302
+ url = ret.effective_url
303
+ log = "#{mtd} #{url} -> #{ret.code}"
304
+ if allowed.include?(ret.code)
305
+ @loog.debug(log)
306
+ return ret
307
+ end
308
+ @loog.debug("#{log}\n #{(ret.headers || {}).map { |k, v| "#{k}: #{v}" }.join("\n ")}")
309
+ msg = [
310
+ "Invalid response code ##{ret.code} ",
311
+ "at #{mtd} #{url}",
312
+ ret.headers['X-Zerocracy-Flash'] ? " (#{ret.headers['X-Zerocracy-Flash'].inspect})" : ''
313
+ ].join
314
+ case ret.code
315
+ when 500
316
+ msg +=
317
+ ', most probably it\'s an internal error on the server, ' \
318
+ 'please report this to https://github.com/zerocracy/baza'
319
+ when 503
320
+ msg +=
321
+ ", most probably it's an internal error on the server (#{ret.headers['X-Zerocracy-Failure'].inspect}), " \
322
+ 'please report this to https://github.com/zerocracy/baza.rb'
323
+ when 404
324
+ msg +=
325
+ ', most probably you are trying to reach a wrong server, which doesn\'t ' \
326
+ 'have the URL that it is expected to have'
327
+ when 0
328
+ msg += ', most likely an internal error'
329
+ end
330
+ raise msg
331
+ end
332
+ end
data/renovate.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": [
4
+ "config:base"
5
+ ]
6
+ }
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2024 Zerocracy
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ $stdout.sync = true
24
+
25
+ require 'minitest/reporters'
26
+ Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
27
+
28
+ require 'simplecov'
29
+ SimpleCov.start
30
+
31
+ require 'simplecov-cobertura'
32
+ SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
33
+
34
+ require 'minitest/autorun'
data/test/test_baza.rb ADDED
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2024 Zerocracy
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require 'minitest/autorun'
24
+ require 'webmock/minitest'
25
+ require 'webrick'
26
+ require 'loog'
27
+ require 'socket'
28
+ require 'stringio'
29
+ require 'random-port'
30
+ require 'factbase'
31
+ require 'securerandom'
32
+ require 'net/ping'
33
+ require_relative '../lib/baza'
34
+
35
+ # Test.
36
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
37
+ # Copyright:: Copyright (c) 2024 Yegor Bugayenko
38
+ # License:: MIT
39
+ class TestBaza < Minitest::Test
40
+ TOKEN = '00000000-0000-0000-0000-000000000000'
41
+ HOST = 'api.zerocracy.com'
42
+ PORT = 443
43
+ LIVE = Baza.new(HOST, PORT, TOKEN)
44
+
45
+ def test_live_recent_check
46
+ WebMock.enable_net_connect!
47
+ skip unless we_are_online
48
+ assert(LIVE.recent('zerocracy').positive?)
49
+ end
50
+
51
+ def test_live_name_exists_check
52
+ WebMock.enable_net_connect!
53
+ skip unless we_are_online
54
+ assert(LIVE.name_exists?('zerocracy'))
55
+ end
56
+
57
+ def test_live_push
58
+ WebMock.enable_net_connect!
59
+ skip unless we_are_online
60
+ fb = Factbase.new
61
+ fb.insert.foo = 'test-' * 10_000
62
+ fb.insert
63
+ assert(LIVE.push(fake_name, fb.export, []).positive?)
64
+ end
65
+
66
+ def test_live_push_no_compression
67
+ WebMock.enable_net_connect!
68
+ skip unless we_are_online
69
+ fb = Factbase.new
70
+ fb.insert.foo = 'test-' * 10_000
71
+ fb.insert
72
+ baza = Baza.new(HOST, PORT, TOKEN, compression: false)
73
+ assert(baza.push(fake_name, fb.export, []).positive?)
74
+ end
75
+
76
+ def test_live_pull
77
+ WebMock.enable_net_connect!
78
+ skip unless we_are_online
79
+ id = LIVE.recent('zerocracy')
80
+ assert(!LIVE.pull(id).nil?)
81
+ end
82
+
83
+ def test_live_check_finished
84
+ WebMock.enable_net_connect!
85
+ skip unless we_are_online
86
+ id = LIVE.recent('zerocracy')
87
+ assert(!LIVE.finished?(id).nil?)
88
+ end
89
+
90
+ def test_live_read_stdout
91
+ WebMock.enable_net_connect!
92
+ skip unless we_are_online
93
+ id = LIVE.recent('zerocracy')
94
+ assert(!LIVE.stdout(id).nil?)
95
+ end
96
+
97
+ def test_live_read_exit_code
98
+ WebMock.enable_net_connect!
99
+ skip unless we_are_online
100
+ id = LIVE.recent('zerocracy')
101
+ assert(!LIVE.exit_code(id).nil?)
102
+ end
103
+
104
+ def test_live_lock_unlock
105
+ WebMock.enable_net_connect!
106
+ skip unless we_are_online
107
+ n = fake_name
108
+ owner = 'judges teesting'
109
+ assert(!LIVE.lock(n, owner).nil?)
110
+ assert(!LIVE.unlock(n, owner).nil?)
111
+ end
112
+
113
+ def test_simple_push
114
+ WebMock.disable_net_connect!
115
+ stub_request(:put, 'https://example.org/push/simple').to_return(
116
+ status: 200, body: '42'
117
+ )
118
+ assert_equal(
119
+ 42,
120
+ Baza.new('example.org', 443, '000').push('simple', 'hello, world!', [])
121
+ )
122
+ end
123
+
124
+ def test_simple_recent_check
125
+ WebMock.disable_net_connect!
126
+ stub_request(:get, 'https://example.org/recent/simple.txt')
127
+ .with(body: '', headers: { 'User-Agent' => /^baza.rb .*$/ })
128
+ .to_return(status: 200, body: '42')
129
+ assert_equal(
130
+ 42,
131
+ Baza.new('example.org', 443, '000').recent('simple')
132
+ )
133
+ end
134
+
135
+ def test_simple_exists_check
136
+ WebMock.disable_net_connect!
137
+ stub_request(:get, 'https://example.org/exists/simple').to_return(
138
+ status: 200, body: 'yes'
139
+ )
140
+ assert(
141
+ Baza.new('example.org', 443, '000').name_exists?('simple')
142
+ )
143
+ end
144
+
145
+ def test_exit_code_check
146
+ WebMock.disable_net_connect!
147
+ stub_request(:get, 'https://example.org/exit/42.txt').to_return(
148
+ status: 200, body: '0'
149
+ )
150
+ assert(
151
+ Baza.new('example.org', 443, '000').exit_code(42).zero?
152
+ )
153
+ end
154
+
155
+ def test_stdout_read
156
+ WebMock.disable_net_connect!
157
+ stub_request(:get, 'https://example.org/stdout/42.txt').to_return(
158
+ status: 200, body: 'hello!'
159
+ )
160
+ assert(
161
+ !Baza.new('example.org', 443, '000').stdout(42).empty?
162
+ )
163
+ end
164
+
165
+ def test_simple_pull
166
+ WebMock.disable_net_connect!
167
+ stub_request(:get, 'https://example.org/pull/333.fb').to_return(
168
+ status: 200, body: 'hello, world!'
169
+ )
170
+ assert(
171
+ Baza.new('example.org', 443, '000').pull(333).start_with?('hello')
172
+ )
173
+ end
174
+
175
+ def test_real_http
176
+ req =
177
+ with_http_server(200, 'yes') do |baza|
178
+ baza.name_exists?('simple')
179
+ end
180
+ assert_equal("baza.rb #{Baza::VERSION}", req['user-agent'])
181
+ end
182
+
183
+ def test_push_with_meta
184
+ req =
185
+ with_http_server(200, 'yes') do |baza|
186
+ baza.push('simple', 'hello, world!', ['boom!', 'хей!'])
187
+ end
188
+ assert_equal('Ym9vbSE= 0YXQtdC5IQ==', req['x-zerocracy-meta'])
189
+ end
190
+
191
+ def test_push_with_big_meta
192
+ req =
193
+ with_http_server(200, 'yes') do |baza|
194
+ baza.push(
195
+ 'simple',
196
+ 'hello, world!',
197
+ [
198
+ 'pages_url:https://zerocracy.github.io/zerocracy.html',
199
+ 'others:https://zerocracy.github.io/zerocracy.html',
200
+ 'duration:59595'
201
+ ]
202
+ )
203
+ end
204
+ assert(req['x-zerocracy-meta'])
205
+ end
206
+
207
+ def test_push_compressed_content
208
+ skip # this test is not stable, see https://github.com/yegor256/judges/issues/105
209
+ req =
210
+ with_http_server(200, 'yes') do |baza|
211
+ baza.push('simple', 'hello, world!', %w[meta1 meta2 meta3])
212
+ end
213
+ assert_equal('application/zip', req.content_type)
214
+ assert_equal('gzip', req['content-encoding'])
215
+ body = Zlib::GzipReader.zcat(StringIO.new(req.body))
216
+ assert_equal('hello, world!', body)
217
+ end
218
+
219
+ def test_push_compression_disabled
220
+ req =
221
+ with_http_server(200, 'yes', compression: false) do |baza|
222
+ baza.push('simple', 'hello, world!', %w[meta1 meta2 meta3])
223
+ end
224
+ assert_equal('application/octet-stream', req.content_type)
225
+ assert_equal('hello, world!', req.body)
226
+ end
227
+
228
+ private
229
+
230
+ def with_http_server(code, response, opts = {})
231
+ opts = { ssl: false, timeout: 1 }.merge(opts)
232
+ WebMock.enable_net_connect!
233
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
234
+ host = '127.0.0.1'
235
+ RandomPort::Pool::SINGLETON.acquire do |port|
236
+ server = TCPServer.new(host, port)
237
+ t =
238
+ Thread.new do
239
+ socket = server.accept
240
+ req.parse(socket)
241
+ req.body
242
+ socket.puts "HTTP/1.1 #{code} OK\r\nContent-Length: #{response.length}\r\n\r\n#{response}"
243
+ socket.close
244
+ end
245
+ yield Baza.new(host, port, '0000', **opts)
246
+ t.join
247
+ end
248
+ req
249
+ end
250
+
251
+ def fake_name
252
+ "fake-#{SecureRandom.hex(8)}"
253
+ end
254
+
255
+ def we_are_online
256
+ Net::Ping::External.new('8.8.8.8').ping?
257
+ end
258
+ end