@ethernity-cloud/sdk-js 1.1.1 → 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.
- package/build.js +29 -0
- package/init.js +1 -1
- package/nodenithy/build/securelock/Dockerfile.tmpl +21 -5
- package/nodenithy/build.mjs +42 -5
- package/nodenithy/ipfs.mjs +340 -426
- package/nodenithy/run/docker-compose-final.yml.tmpl +3 -0
- package/nodenithy/run/docker-compose.yml.tmpl +10 -8
- package/nodenithy/run.js +44 -1
- package/package.json +46 -46
- package/pynithy/build/securelock/Dockerfile.tmpl +18 -2
- package/pynithy/build.mjs +31 -5
- package/pynithy/ipfs.mjs +376 -155
- package/pynithy/run/docker-compose-final.yml.tmpl +9 -0
- package/pynithy/run/docker-compose.yml.tmpl +10 -6
- package/pynithy/run.js +40 -1
package/nodenithy/ipfs.mjs
CHANGED
|
@@ -1,360 +1,349 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
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 {
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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.
|
|
335
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
420
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('ipfs.mjs');
|
|
499
421
|
|
|
500
|
-
|
|
422
|
+
if (isMain) {
|
|
423
|
+
program.parse(process.argv);
|
|
424
|
+
const options = program.opts();
|
|
501
425
|
|
|
502
|
-
const main = async () => {
|
|
503
|
-
|
|
504
|
-
|
|
426
|
+
const main = async () => {
|
|
427
|
+
const host = options.host || 'localhost';
|
|
428
|
+
initialize(host, undefined, undefined, options.token || '');
|
|
505
429
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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 (
|
|
528
|
-
|
|
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
|
|
459
|
+
console.error('Please provide a valid action (upload, download).');
|
|
460
|
+
process.exit(1);
|
|
545
461
|
}
|
|
546
|
-
}
|
|
547
|
-
console.error('Please provide a valid action (upload, download).');
|
|
548
|
-
}
|
|
549
|
-
};
|
|
462
|
+
};
|
|
550
463
|
|
|
551
|
-
main();
|
|
464
|
+
main();
|
|
465
|
+
}
|