kstor 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +6 -2
- data/bin/kstor +48 -40
- data/bin/kstor-srv +2 -2
- data/lib/kstor/config.rb +2 -1
- data/lib/kstor/controller/authentication.rb +25 -4
- data/lib/kstor/controller/base.rb +61 -0
- data/lib/kstor/controller/request_handler.rb +84 -16
- data/lib/kstor/controller/secret.rb +63 -64
- data/lib/kstor/controller/users.rb +11 -22
- data/lib/kstor/controller.rb +3 -3
- data/lib/kstor/crypto/armored_value.rb +82 -0
- data/lib/kstor/crypto/keys.rb +9 -41
- data/lib/kstor/crypto.rb +0 -1
- data/lib/kstor/error.rb +36 -3
- data/lib/kstor/log/simple_logger.rb +83 -0
- data/lib/kstor/log/systemd_logger.rb +22 -0
- data/lib/kstor/log.rb +53 -4
- data/lib/kstor/message/base.rb +223 -0
- data/lib/kstor/message/error.rb +15 -0
- data/lib/kstor/message/group_create.rb +14 -0
- data/lib/kstor/message/group_created.rb +16 -0
- data/lib/kstor/message/ping.rb +14 -0
- data/lib/kstor/message/pong.rb +14 -0
- data/lib/kstor/message/secret_create.rb +16 -0
- data/lib/kstor/message/secret_created.rb +14 -0
- data/lib/kstor/message/secret_delete.rb +14 -0
- data/lib/kstor/message/secret_deleted.rb +14 -0
- data/lib/kstor/message/secret_list.rb +14 -0
- data/lib/kstor/message/secret_search.rb +14 -0
- data/lib/kstor/message/secret_unlock.rb +14 -0
- data/lib/kstor/message/secret_update_meta.rb +15 -0
- data/lib/kstor/message/secret_update_value.rb +15 -0
- data/lib/kstor/message/secret_updated.rb +14 -0
- data/lib/kstor/message/secret_value.rb +35 -0
- data/lib/kstor/message.rb +26 -113
- data/lib/kstor/model.rb +42 -25
- data/lib/kstor/server.rb +15 -3
- data/lib/kstor/session.rb +34 -2
- data/lib/kstor/socket_server.rb +20 -1
- data/lib/kstor/sql_connection.rb +16 -1
- data/lib/kstor/store.rb +182 -72
- data/lib/kstor/systemd.rb +5 -0
- data/lib/kstor/version.rb +2 -1
- data/lib/kstor.rb +1 -1
- metadata +25 -3
data/lib/kstor/message.rb
CHANGED
@@ -3,130 +3,43 @@
|
|
3
3
|
require 'json'
|
4
4
|
|
5
5
|
module KStor
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
# We can't use a KStor::Error here because kstor/error.rb require()s
|
10
|
-
# kstor/message.rb .
|
11
|
-
class RequestMissesAuthData < RuntimeError
|
12
|
-
end
|
13
|
-
|
14
|
-
class UnparsableResponse < RuntimeError
|
15
|
-
end
|
16
|
-
|
17
|
-
# A user request or response.
|
18
|
-
class Message
|
19
|
-
attr_reader :type
|
20
|
-
attr_reader :args
|
21
|
-
|
22
|
-
def initialize(type, args)
|
23
|
-
@type = type
|
24
|
-
@args = args
|
25
|
-
end
|
26
|
-
|
27
|
-
def to_h
|
28
|
-
{ 'type' => @type, 'args' => @args }
|
29
|
-
end
|
30
|
-
|
31
|
-
def serialize
|
32
|
-
to_h.to_json
|
33
|
-
end
|
34
|
-
|
35
|
-
# Parse a user request, freshly arrived from the network.
|
6
|
+
module Message
|
7
|
+
# Internal exception when a request is received with neither a session ID
|
8
|
+
# nor a login/password pair.
|
36
9
|
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
|
40
|
-
def self.parse_request(str)
|
41
|
-
data = JSON.parse(str)
|
42
|
-
if data.key?('login') && data.key?('password')
|
43
|
-
LoginRequest.new(
|
44
|
-
data['login'], data['password'],
|
45
|
-
data['type'], data['args']
|
46
|
-
)
|
47
|
-
elsif data.key?('session_id')
|
48
|
-
SessionRequest.new(
|
49
|
-
data['session_id'], data['type'], data['args']
|
50
|
-
)
|
51
|
-
else
|
52
|
-
raise RequestMissesAuthData
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
# A user authentication request.
|
58
|
-
class LoginRequest < Message
|
59
|
-
attr_reader :login
|
60
|
-
attr_reader :password
|
61
|
-
|
62
|
-
def initialize(login, password, type, args)
|
63
|
-
@login = login
|
64
|
-
@password = password
|
65
|
-
super(type, args)
|
66
|
-
end
|
67
|
-
|
68
|
-
def inspect
|
69
|
-
fmt = [
|
70
|
-
'#<KStor::LoginRequest:%<id>x',
|
71
|
-
'@login=%<login>s',
|
72
|
-
'@password="******"',
|
73
|
-
'@args=%<args>s>'
|
74
|
-
].join(' ')
|
75
|
-
format(fmt, id: object_id, login: @login.inspect, args: @args.inspect)
|
10
|
+
# We can't use a KStor::Error here because kstor/error.rb require()s
|
11
|
+
# kstor/message.rb .
|
12
|
+
class RequestMissesAuthData < RuntimeError
|
76
13
|
end
|
77
14
|
|
78
|
-
|
79
|
-
|
15
|
+
# Response data was invalid JSON.
|
16
|
+
class UnparsableResponse < RuntimeError
|
80
17
|
end
|
81
18
|
end
|
19
|
+
end
|
82
20
|
|
83
|
-
|
84
|
-
class SessionRequest < Message
|
85
|
-
attr_reader :session_id
|
21
|
+
require 'kstor/message/error'
|
86
22
|
|
87
|
-
|
88
|
-
|
89
|
-
super(type, args)
|
90
|
-
end
|
23
|
+
require 'kstor/message/ping'
|
24
|
+
require 'kstor/message/pong'
|
91
25
|
|
92
|
-
|
93
|
-
|
94
|
-
'#<KStor::SessionRequest:%<id>x',
|
95
|
-
'@session_id=******',
|
96
|
-
'@args=%<args>s>'
|
97
|
-
].join(' ')
|
98
|
-
format(fmt, id: object_id, args: @args.inspect)
|
99
|
-
end
|
26
|
+
require 'kstor/message/group_create'
|
27
|
+
require 'kstor/message/group_created'
|
100
28
|
|
101
|
-
|
102
|
-
|
103
|
-
end
|
104
|
-
end
|
29
|
+
require 'kstor/message/secret_create'
|
30
|
+
require 'kstor/message/secret_created'
|
105
31
|
|
106
|
-
|
107
|
-
|
108
|
-
attr_accessor :session_id
|
32
|
+
require 'kstor/message/secret_delete'
|
33
|
+
require 'kstor/message/secret_deleted'
|
109
34
|
|
110
|
-
|
111
|
-
|
112
|
-
super
|
113
|
-
end
|
35
|
+
require 'kstor/message/secret_search'
|
36
|
+
require 'kstor/message/secret_list'
|
114
37
|
|
115
|
-
|
116
|
-
|
117
|
-
resp = new(data['type'], data['args'])
|
118
|
-
resp.session_id = data['session_id']
|
119
|
-
resp
|
120
|
-
rescue JSON::ParserError => e
|
121
|
-
raise UnparsableResponse, e.message
|
122
|
-
end
|
38
|
+
require 'kstor/message/secret_unlock'
|
39
|
+
require 'kstor/message/secret_value'
|
123
40
|
|
124
|
-
|
125
|
-
|
126
|
-
|
41
|
+
require 'kstor/message/secret_update_meta'
|
42
|
+
require 'kstor/message/secret_update_value'
|
43
|
+
require 'kstor/message/secret_updated'
|
127
44
|
|
128
|
-
|
129
|
-
super.merge('session_id' => @session_id)
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
45
|
+
KStor::Message.register_all_message_types
|
data/lib/kstor/model.rb
CHANGED
@@ -86,10 +86,7 @@ module KStor
|
|
86
86
|
|
87
87
|
# Dump properties except pubk.
|
88
88
|
def to_h
|
89
|
-
|
90
|
-
h.delete('pubk')
|
91
|
-
|
92
|
-
h
|
89
|
+
super.except('pubk')
|
93
90
|
end
|
94
91
|
end
|
95
92
|
|
@@ -154,9 +151,7 @@ module KStor
|
|
154
151
|
|
155
152
|
# Dump properties except {#encrypted_privk}.
|
156
153
|
def to_h
|
157
|
-
|
158
|
-
h.delete('encrypted_privk')
|
159
|
-
h
|
154
|
+
super.except('encrypted_privk')
|
160
155
|
end
|
161
156
|
end
|
162
157
|
|
@@ -263,9 +258,6 @@ module KStor
|
|
263
258
|
self.kdf_params = secret_key.kdf_params
|
264
259
|
encrypt(secret_key)
|
265
260
|
self.keychain = {}
|
266
|
-
# FIXME: delete keychain items from database!
|
267
|
-
# they won't be useable (decryption key is lost) but will provoke
|
268
|
-
# errors.
|
269
261
|
end
|
270
262
|
|
271
263
|
# Re-encrypt private key and keychain with a new secret key derived from
|
@@ -283,9 +275,7 @@ module KStor
|
|
283
275
|
|
284
276
|
# Dump properties except {#encrypted_privk} and {#pubk}.
|
285
277
|
def to_h
|
286
|
-
h = super
|
287
|
-
h.delete('encrypted_privk')
|
288
|
-
h.delete('pubk')
|
278
|
+
h = super.except('encrypted_privk', 'pubk')
|
289
279
|
h['keychain'] = keychain.transform_values(&:to_h) if keychain
|
290
280
|
|
291
281
|
h
|
@@ -320,6 +310,13 @@ module KStor
|
|
320
310
|
# Secret should be used at this URL
|
321
311
|
attr_accessor :url
|
322
312
|
|
313
|
+
# Create new metadata for a secret.
|
314
|
+
#
|
315
|
+
# Hash param can contains keys for "app", "database", "login", "server"
|
316
|
+
# and "url". Any other key is ignored.
|
317
|
+
#
|
318
|
+
# @param values [Hash, KStor::Crypto::ArmoredHash] metadata
|
319
|
+
# @return [KStor::Model::SecretMeta] secret metadata
|
323
320
|
def initialize(values)
|
324
321
|
@app = values['app']
|
325
322
|
@database = values['database']
|
@@ -328,30 +325,50 @@ module KStor
|
|
328
325
|
@url = values['url']
|
329
326
|
end
|
330
327
|
|
328
|
+
# Convert this metadata to a Hash.
|
329
|
+
#
|
330
|
+
# Empty values will not be included.
|
331
|
+
#
|
332
|
+
# @return [Hash[String, String]] metadata as a Hash
|
331
333
|
def to_h
|
332
334
|
{ 'app' => @app, 'database' => @database, 'login' => @login,
|
333
335
|
'server' => @server, 'url' => @url }.compact
|
334
336
|
end
|
335
337
|
|
338
|
+
# Prepare metadata to be written to disk or database.
|
339
|
+
#
|
340
|
+
# @return [KStor::Crypto::ArmoredHash] serialized metadata
|
336
341
|
def serialize
|
337
342
|
Crypto::ArmoredHash.from_hash(to_h)
|
338
343
|
end
|
339
344
|
|
345
|
+
# Merge metadata.
|
346
|
+
#
|
347
|
+
# @param other [KStor::Model::SecretMeta] other metadata that will
|
348
|
+
# override this object's values.
|
340
349
|
def merge(other)
|
341
350
|
values = to_h.merge(other.to_h)
|
342
351
|
values.reject! { |_, v| v.empty? }
|
343
352
|
self.class.new(values)
|
344
353
|
end
|
345
354
|
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
355
|
+
# Match against wildcards.
|
356
|
+
#
|
357
|
+
# Metadata will be matched against another metadata object with wildcard
|
358
|
+
# values. This uses roughly the same rules that shell wildcards (e.g.
|
359
|
+
# fnmatch(3) C function).
|
360
|
+
#
|
361
|
+
# @see File.fnmatch?
|
362
|
+
#
|
363
|
+
# @param meta [KStor::Model::SecretMeta] wildcard metadata
|
364
|
+
# @return [Boolean] true if matched
|
365
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
350
366
|
def match?(meta)
|
351
367
|
self_h = to_h
|
352
368
|
other_h = meta.to_h
|
353
369
|
other_h.each do |k, wildcard|
|
354
370
|
val = self_h[k]
|
371
|
+
return false if val.nil? && !wildcard.nil? && wildcard != '*'
|
355
372
|
next if val.nil?
|
356
373
|
next if wildcard.nil?
|
357
374
|
|
@@ -362,6 +379,7 @@ module KStor
|
|
362
379
|
end
|
363
380
|
true
|
364
381
|
end
|
382
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
365
383
|
end
|
366
384
|
|
367
385
|
# A secret, with metadata and a value that are kept encrypted on disk.
|
@@ -383,8 +401,11 @@ module KStor
|
|
383
401
|
# @!macro dsl_model_properties_rw
|
384
402
|
property :metadata, read_only: true
|
385
403
|
|
404
|
+
# Set metadata (or unset if nil).
|
405
|
+
#
|
406
|
+
# @param armored_hash [KStor::Crypt::ArmoredHash] metadata to load
|
386
407
|
def metadata=(armored_hash)
|
387
|
-
@data[:metadata] = armored_hash ? SecretMeta.
|
408
|
+
@data[:metadata] = armored_hash ? SecretMeta.new(armored_hash) : nil
|
388
409
|
end
|
389
410
|
|
390
411
|
# Decrypt secret value.
|
@@ -424,13 +445,9 @@ module KStor
|
|
424
445
|
# Dump properties except {#ciphertext}, {#encrypted_metadata},
|
425
446
|
# {#value_author_id} and {#meta_author_id}.
|
426
447
|
def to_h
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
h.delete('value_author_id')
|
431
|
-
h.delete('meta_author_id')
|
432
|
-
|
433
|
-
h
|
448
|
+
super.except(
|
449
|
+
*%w[ciphertext encrypted_metadata value_author_id meta_author_id]
|
450
|
+
)
|
434
451
|
end
|
435
452
|
end
|
436
453
|
end
|
data/lib/kstor/server.rb
CHANGED
@@ -15,11 +15,24 @@ module KStor
|
|
15
15
|
|
16
16
|
# Listen for clients and respond to their requests.
|
17
17
|
class Server < SocketServer
|
18
|
+
# Create a new server object.
|
19
|
+
#
|
20
|
+
# @param controller [KStor::Controller::RequestHandler] client request
|
21
|
+
# handler
|
22
|
+
# @param args [Hash] parameters to apss to parent class
|
23
|
+
# {KStor::SocketServer}
|
24
|
+
# @return [KStor::Server] new KStor server.
|
18
25
|
def initialize(controller:, **args)
|
19
26
|
@controller = controller
|
20
27
|
super(**args)
|
21
28
|
end
|
22
29
|
|
30
|
+
# Implement {KStor::SocketServer#work} to actually serve clients.
|
31
|
+
#
|
32
|
+
# This method must read client request, write a response and close the
|
33
|
+
# socket.
|
34
|
+
#
|
35
|
+
# @param client [Socket] channel of communication to a client
|
23
36
|
def work(client)
|
24
37
|
client_data, = client.recvfrom(4096)
|
25
38
|
Log.debug("server: read #{client_data.bytesize} bytes from client")
|
@@ -36,15 +49,14 @@ module KStor
|
|
36
49
|
private
|
37
50
|
|
38
51
|
def handle_client_data(data)
|
39
|
-
req = Message.
|
52
|
+
req = Message::Base.parse(data)
|
40
53
|
resp = @controller.handle_request(req)
|
41
|
-
Log.debug("server: response = #{resp.inspect}")
|
42
54
|
resp.serialize
|
43
55
|
rescue JSON::ParserError => e
|
44
56
|
err = Error.for_code('MSG/INVALID', e.message)
|
45
57
|
Log.info("server: #{err}")
|
46
58
|
err.response.serialize
|
47
|
-
rescue RequestMissesAuthData
|
59
|
+
rescue Message::RequestMissesAuthData
|
48
60
|
raise Error.for_code('MSG/INVALID', 'Missing authentication data')
|
49
61
|
end
|
50
62
|
end
|
data/lib/kstor/session.rb
CHANGED
@@ -11,6 +11,13 @@ module KStor
|
|
11
11
|
attr_reader :created_at
|
12
12
|
attr_reader :updated_at
|
13
13
|
|
14
|
+
# Create a new user session.
|
15
|
+
#
|
16
|
+
# @param sid [String] Session ID
|
17
|
+
# @param user [KStor::Model::User] user owning the session
|
18
|
+
# @param secret_key [KStor::Crypto::SecretKey] user secret key, derived
|
19
|
+
# from password
|
20
|
+
# @return [KStor::Session] new session
|
14
21
|
def initialize(sid, user, secret_key)
|
15
22
|
@id = sid
|
16
23
|
@user = user
|
@@ -19,21 +26,37 @@ module KStor
|
|
19
26
|
@updated_at = Time.now
|
20
27
|
end
|
21
28
|
|
29
|
+
# Update access time, for idle sessions weeding.
|
30
|
+
#
|
31
|
+
# @return [KStor::Session] updated session
|
22
32
|
def update
|
23
33
|
@updated_at = Time.now
|
24
34
|
self
|
25
35
|
end
|
26
36
|
|
37
|
+
# Create a new session for a user.
|
38
|
+
#
|
39
|
+
# @param user [KStor::Model::User] user owning the session
|
40
|
+
# @param secret_key [KStor::Crypto::SecretKey] user secret key, derived
|
41
|
+
# from password
|
42
|
+
# @return [KStor::Session] new session with a random SID
|
27
43
|
def self.create(user, secret_key)
|
28
44
|
sid = SecureRandom.urlsafe_base64(16)
|
29
45
|
new(sid, user, secret_key)
|
30
46
|
end
|
31
47
|
end
|
32
48
|
|
33
|
-
# Collection of user sessions (in memory)
|
49
|
+
# Collection of user sessions (in memory).
|
34
50
|
#
|
35
|
-
#
|
51
|
+
# Concurrent accesses are synchronized on a mutex.
|
36
52
|
class SessionStore
|
53
|
+
# Create new session store.
|
54
|
+
#
|
55
|
+
# @param idle_timeout [Integer] sessions that aren't updated for this
|
56
|
+
# number of seconds are considered invalid
|
57
|
+
# @param life_timeout [Integer] sessions that are older than this number of
|
58
|
+
# seconds are considered invalid
|
59
|
+
# @return [KStor::SessionStore] a new session store.
|
37
60
|
def initialize(idle_timeout, life_timeout)
|
38
61
|
@idle_timeout = idle_timeout
|
39
62
|
@life_timeout = life_timeout
|
@@ -41,12 +64,20 @@ module KStor
|
|
41
64
|
@sessions.extend(Mutex_m)
|
42
65
|
end
|
43
66
|
|
67
|
+
# Add a session to the store.
|
68
|
+
#
|
69
|
+
# @param session [KStor::Session] session to store
|
44
70
|
def <<(session)
|
45
71
|
@sessions.synchronize do
|
46
72
|
@sessions[session.id] = session
|
47
73
|
end
|
48
74
|
end
|
49
75
|
|
76
|
+
# Fetch a session from it's ID.
|
77
|
+
#
|
78
|
+
# @param sid [String] session ID to lookup
|
79
|
+
# @return [KStor::Session, nil] session or nil if session ID was not found
|
80
|
+
# or session has expired
|
50
81
|
def [](sid)
|
51
82
|
@sessions.synchronize do
|
52
83
|
s = @sessions[sid.to_s]
|
@@ -61,6 +92,7 @@ module KStor
|
|
61
92
|
end
|
62
93
|
end
|
63
94
|
|
95
|
+
# Delete expired sessions.
|
64
96
|
def purge
|
65
97
|
now = Time.now
|
66
98
|
@sessions.synchronize do
|
data/lib/kstor/socket_server.rb
CHANGED
@@ -9,8 +9,14 @@ require 'timeout'
|
|
9
9
|
module KStor
|
10
10
|
# Serve clients on UNIX sockets.
|
11
11
|
class SocketServer
|
12
|
+
# Wait this number of seconds for worker threads to terminate before
|
13
|
+
# killing them without mercy.
|
12
14
|
GRACEFUL_TIMEOUT = 10
|
13
15
|
|
16
|
+
# Create a new server.
|
17
|
+
#
|
18
|
+
# @param socket_path [String] path to listening socket
|
19
|
+
# @param nworkers [Integer] number of worker threads
|
14
20
|
def initialize(socket_path:, nworkers:)
|
15
21
|
@path = socket_path
|
16
22
|
@nworkers = nworkers
|
@@ -18,10 +24,14 @@ module KStor
|
|
18
24
|
@workers = []
|
19
25
|
end
|
20
26
|
|
27
|
+
# Start serving clients.
|
28
|
+
#
|
29
|
+
# Send interrupt signal to cleanly stop.
|
21
30
|
def start
|
22
31
|
start_workers
|
23
|
-
server =
|
32
|
+
server = server_socket
|
24
33
|
Systemd.service_ready
|
34
|
+
Log.info('socket_server: started')
|
25
35
|
loop do
|
26
36
|
maintain_workers
|
27
37
|
@client_queue.enq(server.accept.first)
|
@@ -31,6 +41,8 @@ module KStor
|
|
31
41
|
Log.info('socket_server: stopped.')
|
32
42
|
end
|
33
43
|
|
44
|
+
# Do some work for a client and write a response.
|
45
|
+
# @abstract
|
34
46
|
def work(client)
|
35
47
|
# Abstract method.
|
36
48
|
client.close
|
@@ -38,6 +50,13 @@ module KStor
|
|
38
50
|
|
39
51
|
private
|
40
52
|
|
53
|
+
def server_socket
|
54
|
+
s = Systemd.socket
|
55
|
+
return s if s
|
56
|
+
|
57
|
+
UNIXServer.new(@path)
|
58
|
+
end
|
59
|
+
|
41
60
|
def worker_run
|
42
61
|
while (client = @client_queue.deq)
|
43
62
|
Log.debug("socket_server: #{Thread.current.name} serving one client")
|
data/lib/kstor/sql_connection.rb
CHANGED
@@ -14,18 +14,33 @@ module KStor
|
|
14
14
|
|
15
15
|
# Execute SQL commands in a per-thread SQLite connection.
|
16
16
|
class SQLConnection
|
17
|
+
# Create a new SQL connection.
|
18
|
+
#
|
19
|
+
# @param file_path [String] path to SQLite database
|
20
|
+
# @return [KStor::SQLConnection] the new connection
|
17
21
|
def initialize(file_path)
|
18
22
|
@file_path = file_path
|
19
23
|
end
|
20
24
|
|
25
|
+
# Execute SQL statement.
|
26
|
+
#
|
27
|
+
# @param sql [String] SQL statement
|
28
|
+
# @param params [Array] parameters to fill placeholders in the statement
|
29
|
+
# @return [Any] Whatever SQLite returns
|
21
30
|
def execute(sql, *params, &)
|
22
31
|
database.execute(sql, *params, &)
|
23
32
|
end
|
24
33
|
|
34
|
+
# Last generated ID (from an INSERT statement).
|
35
|
+
#
|
36
|
+
# @return [Integer] generated ID from last insert statement.
|
25
37
|
def last_insert_row_id
|
26
38
|
database.last_insert_row_id
|
27
39
|
end
|
28
40
|
|
41
|
+
# Execute given block of code in a database transaction.
|
42
|
+
#
|
43
|
+
# @return [Any] Whatever the block returns
|
29
44
|
def transaction(&block)
|
30
45
|
result = nil
|
31
46
|
database.transaction do |db|
|
@@ -52,7 +67,7 @@ module KStor
|
|
52
67
|
def setup_thread_connection(key)
|
53
68
|
return if Thread.current[key]
|
54
69
|
|
55
|
-
Log.
|
70
|
+
Log.debug(
|
56
71
|
"sqlite: initializing connection in thread #{Thread.current.name}"
|
57
72
|
)
|
58
73
|
Thread.current[key] = connect(@file_path)
|