cbyte 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.
- package/README.md +208 -0
- package/cbyte.js +230 -0
- package/package.json +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# cbyte
|
|
2
|
+
|
|
3
|
+
**cbyte** is a Node.js CLI utility for content-addressed, deduplicated storage using IPFS. It allows you to pack files into manifests (`.cbyte`), reconstruct them, verify integrity, and manage IPFS pinning.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
* Deduplicated storage using SHA-256 hashes
|
|
10
|
+
* Fixed-size chunking for large files
|
|
11
|
+
* IPFS raw block backend (no UnixFS overhead)
|
|
12
|
+
* Deterministic `.cbyte` manifests
|
|
13
|
+
* Parallel unpacking with safe ordered writes
|
|
14
|
+
* ASCII progress bar during unpack
|
|
15
|
+
* CLI commands: `pack`, `unpack`, `verify`, `pin`
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
* Node.js >= 20
|
|
22
|
+
* npm
|
|
23
|
+
* IPFS (Kubo) >= 0.21
|
|
24
|
+
* Linux environment (POSIX-compliant filesystem)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
### Using npm
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g cbyte
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Manual (from GitHub or tarball)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/yourusername/cbyte.git
|
|
40
|
+
cd cbyte
|
|
41
|
+
npm install
|
|
42
|
+
chmod +x cbyte.js
|
|
43
|
+
sudo ln -sf $(pwd)/cbyte.js /usr/local/bin/cbyte
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Ensure IPFS daemon is running:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
ipfs daemon &
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## CLI Usage
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
cbyte <command> [options]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Commands
|
|
61
|
+
|
|
62
|
+
| Command | Description |
|
|
63
|
+
| ----------------- | ------------------------------------------- |
|
|
64
|
+
| pack <file> | Pack a file into a `.cbyte` manifest |
|
|
65
|
+
| unpack <manifest> | Reconstruct the original file from manifest |
|
|
66
|
+
| verify <manifest> | Verify availability & integrity of blocks |
|
|
67
|
+
| pin <manifest> | Pin all blocks referenced by manifest |
|
|
68
|
+
|
|
69
|
+
### Options
|
|
70
|
+
|
|
71
|
+
* `-o, --output <file>`: specify output file (manifest or restored file)
|
|
72
|
+
* `--chunk-size <bytes>`: size of chunks in bytes (default 4096)
|
|
73
|
+
* `--pin`: automatically pin blocks during pack
|
|
74
|
+
* `-h, --help`: show help for command
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Examples
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Pack a file
|
|
82
|
+
cbyte pack bigfile.bin -o bigfile.cbyte --chunk-size 4096 --pin
|
|
83
|
+
|
|
84
|
+
# Unpack with ASCII progress
|
|
85
|
+
cbyte unpack bigfile.cbyte -o bigfile_restored.bin
|
|
86
|
+
|
|
87
|
+
# Verify blocks
|
|
88
|
+
cbyte verify bigfile.cbyte
|
|
89
|
+
|
|
90
|
+
# Pin all blocks
|
|
91
|
+
cbyte pin bigfile.cbyte
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
ASCII progress bar example during unpack:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
[====================== ] 62.5% (500/800)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Manifest Format
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"format": "cbyte",
|
|
107
|
+
"version": 1,
|
|
108
|
+
"chunk_size": 4096,
|
|
109
|
+
"original_size": 5242880,
|
|
110
|
+
"chunks": [
|
|
111
|
+
{ "cid": "bafkreigh2akiscaildcg...", "length": 4096, "count": 2 }
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
* `cid`: IPFS content identifier
|
|
117
|
+
* `length`: bytes in the chunk
|
|
118
|
+
* `count`: repeated occurrences
|
|
119
|
+
* `chunk_size`: chunk size used during packing
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Testing
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Small file
|
|
127
|
+
echo "test data" > testfile.txt
|
|
128
|
+
cbyte pack testfile.txt -o testfile.cbyte --chunk-size 32 --pin
|
|
129
|
+
cbyte unpack testfile.cbyte -o restored.txt
|
|
130
|
+
diff testfile.txt restored.txt
|
|
131
|
+
|
|
132
|
+
# Large file
|
|
133
|
+
dd if=/dev/urandom of=bigfile.bin bs=1M count=5
|
|
134
|
+
cbyte pack bigfile.bin -o bigfile.cbyte --chunk-size 4096 --pin
|
|
135
|
+
cbyte unpack bigfile.cbyte -o bigfile_restored.bin
|
|
136
|
+
diff bigfile.bin bigfile_restored.bin
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Safety Notes
|
|
142
|
+
|
|
143
|
+
* Fetching or unpacking fails if any block is missing
|
|
144
|
+
* Parallel fetching does not reorder writes; output is deterministic
|
|
145
|
+
* CID ensures cryptographic integrity
|
|
146
|
+
* Use `--pin` to persist blocks on your IPFS node
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Repository and Issues
|
|
157
|
+
|
|
158
|
+
* GitHub: [https://github.com/yourusername/cbyte](https://github.com/yourusername/cbyte)
|
|
159
|
+
* Issues: [https://github.com/yourusername/cbyte/issues](https://github.com/yourusername/cbyte/issues)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
**cbyte** is a Node.js CLI utility for content-addressed, deduplicated storage using IPFS. It allows you to pack files into manifests (`.cbyte`), reconstruct them, verify integrity, and manage IPFS pinning.
|
|
163
|
+
|
|
164
|
+
## Features
|
|
165
|
+
- Deduplicated storage using SHA-256 hashes
|
|
166
|
+
- Fixed-size chunking for large files
|
|
167
|
+
- IPFS raw block backend (no UnixFS overhead)
|
|
168
|
+
- Deterministic `.cbyte` manifests
|
|
169
|
+
- Parallel unpacking with safe ordered writes
|
|
170
|
+
- ASCII progress bar during unpack
|
|
171
|
+
- CLI commands: `pack`, `unpack`, `verify`, `pin`
|
|
172
|
+
|
|
173
|
+
## Installation
|
|
174
|
+
|
|
175
|
+
### Using npm
|
|
176
|
+
```
|
|
177
|
+
npm install -g cbyte
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Manual
|
|
181
|
+
```
|
|
182
|
+
git clone https://github.com/yourusername/cbyte.git
|
|
183
|
+
cd cbyte
|
|
184
|
+
npm install
|
|
185
|
+
chmod +x cbyte.js
|
|
186
|
+
sudo ln -sf $(pwd)/cbyte.js /usr/local/bin/cbyte
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Ensure IPFS daemon is running:
|
|
190
|
+
```
|
|
191
|
+
ipfs daemon &
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## CLI Usage
|
|
195
|
+
```
|
|
196
|
+
cbyte <command> [options]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Commands: `pack`, `unpack`, `verify`, `pin`
|
|
200
|
+
|
|
201
|
+
## Examples
|
|
202
|
+
```
|
|
203
|
+
cbyte pack bigfile.bin -o bigfile.cbyte --chunk-size 4096 --pin
|
|
204
|
+
cbyte unpack bigfile.cbyte -o bigfile_restored.bin
|
|
205
|
+
cbyte verify bigfile.cbyte
|
|
206
|
+
cbyte pin bigfile.cbyte
|
|
207
|
+
```
|
|
208
|
+
|
package/cbyte.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Prevent MaxListenersExceededWarning
|
|
4
|
+
require('events').defaultMaxListeners = 6000;
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const { program } = require('commander');
|
|
9
|
+
const { request } = require('undici');
|
|
10
|
+
const FormData = require('form-data');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { spawn } = require('child_process');
|
|
14
|
+
const net = require('net');
|
|
15
|
+
|
|
16
|
+
const IPFS_API = process.env.IPFS_API || 'http://127.0.0.1:5001/api/v0';
|
|
17
|
+
|
|
18
|
+
// ------------------- IPFS Daemon Helpers -------------------
|
|
19
|
+
function isPortOpen(host, port) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const socket = new net.Socket();
|
|
22
|
+
socket.setTimeout(1000);
|
|
23
|
+
socket.once('connect', () => { socket.destroy(); resolve(true); });
|
|
24
|
+
socket.once('timeout', () => { socket.destroy(); resolve(false); });
|
|
25
|
+
socket.once('error', () => { resolve(false); });
|
|
26
|
+
socket.connect(port, host);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function ensureIpfsDaemon() {
|
|
31
|
+
const host = '127.0.0.1';
|
|
32
|
+
const port = 5001;
|
|
33
|
+
|
|
34
|
+
const running = await isPortOpen(host, port);
|
|
35
|
+
if (running) return;
|
|
36
|
+
|
|
37
|
+
console.log('IPFS daemon not running. Starting it now...');
|
|
38
|
+
const daemon = spawn('ipfs', ['daemon'], { stdio: 'ignore', detached: true });
|
|
39
|
+
daemon.unref();
|
|
40
|
+
|
|
41
|
+
let attempts = 0;
|
|
42
|
+
while (!(await isPortOpen(host, port))) {
|
|
43
|
+
if (attempts++ > 20) throw new Error('Failed to start IPFS daemon.');
|
|
44
|
+
await new Promise(r => setTimeout(r, 500));
|
|
45
|
+
}
|
|
46
|
+
console.log('IPFS daemon started.');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ------------------- Utility Functions -------------------
|
|
50
|
+
function sha256(data) {
|
|
51
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderProgress(completed, total, label = '') {
|
|
55
|
+
const width = 40;
|
|
56
|
+
const ratio = completed / total;
|
|
57
|
+
const filled = Math.floor(ratio * width);
|
|
58
|
+
const bar = '='.repeat(filled) + ' '.repeat(width - filled);
|
|
59
|
+
process.stderr.write(`\r${label} [${bar}] ${(ratio*100).toFixed(1)}% (${completed}/${total})`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ------------------- IPFS Block Operations -------------------
|
|
63
|
+
async function ipfsBlockPut(data) {
|
|
64
|
+
const form = new FormData();
|
|
65
|
+
form.append('file', data, { filename: 'chunk' });
|
|
66
|
+
|
|
67
|
+
const res = await request(`${IPFS_API}/block/put?format=raw&mhtype=sha2-256`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
body: form,
|
|
70
|
+
headers: form.getHeaders()
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const body = await res.body.text();
|
|
74
|
+
const json = JSON.parse(body);
|
|
75
|
+
return json.Key;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function ipfsBlockGet(cid) {
|
|
79
|
+
const res = await request(`${IPFS_API}/block/get?arg=${cid}`);
|
|
80
|
+
const buffer = await res.body.arrayBuffer();
|
|
81
|
+
return Buffer.from(buffer);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ------------------- Pack Function -------------------
|
|
85
|
+
async function pack(filePath, output, chunkSize = 4096, pin = false) {
|
|
86
|
+
if (!output) {
|
|
87
|
+
const base = path.basename(filePath, path.extname(filePath));
|
|
88
|
+
output = `${base}.cbyte`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = fs.readFileSync(filePath);
|
|
92
|
+
const chunksMap = {};
|
|
93
|
+
for (let offset = 0; offset < data.length; offset += chunkSize) {
|
|
94
|
+
const chunk = data.slice(offset, offset + chunkSize);
|
|
95
|
+
const hash = sha256(chunk);
|
|
96
|
+
if (!chunksMap[hash]) chunksMap[hash] = { chunk, count: 0 };
|
|
97
|
+
chunksMap[hash].count++;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const manifest = {
|
|
101
|
+
format: 'cbyte',
|
|
102
|
+
version: 1,
|
|
103
|
+
chunk_size: chunkSize,
|
|
104
|
+
original_size: data.length,
|
|
105
|
+
original_ext: path.extname(filePath), // store original file extension
|
|
106
|
+
chunks: []
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const hashes = Object.keys(chunksMap);
|
|
110
|
+
const total = hashes.length;
|
|
111
|
+
let uploaded = 0;
|
|
112
|
+
|
|
113
|
+
const concurrency = Math.max(1, Math.min(8, os.cpus().length));
|
|
114
|
+
for (let i = 0; i < hashes.length; i += concurrency) {
|
|
115
|
+
const batch = hashes.slice(i, i + concurrency);
|
|
116
|
+
await Promise.all(batch.map(async (hash) => {
|
|
117
|
+
const info = chunksMap[hash];
|
|
118
|
+
const cid = await ipfsBlockPut(info.chunk);
|
|
119
|
+
if (pin) await request(`${IPFS_API}/pin/add?arg=${cid}`);
|
|
120
|
+
manifest.chunks.push({ cid, length: info.chunk.length, count: info.count });
|
|
121
|
+
uploaded++;
|
|
122
|
+
renderProgress(uploaded, total, 'Uploading chunks');
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
process.stderr.write('\n');
|
|
126
|
+
|
|
127
|
+
fs.writeFileSync(output, JSON.stringify(manifest, null, 2));
|
|
128
|
+
console.log(`Packed ${filePath} -> ${output}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ------------------- Unpack Function -------------------
|
|
132
|
+
async function unpack(manifestPath, output) {
|
|
133
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath));
|
|
134
|
+
if (!output) {
|
|
135
|
+
const base = path.basename(manifestPath, path.extname(manifestPath));
|
|
136
|
+
const ext = manifest.original_ext || '.out';
|
|
137
|
+
output = `${base}${ext}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const outFd = fs.openSync(output, 'w');
|
|
141
|
+
const totalChunks = manifest.chunks.reduce((a, c) => a + c.count, 0);
|
|
142
|
+
let completed = 0;
|
|
143
|
+
|
|
144
|
+
const concurrency = Math.max(1, Math.min(8, os.cpus().length));
|
|
145
|
+
for (let i = 0; i < manifest.chunks.length; i += concurrency) {
|
|
146
|
+
const batch = manifest.chunks.slice(i, i + concurrency);
|
|
147
|
+
await Promise.all(batch.map(async (entry) => {
|
|
148
|
+
const data = await ipfsBlockGet(entry.cid);
|
|
149
|
+
for (let j = 0; j < entry.count; j++) {
|
|
150
|
+
fs.writeSync(outFd, data.slice(0, entry.length));
|
|
151
|
+
completed++;
|
|
152
|
+
renderProgress(completed, totalChunks, 'Unpacking');
|
|
153
|
+
}
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
process.stderr.write('\n');
|
|
158
|
+
fs.ftruncateSync(outFd, manifest.original_size);
|
|
159
|
+
fs.closeSync(outFd);
|
|
160
|
+
console.log(`Unpacked -> ${output}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ------------------- Verify Function -------------------
|
|
164
|
+
async function verify(manifestPath) {
|
|
165
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath));
|
|
166
|
+
let verified = 0;
|
|
167
|
+
for (const { cid } of manifest.chunks) {
|
|
168
|
+
try {
|
|
169
|
+
await ipfsBlockGet(cid);
|
|
170
|
+
verified++;
|
|
171
|
+
renderProgress(verified, manifest.chunks.length, 'Verifying');
|
|
172
|
+
} catch (e) {
|
|
173
|
+
console.error(`Missing block ${cid}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
process.stderr.write('\n');
|
|
178
|
+
console.log('All blocks verified');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ------------------- Pin Function -------------------
|
|
182
|
+
async function pin(manifestPath) {
|
|
183
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath));
|
|
184
|
+
let pinned = 0;
|
|
185
|
+
for (const { cid } of manifest.chunks) {
|
|
186
|
+
await request(`${IPFS_API}/pin/add?arg=${cid}`);
|
|
187
|
+
pinned++;
|
|
188
|
+
renderProgress(pinned, manifest.chunks.length, 'Pinning');
|
|
189
|
+
}
|
|
190
|
+
process.stderr.write('\n');
|
|
191
|
+
console.log('All blocks pinned');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ------------------- CLI -------------------
|
|
195
|
+
program.version('1.0.0');
|
|
196
|
+
|
|
197
|
+
program
|
|
198
|
+
.command('pack <file>')
|
|
199
|
+
.option('-o, --output <file>', 'Output manifest')
|
|
200
|
+
.option('--chunk-size <bytes>', 'Chunk size', '4096')
|
|
201
|
+
.option('--pin', 'Pin blocks after upload', false)
|
|
202
|
+
.action(async (file, options) => {
|
|
203
|
+
await ensureIpfsDaemon();
|
|
204
|
+
await pack(file, options.output, parseInt(options['chunkSize']), options.pin);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
program
|
|
208
|
+
.command('unpack <manifest>')
|
|
209
|
+
.option('-o, --output <file>', 'Output file')
|
|
210
|
+
.action(async (manifest, options) => {
|
|
211
|
+
await ensureIpfsDaemon();
|
|
212
|
+
await unpack(manifest, options.output);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
program
|
|
216
|
+
.command('verify <manifest>')
|
|
217
|
+
.action(async (manifest) => {
|
|
218
|
+
await ensureIpfsDaemon();
|
|
219
|
+
await verify(manifest);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
program
|
|
223
|
+
.command('pin <manifest>')
|
|
224
|
+
.action(async (manifest) => {
|
|
225
|
+
await ensureIpfsDaemon();
|
|
226
|
+
await pin(manifest);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
program.parse(process.argv);
|
|
230
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cbyte",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Content-addressed deduplicated storage CLI using IPFS with automatic unpack extension retention",
|
|
5
|
+
"main": "cbyte.js",
|
|
6
|
+
"bin": { "cbyte": "./cbyte.js" },
|
|
7
|
+
"scripts": { "test": "node test.js" },
|
|
8
|
+
"keywords": ["ipfs","cli","deduplication","storage","content-addressed"],
|
|
9
|
+
"author": "Your Name <you@example.com>",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"commander": "^11.0.0",
|
|
13
|
+
"undici": "^6.0.0",
|
|
14
|
+
"form-data": "^4.0.0"
|
|
15
|
+
},
|
|
16
|
+
"engines": { "node": ">=20" }
|
|
17
|
+
}
|
|
18
|
+
|