cbyte 1.4.0 → 1.6.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.
- package/cbyte.js +83 -81
- package/package.json +7 -4
package/cbyte.js
CHANGED
|
@@ -1,45 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
3
|
+
// Increase listener limit to avoid warnings
|
|
4
|
+
// require('events').defaultMaxListeners = 6000;
|
|
5
|
+
import evt from 'events';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import { createHelia } from 'helia';
|
|
10
|
+
import * as Block from 'multiformats/block';
|
|
11
|
+
import * as rawCodec from 'multiformats/codecs/raw';
|
|
12
|
+
import * as sha256 from 'multiformats/hashes/sha2';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
|
|
16
|
+
evt.defaultMaxListeners = 6000;
|
|
17
|
+
|
|
18
|
+
const program = new Command();
|
|
19
|
+
let helia = null;
|
|
20
|
+
|
|
21
|
+
// Initialize Helia node once
|
|
22
|
+
async function ensureHelia() {
|
|
23
|
+
if (!helia) {
|
|
24
|
+
helia = await createHelia();
|
|
20
25
|
}
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
//
|
|
24
|
-
function sha256(data) {
|
|
25
|
-
return crypto.createHash('sha256').update(data).digest('hex');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
+
// Simple SHA‑256 progress bar
|
|
28
29
|
function renderProgress(completed, total, label = '') {
|
|
29
30
|
const width = 40;
|
|
30
31
|
const ratio = completed / total;
|
|
31
32
|
const filled = Math.floor(ratio * width);
|
|
32
33
|
const bar = '='.repeat(filled) + ' '.repeat(width - filled);
|
|
33
|
-
process.stderr.write(`\r${label} [${bar}] ${(ratio*100).toFixed(1)}% (${completed}/${total})`);
|
|
34
|
+
process.stderr.write(`\r${label} [${bar}] ${(ratio * 100).toFixed(1)}% (${completed}/${total})`);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
//
|
|
37
|
-
async function pack(filePath,
|
|
38
|
-
await
|
|
37
|
+
// Pack operation with Helia
|
|
38
|
+
async function pack(filePath, outFile, chunkSize = 4096, pin = false) {
|
|
39
|
+
await ensureHelia();
|
|
39
40
|
|
|
40
|
-
if (!
|
|
41
|
+
if (!outFile) {
|
|
41
42
|
const base = path.basename(filePath, path.extname(filePath));
|
|
42
|
-
|
|
43
|
+
outFile = `${base}.cbyte`;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
const data = fs.readFileSync(filePath);
|
|
@@ -47,7 +48,8 @@ async function pack(filePath, output, chunkSize = 4096, pin = false) {
|
|
|
47
48
|
|
|
48
49
|
for (let offset = 0; offset < data.length; offset += chunkSize) {
|
|
49
50
|
const chunk = data.slice(offset, offset + chunkSize);
|
|
50
|
-
const
|
|
51
|
+
const hashVal = crypto.createHash('sha256').update(chunk).digest();
|
|
52
|
+
const hash = Buffer.from(hashVal).toString('hex');
|
|
51
53
|
if (!chunksMap[hash]) chunksMap[hash] = { chunk, count: 0 };
|
|
52
54
|
chunksMap[hash].count++;
|
|
53
55
|
}
|
|
@@ -62,73 +64,83 @@ async function pack(filePath, output, chunkSize = 4096, pin = false) {
|
|
|
62
64
|
};
|
|
63
65
|
|
|
64
66
|
const hashes = Object.keys(chunksMap);
|
|
65
|
-
|
|
66
|
-
let completed = 0;
|
|
67
|
+
let uploaded = 0;
|
|
67
68
|
|
|
68
69
|
const concurrency = Math.max(1, Math.min(8, os.cpus().length));
|
|
69
70
|
for (let i = 0; i < hashes.length; i += concurrency) {
|
|
70
71
|
const batch = hashes.slice(i, i + concurrency);
|
|
71
|
-
await Promise.all(batch.map(async (
|
|
72
|
-
const info = chunksMap[
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
await Promise.all(batch.map(async (h) => {
|
|
73
|
+
const info = chunksMap[h];
|
|
74
|
+
|
|
75
|
+
// Create a raw block; multiformats block API
|
|
76
|
+
const block = await Block.encode({
|
|
77
|
+
value: info.chunk,
|
|
78
|
+
codec: rawCodec,
|
|
79
|
+
hasher: sha256.sha256
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await helia.blockstore.put(block.cid, block.bytes);
|
|
83
|
+
|
|
84
|
+
manifest.chunks.push({
|
|
85
|
+
cid: block.cid.toString(),
|
|
86
|
+
length: info.chunk.length,
|
|
87
|
+
count: info.count
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
uploaded++;
|
|
91
|
+
renderProgress(uploaded, hashes.length, 'Packing');
|
|
78
92
|
}));
|
|
79
93
|
}
|
|
80
94
|
process.stderr.write('\n');
|
|
81
95
|
|
|
82
|
-
fs.writeFileSync(
|
|
83
|
-
console.log(`Packed ${filePath} -> ${
|
|
96
|
+
fs.writeFileSync(outFile, JSON.stringify(manifest, null, 2));
|
|
97
|
+
console.log(`Packed ${filePath} -> ${outFile}`);
|
|
84
98
|
}
|
|
85
99
|
|
|
86
|
-
//
|
|
87
|
-
async function unpack(manifestPath,
|
|
88
|
-
await
|
|
100
|
+
// Unpack using Helia
|
|
101
|
+
async function unpack(manifestPath, outFile) {
|
|
102
|
+
await ensureHelia();
|
|
89
103
|
|
|
90
104
|
const manifest = JSON.parse(fs.readFileSync(manifestPath));
|
|
91
|
-
if (!
|
|
105
|
+
if (!outFile) {
|
|
92
106
|
const base = path.basename(manifestPath, path.extname(manifestPath));
|
|
93
107
|
const ext = manifest.original_ext || '.out';
|
|
94
|
-
|
|
108
|
+
outFile = `${base}${ext}`;
|
|
95
109
|
}
|
|
96
110
|
|
|
97
|
-
const
|
|
98
|
-
const
|
|
111
|
+
const fd = fs.openSync(outFile, 'w');
|
|
112
|
+
const total = manifest.chunks.reduce((sum, e) => sum + e.count, 0);
|
|
99
113
|
let completed = 0;
|
|
100
114
|
|
|
101
115
|
const concurrency = Math.max(1, Math.min(8, os.cpus().length));
|
|
102
116
|
for (let i = 0; i < manifest.chunks.length; i += concurrency) {
|
|
103
117
|
const batch = manifest.chunks.slice(i, i + concurrency);
|
|
104
118
|
await Promise.all(batch.map(async (entry) => {
|
|
105
|
-
const
|
|
119
|
+
const blockBytes = await helia.blockstore.get(entry.cid);
|
|
106
120
|
for (let j = 0; j < entry.count; j++) {
|
|
107
|
-
fs.writeSync(
|
|
121
|
+
fs.writeSync(fd, blockBytes.slice(0, entry.length));
|
|
108
122
|
completed++;
|
|
109
|
-
renderProgress(completed,
|
|
123
|
+
renderProgress(completed, total, 'Unpacking');
|
|
110
124
|
}
|
|
111
125
|
}));
|
|
112
126
|
}
|
|
113
|
-
|
|
114
127
|
process.stderr.write('\n');
|
|
115
|
-
|
|
116
|
-
fs.
|
|
117
|
-
|
|
128
|
+
|
|
129
|
+
fs.ftruncateSync(fd, manifest.original_size);
|
|
130
|
+
fs.closeSync(fd);
|
|
131
|
+
console.log(`Unpacked -> ${outFile}`);
|
|
118
132
|
}
|
|
119
133
|
|
|
120
|
-
//
|
|
134
|
+
// Verify blocks exist in Helia
|
|
121
135
|
async function verify(manifestPath) {
|
|
122
|
-
await
|
|
123
|
-
|
|
136
|
+
await ensureHelia();
|
|
124
137
|
const manifest = JSON.parse(fs.readFileSync(manifestPath));
|
|
125
|
-
let
|
|
126
|
-
|
|
138
|
+
let checked = 0;
|
|
127
139
|
for (const { cid } of manifest.chunks) {
|
|
128
140
|
try {
|
|
129
|
-
await
|
|
130
|
-
|
|
131
|
-
renderProgress(
|
|
141
|
+
await helia.blockstore.get(cid);
|
|
142
|
+
checked++;
|
|
143
|
+
renderProgress(checked, manifest.chunks.length, 'Verifying');
|
|
132
144
|
} catch {
|
|
133
145
|
console.error(`Missing block ${cid}`);
|
|
134
146
|
process.exit(1);
|
|
@@ -138,15 +150,13 @@ async function verify(manifestPath) {
|
|
|
138
150
|
console.log('All blocks verified');
|
|
139
151
|
}
|
|
140
152
|
|
|
141
|
-
//
|
|
153
|
+
// Pin via Helia’s pin API
|
|
142
154
|
async function pin(manifestPath) {
|
|
143
|
-
await
|
|
144
|
-
|
|
155
|
+
await ensureHelia();
|
|
145
156
|
const manifest = JSON.parse(fs.readFileSync(manifestPath));
|
|
146
157
|
let pinned = 0;
|
|
147
|
-
|
|
148
158
|
for (const { cid } of manifest.chunks) {
|
|
149
|
-
await
|
|
159
|
+
await helia.pin.add(cid);
|
|
150
160
|
pinned++;
|
|
151
161
|
renderProgress(pinned, manifest.chunks.length, 'Pinning');
|
|
152
162
|
}
|
|
@@ -154,36 +164,28 @@ async function pin(manifestPath) {
|
|
|
154
164
|
console.log('All blocks pinned');
|
|
155
165
|
}
|
|
156
166
|
|
|
157
|
-
//
|
|
167
|
+
// CLI definition
|
|
158
168
|
program.version('1.0.0');
|
|
159
169
|
|
|
160
170
|
program
|
|
161
171
|
.command('pack <file>')
|
|
162
172
|
.option('-o, --output <file>', 'Output manifest')
|
|
163
173
|
.option('--chunk-size <bytes>', 'Chunk size', '4096')
|
|
164
|
-
.option('--pin', 'Pin blocks after
|
|
165
|
-
.action(
|
|
166
|
-
await pack(file, options.output, parseInt(options.chunkSize), options.pin);
|
|
167
|
-
});
|
|
174
|
+
.option('--pin', 'Pin blocks after pack', false)
|
|
175
|
+
.action((file, opts) => pack(file, opts.output, parseInt(opts['chunkSize']), opts.pin));
|
|
168
176
|
|
|
169
177
|
program
|
|
170
178
|
.command('unpack <manifest>')
|
|
171
179
|
.option('-o, --output <file>', 'Output file')
|
|
172
|
-
.action(
|
|
173
|
-
await unpack(manifest, options.output);
|
|
174
|
-
});
|
|
180
|
+
.action((m, opts) => unpack(m, opts.output));
|
|
175
181
|
|
|
176
182
|
program
|
|
177
183
|
.command('verify <manifest>')
|
|
178
|
-
.action(
|
|
179
|
-
await verify(manifest);
|
|
180
|
-
});
|
|
184
|
+
.action((m) => verify(m));
|
|
181
185
|
|
|
182
186
|
program
|
|
183
187
|
.command('pin <manifest>')
|
|
184
|
-
.action(
|
|
185
|
-
await pin(manifest);
|
|
186
|
-
});
|
|
188
|
+
.action((m) => pin(m));
|
|
187
189
|
|
|
188
190
|
program.parse(process.argv);
|
|
189
191
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cbyte",
|
|
3
|
-
"
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.6.0",
|
|
4
5
|
"description": "Content-addressed deduplicated storage CLI using IPFS with automatic unpack extension retention",
|
|
5
6
|
"main": "cbyte.js",
|
|
6
7
|
"bin": {
|
|
@@ -16,13 +17,15 @@
|
|
|
16
17
|
"storage",
|
|
17
18
|
"content-addressed"
|
|
18
19
|
],
|
|
19
|
-
"author": "
|
|
20
|
+
"author": "Coperbyte <coperbyte@coperbyte.com>",
|
|
20
21
|
"license": "MIT",
|
|
21
22
|
"dependencies": {
|
|
23
|
+
"@helia/unixfs": "^7.0.1",
|
|
22
24
|
"commander": "^11.0.0",
|
|
23
25
|
"form-data": "^4.0.0",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
+
"helia": "^6.0.16",
|
|
27
|
+
"multiformats": "^13.4.2",
|
|
28
|
+
"undici": "^6.0.0"
|
|
26
29
|
},
|
|
27
30
|
"engines": {
|
|
28
31
|
"node": ">=20"
|