2
2
import fs from 'fs'
3
3
import os from 'os'
4
4
import { join } from 'path'
5
- import { Readable } from 'stream'
5
+ import { Readable , Writable } from 'stream'
6
6
import sade from 'sade'
7
7
import { CID } from 'multiformats/cid'
8
- import { CarIndexedReader , CarReader , CarWriter } from '@ipld/car '
8
+ import { CARReaderStream , CARWriterStream } from 'carstream '
9
9
import clc from 'cli-color'
10
10
import archy from 'archy'
11
11
// eslint-disable-next-line no-unused-vars
12
12
import * as API from './src/api.js'
13
- import { put , get , del , entries } from './src/v1/index.js'
14
- import { ShardFetcher , ShardBlock } from './src/v1/shard.js'
15
- import { MaxShardSize } from './src/shard.js'
13
+ import { put , get , del , entries } from './src/index.js'
14
+ import { ShardFetcher , ShardBlock , isShardLink } from './src/shard.js'
16
15
import { difference } from './src/diff.js'
17
16
import { merge } from './src/merge.js'
17
+ import { MemoryBlockstore , MultiBlockFetcher } from './src/block.js'
18
18
19
19
const cli = sade ( 'pail' )
20
20
. option ( '--path' , 'Path to data store.' , './pail.car' )
21
21
22
22
cli . command ( 'put <key> <value>' )
23
23
. describe ( 'Put a value (a CID) for the given key. If the key exists it\'s value is overwritten.' )
24
24
. alias ( 'set' )
25
- . option ( '--max-shard-size' , 'Maximum shard size in bytes.' , MaxShardSize )
26
25
. action ( async ( key , value , opts ) => {
27
- const maxShardSize = opts [ 'max-shard-size' ] ?? MaxShardSize
28
- const blocks = await openPail ( opts . path , { maxSize : maxShardSize } )
29
- const roots = await blocks . getRoots ( )
30
- // @ts -expect-error
31
- const { root, additions, removals } = await put ( blocks , roots [ 0 ] , key , CID . parse ( value ) )
26
+ const { root : prevRoot , blocks } = await openPail ( opts . path )
27
+ const { root, additions, removals } = await put ( blocks , prevRoot , key , CID . parse ( value ) )
32
28
await updatePail ( opts . path , blocks , root , { additions, removals } )
33
29
34
- console . log ( clc . red ( `--- ${ roots [ 0 ] } ` ) )
30
+ console . log ( clc . red ( `--- ${ prevRoot } ` ) )
35
31
console . log ( clc . green ( `+++ ${ root } ` ) )
36
32
console . log ( clc . magenta ( '@@ -1 +1 @@' ) )
37
33
additions . forEach ( b => console . log ( clc . green ( `+${ b . cid } ` ) ) )
38
34
removals . forEach ( b => console . log ( clc . red ( `-${ b . cid } ` ) ) )
39
- await closePail ( blocks )
40
35
} )
41
36
42
37
cli . command ( 'get <key>' )
43
38
. describe ( 'Get the stored value for the given key from the pail. If the key is not found, `undefined` is returned.' )
44
39
. action ( async ( key , opts ) => {
45
- const blocks = await openPail ( opts . path )
46
- // @ts -expect-error
47
- const value = await get ( blocks , ( await blocks . getRoots ( ) ) [ 0 ] , key )
40
+ const { root, blocks } = await openPail ( opts . path )
41
+ const value = await get ( blocks , root , key )
48
42
if ( value ) console . log ( value . toString ( ) )
49
- await closePail ( blocks )
50
43
} )
51
44
52
45
cli . command ( 'del <key>' )
53
46
. describe ( 'Delete the value for the given key from the pail. If the key is not found no operation occurs.' )
54
47
. alias ( 'delete' , 'rm' , 'remove' )
55
48
. action ( async ( key , opts ) => {
56
- const blocks = await openPail ( opts . path )
57
- const roots = await blocks . getRoots ( )
58
- // @ts -expect-error
59
- const { root, additions, removals } = await del ( blocks , roots [ 0 ] , key )
49
+ const { root : prevRoot , blocks } = await openPail ( opts . path )
50
+ const { root, additions, removals } = await del ( blocks , prevRoot , key )
60
51
await updatePail ( opts . path , blocks , root , { additions, removals } )
61
52
62
- console . log ( clc . red ( `--- ${ roots [ 0 ] } ` ) )
53
+ console . log ( clc . red ( `--- ${ prevRoot } ` ) )
63
54
console . log ( clc . green ( `+++ ${ root } ` ) )
64
55
console . log ( clc . magenta ( '@@ -1 +1 @@' ) )
65
56
additions . forEach ( b => console . log ( clc . green ( `+ ${ b . cid } ` ) ) )
66
57
removals . forEach ( b => console . log ( clc . red ( `- ${ b . cid } ` ) ) )
67
- await closePail ( blocks )
68
58
} )
69
59
70
60
cli . command ( 'ls' )
71
61
. describe ( 'List entries in the pail.' )
72
62
. alias ( 'list' )
73
63
. option ( '-p, --prefix' , 'Key prefix to filter by.' )
64
+ . option ( '--gt' , 'Filter results by keys greater than this string.' )
65
+ . option ( '--lt' , 'Filter results by keys less than this string.' )
74
66
. option ( '--json' , 'Format output as newline delimted JSON.' )
75
67
. action ( async ( opts ) => {
76
- const blocks = await openPail ( opts . path )
77
- const root = ( await blocks . getRoots ( ) ) [ 0 ]
68
+ const { root, blocks } = await openPail ( opts . path )
78
69
let n = 0
79
- // @ts -expect-error
80
- for await ( const [ k , v ] of entries ( blocks , root , { prefix : opts . prefix } ) ) {
70
+ for await ( const [ k , v ] of entries ( blocks , root , { prefix : opts . prefix , gt : opts . gt , lt : opts . lt } ) ) {
81
71
console . log ( opts . json ? JSON . stringify ( { key : k , value : v . toString ( ) } ) : `${ k } \t${ v } ` )
82
72
n ++
83
73
}
84
74
if ( ! opts . json ) console . log ( `total ${ n } ` )
85
- await closePail ( blocks )
86
75
} )
87
76
88
77
cli . command ( 'tree' )
89
78
. describe ( 'Visualise the pail.' )
79
+ . alias ( 'vis' )
90
80
. action ( async ( opts ) => {
91
- const blocks = await openPail ( opts . path )
92
- const root = ( await blocks . getRoots ( ) ) [ 0 ]
93
- // @ts -expect-error
81
+ const { root, blocks } = await openPail ( opts . path )
94
82
const shards = new ShardFetcher ( blocks )
95
- // @ts -expect-error
96
83
const rshard = await shards . get ( root )
97
84
98
85
/** @type {archy.Data } */
@@ -119,27 +106,19 @@ cli.command('tree')
119
106
}
120
107
121
108
console . log ( archy ( archyRoot ) )
122
- await closePail ( blocks )
123
109
} )
124
110
125
111
cli . command ( 'diff <path>' )
126
112
. describe ( 'Find the differences between this pail and the passed pail.' )
127
113
. option ( '-k, --keys' , 'Output key/value diff.' )
128
114
. action ( async ( path , opts ) => {
129
- const [ ablocks , bblocks ] = await Promise . all ( [ openPail ( opts . path ) , openPail ( path ) ] )
130
- const [ aroot , broot ] = await Promise . all ( [ ablocks , bblocks ] . map ( async blocks => {
131
- return /** @type { API.ShardLink } */ ( ( await blocks . getRoots ( ) ) [ 0 ] )
132
- } ) )
115
+ const [
116
+ { root : aroot , blocks : ablocks } ,
117
+ { root : broot , blocks : bblocks }
118
+ ] = await Promise . all ( [ openPail ( opts . path ) , openPail ( path ) ] )
133
119
if ( aroot . toString ( ) === broot . toString ( ) ) return
134
120
135
- const fetcher = {
136
- async get ( cid ) {
137
- const blk = await ablocks . get ( cid )
138
- if ( blk ) return blk
139
- return bblocks . get ( cid )
140
- }
141
- }
142
- // @ts -expect-error CarReader is not BlockFetcher
121
+ const fetcher = new MultiBlockFetcher ( ablocks , bblocks )
143
122
const { shards : { additions, removals } , keys } = await difference ( fetcher , aroot , broot )
144
123
145
124
console . log ( clc . red ( `--- ${ aroot } ` ) )
@@ -155,27 +134,18 @@ cli.command('diff <path>')
155
134
additions . forEach ( b => console . log ( clc . green ( `+ ${ b . cid } ` ) ) )
156
135
removals . forEach ( b => console . log ( clc . red ( `- ${ b . cid } ` ) ) )
157
136
}
158
-
159
- await Promise . all ( [ closePail ( ablocks ) , closePail ( bblocks ) ] )
160
137
} )
161
138
162
139
cli . command ( 'merge <path>' )
163
140
. describe ( 'Merge the passed pail into this pail.' )
164
141
. action ( async ( path , opts ) => {
165
- const [ ablocks , bblocks ] = await Promise . all ( [ openPail ( opts . path ) , openPail ( path ) ] )
166
- const [ aroot , broot ] = await Promise . all ( [ ablocks , bblocks ] . map ( async blocks => {
167
- return /** @type { API.ShardLink } */ ( ( await blocks . getRoots ( ) ) [ 0 ] )
168
- } ) )
142
+ const [
143
+ { root : aroot , blocks : ablocks } ,
144
+ { root : broot , blocks : bblocks }
145
+ ] = await Promise . all ( [ openPail ( opts . path ) , openPail ( path ) ] )
169
146
if ( aroot . toString ( ) === broot . toString ( ) ) return
170
147
171
- const fetcher = {
172
- async get ( cid ) {
173
- const blk = await ablocks . get ( cid )
174
- if ( blk ) return blk
175
- return bblocks . get ( cid )
176
- }
177
- }
178
- // @ts -expect-error CarReader is not BlockFetcher
148
+ const fetcher = new MultiBlockFetcher ( ablocks , bblocks )
179
149
const { root, additions, removals } = await merge ( fetcher , aroot , [ broot ] )
180
150
181
151
await updatePail ( opts . path , ablocks , root , { additions, removals } )
@@ -185,65 +155,53 @@ cli.command('merge <path>')
185
155
console . log ( clc . magenta ( '@@ -1 +1 @@' ) )
186
156
additions . forEach ( b => console . log ( clc . green ( `+ ${ b . cid } ` ) ) )
187
157
removals . forEach ( b => console . log ( clc . red ( `- ${ b . cid } ` ) ) )
188
-
189
- await Promise . all ( [ closePail ( ablocks ) , closePail ( bblocks ) ] )
190
158
} )
191
159
192
160
cli . parse ( process . argv )
193
161
194
162
/**
195
163
* @param {string } path
196
- * @param {{ maxSize?: number } } [config]
197
- * @returns {Promise<import('@ipld/car/api').CarReader> }
164
+ * @returns {Promise<{ root: API.ShardLink, blocks: MemoryBlockstore }> }
198
165
*/
199
- async function openPail ( path , config ) {
166
+ async function openPail ( path ) {
167
+ const blocks = new MemoryBlockstore ( )
200
168
try {
201
- return await CarIndexedReader . fromFile ( path )
169
+ const carReader = new CARReaderStream ( )
170
+ const readable = /** @type {ReadableStream<Uint8Array> } */ ( Readable . toWeb ( fs . createReadStream ( path ) ) )
171
+ await readable . pipeThrough ( carReader ) . pipeTo ( new WritableStream ( { write : b => blocks . put ( b . cid , b . bytes ) } ) )
172
+ const header = await carReader . getHeader ( )
173
+ if ( ! isShardLink ( header . roots [ 0 ] ) ) throw new Error ( `not a shard: ${ header . roots [ 0 ] } ` )
174
+ return { root : header . roots [ 0 ] , blocks }
202
175
} catch ( err ) {
203
176
if ( err . code !== 'ENOENT' ) throw new Error ( 'failed to open bucket' , { cause : err } )
204
- const rootblk = await ShardBlock . create ( config )
205
- const { writer, out } = CarWriter . create ( rootblk . cid )
206
- writer . put ( rootblk )
207
- writer . close ( )
208
- return CarReader . fromIterable ( out )
209
- }
210
- }
211
-
212
- /** @param {import('@ipld/car/api').CarReader } reader */
213
- async function closePail ( reader ) {
214
- if ( reader instanceof CarIndexedReader ) {
215
- await reader . close ( )
177
+ const rootblk = await ShardBlock . create ( )
178
+ blocks . put ( rootblk . cid , rootblk . bytes )
179
+ return { root : rootblk . cid , blocks }
216
180
}
217
181
}
218
182
219
183
/**
220
184
* @param {string } path
221
- * @param {import('@ipld/car/api').CarReader } reader
185
+ * @param {MemoryBlockstore } blocks
222
186
* @param {API.ShardLink } root
223
187
* @param {API.ShardDiff } diff
224
188
*/
225
- async function updatePail ( path , reader , root , { additions, removals } ) {
226
- // @ts -expect-error
227
- const { writer, out } = CarWriter . create ( root )
189
+ async function updatePail ( path , blocks , root , { additions, removals } ) {
228
190
const tmp = join ( os . tmpdir ( ) , `pail${ Date . now ( ) } .car` )
229
-
230
- const finishPromise = new Promise ( resolve => {
231
- Readable . from ( out ) . pipe ( fs . createWriteStream ( tmp ) ) . on ( 'finish' , resolve )
232
- } )
233
-
234
- // put new blocks
235
- for ( const b of additions ) {
236
- await writer . put ( b )
237
- }
238
- // put old blocks without removals
239
- for await ( const b of reader . blocks ( ) ) {
240
- if ( removals . some ( r => b . cid . toString ( ) === r . cid . toString ( ) ) ) {
241
- continue
191
+ const iterator = blocks . entries ( )
192
+ const readable = new ReadableStream ( {
193
+ start ( controller ) {
194
+ for ( const b of additions ) controller . enqueue ( b )
195
+ } ,
196
+ pull ( controller ) {
197
+ for ( const b of iterator ) {
198
+ if ( removals . some ( r => b . cid . toString ( ) === r . cid . toString ( ) ) ) continue
199
+ return controller . enqueue ( b )
200
+ }
201
+ controller . close ( )
242
202
}
243
- await writer . put ( b )
244
- }
245
- await writer . close ( )
246
- await finishPromise
203
+ } )
204
+ await readable . pipeThrough ( new CARWriterStream ( [ root ] ) ) . pipeTo ( Writable . toWeb ( fs . createWriteStream ( tmp ) ) )
247
205
248
206
const old = `${ path } -${ new Date ( ) . toISOString ( ) } `
249
207
try {
0 commit comments