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