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.
Files changed (3) hide show
  1. package/README.md +208 -0
  2. package/cbyte.js +230 -0
  3. 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
+