@ethernity-cloud/sdk-js 1.1.0 → 1.2.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.
@@ -1,360 +1,349 @@
1
- // ipfsClient.js
2
- import { create, globSource } from 'kubo-rpc-client';
1
+ // ipfs.mjs
2
+ //
3
+ // IPFS client for the Ethernity Cloud JS SDK.
4
+ //
5
+ // The UPLOAD path is a faithful port of the Python SDK's IPFSClient
6
+ // (ethernity_cloud_sdk_py/commands/pynithy/ipfs_client.py): it posts directly
7
+ // to the IPFS HTTP API `/api/v0/add` with a raw multipart body,
8
+ // `wrap-with-directory=true`, `Expect: 100-continue`, an explicit
9
+ // `Content-Length`, and parses the newline-delimited JSON response to find the
10
+ // wrapping-directory root hash (the entry whose `Name` is empty). A 10-attempt
11
+ // exponential-backoff retry wraps both file and directory uploads. This matches
12
+ // the Python pipeline step-for-step so both SDKs upload identically (e.g. for
13
+ // the certex / public-key extraction flow).
14
+ //
15
+ // The DOWNLOAD path keeps the existing kubo-rpc-client based helpers.
16
+ import http from 'http';
17
+ import https from 'https';
18
+ import { URL } from 'url';
3
19
  import { promisify } from 'util';
4
20
  import fs from 'fs';
5
21
  import { Command } from 'commander';
6
22
  import { promises as fss } from 'fs';
7
23
  import path from 'path';
8
- import { MultiBar, Presets } from 'cli-progress';
24
+ import { create } from 'kubo-rpc-client';
9
25
 
10
26
  const program = new Command();
11
27
  const writeFile = promisify(fs.writeFile);
12
28
  const delay = (ms) => new Promise((res) => setTimeout(res, ms));
13
29
  const getRetryDelay = (retryCount, baseDelay = 1) => baseDelay * 2 ** retryCount;
14
30
 
31
+ const RETRY_COUNT = 10; // mirrors Python ipfs_client.RETRY_COUNT
32
+
33
+ // Endpoint + auth, mirroring the Python IPFSClient constructor.
34
+ // apiUrl : e.g. "https://ipfs.ethernity.cloud"
35
+ // addUrl : `${apiUrl}/api/v0/add`
36
+ let apiUrl = null;
37
+ let authHeader = null; // Authorization header value, or null
38
+
39
+ // kubo-rpc-client instance, used only for the download helpers below.
15
40
  let ipfs = null;
16
41
 
17
42
  const initialize = (host, protocol, port, token) => {
18
- if (host.search('http') !== -1) {
19
- ipfs = create(host); //{ timeout: '4m' }
20
- } else if (token === '') {
21
- ipfs = create({
22
- host,
23
- protocol,
24
- port,
25
- // timeout: '4m'
26
- });
43
+ // Normalise the endpoint to a base URL string, matching how the Python client
44
+ // treats `ipfs_endpoint` (it just prepends it to `/api/v0/add`).
45
+ if (host && host.search('http') !== -1) {
46
+ apiUrl = host.replace(/\/+$/, '');
47
+ ipfs = create(host);
27
48
  } else {
28
- ipfs = create({
29
- host,
30
- protocol,
31
- port,
32
- headers: { authorization: token },
33
- // timeout: '4m'
34
- });
49
+ const proto = protocol || 'http';
50
+ const p = port ? `:${port}` : '';
51
+ apiUrl = `${proto}://${host}${p}`.replace(/\/+$/, '');
52
+ const opts = { host, protocol: proto, port };
53
+ if (token) opts.headers = { authorization: token };
54
+ ipfs = create(opts);
35
55
  }
56
+ authHeader = token && token !== '' ? token : null;
36
57
  };
37
58
 
38
- const uploadFileToIPFS = async (filePath) => {
39
- try {
40
- const fileContent = await fss.readFile(filePath);
41
- const response = await ipfs.add({ path: filePath, content: fileContent });
42
- await fss.writeFile(`./IPFS_DOCKER_COMPOSE_HASH.ipfs`, response.cid.toString());
43
- return response.cid.toString();
44
- } catch (e) {
45
- console.error();
46
- return "Failed to upload file to IPFS, please try again.";
47
- }
59
+ // ---------------------------------------------------------------------------
60
+ // Raw multipart upload (Python-parity)
61
+ // ---------------------------------------------------------------------------
62
+
63
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
64
+ const CHECK = '\x1b[92m✔\x1b[0m ';
65
+ const FAIL = '\x1b[91m✘\x1b[0m ';
66
+
67
+ // Recursively collect every file under a directory (absolute paths), like
68
+ // Python's os.walk in upload_dir.
69
+ const gatherFiles = (dirPath) => {
70
+ const out = [];
71
+ const walk = (dir) => {
72
+ for (const name of fs.readdirSync(dir)) {
73
+ const full = path.join(dir, name);
74
+ if (fs.statSync(full).isDirectory()) walk(full);
75
+ else out.push(full);
76
+ }
77
+ };
78
+ walk(dirPath);
79
+ return out;
48
80
  };
49
81
 
50
- const uploadFolderToIPFS2 = async (path) => {
51
- try {
52
- console.log(`Uploading folder to IPFS: ${path}`);
53
- const response = await ipfs.add(path, { pin: true , recursive: true }).catch((e) => console.log(e));
54
- // console.log(JSON.stringify(response, null, 2));
55
- const { cid } = response;
56
- // while (true) {
57
- // const { cid } = response;
58
- // if (response.path) {
59
- // break;
60
- // }
61
- // // console.log(`Added file: ${cid}`);
62
- // delay(50000);
63
- // }
64
- // console.log(`response: ${response}`);
65
- return cid;
66
- } catch (e) {
67
- console.log(e);
68
- return null;
82
+ // Build a multipart/form-data body identical in shape to requests_toolbelt's
83
+ // MultipartEncoder: one part per file, the field name AND filename both set to
84
+ // the POSIX relative path, Content-Type application/octet-stream. Returns the
85
+ // full body Buffer + the boundary + total byte length.
86
+ const buildMultipartBody = (files, dirPath) => {
87
+ const boundary =
88
+ '----EthernityCloudFormBoundary' +
89
+ Buffer.from(`${files.length}-${dirPath}`).toString('hex').slice(0, 24);
90
+ const parts = [];
91
+ for (const filepath of files) {
92
+ const rel = path.relative(dirPath, filepath).replace(/\\/g, '/');
93
+ const header =
94
+ `--${boundary}\r\n` +
95
+ `Content-Disposition: form-data; name="${rel}"; filename="${rel}"\r\n` +
96
+ `Content-Type: application/octet-stream\r\n\r\n`;
97
+ parts.push(Buffer.from(header, 'utf8'));
98
+ parts.push(fs.readFileSync(filepath));
99
+ parts.push(Buffer.from('\r\n', 'utf8'));
69
100
  }
101
+ parts.push(Buffer.from(`--${boundary}--\r\n`, 'utf8'));
102
+ const body = Buffer.concat(parts);
103
+ return { body, boundary, length: body.length };
70
104
  };
71
105
 
72
- const MAX_BATCH_SIZE = 20 * 1024 * 1024; // 100MB
73
-
74
- const uploadFolderToIPFSBatch = async (folderPath) => {
75
- try {
76
- const files = [];
77
- const readDirectory = (dir) => {
78
- fs.readdirSync(dir).forEach((file) => {
79
- const fullPath = path.join(dir, file);
80
- if (fs.statSync(fullPath).isDirectory()) {
81
- readDirectory(fullPath);
82
- } else {
83
- files.push({
84
- path: path.relative(folderPath, fullPath),
85
- content: fs.readFileSync(fullPath),
86
- size: fs.statSync(fullPath).size
87
- });
88
- }
89
- });
106
+ // Low-level POST to the IPFS add endpoint using Node's http/https so we can set
107
+ // Content-Length + Expect: 100-continue exactly like the Python client and read
108
+ // the streamed NDJSON response. Resolves { statusCode, text }.
109
+ const postAdd = (query, body, boundary, contentLength, onProgress) =>
110
+ new Promise((resolve, reject) => {
111
+ const url = new URL(`${apiUrl}/api/v0/add${query}`);
112
+ const mod = url.protocol === 'https:' ? https : http;
113
+ const headers = {
114
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
115
+ 'Content-Length': String(contentLength),
116
+ Expect: '100-continue'
90
117
  };
91
-
92
- readDirectory(folderPath);
93
- console.log(`Uploading folder to IPFS: ${folderPath}`);
94
- console.log(`Number of files: ${files.length}`);
95
-
96
- const ipfsOptions = {
97
- wrapWithDirectory: false,
98
- pin: true,
99
- progress: (prog) => console.log(`Added ${prog / 1024 / 1024} MB`),
100
- timeout: '2m'
118
+ if (authHeader) headers.Authorization = authHeader;
119
+
120
+ const req = mod.request(
121
+ {
122
+ method: 'POST',
123
+ hostname: url.hostname,
124
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
125
+ path: `${url.pathname}${url.search}`,
126
+ headers
127
+ },
128
+ (res) => {
129
+ let text = '';
130
+ res.setEncoding('utf8');
131
+ res.on('data', (chunk) => {
132
+ text += chunk;
133
+ });
134
+ res.on('end', () => resolve({ statusCode: res.statusCode, text }));
135
+ }
136
+ );
137
+
138
+ req.on('error', reject);
139
+
140
+ // Honour Expect: 100-continue -- only stream the body once the server
141
+ // approves (mirrors requests' behaviour with Expect: 100-continue).
142
+ let sent = 0;
143
+ const sendBody = () => {
144
+ // Write in MB-ish chunks so we can report progress like Python.
145
+ const CHUNK = 1024 * 1024;
146
+ let offset = 0;
147
+ const writeNext = () => {
148
+ if (offset >= body.length) {
149
+ req.end();
150
+ return;
151
+ }
152
+ const end = Math.min(offset + CHUNK, body.length);
153
+ const ok = req.write(body.subarray(offset, end));
154
+ sent += end - offset;
155
+ offset = end;
156
+ if (onProgress) onProgress(sent);
157
+ if (ok) setImmediate(writeNext);
158
+ else req.once('drain', writeNext);
159
+ };
160
+ writeNext();
101
161
  };
102
162
 
103
- const allCids = [];
104
- let currentBatch = [];
105
- let currentBatchSize = 0;
106
-
107
- for (const file of files) {
108
- if (file.size > MAX_BATCH_SIZE) {
109
- // Upload large files individually
110
- console.log(`Uploading large file individually: ${file.path}, size: ${file.size / 1024 / 1024} MB`);
111
- for await (const result of ipfs.addAll([file], ipfsOptions)) {
112
- allCids.push(result.cid);
113
- }
114
- } else {
115
- // Add file to current batch
116
- if (currentBatchSize + file.size > MAX_BATCH_SIZE) {
117
- // await delay(30000);
118
- // Upload current batch, display in mb
119
- console.log(`Uploading batch of size ${currentBatchSize / 1024 / 1024} MB`);
120
- for await (const result of ipfs.addAll(currentBatch, ipfsOptions)) {
121
- allCids.push(result.cid);
122
- }
123
- // Reset batch
124
- currentBatch = [];
125
- currentBatchSize = 0;
126
- }
127
- currentBatch.push(file);
128
- currentBatchSize += file.size;
163
+ let bodySent = false;
164
+ req.on('continue', () => {
165
+ if (bodySent) return;
166
+ bodySent = true;
167
+ sendBody();
168
+ });
169
+ // Fallback: if the server never sends 100-continue within a short grace
170
+ // period, send the body anyway (some gateways skip it).
171
+ setTimeout(() => {
172
+ if (bodySent) return;
173
+ bodySent = true;
174
+ sendBody();
175
+ }, 3000);
176
+ });
177
+
178
+ // Parse the NDJSON add response and return the wrapping-directory root hash --
179
+ // the entry whose "Name" is "" (matches Python upload_dir).
180
+ const parseRootHash = (text) => {
181
+ const rootLineHash = () => {
182
+ let root = null;
183
+ let last = null;
184
+ for (const line of text.split('\n')) {
185
+ const t = line.trim();
186
+ if (!t) continue;
187
+ let obj;
188
+ try {
189
+ obj = JSON.parse(t);
190
+ } catch {
191
+ continue;
129
192
  }
193
+ last = obj;
194
+ if (obj.Name === '') root = obj.Hash;
130
195
  }
196
+ // Some gateways don't emit an empty-Name entry; fall back to the last hash.
197
+ return root || (last && last.Hash) || null;
198
+ };
199
+ return rootLineHash();
200
+ };
131
201
 
132
- // Upload any remaining files in the last batch
133
- if (currentBatch.length > 0) {
134
- console.log(`Uploading final batch of size ${currentBatchSize / 1024 / 1024} MB`);
135
- for await (const result of ipfs.addAll(currentBatch, ipfsOptions)) {
136
- allCids.push(result.cid);
202
+ // Generic retry wrapper mirroring Python's retry_on_failure (10 attempts,
203
+ // 10s initial delay, x2 backoff, small jitter).
204
+ const retryOnFailure = async (fn, label) => {
205
+ let delayMs = 10000;
206
+ const backoff = 2;
207
+ for (let attempt = 0; attempt < RETRY_COUNT; attempt++) {
208
+ try {
209
+ const result = await fn();
210
+ if (result !== null && result !== false && result !== 'Upload failed.') return result;
211
+ throw new Error('empty result');
212
+ } catch (e) {
213
+ process.stdout.write(
214
+ `\n[Retry ${attempt + 1}/${RETRY_COUNT}] ${label} failed: ${e && e.message ? e.message : e}\n`
215
+ );
216
+ if (attempt < RETRY_COUNT - 1) {
217
+ await delay(delayMs + Math.random() * 100);
218
+ delayMs *= backoff;
137
219
  }
138
220
  }
139
-
140
- // Wrap all files into a single directory
141
- const directory = await ipfs.addAll(allCids.map(cid => ({ path: cid.toString(), content: ipfs.cat(cid) })), {
142
- wrapWithDirectory: true,
143
- pin: true,
144
- timeout: '2m'
145
- });
146
-
147
- console.log(`response: ${JSON.stringify(directory, null, 2)}`);
148
- return directory.cid;
149
- } catch (e) {
150
- console.error(e);
151
- return "error";
152
221
  }
222
+ console.log('Max retry attempts reached. Operation failed.');
223
+ return null;
153
224
  };
154
225
 
155
- const uploadFolderToIPFS3 = async (folderPath) => {
156
- try {
157
- const files = [];
158
-
159
- const readDirectory = (dir) => {
160
- const items = fs.readdirSync(dir);
161
- items.forEach((item) => {
162
- const fullPath = path.join(dir, item);
163
- const stats = fs.statSync(fullPath);
164
- if (stats.isDirectory()) {
165
- readDirectory(fullPath);
166
- } else {
167
- files.push({
168
- path: path.relative(folderPath, fullPath),
169
- content: fs.readFileSync(fullPath)
170
- });
171
- }
172
- });
173
- };
174
-
175
- readDirectory(folderPath);
176
- console.log(`Uploading folder to IPFS: ${folderPath}`);
177
- // console.log(`Files: ${JSON.stringify(files[0], null, 2)}`);
178
- console.log(`number of files: ${files.length}`);
179
- // const response = await ipfs.addAll(files, { wrapWithDirectory: true, pin: true, timeout: 600000, progress: (prog) => console.log(`Added ${prog} bytes`)});
180
- // const response = await ipfs.addAll(files);
181
- for await (const file of ipfs.addAll(files, { wrapWithDirectory: true, pin: true, timeout: 600000, progress: (prog) => console.log(`Added ${prog} bytes`)})) {
182
- console.log(file)
226
+ // Upload a single file: POST it to /api/v0/add and return its hash.
227
+ const uploadFileToIPFSOnce = async (filePath) => {
228
+ const files = [filePath];
229
+ const dir = path.dirname(filePath);
230
+ const { body, boundary, length } = buildMultipartBody(files, dir);
231
+ const { statusCode, text } = await postAdd('', body, boundary, length, null);
232
+ if (statusCode !== 200) {
233
+ console.log(`Failed to upload to IPFS. Status code: ${statusCode}`);
234
+ return null;
235
+ }
236
+ // For a single file the last NDJSON line carries its hash.
237
+ let last = null;
238
+ for (const line of text.split('\n')) {
239
+ const t = line.trim();
240
+ if (!t) continue;
241
+ try {
242
+ last = JSON.parse(t);
243
+ } catch {
244
+ /* ignore */
183
245
  }
184
- console.log(`response: ${JSON.stringify(response, null, 2)}`);
185
- return response.cid;
186
- } catch (e) {
187
- console.log(e);
188
- return "error";
189
246
  }
247
+ return last ? last.Hash : null;
190
248
  };
191
249
 
192
- // const uploadFolderToIPFS = async (folderPath) => {
193
- // try {
194
- // const addedFiles = [nume, continut];
195
- // const ipfsOptions = {
196
- // wrapWithDirectory: true,
197
- // pin: true,
198
- // progress: (prog) => console.log(`Added ${prog / 1024 / 1024} MB`),
199
- // timeout: '5m'
200
- // };
201
- // console.log(`Uploading folder to IPFS: ${folderPath}`);
202
- // for await (const file of ipfs.addAll(globSource(folderPath, '**/*'),{ ...ipfsOptions })) {
203
- // addedFiles.push({
204
- // cid: file.cid.toString(),
205
- // path: file.path,
206
- // size: file.size,
207
- // });
208
- // }
209
-
210
- // // for await (const file of ipfs.addAll(
211
- // // globSource(folderPath, '**/*', { hidden: true }),
212
- // // { ...ipfsOptions, fileImportConcurrency: 1 }
213
- // // )) {
214
- // // // export each added file to a json file
215
- // // await writeFile(`./files/addedFile${file.cid.toString()}.json`, JSON.stringify(file, null, 2));
216
- // // console.log(`Added file: ${file.path} with CID: ${file.cid.toString()}`);
217
- // // addedFiles.push({
218
- // // cid: file.cid.toString(),
219
- // // path: file.path,
220
- // // size: file.size,
221
- // // });
222
- // // }
223
- // // // export added files to a json file
224
- // // await writeFile('addedFiles, json', JSON.stringify(addedFiles, null, 2));
225
-
226
- // // console.log("addedFiles: ", JSON.stringify(addedFiles, null, 2));
227
-
228
- // // Return the CID of the root directory
229
- // return addedFiles.find(file => file.path === '').cid;
230
- // // return;
231
- // } catch (e) {
232
- // console.error(e);
233
- // return "err";
234
- // }
235
- // };
236
-
237
- const uploadFolderToIPFS = async (folderPath) => {
238
- try {
239
- const addedFiles = [];
240
- const ipfsOptions = {
241
- wrapWithDirectory: true,
242
- pin: true,
243
- hidden: true,
244
- timeout: '5m'
245
- };
246
-
247
- console.log(`Uploading folder to IPFS: ${folderPath}`);
250
+ const uploadFileToIPFS = async (filePath) => {
251
+ const hash = await retryOnFailure(() => uploadFileToIPFSOnce(filePath), 'upload_file');
252
+ if (hash) {
253
+ try {
254
+ await fss.writeFile(`./IPFS_DOCKER_COMPOSE_HASH.ipfs`, hash);
255
+ } catch {
256
+ /* best effort */
257
+ }
258
+ }
259
+ return hash;
260
+ };
248
261
 
249
- const multiBar = new MultiBar({
250
- clearOnComplete: false,
251
- hideCursor: true,
252
- format: 'Progress [{bar}] {percentage}% | {value}/{total} MB'
253
- }, Presets.shades_classic);
262
+ // Upload a whole directory with wrap-with-directory=true and return the root
263
+ // directory hash -- the exact behaviour of Python's upload_dir.
264
+ const uploadDirOnce = async (dirPath) => {
265
+ const abs = path.resolve(dirPath);
266
+ const files = gatherFiles(abs);
267
+ const totalSize = files.reduce((acc, f) => acc + fs.statSync(f).size, 0);
268
+ const totalMb = Math.floor(totalSize / (1024 * 1024));
269
+ const { body, boundary, length } = buildMultipartBody(files, abs);
270
+
271
+ let lastShownMb = -1;
272
+ let frame = 0;
273
+ const onProgress = (sent) => {
274
+ const mb = Math.floor(sent / (1024 * 1024));
275
+ if (mb > lastShownMb) {
276
+ frame = (frame + 1) % SPINNER_FRAMES.length;
277
+ lastShownMb = mb;
278
+ process.stdout.write(
279
+ `\r\t${SPINNER_FRAMES[frame]} Uploading and pinning enclave to IPFS... ${mb}MB/${totalMb}MB`
280
+ );
281
+ }
282
+ };
254
283
 
255
- const files = [];
256
- const readDirectory = (dir) => {
257
- fs.readdirSync(dir).forEach((file) => {
258
- const fullPath = path.join(dir, file);
259
- if (fs.statSync(fullPath).isDirectory()) {
260
- readDirectory(fullPath);
261
- } else {
262
- files.push({
263
- path: path.relative(folderPath, fullPath),
264
- size: fs.statSync(fullPath).size
265
- });
266
- }
267
- });
268
- };
284
+ const query =
285
+ '?quieter=true&stream-channels=true&wrap-with-directory=true&progress=false&timeout=5m';
286
+ const { statusCode, text } = await postAdd(query, body, boundary, length, onProgress);
269
287
 
270
- readDirectory(folderPath);
271
- const totalSize = files.reduce((acc, file) => acc + file.size, 0);
272
-
273
- const progressBar = multiBar.create(totalSize / 1024 / 1024, 0);
274
-
275
- for await (const file of ipfs.addAll(files.map(file => ({
276
- path: file.path.replace(/\\/g, '/'),
277
- content: fs.createReadStream(path.join(folderPath, file.path))
278
- })), { ...ipfsOptions })) {
279
- addedFiles.push({
280
- cid: file.cid.toString(),
281
- path: file.path,
282
- size: file.size,
283
- });
284
- progressBar.increment(file.size / 1024 / 1024);
285
- }
286
- progressBar.update(totalSize / 1024 / 1024);
287
- multiBar.stop();
288
- const _hash = addedFiles.find(file => file.path === '').cid;
289
- // Return the CID of the root directory
290
- await fss.writeFile(`./IPFS_HASH.ipfs`, _hash);
291
- return _hash;
292
- } catch (e) {
293
- return "Upload failed.";
288
+ if (statusCode !== 200) {
289
+ process.stdout.write(`\r\t${FAIL}Uploading and pinning enclave to IPFS\n`);
290
+ console.log(`Failed to upload to IPFS. Status code: ${statusCode}`);
291
+ return false;
294
292
  }
293
+ const rootHash = parseRootHash(text);
294
+ if (!rootHash) {
295
+ process.stdout.write(`\r\t${FAIL}Uploading and pinning enclave to IPFS\n`);
296
+ console.log('Failed to upload to IPFS. Could not determine root hash.');
297
+ return false;
298
+ }
299
+ process.stdout.write(`\r\t${CHECK}Uploading and pinning enclave to IPFS\n`);
300
+ return rootHash;
295
301
  };
296
302
 
297
- const getFromIPFS = async (hhash, filePath, maxRetries = process.env.REACT_APP_IPFS_RETRIES || 5) => {
298
- let res = '';
299
- let retryCount = 0;
300
- // console.log('Downloading file from IPFS...');
301
- // console.log(`Hash: ${hhash}`);
302
- // console.log(`Max Retries: ${maxRetries}`);
303
-
304
- while (retryCount < maxRetries) {
303
+ const uploadFolderToIPFS = async (folderPath) => {
304
+ const hash = await retryOnFailure(() => uploadDirOnce(folderPath), 'upload_dir');
305
+ if (hash) {
305
306
  try {
306
- for await (const file of ipfs.cat(hhash)) {
307
- res += new TextDecoder().decode(file.buffer);
308
- }
309
- await writeFile(filePath, res);
310
- return;
311
- } catch (error) {
312
- console.error(`Error: ${error.message}`);
313
- retryCount += 1;
314
-
315
- if (retryCount < maxRetries) {
316
- console.log(`Retrying... (${retryCount}/${maxRetries})`);
317
- await delay(getRetryDelay(retryCount));
318
- continue;
319
- } else {
320
- throw new Error("ECError.IPFS_DOWNLOAD_ERROR");
321
- }
307
+ await fss.writeFile(`./IPFS_HASH.ipfs`, hash);
308
+ } catch {
309
+ /* best effort */
322
310
  }
311
+ return hash;
323
312
  }
313
+ return 'Upload failed.';
324
314
  };
325
315
 
326
- const downloadFromIPFS = async (hash, folderPath, maxRetries = process.env.REACT_APP_IPFS_RETRIES || 5) => {
327
- let retryCount = 0;
328
- console.log(`Hash: ${hash}`);
329
- console.log(`Folder Path: ${folderPath}`);
330
- console.log(`Max Retries: ${maxRetries}`);
316
+ // Dispatcher mirroring Python's IPFSClient.upload(path).
317
+ const upload = async (p) => {
318
+ const stat = fs.statSync(p);
319
+ if (stat.isFile()) return uploadFileToIPFS(p);
320
+ if (stat.isDirectory()) return uploadFolderToIPFS(p);
321
+ console.log(`Path ${p} is neither a file nor a directory.`);
322
+ return null;
323
+ };
324
+
325
+ // ---------------------------------------------------------------------------
326
+ // Download helpers (kubo-rpc-client based; unchanged behaviour)
327
+ // ---------------------------------------------------------------------------
331
328
 
329
+ const getFromIPFS = async (hhash, filePath, maxRetries = process.env.REACT_APP_IPFS_RETRIES || 5) => {
330
+ let res = '';
331
+ let retryCount = 0;
332
332
  while (retryCount < maxRetries) {
333
333
  try {
334
- for await (const file of ipfs.get(hash)) {
335
- console.log(`file: ${JSON.stringify(file, null, 2)}`);
336
- const filePathToWrite = path.join(folderPath, file.path);
337
-
338
- if (file.type === 'dir') {
339
- await fs.mkdir(filePathToWrite, { recursive: true });
340
- } else {
341
- const content = new TextDecoder().decode(file.content);
342
- await fs.mkdir(path.dirname(filePathToWrite), { recursive: true });
343
- await fs.writeFile(filePathToWrite, content);
344
- }
334
+ for await (const file of ipfs.cat(hhash)) {
335
+ res += new TextDecoder().decode(file.buffer || file);
345
336
  }
346
- console.log(`Download complete. Files saved to ${folderPath}`);
337
+ await writeFile(filePath, res);
347
338
  return;
348
339
  } catch (error) {
349
340
  console.error(`Error: ${error.message}`);
350
341
  retryCount += 1;
351
-
352
342
  if (retryCount < maxRetries) {
353
- console.log(`Retrying... (${retryCount}/${maxRetries})`);
354
343
  await delay(getRetryDelay(retryCount));
355
344
  continue;
356
345
  } else {
357
- throw new Error("ECError.IPFS_DOWNLOAD_ERROR");
346
+ throw new Error('ECError.IPFS_DOWNLOAD_ERROR');
358
347
  }
359
348
  }
360
349
  }
@@ -362,190 +351,115 @@ const downloadFromIPFS = async (hash, folderPath, maxRetries = process.env.REACT
362
351
 
363
352
  const downloadFolderFromIPFS = async (cid, outputPath) => {
364
353
  try {
365
- // const ipfs = create({ url: 'http://localhost:5001' }); // Adjust the IPFS API URL if necessary
366
-
367
354
  console.log(`Downloading folder from IPFS: ${cid}`);
368
-
369
- const multiBar = new MultiBar({
370
- clearOnComplete: false,
371
- hideCursor: true,
372
- format: 'Progress [{bar}] {percentage}% | {value}/{total} MB'
373
- }, Presets.shades_classic);
374
-
375
355
  const files = [];
376
356
  for await (const file of ipfs.get(cid)) {
377
- console.log(`file: ${JSON.stringify(file, null, 2)}`);
378
357
  if (!file.content) continue;
379
-
380
358
  const filePath = path.join(outputPath, file.path);
381
359
  const dir = path.dirname(filePath);
382
-
383
- if (!fs.existsSync(dir)) {
384
- fs.mkdirSync(dir, { recursive: true });
385
- }
386
-
360
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
387
361
  const writeStream = fs.createWriteStream(filePath);
388
- const progressBar = multiBar.create(file.size / 1024 / 1024, 0);
389
-
390
362
  for await (const chunk of file.content) {
391
363
  writeStream.write(chunk);
392
- progressBar.increment(chunk.length / 1024 / 1024);
393
364
  }
394
-
395
365
  writeStream.end();
396
- progressBar.update(file.size / 1024 / 1024);
397
366
  files.push(filePath);
398
367
  }
399
-
400
- multiBar.stop();
401
368
  console.log(`Downloaded ${files.length} files to ${outputPath}`);
402
369
  } catch (e) {
403
370
  console.error(e);
404
- return "err";
371
+ return 'err';
405
372
  }
406
373
  };
407
374
 
408
- // const downloadFolderFromIPFS = async (cid, folderPath) => {
409
- // try {
410
- // console.log(`Downloading folder from IPFS: ${cid}`);
411
-
412
- // // let res = await ipfs.get(cid);
413
-
414
- // // const buffer = res[0];
415
-
416
- // const utf8Decode = new TextDecoder('utf-8');
417
- // // const string = utf8Decode.decode(buffer);
418
-
419
- // // const object = JSON.parse(string);
420
- // // console.log(`object: ${JSON.stringify(string, null, 2)}`);
421
- // for await (const file of ipfs.get(cid)) {
422
-
423
- // // console.log(`file: ${JSON.stringify(file, null, 2)}`);
424
- // console.log(`file decoded: ${JSON.stringify(utf8Decode.decode(file), null, 2)}`);
425
- // // const buffer = file[0];
426
- // // console.log(`file: ${JSON.stringify(utf8Decode.decode(buffer), null, 2)}`);
427
- // process.exit(0);
428
- // // console.log(`${JSON.stringify(new TextDecoder().decode(file), null, 2)}`)
429
- // // console.log(`file: ${JSON.stringify(file, null, 2)}`);
430
- // // console.log(`file: ${JSON.stringify(file, null, 2)}`);
431
- // // for await (const ffile of ipfs.ls(file.path)) {
432
- // // console.log(`ffile: ${JSON.stringify(ffile, null, 2)}`);
433
- // // }
434
- // // for await (const ffile of ipfs.get("QmbjQpKFbueKR6QDU2Cj4ChsyGBR2spMwKV2S3yqrjF6w7")) {
435
- // // console.log(`ffile: ${JSON.stringify(ffile, null, 2)}`);
436
- // // }
437
- // // console.log(`file cid: ${JSON.stringify(await ipfs.get("QmbjQpKFbueKR6QDU2Cj4ChsyGBR2spMwKV2S3yqrjF6w7"), null, 2)}`);
438
-
439
- // // if (!file.path) {
440
- // // // console.warn(`Skipping file with undefined path: ${JSON.stringify(file)}`);
441
- // // continue;
442
- // // }
443
-
444
- // // const filePath = path.join(folderPath, file.path);
445
- // // console.log(`Downloading file: ${filePath}`);
446
- // // if (file.type === 'dir') {
447
- // // fs.mkdirSync(filePath, { recursive: true });
448
- // // } else {
449
- // // fs.mkdirSync(path.dirname(filePath), { recursive: true });
450
- // // const content = [];
451
- // // for await (const chunk of file.content) {
452
- // // content.push(chunk);
453
- // // }
454
- // // fs.writeFileSync(filePath, Buffer.concat(content));
455
- // // }
456
- // }
457
-
458
- // console.log(`Download complete: ${folderPath}`);
459
- // } catch (e) {
460
- // console.error(e);
461
- // return "err";
462
- // }
463
- // };
464
-
465
375
  const getContentFromIPFS = async (hash, maxRetries = process.env.REACT_APP_IPFS_RETRIES || 5) => {
466
376
  let res = '';
467
377
  let retryCount = 0;
468
-
469
378
  while (retryCount < maxRetries) {
470
379
  try {
471
380
  for await (const file of ipfs.cat(hash)) {
472
- res += new TextDecoder().decode(file.buffer);
381
+ res += new TextDecoder().decode(file.buffer || file);
473
382
  }
474
-
475
383
  return res;
476
384
  } catch (error) {
477
385
  console.error(error.message);
478
386
  retryCount += 1;
479
-
480
387
  if (retryCount < maxRetries) {
481
388
  await delay(getRetryDelay(retryCount));
482
389
  continue;
483
390
  } else {
484
- throw new Error("ECIPFSDownloadError");
391
+ throw new Error('ECIPFSDownloadError');
485
392
  }
486
393
  }
487
394
  }
488
395
  };
489
396
 
397
+ export {
398
+ initialize,
399
+ upload,
400
+ uploadFileToIPFS,
401
+ uploadFolderToIPFS,
402
+ getFromIPFS,
403
+ downloadFolderFromIPFS,
404
+ getContentFromIPFS
405
+ };
406
+
407
+ // ---------------------------------------------------------------------------
408
+ // CLI (unchanged interface: --host --action upload/download --filePath/--folderPath)
409
+ // ---------------------------------------------------------------------------
410
+
490
411
  program
491
412
  .option('--host <host>', 'IPFS host')
492
413
  .option('--hhash <hhash>', 'IPFS hash')
493
414
  .option('--filePath <path>', 'Path to the file')
494
415
  .option('--folderPath <path>', 'Path to the folder')
495
416
  .option('--action <action>', 'Action to perform (upload, download)')
417
+ .option('--token <token>', 'IPFS authorization token')
496
418
  .option('--output <path>', 'Output path for download');
497
419
 
498
- program.parse(process.argv);
420
+ const isMain = process.argv[1] && process.argv[1].endsWith('ipfs.mjs');
499
421
 
500
- const options = program.opts();
422
+ if (isMain) {
423
+ program.parse(process.argv);
424
+ const options = program.opts();
501
425
 
502
- const main = async () => {
503
- const host = options.host || 'localhost';
504
- initialize(host);
426
+ const main = async () => {
427
+ const host = options.host || 'localhost';
428
+ initialize(host, undefined, undefined, options.token || '');
505
429
 
506
- if (options.action === 'upload') {
507
- if (options.filePath) {
508
- const hhash = await uploadFileToIPFS(options.filePath);
509
- console.log(`${hhash}`);
510
- } else if (options.folderPath) {
511
- // const hhash = await uploadFolderToIPFSBatch(options.folderPath);
512
- let retryCount = 0;
513
- let hhash = null;
514
- try {
515
- hhash = await uploadFolderToIPFS(options.folderPath);
430
+ if (options.action === 'upload') {
431
+ if (options.filePath) {
432
+ const hhash = await uploadFileToIPFS(options.filePath);
516
433
  console.log(`${hhash}`);
517
- } catch (e) {
518
- // console.log(e);
519
- }
520
- while ((!hhash || hhash === 'Upload failed.') && retryCount < 3) {
521
- console.log(`Retrying... (${retryCount}/3)`);
522
- hhash = await uploadFolderToIPFS(options.folderPath);
434
+ process.exit(hhash ? 0 : 1);
435
+ } else if (options.folderPath) {
436
+ // retryOnFailure already retries 10x internally; a single call suffices.
437
+ const hhash = await uploadFolderToIPFS(options.folderPath);
523
438
  console.log(`${hhash}`);
524
- retryCount += 1;
439
+ if (!hhash || hhash === 'Upload failed.') {
440
+ console.log(`Failed to upload folder to IPFS, please try again.`);
441
+ process.exit(1);
442
+ }
443
+ process.exit(0);
444
+ } else {
445
+ console.error('Please provide a filePath or folderPath for upload.');
446
+ process.exit(1);
525
447
  }
526
-
527
- if (!hhash || hhash === 'Upload failed.') {
528
- console.log(`Failed to upload folder to IPFS, please try again.`);
448
+ } else if (options.action === 'download') {
449
+ if (options.filePath) {
450
+ await getFromIPFS(options.hhash, options.filePath);
451
+ console.log(`File downloaded. ${options.hhash}`);
452
+ } else if (options.folderPath) {
453
+ await downloadFolderFromIPFS(options.hhash, options.folderPath);
454
+ console.log(`Folder downloaded. ${options.hhash}`);
455
+ } else {
456
+ console.error('Please provide a filePath or folderPath for download.');
529
457
  }
530
- process.exit(0);
531
- } else {
532
- console.error('Please provide a filePath or folderPath for upload.');
533
- }
534
- } else if (options.action === 'download') {
535
- if (options.filePath) {
536
- console.log(`Downloading file from IPFS: ${options.hhash}`);
537
- const content = await getFromIPFS(options.hhash, options.filePath);
538
- console.log(`File downloaded. ${options.hhash}`);
539
- } else if (options.folderPath) {
540
- console.log(`Downloading folder from IPFS: ${options.hhash}`);
541
- const content = await downloadFolderFromIPFS(options.hhash, options.folderPath);
542
- console.log(`Folder downloaded. ${options.hhash}`);
543
458
  } else {
544
- console.error('Please provide a filePath or folderPath for download.');
459
+ console.error('Please provide a valid action (upload, download).');
460
+ process.exit(1);
545
461
  }
546
- } else {
547
- console.error('Please provide a valid action (upload, download).');
548
- }
549
- };
462
+ };
550
463
 
551
- main();
464
+ main();
465
+ }