store-digest 0.3.1 → 0.4.4

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.
@@ -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