store-digest 0.3.0 → 0.4.3
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/TODO.org +115 -4
- data/lib/store/digest/blob/filesystem.rb +9 -4
- data/lib/store/digest/driver.rb +4 -0
- data/lib/store/digest/entry.rb +1214 -0
- data/lib/store/digest/error.rb +28 -0
- data/lib/store/digest/meta/lmdb/v0.rb +388 -0
- data/lib/store/digest/meta/lmdb/v1.rb +737 -0
- data/lib/store/digest/meta/lmdb.rb +59 -1041
- data/lib/store/digest/meta.rb +1 -1
- data/lib/store/digest/readwrapper.rb +174 -0
- data/lib/store/digest/version.rb +1 -1
- data/lib/store/digest.rb +335 -117
- data/store-digest.gemspec +6 -7
- metadata +47 -19
- data/lib/store/digest/object.rb +0 -602
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
require 'store/digest/meta'
|
|
2
|
+
|
|
3
|
+
module Store::Digest::Meta::LMDB
|
|
4
|
+
# This is the version 1 database layout.
|
|
5
|
+
module V1
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# import the flags
|
|
10
|
+
Flags = Store::Digest::Entry::Flags
|
|
11
|
+
|
|
12
|
+
# XXX do we want to introduce dry-types? didn't i try before and
|
|
13
|
+
# it was a huge clusterfuck?
|
|
14
|
+
|
|
15
|
+
# i think?? are there others?? lol
|
|
16
|
+
ARCH = [''].pack(?p).size == 8 ? 64 : 32
|
|
17
|
+
LONG = ARCH == 64 ? ?Q : ?L
|
|
18
|
+
|
|
19
|
+
ENCODE_NOOP = -> x { x }
|
|
20
|
+
DECODE_NOOP = ENCODE_NOOP
|
|
21
|
+
ENCODE_TOKEN = -> x { x.to_s }
|
|
22
|
+
DECODE_TOKEN = -> x { x.empty? ? nil : x }
|
|
23
|
+
ENCODE_FLAGS = -> x { Flags.to_i x }
|
|
24
|
+
DECODE_FLAGS = -> x { Flags.from x }
|
|
25
|
+
if ARCH == 64
|
|
26
|
+
# you get microsecond resolution
|
|
27
|
+
ENCODE_TIME = -> x { x ? x.to_i * 1_000_000 + x.usec : 0 }
|
|
28
|
+
DECODE_TIME = -> x {
|
|
29
|
+
x == 0 ? nil : Time.at(x / 1_000_000, x % 1_000_000, :usec, in: ?Z)
|
|
30
|
+
}
|
|
31
|
+
else
|
|
32
|
+
# and you do not
|
|
33
|
+
ENCODE_TIME = -> x { x ? x.to_i : 0 }
|
|
34
|
+
DECODE_TIME = -> x { x == 0 ? nil : Time.at(x, in: ?Z) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# { Class => [pack, encode, decode] }
|
|
38
|
+
COERCE = {
|
|
39
|
+
Integer => [LONG, ENCODE_NOOP, DECODE_NOOP ],
|
|
40
|
+
String => ['Z*', ENCODE_TOKEN, DECODE_TOKEN],
|
|
41
|
+
Time => [LONG, ENCODE_TIME, DECODE_TIME ],
|
|
42
|
+
Flags => [?S, ENCODE_FLAGS, DECODE_FLAGS],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# one difference between V0 records and V1 records is we don't
|
|
46
|
+
# force network-endianness, since we can't force it for the
|
|
47
|
+
# integer keys. the other difference is that the flags are now an
|
|
48
|
+
# unsigned short.
|
|
49
|
+
|
|
50
|
+
# control records
|
|
51
|
+
CONTROL = {
|
|
52
|
+
version: String,
|
|
53
|
+
ctime: Time,
|
|
54
|
+
mtime: Time,
|
|
55
|
+
expiry: Integer,
|
|
56
|
+
objects: Integer,
|
|
57
|
+
deleted: Integer,
|
|
58
|
+
bytes: Integer,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# object records
|
|
62
|
+
RECORD = {
|
|
63
|
+
size: Integer,
|
|
64
|
+
ctime: Time,
|
|
65
|
+
mtime: Time,
|
|
66
|
+
ptime: Time,
|
|
67
|
+
dtime: Time,
|
|
68
|
+
flags: Flags,
|
|
69
|
+
type: String,
|
|
70
|
+
language: String,
|
|
71
|
+
charset: String,
|
|
72
|
+
encoding: String,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# the record string (after the hashes are removed)
|
|
76
|
+
PACKED = RECORD.values.map { |v| COERCE[v].first }.join
|
|
77
|
+
|
|
78
|
+
# Set up the V1 database layout.
|
|
79
|
+
#
|
|
80
|
+
# @return [void]
|
|
81
|
+
#
|
|
82
|
+
def setup_dbs
|
|
83
|
+
# in the v1 layout, `primary` is only cosmetic and we have an
|
|
84
|
+
# `entry` database keyed by (native-endian) integer
|
|
85
|
+
|
|
86
|
+
now = Time.now in: ?Z
|
|
87
|
+
|
|
88
|
+
%i[ctime mtime].each { |k| control_set k, now, maybe: true }
|
|
89
|
+
|
|
90
|
+
# clever if i do say so myself
|
|
91
|
+
%i[objects deleted bytes].each { |k| control_set k, 0, maybe: true }
|
|
92
|
+
|
|
93
|
+
# default cache expiration
|
|
94
|
+
control_set :expiry, 86400, maybe: true
|
|
95
|
+
|
|
96
|
+
# this snarl takes the record layout (popping in a cheeky
|
|
97
|
+
# "etime" index for cache entry expirations) and pairs it with
|
|
98
|
+
# hash algorithm indices to attach them to database flags, which
|
|
99
|
+
# are then shoveled en masse into the LMDB factory method.
|
|
100
|
+
dbs = RECORD.except(:flags).merge({ etime: Time }).transform_values do |type|
|
|
101
|
+
flags = %i[dupsort]
|
|
102
|
+
flags += [Integer, Time].include?(type) ? %i[integerkey integerdup] : []
|
|
103
|
+
end.merge(
|
|
104
|
+
# these are always going to be a fixed length (hash -> size_t)
|
|
105
|
+
algorithms.map { |k| [k, %i[dupsort]] }.to_h, # dupfixed bad?
|
|
106
|
+
{ entry: [:integerkey] }
|
|
107
|
+
).transform_values do |flags|
|
|
108
|
+
(flags + [:create]).map { |flag| [flag, true] }.to_h
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# XXX we don't need to do this because we don't have a @dbs
|
|
112
|
+
# anymore; we just need to make sure the databases are created
|
|
113
|
+
dbs.map { |n, f| [n, lmdb.database(n.to_s, f)] }.to_h
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Encode an individual value.
|
|
117
|
+
#
|
|
118
|
+
# @param value [Object] the value to be encoded
|
|
119
|
+
# @param type [Class] the value's type if not specified
|
|
120
|
+
#
|
|
121
|
+
# @return [String] the raw value for the database
|
|
122
|
+
#
|
|
123
|
+
def db_encode value, type = value.class
|
|
124
|
+
type = CONTROL[type] || RECORD[type] if type.is_a? Symbol
|
|
125
|
+
|
|
126
|
+
pack, encode, _ = COERCE[type]
|
|
127
|
+
raise ArgumentError, "Unsupported type #{type}" unless pack
|
|
128
|
+
|
|
129
|
+
[encode.call(value)].pack pack
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Decode an individual value.
|
|
133
|
+
#
|
|
134
|
+
# @param raw [String] a raw value from the database
|
|
135
|
+
# @param type [Class] the type to decode it into
|
|
136
|
+
#
|
|
137
|
+
# @return [Object] whatever `type` object was intended
|
|
138
|
+
#
|
|
139
|
+
def db_decode raw, type
|
|
140
|
+
type = CONTROL[type] || RECORD[type] if type.is_a? Symbol
|
|
141
|
+
pack, _, decode = COERCE[type]
|
|
142
|
+
raise ArgumentError, "Unsupported type #{type}" unless pack
|
|
143
|
+
|
|
144
|
+
decode.call raw.unpack1(pack)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Get the "last" (highest-ordinal) key of an integer-keyed database.
|
|
148
|
+
#
|
|
149
|
+
# @param db [LMDB::Database,Symbol]
|
|
150
|
+
# @param raw [false, true] whether to decode the pointer
|
|
151
|
+
#
|
|
152
|
+
# @return [Integer]
|
|
153
|
+
#
|
|
154
|
+
def last_key db, raw: false
|
|
155
|
+
db = lmdb[db] if db.is_a? Symbol
|
|
156
|
+
raise ArgumentError, 'Wrong/malformed database' unless
|
|
157
|
+
db.is_a? ::LMDB::Database and db.flags[:integerkey]
|
|
158
|
+
|
|
159
|
+
# the last entry in the database should be the highest number,
|
|
160
|
+
# but also not sure if we want to reserve zero
|
|
161
|
+
out = db.empty? ? 0 : (db.cursor { |c| c.last }.first.unpack1(?J) + 1)
|
|
162
|
+
|
|
163
|
+
# return raw pointer
|
|
164
|
+
raw ? [out].pack(?J) : out
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Retrieve the value of a control field.
|
|
168
|
+
#
|
|
169
|
+
# @param key [Symbol]
|
|
170
|
+
#
|
|
171
|
+
# @return [Object, nil] the value of the key
|
|
172
|
+
#
|
|
173
|
+
def control_get key
|
|
174
|
+
type = CONTROL[key.to_sym] or raise ArgumentError,
|
|
175
|
+
"invalid control key #{key}"
|
|
176
|
+
|
|
177
|
+
raw = lmdb[:control][key.to_s]
|
|
178
|
+
db_decode raw, type if raw
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Set a control field with an explicit value.
|
|
182
|
+
#
|
|
183
|
+
# @param key [Symbol]
|
|
184
|
+
# @param value [Object]
|
|
185
|
+
# @param maybe [false, true] only set if uninitialized
|
|
186
|
+
#
|
|
187
|
+
# @return [Object] the original value passed through
|
|
188
|
+
#
|
|
189
|
+
def control_set key, value, maybe: false
|
|
190
|
+
type = CONTROL[key] or raise ArgumentError, "invalid control key #{key}"
|
|
191
|
+
raise ArgumentError,
|
|
192
|
+
"value should be instance of #{type}" unless value.is_a? type
|
|
193
|
+
|
|
194
|
+
lmdb[:control][key.to_s] = db_encode value, type unless
|
|
195
|
+
maybe && lmdb[:control].has?(key.to_s)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Increment an existing ({Integer}) control field by a value.
|
|
199
|
+
#
|
|
200
|
+
# @param key [Symbol]
|
|
201
|
+
# @param value [Numeric]
|
|
202
|
+
#
|
|
203
|
+
# @raise [RuntimeError] if the field is uninitialized
|
|
204
|
+
#
|
|
205
|
+
# @return [Integer, Time] the new value
|
|
206
|
+
#
|
|
207
|
+
def control_add key, value
|
|
208
|
+
raise "value must be numeric" unless value.is_a? Numeric
|
|
209
|
+
type = CONTROL[key] or raise ArgumentError, "invalid control key #{key}"
|
|
210
|
+
|
|
211
|
+
# value may be uninitialized
|
|
212
|
+
raise "Attempted to change an uninitialized value" unless
|
|
213
|
+
old = control_get(key)
|
|
214
|
+
|
|
215
|
+
# early bailout
|
|
216
|
+
return value if value == 0
|
|
217
|
+
|
|
218
|
+
# overwrite the value
|
|
219
|
+
control_set key, old + value
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Add an entry to an index.
|
|
223
|
+
#
|
|
224
|
+
# @note The indexes point to the integer keys in v1 rather than hashes in v0
|
|
225
|
+
#
|
|
226
|
+
# @param index [Symbol] the index table name
|
|
227
|
+
# @param key [Object] the datum to become the index key
|
|
228
|
+
# @param ptr [Integer] the key for the entry
|
|
229
|
+
#
|
|
230
|
+
# @return [void]
|
|
231
|
+
#
|
|
232
|
+
def index_add index, key, ptr
|
|
233
|
+
# XXX just add etime here for now
|
|
234
|
+
cls = RECORD.merge({etime: Time})[index] or raise ArgumentError,
|
|
235
|
+
"No record for #{index}"
|
|
236
|
+
|
|
237
|
+
# warn "#{index}, #{key.inspect}"
|
|
238
|
+
|
|
239
|
+
key = db_encode key, cls
|
|
240
|
+
ptr = ptr.is_a?(String) ? ptr : [ptr].pack(?J)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
lmdb[index.to_sym].put? key, ptr
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Remove an entry from an index.
|
|
247
|
+
#
|
|
248
|
+
# @param index [Symbol] the index table name
|
|
249
|
+
# @param key [Object] the datum to become the index key
|
|
250
|
+
# @param ptr [Integer] the key for the entry
|
|
251
|
+
#
|
|
252
|
+
# @return [void]
|
|
253
|
+
#
|
|
254
|
+
def index_rm index, key, ptr
|
|
255
|
+
# XXX etime lol
|
|
256
|
+
cls = RECORD.merge({etime: Time})[index] or raise ArgumentError,
|
|
257
|
+
"No record for #{index}"
|
|
258
|
+
key = db_encode key, cls
|
|
259
|
+
ptr = ptr.is_a?(String) ? ptr : [ptr].pack(?J)
|
|
260
|
+
|
|
261
|
+
lmdb[index.to_sym].delete? key, ptr
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# the v1 record is substantively different from v0; also all the
|
|
265
|
+
# hashes are in the v1 record whereas the primary hash is used as
|
|
266
|
+
# the key in v0 and so is not duplicated. this also means we only
|
|
267
|
+
# need the one argument because we don't need the information from
|
|
268
|
+
# the key.
|
|
269
|
+
|
|
270
|
+
# Return a hash of a record.
|
|
271
|
+
#
|
|
272
|
+
# @param raw [String] the raw record from the database
|
|
273
|
+
#
|
|
274
|
+
# @return [Hash]
|
|
275
|
+
#
|
|
276
|
+
def inflate raw
|
|
277
|
+
# we're about to chomp through this
|
|
278
|
+
raw = raw.dup
|
|
279
|
+
|
|
280
|
+
# get the digest algos
|
|
281
|
+
ds = algorithms.map do |a|
|
|
282
|
+
uri = URI::NI.build(scheme: 'ni', path: "/#{a}")
|
|
283
|
+
uri.digest = raw.slice!(0, DIGESTS[a])
|
|
284
|
+
[a, uri]
|
|
285
|
+
end.to_h
|
|
286
|
+
|
|
287
|
+
# love this for me
|
|
288
|
+
{ digests: ds }.merge(RECORD.keys.zip(raw.unpack(PACKED)).map do |k, v|
|
|
289
|
+
[k, COERCE[RECORD[k]].last.call(v)]
|
|
290
|
+
end.to_h)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Return a packed string suitable to store as a record.
|
|
294
|
+
#
|
|
295
|
+
# @param obj [Store::Digest::Entry, Hash]
|
|
296
|
+
#
|
|
297
|
+
# @return [String]
|
|
298
|
+
#
|
|
299
|
+
def deflate obj
|
|
300
|
+
obj = obj.to_h
|
|
301
|
+
algos = algorithms.map { |a| obj[:digests][a].digest }.join
|
|
302
|
+
rec = RECORD.map { |k, cls| COERCE[cls][1].call obj[k] }
|
|
303
|
+
|
|
304
|
+
algos + rec.pack(PACKED)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Get an integer entry key from a {Store::Digest::Entry} or
|
|
308
|
+
# {Hash} representation thereof, or hash of digests to {URI::NI}
|
|
309
|
+
# objects.
|
|
310
|
+
#
|
|
311
|
+
# @param obj [Store::Digest::Entry, Hash]
|
|
312
|
+
# @param raw [false, true] whether to return the raw bytes
|
|
313
|
+
#
|
|
314
|
+
# @return [Integer, nil]
|
|
315
|
+
#
|
|
316
|
+
def get_ptr obj, raw: false
|
|
317
|
+
uri = coerce_uri(obj) or return
|
|
318
|
+
|
|
319
|
+
# now return the pointer (or nil)
|
|
320
|
+
out = lmdb[uri.algorithm][uri.digest] or return
|
|
321
|
+
raw ? out : out.unpack1(?J)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Returns a comparator function suitable for picking the right mtime.
|
|
325
|
+
#
|
|
326
|
+
# @example
|
|
327
|
+
# cmp = mtime_cmp
|
|
328
|
+
# newh[:mtime] = [oldh[:mtime], newh[:mtime]].sort(&cmp).first
|
|
329
|
+
#
|
|
330
|
+
# @return [Proc] the comparator
|
|
331
|
+
#
|
|
332
|
+
def mtime_cmp
|
|
333
|
+
policy = {
|
|
334
|
+
preserve: -> a, b { -1 },
|
|
335
|
+
replace: -> a, b { 1 },
|
|
336
|
+
oldest: -> a, b { a <=> b },
|
|
337
|
+
newest: -> a, b { b <=> a },
|
|
338
|
+
}[mtimes]
|
|
339
|
+
|
|
340
|
+
raise Store::Digest::Error::Configuration,
|
|
341
|
+
"Can't find modification time comparator for #{mtimes}" unless policy
|
|
342
|
+
|
|
343
|
+
return -> a, b do
|
|
344
|
+
raise ArgumentError, 'both comparands are nil' if a.nil? && b.nil?
|
|
345
|
+
return -1 if b.nil?
|
|
346
|
+
return 1 if a.nil?
|
|
347
|
+
|
|
348
|
+
policy.call a, b
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
protected
|
|
353
|
+
|
|
354
|
+
# Retrieve a record from the database.
|
|
355
|
+
#
|
|
356
|
+
# @param obj [Store::Digest::Entry, Hash, URI::NI, Integer] the
|
|
357
|
+
# entry's key, or an object from which it can be resolved
|
|
358
|
+
# @param raw [false, true] whether to leave the result as raw bytes
|
|
359
|
+
#
|
|
360
|
+
# @return [Hash, String, nil] inflated or raw record, if present
|
|
361
|
+
#
|
|
362
|
+
def get_meta obj, raw: false
|
|
363
|
+
op = -> do
|
|
364
|
+
# get the pointer
|
|
365
|
+
ptr = case obj
|
|
366
|
+
when String then obj
|
|
367
|
+
when Hash, Store::Digest::Entry then get_ptr obj, raw: true
|
|
368
|
+
when Integer then [obj].pack ?J
|
|
369
|
+
when URI::NI then lmdb[obj.algorithm.to_sym][obj.digest]
|
|
370
|
+
else
|
|
371
|
+
raise ArgumentError, "Cannot process an #{obj.class}"
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
if ptr && out = lmdb[:entry][ptr]
|
|
375
|
+
raw ? out : inflate(out)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# again we aren't supposed to have to do this
|
|
380
|
+
transaction readonly: true, &op
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Persist the metadata for a {Store::Digest::Entry}.
|
|
384
|
+
#
|
|
385
|
+
# @param obj [Store::Digest::Entry, Hash]
|
|
386
|
+
#
|
|
387
|
+
# @return [Hash] the updated metadata hash.
|
|
388
|
+
#
|
|
389
|
+
def set_meta obj
|
|
390
|
+
# * create a new entry
|
|
391
|
+
# * update metadata of an existing entry
|
|
392
|
+
# * update fields (no change to status)
|
|
393
|
+
# * can't update a tombstone
|
|
394
|
+
# * can't turn non-cache into cache
|
|
395
|
+
# * turn a cache entry to non-cache
|
|
396
|
+
# * (remove from etime index and clear out dtime)
|
|
397
|
+
# * undelete a tombstone
|
|
398
|
+
# * (remove from dtime index and clear out dtime)
|
|
399
|
+
# * mark an entry deleted
|
|
400
|
+
# * note you need the whole record here instead of just the
|
|
401
|
+
# hash, but we'll support it for parity so the stat counts
|
|
402
|
+
# don't get messed up
|
|
403
|
+
#
|
|
404
|
+
# deltas:
|
|
405
|
+
# * if new:
|
|
406
|
+
# * entries + 1
|
|
407
|
+
# * bytes + N
|
|
408
|
+
# * if undeleting tombstone:
|
|
409
|
+
# * entries + 0
|
|
410
|
+
# * deleted - 1
|
|
411
|
+
# * bytes + N
|
|
412
|
+
# * if marking deleted
|
|
413
|
+
# * entries - 0
|
|
414
|
+
# * deleted + 1
|
|
415
|
+
# * bytes - N
|
|
416
|
+
#
|
|
417
|
+
|
|
418
|
+
# check if the object has all the hashes
|
|
419
|
+
raise ArgumentError,
|
|
420
|
+
'Object does not have a complete set of digests' unless
|
|
421
|
+
(algorithms - obj[:digests].keys).empty?
|
|
422
|
+
|
|
423
|
+
now = Time.now(in: ?Z)
|
|
424
|
+
newh = obj.to_h.dup
|
|
425
|
+
oldh = nil
|
|
426
|
+
changes = Set[]
|
|
427
|
+
|
|
428
|
+
# warn newh.inspect
|
|
429
|
+
|
|
430
|
+
# determine if newh is cache
|
|
431
|
+
if is_cache = (newh[:flags] ||= Flags.from(0)).cache
|
|
432
|
+
raise ArgumentError,
|
|
433
|
+
'Cache flag set but expiry is not' unless newh[:dtime]
|
|
434
|
+
if newh[:dtime].is_a? Numeric
|
|
435
|
+
raise ArgumentError,
|
|
436
|
+
'Cache expiry offset must be non-negative' if newh[:dtime] < 0
|
|
437
|
+
newh[:dtime] = now + newh[:dtime]
|
|
438
|
+
elsif !newh[:dtime].is_a?(Time)
|
|
439
|
+
newh[:dtime] = now + cache_ttl
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# determine if newh is a tombstone
|
|
444
|
+
if is_ts = newh[:dtime] && newh[:dtime] <= now
|
|
445
|
+
# warn "#{coerce_uri obj} is tombstone: #{newh[:dtime]}"
|
|
446
|
+
newh[:dtime] = now # normalize dtime to now
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# if newh[:flags].cache is true:
|
|
450
|
+
# * newh[:dtime] must be truthy
|
|
451
|
+
# * newh[:dtime] can be a Time, a positive Numeric, or coercible to `true`
|
|
452
|
+
# * if newh[:dtime] is a Time it must be in the future
|
|
453
|
+
# * if newh[:dtime] is a number it is added to `ptime` (`Time.now`)
|
|
454
|
+
# * otherwise newh[:dtime] is set to now + CACHE_TTL
|
|
455
|
+
|
|
456
|
+
# * if it turns out that oldh[:flags].cache is falsy:
|
|
457
|
+
# * unless oldh is a tombstone (has a dtime in the past):
|
|
458
|
+
# * newh[:flags].cache is cleared
|
|
459
|
+
# * newh[:dtime] is cleared
|
|
460
|
+
# * (unless newh is also a tombstone in which case oldh[:dtime] is used)
|
|
461
|
+
# if newh is a tombstone it doesn't matter whether it's cache, however:
|
|
462
|
+
# * can't update a non-cache entry to cache, even if it's a tombstone
|
|
463
|
+
# * can't update the dtime on a tombstone (it's already dead)
|
|
464
|
+
# * the only legal moves are to change the inevitable expiry
|
|
465
|
+
# date of an existing cache entry, including into the past
|
|
466
|
+
# (clipped at Time.now).
|
|
467
|
+
|
|
468
|
+
op = -> do
|
|
469
|
+
# `last_key` gives us a new pointer if one does not exist
|
|
470
|
+
ptr = get_ptr(obj, raw: true) || last_key(:entry, raw: true)
|
|
471
|
+
|
|
472
|
+
# warn ptr.unpack1(?J)
|
|
473
|
+
|
|
474
|
+
added = false
|
|
475
|
+
was_ts = nil
|
|
476
|
+
|
|
477
|
+
if oldrec = lmdb[:entry][ptr]
|
|
478
|
+
# there are only three legal operations with an existing record:
|
|
479
|
+
#
|
|
480
|
+
# * mark the record as a tombstone
|
|
481
|
+
# * reinstate a tombstone as a live record
|
|
482
|
+
# * change some other metadata on a live record:
|
|
483
|
+
# * change a cache record to non-cache
|
|
484
|
+
# * update some other metadata that doesn't touch the
|
|
485
|
+
# cache flag or dtime field (unless changing it)
|
|
486
|
+
#
|
|
487
|
+
# these all basically reduce to "change some metadata" with
|
|
488
|
+
# a handful of rules attached:
|
|
489
|
+
#
|
|
490
|
+
# ctime is minted once and never changes
|
|
491
|
+
# ptime is always set to now if there is something to update
|
|
492
|
+
# mtime goes according to policy:
|
|
493
|
+
# * :preserve keeps the original mtime and never updates it
|
|
494
|
+
# * :update always picks the replacement mtime
|
|
495
|
+
# * :oldest always picks the older of the two
|
|
496
|
+
# * :newest always picks the younger of the two
|
|
497
|
+
# dtime meaning changes depending on whether the entry is cache
|
|
498
|
+
# * if non-nil and not cache, it represents a tombstone (and
|
|
499
|
+
# MUST be in the past, or actually overwritten to now)
|
|
500
|
+
# * if non-nil and cache, it represents an expiry date
|
|
501
|
+
|
|
502
|
+
oldh = inflate oldrec
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# a change in size should blow up
|
|
506
|
+
raise Store::Digest::Error::Integrity,
|
|
507
|
+
"attempt to overwrite size #{oldh[:size]} with #{newh[:size]}" if
|
|
508
|
+
newh[:size] && newh[:size] != oldh[:size]
|
|
509
|
+
|
|
510
|
+
# do all the assignments/folding/merging
|
|
511
|
+
|
|
512
|
+
# these are always going to be whatever they were
|
|
513
|
+
newh[:size] = oldh[:size]
|
|
514
|
+
newh[:ctime] = oldh[:ctime]
|
|
515
|
+
|
|
516
|
+
# warn "#{oldh[:type]} -> #{newh[:type] || oldh[:type]}"
|
|
517
|
+
|
|
518
|
+
%i[type charset encoding language].each do |key|
|
|
519
|
+
newh[key] ||= oldh[key]
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
cmp = mtime_cmp
|
|
523
|
+
newh[:mtime] = [oldh[:mtime], newh[:mtime]].sort(&cmp).first
|
|
524
|
+
|
|
525
|
+
# determine if oldh is cache
|
|
526
|
+
was_cache = oldh[:flags].cache
|
|
527
|
+
# deterimine if oldh is a tombstone
|
|
528
|
+
was_ts = oldh[:dtime] && oldh[:dtime] <= now
|
|
529
|
+
added = was_ts && !is_ts
|
|
530
|
+
|
|
531
|
+
if was_ts
|
|
532
|
+
# noop because the only way to change a tombstone is to reinstate it
|
|
533
|
+
newh = oldh.dup if is_ts
|
|
534
|
+
elsif !was_cache && is_cache
|
|
535
|
+
# wipe out cache flag and clear dtime on newh
|
|
536
|
+
is_cache = newh[:flags].cache = false
|
|
537
|
+
newh[:dtime] = is_ts ? now : nil
|
|
538
|
+
elsif is_ts
|
|
539
|
+
newh[:dtime] = now
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
%i[mtime dtime flags type charset encoding language].each do |key|
|
|
543
|
+
changes << key unless oldh[key] == newh[key]
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# now we know there is a change so set ptime
|
|
547
|
+
unless changes.empty?
|
|
548
|
+
newh[:ptime] = now
|
|
549
|
+
changes << :ptime
|
|
550
|
+
end
|
|
551
|
+
else
|
|
552
|
+
# always a new entry
|
|
553
|
+
newh[:ctime] = now
|
|
554
|
+
newh[:ptime] = now
|
|
555
|
+
newh[:mtime] ||= now
|
|
556
|
+
newh[:type] ||= MimeMagic['application/octet-stream']
|
|
557
|
+
|
|
558
|
+
# set the algo mappings
|
|
559
|
+
algorithms.each do |algo|
|
|
560
|
+
# warn "setting #{algo} -> #{obj[algo].hexdigest}"
|
|
561
|
+
lmdb[algo].put? obj[:digests][algo].digest, ptr
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
added = true
|
|
565
|
+
changes |= RECORD.keys
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# warn "got here with #{coerce_uri obj}: #{changes}"
|
|
569
|
+
|
|
570
|
+
# update indices and control
|
|
571
|
+
unless changes.empty?
|
|
572
|
+
# update the record
|
|
573
|
+
lmdb[:entry][ptr] = deflate newh
|
|
574
|
+
# dummy oldh
|
|
575
|
+
oldh ||= {}
|
|
576
|
+
|
|
577
|
+
# do the indices
|
|
578
|
+
(changes - [:flags]).each do |key|
|
|
579
|
+
# delete old and index entry and add new
|
|
580
|
+
if key == :dtime
|
|
581
|
+
oldk = was_cache ? :etime : :dtime
|
|
582
|
+
newk = is_cache ? :etime : :dtime
|
|
583
|
+
|
|
584
|
+
index_rm oldk, oldh[:dtime], ptr if oldh[:dtime]
|
|
585
|
+
index_add newk, newh[:dtime], ptr
|
|
586
|
+
else
|
|
587
|
+
index_rm key, oldh[key], ptr if oldh[key]
|
|
588
|
+
index_add key, newh[key], ptr if newh[key]
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# do the stats
|
|
593
|
+
if oldrec
|
|
594
|
+
if !was_ts && is_ts
|
|
595
|
+
control_add :deleted, 1
|
|
596
|
+
control_add :bytes, -oldh[:size]
|
|
597
|
+
elsif was_ts && !is_ts
|
|
598
|
+
control_add :deleted, -1
|
|
599
|
+
control_add :bytes, oldh[:size]
|
|
600
|
+
end
|
|
601
|
+
else
|
|
602
|
+
control_add :objects, 1
|
|
603
|
+
control_add :bytes, newh[:size]
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
# set the global modification time
|
|
607
|
+
control_set :mtime, now
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# txn.commit
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
transaction &op
|
|
614
|
+
|
|
615
|
+
newh
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
# Set `dtime` to the current timestamp and update the indices and stats.
|
|
619
|
+
#
|
|
620
|
+
# @param obj [Store::Digest::Entry, Hash, URI::NI, Integer] the
|
|
621
|
+
# entry's key, or an object from which it can be resolved
|
|
622
|
+
#
|
|
623
|
+
# @return [Hash, nil] the record, if it exists
|
|
624
|
+
#
|
|
625
|
+
def mark_meta_deleted obj
|
|
626
|
+
op = -> do
|
|
627
|
+
# nothing to do if there's no entry
|
|
628
|
+
if ptr = get_ptr(obj, raw: true)
|
|
629
|
+
rec = get_meta ptr
|
|
630
|
+
now = Time.now in: ?Z
|
|
631
|
+
|
|
632
|
+
# it's already deleted and we don't need to do anything
|
|
633
|
+
unless rec[:dtime] and rec[:dtime] <= now
|
|
634
|
+
|
|
635
|
+
# grab this to get the index
|
|
636
|
+
old = rec[:dtime]
|
|
637
|
+
|
|
638
|
+
# set the new dtime
|
|
639
|
+
rec[:dtime] = now
|
|
640
|
+
|
|
641
|
+
# update the entry
|
|
642
|
+
lmdb[:entry][ptr] = deflate rec
|
|
643
|
+
|
|
644
|
+
# deal with the indices
|
|
645
|
+
%i[dtime etime].each { |k| index_rm k, old, ptr } if old
|
|
646
|
+
index_add :dtime, now, ptr
|
|
647
|
+
|
|
648
|
+
# deal with the stats/mtime
|
|
649
|
+
control_add :deleted, 1
|
|
650
|
+
control_add :bytes, -rec[:size]
|
|
651
|
+
control_set :mtime, now
|
|
652
|
+
|
|
653
|
+
rec
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# this is dumb but i'm determined
|
|
659
|
+
transaction &op
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
# Purge the metadata entry from the database and remove it from
|
|
663
|
+
# the indices.
|
|
664
|
+
#
|
|
665
|
+
# @param obj [Store::Digest::Entry, Hash, URI::NI, Integer] the
|
|
666
|
+
# entry's key, or an object from which it can be resolved
|
|
667
|
+
#
|
|
668
|
+
# @return [Hash, nil] the record, if it exists
|
|
669
|
+
#
|
|
670
|
+
def remove_meta obj
|
|
671
|
+
op = -> do
|
|
672
|
+
# nothing to do if there's no entry
|
|
673
|
+
if ptr = get_ptr(obj)
|
|
674
|
+
rec = get_meta ptr
|
|
675
|
+
now = Time.now in: ?Z
|
|
676
|
+
|
|
677
|
+
# overwrite the dtime
|
|
678
|
+
tombstone = rec[:dtime] && rec[:dtime] <= now
|
|
679
|
+
rec[:dtime] = now
|
|
680
|
+
|
|
681
|
+
# deal with indices
|
|
682
|
+
RECORD.merge({etime: nil}).except(:flags).keys.each do |key|
|
|
683
|
+
index_rm key, rec[key], ptr
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# deal with the hashes
|
|
687
|
+
algorithms.each do |algo|
|
|
688
|
+
# XXX this *should* match?
|
|
689
|
+
uri = rec[:digests][algo]
|
|
690
|
+
lmdb[algo].delete? uri.digest, ptr
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
# deal with stats
|
|
694
|
+
control_add :objects, -1
|
|
695
|
+
if deleted
|
|
696
|
+
control_add :deleted, -1
|
|
697
|
+
else
|
|
698
|
+
control_add :bytes, -rec[:size]
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
# the deleted record
|
|
702
|
+
rec
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
# ugghgh
|
|
707
|
+
transaction &op
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
# Close the store
|
|
711
|
+
#
|
|
712
|
+
def close_internal
|
|
713
|
+
lmdb.sync
|
|
714
|
+
lmdb.close
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
public
|
|
718
|
+
|
|
719
|
+
# Return the default time-to-live on cache entries.
|
|
720
|
+
#
|
|
721
|
+
# @return [Integer] the TTL in seconds
|
|
722
|
+
#
|
|
723
|
+
def cache_ttl
|
|
724
|
+
control_get :expiry
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# Set a new default time-to-live on cache entries.
|
|
728
|
+
#
|
|
729
|
+
# @param ttl [
|
|
730
|
+
#
|
|
731
|
+
# @return [Integer] the new TTL in seconds
|
|
732
|
+
#
|
|
733
|
+
def cache_ttl= ttl
|
|
734
|
+
control_set :expiry, ttl
|
|
735
|
+
end
|
|
736
|
+
end
|
|
737
|
+
end
|