hypercore-storage 0.0.40 → 1.0.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.
@@ -0,0 +1,628 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const { Readable } = require('streamx')
4
+ const b4a = require('b4a')
5
+ const flat = require('flat-tree')
6
+ const crypto = require('hypercore-crypto')
7
+ const c = require('compact-encoding')
8
+ const m = require('./messages.js')
9
+ const View = require('../../lib/view.js')
10
+ const { CorestoreTX, CoreTX, CorestoreRX } = require('../../lib/tx.js')
11
+
12
+ const EMPTY_NODE = b4a.alloc(40)
13
+ const EMPTY_PAGE = b4a.alloc(4096)
14
+
15
+ class CoreListStream extends Readable {
16
+ constructor (storage) {
17
+ super()
18
+
19
+ this.storage = storage
20
+ this.stack = []
21
+ }
22
+
23
+ async _open (cb) {
24
+ for (const a of await readdir(path.join(this.storage, 'cores'))) {
25
+ for (const b of await readdir(path.join(this.storage, 'cores', a))) {
26
+ for (const dkey of await readdir(path.join(this.storage, 'cores', a, b))) {
27
+ this.stack.push(path.join(this.storage, 'cores', a, b, dkey))
28
+ }
29
+ }
30
+ }
31
+
32
+ cb(null)
33
+ }
34
+
35
+ async _read (cb) {
36
+ while (true) {
37
+ const next = this.stack.pop()
38
+ if (!next) {
39
+ this.push(null)
40
+ break
41
+ }
42
+
43
+ const oplog = path.join(next, 'oplog')
44
+ const result = await readOplog(oplog)
45
+ if (!result) continue
46
+
47
+ this.push(result)
48
+ break
49
+ }
50
+
51
+ cb(null)
52
+ }
53
+ }
54
+
55
+ function decodeOplogHeader (state) {
56
+ c.uint32.decode(state) // cksum, ignore for now
57
+
58
+ const l = c.uint32.decode(state)
59
+ const length = l >> 2
60
+ const headerBit = l & 1
61
+ const partialBit = l & 2
62
+
63
+ if (state.end - state.start < length) return null
64
+
65
+ const end = state.start + length
66
+ const result = { header: headerBit, partial: partialBit !== 0, byteLength: length + 8, message: null }
67
+
68
+ try {
69
+ result.message = m.oplog.header.decode({ start: state.start, end, buffer: state.buffer })
70
+ } catch {
71
+ return null
72
+ }
73
+
74
+ state.start = end
75
+ return result
76
+ }
77
+
78
+ function decodeOplogEntry (state) {
79
+ if (state.end - state.start < 8) return null
80
+
81
+ c.uint32.decode(state) // cksum, ignore for now
82
+
83
+ const l = c.uint32.decode(state)
84
+ const length = l >>> 2
85
+ const headerBit = l & 1
86
+ const partialBit = l & 2
87
+
88
+ if (state.end - state.start < length) return null
89
+
90
+ const end = state.start + length
91
+
92
+ const result = { header: headerBit, partial: partialBit !== 0, byteLength: length + 8, message: null }
93
+
94
+ try {
95
+ result.message = m.oplog.entry.decode({ start: state.start, end, buffer: state.buffer })
96
+ } catch {
97
+ return null
98
+ }
99
+
100
+ state.start = end
101
+
102
+ return result
103
+ }
104
+
105
+ module.exports = { store, core }
106
+
107
+ async function store (storage, { version, dryRun = true, gc = true }) {
108
+ const stream = new CoreListStream(storage.path)
109
+ const view = new View()
110
+
111
+ const tx = new CorestoreTX(view)
112
+ const head = await storage._getHead(view)
113
+ const primaryKeyFile = path.join(storage.path, 'primary-key')
114
+
115
+ const primaryKey = await readFile(primaryKeyFile)
116
+
117
+ if (!head.seed) head.seed = primaryKey
118
+
119
+ for await (const data of stream) {
120
+ const key = data.header.key
121
+ const discoveryKey = crypto.discoveryKey(data.header.key)
122
+ const files = getFiles(data.path)
123
+
124
+ if (head.defaultDiscoveryKey === null) head.defaultDiscoveryKey = discoveryKey
125
+
126
+ const core = {
127
+ version: 0, // need later migration
128
+ corePointer: head.allocated.cores++,
129
+ dataPointer: head.allocated.datas++,
130
+ alias: null
131
+ }
132
+
133
+ const ptr = { version: 0, corePointer: core.corePointer, dataPointer: core.dataPointer, dependencies: [] }
134
+ const ctx = new CoreTX(ptr, storage.db, view, [])
135
+ const userData = new Map()
136
+ const treeNodes = new Map()
137
+
138
+ const auth = {
139
+ key,
140
+ discoveryKey,
141
+ manifest: data.header.manifest,
142
+ keyPair: data.header.keyPair,
143
+ encryptionKey: null
144
+ }
145
+
146
+ const tree = {
147
+ length: 0,
148
+ fork: 0,
149
+ rootHash: null,
150
+ signature: null
151
+ }
152
+
153
+ if (data.header.tree && data.header.tree.length) {
154
+ tree.length = data.header.tree.length
155
+ tree.fork = data.header.tree.fork
156
+ tree.rootHash = data.header.tree.rootHash
157
+ tree.signature = data.header.tree.signature
158
+ }
159
+
160
+ for (const { key, value } of data.header.userData) {
161
+ userData.set(key, value)
162
+ }
163
+
164
+ for (const e of data.entries) {
165
+ if (e.userData) userData.set(e.userData.key, e.userData.value)
166
+
167
+ if (e.treeNodes) {
168
+ for (const node of e.treeNodes) {
169
+ treeNodes.set(node.index, node)
170
+ ctx.putTreeNode(node)
171
+ }
172
+ }
173
+
174
+ if (e.treeUpgrade) {
175
+ if (e.treeUpgrade.ancestors !== tree.length) {
176
+ throw new Error('Unflushed truncations not migrate-able atm')
177
+ }
178
+
179
+ tree.length = e.treeUpgrade.length
180
+ tree.fork = e.treeUpgrade.fork
181
+ tree.rootHash = null
182
+ tree.signature = e.treeUpgrade.signature
183
+ }
184
+ }
185
+
186
+ if (userData.has('corestore/name') && userData.has('corestore/namespace')) {
187
+ core.alias = {
188
+ name: b4a.toString(userData.get('corestore/name')),
189
+ namespace: userData.get('corestore/namespace')
190
+ }
191
+ userData.delete('corestore/name')
192
+ userData.delete('corestore/namespace')
193
+ }
194
+
195
+ for (const [key, value] of userData) {
196
+ ctx.putUserData(key, value)
197
+ }
198
+
199
+ ctx.setAuth(auth)
200
+
201
+ const getTreeNode = (index) => (treeNodes.get(index) || getTreeNodeFromFile(files.tree, index))
202
+
203
+ if (tree.length) {
204
+ if (tree.rootHash === null) tree.rootHash = crypto.tree(await getRoots(tree.length, getTreeNode))
205
+ ctx.setHead(tree)
206
+ }
207
+
208
+ tx.putCore(discoveryKey, core)
209
+ if (core.alias) tx.putCoreByAlias(core.alias, discoveryKey)
210
+
211
+ await ctx.flush()
212
+ }
213
+
214
+ head.version = version
215
+ tx.setHead(head)
216
+ tx.apply()
217
+
218
+ if (dryRun) return
219
+
220
+ await View.flush(view.changes, storage.db)
221
+
222
+ if (gc) await rm(primaryKeyFile)
223
+ }
224
+
225
+ class Slicer {
226
+ constructor () {
227
+ this.buffer = null
228
+ this.offset = 0
229
+ }
230
+
231
+ get size () {
232
+ return this.buffer === null ? 0 : this.buffer.byteLength
233
+ }
234
+
235
+ push (data) {
236
+ if (this.buffer === null) this.buffer = data
237
+ else this.buffer = b4a.concat([this.buffer, data])
238
+ this.offset += data.byteLength
239
+ }
240
+
241
+ take (len) {
242
+ if (len <= this.size) {
243
+ const chunk = this.buffer.subarray(0, len)
244
+ this.buffer = this.buffer.subarray(len)
245
+ return chunk
246
+ }
247
+
248
+ return null
249
+ }
250
+ }
251
+
252
+ async function core (core, { version, dryRun = true, gc = true }) {
253
+ if (dryRun) return // dryRun mode not supported atm
254
+
255
+ const rx = core.read()
256
+
257
+ const promises = [rx.getAuth(), rx.getHead()]
258
+ rx.tryFlush()
259
+
260
+ const [auth, head] = await Promise.all(promises)
261
+
262
+ if (!auth) return
263
+
264
+ const dk = b4a.toString(auth.discoveryKey, 'hex')
265
+ const files = getFiles(path.join(core.store.path, 'cores', dk.slice(0, 2), dk.slice(2, 4), dk))
266
+
267
+ if (head === null || head.length === 0) {
268
+ if (gc) await runGC()
269
+ return // no data
270
+ }
271
+
272
+ const oplog = await readOplog(files.oplog)
273
+ if (!oplog) throw new Error('No oplog available')
274
+
275
+ const treeData = new Slicer()
276
+
277
+ let treeIndex = 0
278
+
279
+ if (await exists(files.tree)) {
280
+ for await (const data of fs.createReadStream(files.tree)) {
281
+ treeData.push(data)
282
+
283
+ const write = core.write()
284
+
285
+ while (true) {
286
+ const buf = treeData.take(40)
287
+ if (buf === null) break
288
+
289
+ const index = treeIndex++
290
+ if (b4a.equals(buf, EMPTY_NODE)) continue
291
+
292
+ write.putTreeNode(decodeTreeNode(index, buf))
293
+ }
294
+
295
+ await write.flush()
296
+ }
297
+ }
298
+
299
+ const buf = []
300
+ if (await exists(files.bitfield)) {
301
+ for await (const data of fs.createReadStream(files.bitfield)) {
302
+ buf.push(data)
303
+ }
304
+ }
305
+
306
+ let bitfield = b4a.concat(buf)
307
+ if (bitfield.byteLength & 4095) bitfield = b4a.concat([bitfield, b4a.alloc(4096 - (bitfield.byteLength & 4095))])
308
+
309
+ const pages = new Map()
310
+ const headerBits = new Map()
311
+
312
+ const roots = await getRoots(head.length, getTreeNode)
313
+
314
+ for (const e of oplog.entries) {
315
+ if (!e.bitfield) continue
316
+
317
+ for (let i = 0; i < e.bitfield.length; i++) {
318
+ headerBits.set(i + e.bitfield.start, !e.bitfield.drop)
319
+ }
320
+ }
321
+
322
+ let w = core.write()
323
+ for (const index of allBits(bitfield)) {
324
+ if (headerBits.get(index) === false) continue
325
+
326
+ setBitInPage(index)
327
+
328
+ const blk = await getBlockFromFile(files.data, index, roots, getTreeNode)
329
+
330
+ if (w.changes.length > 1024) {
331
+ await w.flush()
332
+ w = core.write()
333
+ }
334
+
335
+ w.putBlock(index, blk)
336
+ }
337
+
338
+ for (const [index, bit] of headerBits) {
339
+ if (!bit) continue
340
+
341
+ setBitInPage(index)
342
+
343
+ const blk = await getBlockFromFile(files.data, index, roots, getTreeNode)
344
+ w.putBlock(index, blk)
345
+ }
346
+
347
+ for (const [index, page] of pages) {
348
+ w.putBitfieldPage(index, b4a.from(page.buffer, page.byteOffset, page.byteLength))
349
+ }
350
+
351
+ await w.flush()
352
+
353
+ let contiguousLength = 0
354
+ for await (const data of core.createBlockStream()) {
355
+ if (data.index === contiguousLength) contiguousLength++
356
+ else break
357
+ }
358
+
359
+ if (contiguousLength) {
360
+ const w = core.write()
361
+ w.setHints({ contiguousLength })
362
+ await w.flush()
363
+ }
364
+
365
+ await commitCoreMigration(auth, core, version)
366
+
367
+ if (gc) await runGC()
368
+
369
+ async function runGC () {
370
+ await rm(files.path)
371
+ await rmdir(path.join(files.path, '..'))
372
+ await rmdir(path.join(files.path, '../..'))
373
+ await rmdir(path.join(core.store.path, 'cores'))
374
+ }
375
+
376
+ function setBitInPage (index) {
377
+ const n = index & 32767
378
+ const p = (index - n) / 32768
379
+
380
+ let page = pages.get(p)
381
+
382
+ if (!page) {
383
+ page = new Uint32Array(1024)
384
+ pages.set(p, page)
385
+ }
386
+
387
+ const o = n & 31
388
+ const b = (n - o) / 32
389
+ const v = 1 << o
390
+
391
+ page[b] |= v
392
+ }
393
+
394
+ function getTreeNode (index) {
395
+ const read = core.read()
396
+ const promise = read.getTreeNode(index)
397
+ read.tryFlush()
398
+ return promise
399
+ }
400
+ }
401
+
402
+ async function commitCoreMigration (auth, core, version) {
403
+ const view = new View()
404
+ const rx = new CorestoreRX(core.db, view)
405
+
406
+ const storeCorePromise = rx.getCore(auth.discoveryKey)
407
+ rx.tryFlush()
408
+
409
+ const storeCore = await storeCorePromise
410
+
411
+ storeCore.version = version
412
+
413
+ const tx = new CorestoreTX(view)
414
+
415
+ tx.putCore(auth.discoveryKey, storeCore)
416
+ tx.apply()
417
+
418
+ await View.flush(view.changes, core.db)
419
+ }
420
+
421
+ function getFiles (dir) {
422
+ return {
423
+ path: dir,
424
+ oplog: path.join(dir, 'oplog'),
425
+ data: path.join(dir, 'data'),
426
+ tree: path.join(dir, 'tree'),
427
+ bitfield: path.join(dir, 'bitfield')
428
+ }
429
+ }
430
+
431
+ async function getRoots (length, getTreeNode) {
432
+ const all = []
433
+ for (const index of flat.fullRoots(2 * length)) {
434
+ all.push(await getTreeNode(index))
435
+ }
436
+ return all
437
+ }
438
+
439
+ async function getBlockFromFile (file, index, roots, getTreeNode) {
440
+ const size = (await getTreeNode(2 * index)).size
441
+ const offset = await getByteOffset(2 * index, roots, getTreeNode)
442
+
443
+ return new Promise(function (resolve) {
444
+ readAll(file, size, offset, function (err, buf) {
445
+ if (err) return resolve(null)
446
+ resolve(buf)
447
+ })
448
+ })
449
+ }
450
+
451
+ async function getByteOffset (index, roots, getTreeNode) {
452
+ if (index === 0) return 0
453
+ if ((index & 1) === 1) index = flat.leftSpan(index)
454
+
455
+ let head = 0
456
+ let offset = 0
457
+
458
+ for (const node of roots) { // all async ticks happen once we find the root so safe
459
+ head += 2 * ((node.index - head) + 1)
460
+
461
+ if (index >= head) {
462
+ offset += node.size
463
+ continue
464
+ }
465
+
466
+ const ite = flat.iterator(node.index)
467
+
468
+ while (ite.index !== index) {
469
+ if (index < ite.index) {
470
+ ite.leftChild()
471
+ } else {
472
+ offset += (await getTreeNode(ite.leftChild())).size
473
+ ite.sibling()
474
+ }
475
+ }
476
+
477
+ return offset
478
+ }
479
+
480
+ throw new Error('Failed to find offset')
481
+ }
482
+
483
+ function decodeTreeNode (index, buf) {
484
+ return { index, size: c.decode(c.uint64, buf), hash: buf.subarray(8) }
485
+ }
486
+
487
+ async function getTreeNodeFromFile (file, index) {
488
+ return new Promise(function (resolve) {
489
+ readAll(file, 40, index * 40, function (err, buf) {
490
+ if (err) return resolve(null)
491
+ resolve(decodeTreeNode(index, buf))
492
+ })
493
+ })
494
+ }
495
+
496
+ function readAll (filename, length, pos, cb) {
497
+ const buf = b4a.alloc(length)
498
+
499
+ fs.open(filename, 'r', function (err, fd) {
500
+ if (err) return cb(err)
501
+
502
+ let offset = 0
503
+
504
+ fs.read(fd, buf, offset, buf.byteLength, pos, function loop (err, read) {
505
+ if (err) return done(err)
506
+ if (read === 0) return done(new Error('Partial read'))
507
+ offset += read
508
+ if (offset === buf.byteLength) return done(null, buf)
509
+ fs.read(fd, offset, buf.byteLength - offset, buf, pos + offset, loop)
510
+ })
511
+
512
+ function done (err, value) {
513
+ fs.close(fd, () => cb(err, value))
514
+ }
515
+ })
516
+ }
517
+
518
+ async function readdir (dir) {
519
+ try {
520
+ return await fs.promises.readdir(dir)
521
+ } catch {
522
+ return []
523
+ }
524
+ }
525
+
526
+ async function exists (file) {
527
+ try {
528
+ await fs.promises.stat(file)
529
+ return true
530
+ } catch {
531
+ return false
532
+ }
533
+ }
534
+
535
+ async function readFile (file) {
536
+ try {
537
+ return await fs.promises.readFile(file)
538
+ } catch {
539
+ return null
540
+ }
541
+ }
542
+
543
+ async function rm (dir) {
544
+ try {
545
+ await fs.promises.rm(dir, { recursive: true })
546
+ } catch {}
547
+ }
548
+
549
+ async function rmdir (dir) {
550
+ try {
551
+ await fs.promises.rmdir(dir)
552
+ } catch {}
553
+ }
554
+
555
+ function * allBits (buffer) {
556
+ for (let i = 0; i < buffer.byteLength; i += EMPTY_PAGE.byteLength) {
557
+ const page = buffer.subarray(i, i + EMPTY_NODE.byteLength)
558
+ if (b4a.equals(page, EMPTY_PAGE)) continue
559
+
560
+ const view = new Uint32Array(page.buffer, page.byteOffset, EMPTY_PAGE.byteLength / 4)
561
+
562
+ for (let j = 0; j < view.length; j++) {
563
+ const n = view[j]
564
+ if (n === 0) continue
565
+
566
+ for (let k = 0; k < 32; k++) {
567
+ const m = 1 << k
568
+ if (n & m) yield i * 8 + j * 32 + k
569
+ }
570
+ }
571
+ }
572
+ }
573
+
574
+ function readOplog (oplog) {
575
+ return new Promise(function (resolve) {
576
+ fs.readFile(oplog, function (err, buffer) {
577
+ if (err) return resolve(null)
578
+
579
+ const state = { start: 0, end: buffer.byteLength, buffer }
580
+ const headers = [1, 0]
581
+
582
+ const h1 = decodeOplogHeader(state)
583
+ state.start = 4096
584
+
585
+ const h2 = decodeOplogHeader(state)
586
+ state.start = 4096 * 2
587
+
588
+ if (!h1 && !h2) return resolve(null)
589
+
590
+ if (h1 && !h2) {
591
+ headers[0] = h1.header
592
+ headers[1] = h1.header
593
+ } else if (!h1 && h2) {
594
+ headers[0] = (h2.header + 1) & 1
595
+ headers[1] = h2.header
596
+ } else {
597
+ headers[0] = h1.header
598
+ headers[1] = h2.header
599
+ }
600
+
601
+ const header = (headers[0] + headers[1]) & 1
602
+ const result = { path: path.dirname(oplog), header: null, entries: [] }
603
+ const decoded = []
604
+
605
+ result.header = header ? h2.message : h1.message
606
+
607
+ if (result.header.external) {
608
+ throw new Error('External headers not migrate-able atm')
609
+ }
610
+
611
+ while (true) {
612
+ const entry = decodeOplogEntry(state)
613
+ if (!entry) break
614
+ if (entry.header !== header) break
615
+
616
+ decoded.push(entry)
617
+ }
618
+
619
+ while (decoded.length > 0 && decoded[decoded.length - 1].partial) decoded.pop()
620
+
621
+ for (const e of decoded) {
622
+ result.entries.push(e.message)
623
+ }
624
+
625
+ resolve(result)
626
+ })
627
+ })
628
+ }