@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.
- package/build.js +29 -0
- package/init.js +22 -9
- package/nodenithy/build/securelock/Dockerfile.tmpl +21 -5
- package/nodenithy/build.js +6 -0
- package/nodenithy/build.mjs +65 -12
- 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 +54 -12
- 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/pynithy/ipfs.mjs
CHANGED
|
@@ -1,244 +1,465 @@
|
|
|
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
|
|
|
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'));
|
|
100
|
+
}
|
|
101
|
+
parts.push(Buffer.from(`--${boundary}--\r\n`, 'utf8'));
|
|
102
|
+
const body = Buffer.concat(parts);
|
|
103
|
+
return { body, boundary, length: body.length };
|
|
104
|
+
};
|
|
50
105
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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'
|
|
117
|
+
};
|
|
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();
|
|
59
161
|
};
|
|
60
162
|
|
|
61
|
-
|
|
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;
|
|
192
|
+
}
|
|
193
|
+
last = obj;
|
|
194
|
+
if (obj.Name === '') root = obj.Hash;
|
|
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
|
+
};
|
|
201
|
+
|
|
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;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
console.log('Max retry attempts reached. Operation failed.');
|
|
223
|
+
return null;
|
|
224
|
+
};
|
|
62
225
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 */
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return last ? last.Hash : null;
|
|
248
|
+
};
|
|
68
249
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
};
|
|
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
|
+
};
|
|
83
261
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
);
|
|
99
281
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
282
|
+
};
|
|
283
|
+
|
|
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);
|
|
287
|
+
|
|
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;
|
|
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;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const uploadFolderToIPFS = async (folderPath) => {
|
|
304
|
+
const hash = await retryOnFailure(() => uploadDirOnce(folderPath), 'upload_dir');
|
|
305
|
+
if (hash) {
|
|
306
|
+
try {
|
|
307
|
+
await fss.writeFile(`./IPFS_HASH.ipfs`, hash);
|
|
308
|
+
} catch {
|
|
309
|
+
/* best effort */
|
|
310
|
+
}
|
|
311
|
+
return hash;
|
|
108
312
|
}
|
|
313
|
+
return 'Upload failed.';
|
|
109
314
|
};
|
|
110
315
|
|
|
111
|
-
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
const getFromIPFS = async (hhash, filePath, maxRetries = process.env.REACT_APP_IPFS_RETRIES || 5) => {
|
|
112
330
|
let res = '';
|
|
113
331
|
let retryCount = 0;
|
|
114
|
-
// console.log('Downloading file from IPFS...');
|
|
115
|
-
// console.log(`Hash: ${hhash}`);
|
|
116
|
-
// console.log(`Max Retries: ${maxRetries}`);
|
|
117
|
-
|
|
118
332
|
while (retryCount < maxRetries) {
|
|
119
333
|
try {
|
|
120
334
|
for await (const file of ipfs.cat(hhash)) {
|
|
121
|
-
res += new TextDecoder().decode(file.buffer);
|
|
335
|
+
res += new TextDecoder().decode(file.buffer || file);
|
|
122
336
|
}
|
|
123
337
|
await writeFile(filePath, res);
|
|
124
338
|
return;
|
|
125
339
|
} catch (error) {
|
|
126
340
|
console.error(`Error: ${error.message}`);
|
|
127
341
|
retryCount += 1;
|
|
128
|
-
|
|
129
342
|
if (retryCount < maxRetries) {
|
|
130
|
-
console.log(`Retrying... (${retryCount}/${maxRetries})`);
|
|
131
343
|
await delay(getRetryDelay(retryCount));
|
|
132
344
|
continue;
|
|
133
345
|
} else {
|
|
134
|
-
throw new Error(
|
|
346
|
+
throw new Error('ECError.IPFS_DOWNLOAD_ERROR');
|
|
135
347
|
}
|
|
136
348
|
}
|
|
137
349
|
}
|
|
138
350
|
};
|
|
139
351
|
|
|
140
|
-
|
|
141
352
|
const downloadFolderFromIPFS = async (cid, outputPath) => {
|
|
142
353
|
try {
|
|
143
354
|
console.log(`Downloading folder from IPFS: ${cid}`);
|
|
144
|
-
|
|
145
|
-
const multiBar = new MultiBar({
|
|
146
|
-
clearOnComplete: false,
|
|
147
|
-
hideCursor: true,
|
|
148
|
-
format: 'Progress [{bar}] {percentage}% | {value}/{total} MB'
|
|
149
|
-
}, Presets.shades_classic);
|
|
150
|
-
|
|
151
355
|
const files = [];
|
|
152
356
|
for await (const file of ipfs.get(cid)) {
|
|
153
|
-
console.log(`file: ${JSON.stringify(file, null, 2)}`);
|
|
154
357
|
if (!file.content) continue;
|
|
155
|
-
|
|
156
358
|
const filePath = path.join(outputPath, file.path);
|
|
157
359
|
const dir = path.dirname(filePath);
|
|
158
|
-
|
|
159
|
-
if (!fs.existsSync(dir)) {
|
|
160
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
161
|
-
}
|
|
162
|
-
|
|
360
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
163
361
|
const writeStream = fs.createWriteStream(filePath);
|
|
164
|
-
const progressBar = multiBar.create(file.size / 1024 / 1024, 0);
|
|
165
|
-
|
|
166
362
|
for await (const chunk of file.content) {
|
|
167
363
|
writeStream.write(chunk);
|
|
168
|
-
progressBar.increment(chunk.length / 1024 / 1024);
|
|
169
364
|
}
|
|
170
|
-
|
|
171
365
|
writeStream.end();
|
|
172
|
-
progressBar.update(file.size / 1024 / 1024);
|
|
173
366
|
files.push(filePath);
|
|
174
367
|
}
|
|
175
|
-
|
|
176
|
-
multiBar.stop();
|
|
177
368
|
console.log(`Downloaded ${files.length} files to ${outputPath}`);
|
|
178
369
|
} catch (e) {
|
|
179
370
|
console.error(e);
|
|
180
|
-
return
|
|
371
|
+
return 'err';
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const getContentFromIPFS = async (hash, maxRetries = process.env.REACT_APP_IPFS_RETRIES || 5) => {
|
|
376
|
+
let res = '';
|
|
377
|
+
let retryCount = 0;
|
|
378
|
+
while (retryCount < maxRetries) {
|
|
379
|
+
try {
|
|
380
|
+
for await (const file of ipfs.cat(hash)) {
|
|
381
|
+
res += new TextDecoder().decode(file.buffer || file);
|
|
382
|
+
}
|
|
383
|
+
return res;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error(error.message);
|
|
386
|
+
retryCount += 1;
|
|
387
|
+
if (retryCount < maxRetries) {
|
|
388
|
+
await delay(getRetryDelay(retryCount));
|
|
389
|
+
continue;
|
|
390
|
+
} else {
|
|
391
|
+
throw new Error('ECIPFSDownloadError');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
181
394
|
}
|
|
182
395
|
};
|
|
183
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
|
+
|
|
184
411
|
program
|
|
185
412
|
.option('--host <host>', 'IPFS host')
|
|
186
413
|
.option('--hhash <hhash>', 'IPFS hash')
|
|
187
414
|
.option('--filePath <path>', 'Path to the file')
|
|
188
415
|
.option('--folderPath <path>', 'Path to the folder')
|
|
189
416
|
.option('--action <action>', 'Action to perform (upload, download)')
|
|
417
|
+
.option('--token <token>', 'IPFS authorization token')
|
|
190
418
|
.option('--output <path>', 'Output path for download');
|
|
191
419
|
|
|
192
|
-
|
|
420
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('ipfs.mjs');
|
|
193
421
|
|
|
194
|
-
|
|
422
|
+
if (isMain) {
|
|
423
|
+
program.parse(process.argv);
|
|
424
|
+
const options = program.opts();
|
|
195
425
|
|
|
196
|
-
const main = async () => {
|
|
197
|
-
|
|
198
|
-
|
|
426
|
+
const main = async () => {
|
|
427
|
+
const host = options.host || 'localhost';
|
|
428
|
+
initialize(host, undefined, undefined, options.token || '');
|
|
199
429
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
console.log(`${hhash}`);
|
|
204
|
-
} else if (options.folderPath) {
|
|
205
|
-
let retryCount = 0;
|
|
206
|
-
let hhash = null;
|
|
207
|
-
try {
|
|
208
|
-
hhash = await uploadFolderToIPFS(options.folderPath);
|
|
430
|
+
if (options.action === 'upload') {
|
|
431
|
+
if (options.filePath) {
|
|
432
|
+
const hhash = await uploadFileToIPFS(options.filePath);
|
|
209
433
|
console.log(`${hhash}`);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
console.log(`Retrying... (${retryCount}/3)`);
|
|
215
|
-
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);
|
|
216
438
|
console.log(`${hhash}`);
|
|
217
|
-
|
|
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);
|
|
218
447
|
}
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
|
|
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.');
|
|
222
457
|
}
|
|
223
|
-
process.exit(0);
|
|
224
|
-
} else {
|
|
225
|
-
console.error('Please provide a filePath or folderPath for upload.');
|
|
226
|
-
}
|
|
227
|
-
} else if (options.action === 'download') {
|
|
228
|
-
if (options.filePath) {
|
|
229
|
-
console.log(`Downloading file from IPFS: ${options.hhash}`);
|
|
230
|
-
const content = await getFromIPFS(options.hhash, options.filePath);
|
|
231
|
-
console.log(`File downloaded. ${options.hhash}`);
|
|
232
|
-
} else if (options.folderPath) {
|
|
233
|
-
console.log(`Downloading folder from IPFS: ${options.hhash}`);
|
|
234
|
-
const content = await downloadFolderFromIPFS(options.hhash, options.folderPath);
|
|
235
|
-
console.log(`Folder downloaded. ${options.hhash}`);
|
|
236
458
|
} else {
|
|
237
|
-
console.error('Please provide a
|
|
459
|
+
console.error('Please provide a valid action (upload, download).');
|
|
460
|
+
process.exit(1);
|
|
238
461
|
}
|
|
239
|
-
}
|
|
240
|
-
console.error('Please provide a valid action (upload, download).');
|
|
241
|
-
}
|
|
242
|
-
};
|
|
462
|
+
};
|
|
243
463
|
|
|
244
|
-
main();
|
|
464
|
+
main();
|
|
465
|
+
}
|
|
@@ -6,6 +6,8 @@ services:
|
|
|
6
6
|
image: localhost:5000/etny-las
|
|
7
7
|
entrypoint: "/las_entrypoint.sh"
|
|
8
8
|
command: "bash -c '/las_entrypoint.sh && /usr/local/bin/las | tee /var/log/las.log'"
|
|
9
|
+
devices:
|
|
10
|
+
- "/dev/sgx_enclave:/dev/sgx_enclave"
|
|
9
11
|
restart: unless-stopped
|
|
10
12
|
networks:
|
|
11
13
|
- ethernity
|
|
@@ -26,10 +28,15 @@ services:
|
|
|
26
28
|
- SCONE_LAS_ADDR=las
|
|
27
29
|
- SCONE_CONFIG_ID=__ENCLAVE_NAME_SECURELOCK__/application
|
|
28
30
|
- SCONE_HEAP=1024M
|
|
31
|
+
# Must match the --stack the securelock binary was signed with (build.mjs),
|
|
32
|
+
# otherwise SCONE recomputes MRENCLAVE at load and re-signs as debug.
|
|
33
|
+
- SCONE_STACK=4M
|
|
29
34
|
- SCONE_LOG=DEBUG
|
|
30
35
|
- SCONE_ALLOW_DLOPEN=1
|
|
31
36
|
- SCONE_EXTENSIONS_PATH=/lib/libbinary-fs.so
|
|
32
37
|
restart: on-failure
|
|
38
|
+
devices:
|
|
39
|
+
- "/dev/sgx_enclave:/dev/sgx_enclave"
|
|
33
40
|
networks:
|
|
34
41
|
- ethernity
|
|
35
42
|
depends_on:
|
|
@@ -50,6 +57,8 @@ services:
|
|
|
50
57
|
- SCONE_ALLOW_DLOPEN=1
|
|
51
58
|
- SCONE_EXTENSIONS_PATH=/lib/libbinary-fs.so
|
|
52
59
|
restart: on-failure
|
|
60
|
+
devices:
|
|
61
|
+
- "/dev/sgx_enclave:/dev/sgx_enclave"
|
|
53
62
|
networks:
|
|
54
63
|
- ethernity
|
|
55
64
|
depends_on:
|