underpost 3.2.22 → 3.2.30
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/.github/workflows/ghpkg.ci.yml +1 -1
- package/.github/workflows/gitlab.ci.yml +1 -1
- package/.github/workflows/npmpkg.ci.yml +1 -1
- package/.github/workflows/publish.ci.yml +2 -2
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -1
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/CHANGELOG.md +181 -1
- package/CLI-HELP.md +61 -2
- package/README.md +3 -2
- package/baremetal/commission-workflows.json +1 -0
- package/bin/build.js +1 -0
- package/docker-compose.yml +224 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +27 -15
- package/scripts/kubeadm-node-setup.sh +317 -0
- package/scripts/rocky-kickstart.sh +877 -185
- package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
- package/scripts/test-monitor.sh +3 -5
- package/src/cli/baremetal.js +717 -16
- package/src/cli/cluster.js +3 -1
- package/src/cli/deploy.js +118 -9
- package/src/cli/docker-compose.js +591 -0
- package/src/cli/env.js +11 -2
- package/src/cli/fs.js +35 -11
- package/src/cli/index.js +89 -0
- package/src/cli/kickstart.js +142 -20
- package/src/cli/repository.js +153 -11
- package/src/cli/run.js +285 -8
- package/src/cli/secrets.js +7 -2
- package/src/cli/ssh.js +234 -0
- package/src/cli/static.js +2 -2
- package/src/{server → client-builder}/client-build-docs.js +15 -5
- package/src/{server → client-builder}/client-build-live.js +3 -3
- package/src/{server → client-builder}/client-build.js +25 -22
- package/src/{server → client-builder}/client-dev-server.js +3 -3
- package/src/{server → client-builder}/client-icons.js +2 -2
- package/src/{server → client-builder}/ssr.js +5 -5
- package/src/client.build.js +1 -3
- package/src/client.dev.js +1 -1
- package/src/db/mongo/MongoBootstrap.js +12 -12
- package/src/index.js +12 -1
- package/src/mailer/EmailRender.js +1 -1
- package/src/{server → projects/underpost}/catalog-underpost.js +1 -2
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/nginx/Nginx.js +250 -0
- package/src/server/catalog.js +7 -14
- package/src/server/conf.js +3 -3
- package/src/server/runtime-status.js +18 -1
- package/src/server/start.js +12 -2
- package/src/server.js +6 -2
- package/test/deploy-monitor.test.js +26 -10
- package/typedoc.json +3 -1
- package/src/server/ipfs-client.js +0 -599
- /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
- /package/src/{server → client-builder}/client-formatted.js +0 -0
|
@@ -1,599 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lightweight IPFS HTTP client for communicating with a Kubo (go-ipfs) node
|
|
3
|
-
* and an IPFS Cluster daemon running side-by-side in the same StatefulSet.
|
|
4
|
-
*
|
|
5
|
-
* Kubo API (port 5001) – add / pin / cat content.
|
|
6
|
-
* Cluster API (port 9094) – replicate pins across the cluster.
|
|
7
|
-
*
|
|
8
|
-
* Uses native `FormData` + `Blob` (Node ≥ 18) for reliable multipart encoding.
|
|
9
|
-
*
|
|
10
|
-
* @module src/server/ipfs-client.js
|
|
11
|
-
* @namespace IpfsClient
|
|
12
|
-
*/
|
|
13
|
-
import stringify from 'fast-json-stable-stringify';
|
|
14
|
-
import { loggerFactory } from './logger.js';
|
|
15
|
-
import Underpost from '../index.js';
|
|
16
|
-
const logger = loggerFactory(import.meta);
|
|
17
|
-
const DEFAULT_IPFS_HTTP_TIMEOUT_MS = Number(process.env.IPFS_HTTP_TIMEOUT_MS || 10000);
|
|
18
|
-
const getRequestTimeoutMs = (kind = 'kubo') => {
|
|
19
|
-
if (kind === 'cluster') {
|
|
20
|
-
return Number(process.env.IPFS_CLUSTER_TIMEOUT_MS || DEFAULT_IPFS_HTTP_TIMEOUT_MS);
|
|
21
|
-
}
|
|
22
|
-
if (kind === 'gateway') {
|
|
23
|
-
return Number(process.env.IPFS_GATEWAY_TIMEOUT_MS || DEFAULT_IPFS_HTTP_TIMEOUT_MS);
|
|
24
|
-
}
|
|
25
|
-
return Number(process.env.IPFS_KUBO_TIMEOUT_MS || DEFAULT_IPFS_HTTP_TIMEOUT_MS);
|
|
26
|
-
};
|
|
27
|
-
const fetchWithTimeout = async (url, options = {}, { kind = 'kubo', label = url } = {}) => {
|
|
28
|
-
const controller = new AbortController();
|
|
29
|
-
const timeoutMs = getRequestTimeoutMs(kind);
|
|
30
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
31
|
-
try {
|
|
32
|
-
return await fetch(url, { ...options, signal: controller.signal });
|
|
33
|
-
} catch (err) {
|
|
34
|
-
if (err.name === 'AbortError') {
|
|
35
|
-
throw new Error(`${label} timed out after ${timeoutMs}ms`);
|
|
36
|
-
}
|
|
37
|
-
throw err;
|
|
38
|
-
} finally {
|
|
39
|
-
clearTimeout(timeoutId);
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
// ─────────────────────────────────────────────────────────
|
|
43
|
-
// URL helpers
|
|
44
|
-
// ─────────────────────────────────────────────────────────
|
|
45
|
-
/**
|
|
46
|
-
* Base URL of the Kubo RPC API (port 5001).
|
|
47
|
-
* @returns {string}
|
|
48
|
-
*/
|
|
49
|
-
const getIpfsApiUrl = () =>
|
|
50
|
-
process.env.IPFS_API_URL ||
|
|
51
|
-
`http://${process.env.NODE_ENV === 'development' && !Underpost.env.isInsideContainer() ? 'localhost' : 'ipfs-cluster'}:5001`;
|
|
52
|
-
/**
|
|
53
|
-
* Base URL of the IPFS Cluster REST API (port 9094).
|
|
54
|
-
* @returns {string}
|
|
55
|
-
*/
|
|
56
|
-
const getClusterApiUrl = () =>
|
|
57
|
-
process.env.IPFS_CLUSTER_API_URL ||
|
|
58
|
-
`http://${process.env.NODE_ENV === 'development' && !Underpost.env.isInsideContainer() ? 'localhost' : 'ipfs-cluster'}:9094`;
|
|
59
|
-
/**
|
|
60
|
-
* Base URL of the IPFS HTTP Gateway (port 8080).
|
|
61
|
-
* @returns {string}
|
|
62
|
-
*/
|
|
63
|
-
const getGatewayUrl = () =>
|
|
64
|
-
process.env.IPFS_GATEWAY_URL ||
|
|
65
|
-
`http://${process.env.NODE_ENV === 'development' && !Underpost.env.isInsideContainer() ? 'localhost' : 'ipfs-cluster'}:8080`;
|
|
66
|
-
// ─────────────────────────────────────────────────────────
|
|
67
|
-
// Core: add content
|
|
68
|
-
// ─────────────────────────────────────────────────────────
|
|
69
|
-
/**
|
|
70
|
-
* @typedef {Object} IpfsAddResult
|
|
71
|
-
* @property {string} cid – CID (Content Identifier) returned by the node.
|
|
72
|
-
* @property {number} size – Cumulative DAG size reported by the node.
|
|
73
|
-
*/
|
|
74
|
-
/**
|
|
75
|
-
* Add arbitrary bytes to the Kubo node AND pin them on the IPFS Cluster.
|
|
76
|
-
*
|
|
77
|
-
* 1. `POST /api/v0/add?pin=true` to Kubo (5001) – stores + locally pins.
|
|
78
|
-
* 2. `POST /pins/<CID>` to the Cluster REST API (9094) – replicates the pin
|
|
79
|
-
* across every peer so `GET /pins` on the cluster shows the content.
|
|
80
|
-
* 3. Copies into MFS so the Web UI "Files" section shows the file.
|
|
81
|
-
*
|
|
82
|
-
* @param {Buffer|string} content – raw bytes or a UTF-8 string to store.
|
|
83
|
-
* @param {string} [filename='data'] – logical filename for the upload.
|
|
84
|
-
* @param {string} [mfsPath] – optional full MFS path.
|
|
85
|
-
* When omitted defaults to `/pinned/<filename>`.
|
|
86
|
-
* @returns {Promise<IpfsAddResult|null>} `null` when the node is unreachable.
|
|
87
|
-
*/
|
|
88
|
-
const addToIpfs = async (content, filename = 'data', mfsPath) => {
|
|
89
|
-
const kuboUrl = getIpfsApiUrl();
|
|
90
|
-
const clusterUrl = getClusterApiUrl();
|
|
91
|
-
// Build multipart body using native FormData + Blob (Node ≥ 18).
|
|
92
|
-
const buf = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf-8');
|
|
93
|
-
const formData = new FormData();
|
|
94
|
-
formData.append('file', new Blob([buf]), filename);
|
|
95
|
-
// ── Step 1: add to Kubo ──────────────────────────────
|
|
96
|
-
let cid;
|
|
97
|
-
let size;
|
|
98
|
-
try {
|
|
99
|
-
const res = await fetchWithTimeout(
|
|
100
|
-
`${kuboUrl}/api/v0/add?pin=true&cid-version=1`,
|
|
101
|
-
{
|
|
102
|
-
method: 'POST',
|
|
103
|
-
body: formData,
|
|
104
|
-
},
|
|
105
|
-
{ kind: 'kubo', label: `IPFS Kubo add ${filename}` },
|
|
106
|
-
);
|
|
107
|
-
if (!res.ok) {
|
|
108
|
-
const text = await res.text();
|
|
109
|
-
logger.error(`IPFS Kubo add failed (${res.status}): ${text}`);
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
const json = await res.json();
|
|
113
|
-
cid = json.Hash;
|
|
114
|
-
size = Number(json.Size);
|
|
115
|
-
logger.info(`IPFS Kubo add OK – CID: ${cid}, size: ${size}`);
|
|
116
|
-
} catch (err) {
|
|
117
|
-
logger.warn(`IPFS Kubo node unreachable at ${kuboUrl}: ${err.message}`);
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
// ── Step 2: pin to the Cluster ───────────────────────
|
|
121
|
-
try {
|
|
122
|
-
const clusterRes = await fetchWithTimeout(
|
|
123
|
-
`${clusterUrl}/pins/${encodeURIComponent(cid)}`,
|
|
124
|
-
{
|
|
125
|
-
method: 'POST',
|
|
126
|
-
},
|
|
127
|
-
{ kind: 'cluster', label: `IPFS Cluster pin ${cid}` },
|
|
128
|
-
);
|
|
129
|
-
if (!clusterRes.ok) {
|
|
130
|
-
const text = await clusterRes.text();
|
|
131
|
-
logger.warn(`IPFS Cluster pin failed (${clusterRes.status}): ${text}`);
|
|
132
|
-
} else {
|
|
133
|
-
logger.info(`IPFS Cluster pin OK – CID: ${cid}`);
|
|
134
|
-
}
|
|
135
|
-
} catch (err) {
|
|
136
|
-
logger.warn(`IPFS Cluster unreachable at ${clusterUrl}: ${err.message}`);
|
|
137
|
-
}
|
|
138
|
-
// ── Step 3: copy into MFS so the Web UI "Files" section shows it ─
|
|
139
|
-
const destPath = mfsPath || `/pinned/${filename}`;
|
|
140
|
-
const destDir = destPath.substring(0, destPath.lastIndexOf('/')) || '/';
|
|
141
|
-
try {
|
|
142
|
-
// Ensure parent directory exists in MFS
|
|
143
|
-
await fetchWithTimeout(
|
|
144
|
-
`${kuboUrl}/api/v0/files/mkdir?arg=${encodeURIComponent(destDir)}&parents=true`,
|
|
145
|
-
{ method: 'POST' },
|
|
146
|
-
{ kind: 'kubo', label: `IPFS MFS mkdir ${destDir}` },
|
|
147
|
-
);
|
|
148
|
-
// Remove existing entry if present (cp fails on duplicates)
|
|
149
|
-
await fetchWithTimeout(
|
|
150
|
-
`${kuboUrl}/api/v0/files/rm?arg=${encodeURIComponent(destPath)}&force=true`,
|
|
151
|
-
{
|
|
152
|
-
method: 'POST',
|
|
153
|
-
},
|
|
154
|
-
{ kind: 'kubo', label: `IPFS MFS rm ${destPath}` },
|
|
155
|
-
);
|
|
156
|
-
// Copy the CID into MFS
|
|
157
|
-
const cpRes = await fetchWithTimeout(
|
|
158
|
-
`${kuboUrl}/api/v0/files/cp?arg=/ipfs/${encodeURIComponent(cid)}&arg=${encodeURIComponent(destPath)}`,
|
|
159
|
-
{ method: 'POST' },
|
|
160
|
-
{ kind: 'kubo', label: `IPFS MFS cp ${destPath}` },
|
|
161
|
-
);
|
|
162
|
-
if (!cpRes.ok) {
|
|
163
|
-
const text = await cpRes.text();
|
|
164
|
-
logger.warn(`IPFS MFS cp failed (${cpRes.status}): ${text}`);
|
|
165
|
-
} else {
|
|
166
|
-
logger.info(`IPFS MFS cp OK – ${destPath} → ${cid}`);
|
|
167
|
-
}
|
|
168
|
-
} catch (err) {
|
|
169
|
-
logger.warn(`IPFS MFS cp unreachable: ${err.message}`);
|
|
170
|
-
}
|
|
171
|
-
return { cid, size };
|
|
172
|
-
};
|
|
173
|
-
// ─────────────────────────────────────────────────────────
|
|
174
|
-
// Convenience wrappers
|
|
175
|
-
// ─────────────────────────────────────────────────────────
|
|
176
|
-
/**
|
|
177
|
-
* Add a JSON-serialisable object to IPFS.
|
|
178
|
-
*
|
|
179
|
-
* @param {any} obj – value to serialise.
|
|
180
|
-
* @param {string} [filename='data.json']
|
|
181
|
-
* @param {string} [mfsPath] – optional full MFS destination path.
|
|
182
|
-
* @returns {Promise<IpfsAddResult|null>}
|
|
183
|
-
*/
|
|
184
|
-
const addJsonToIpfs = async (obj, filename = 'data.json', mfsPath) => {
|
|
185
|
-
const payload = stringify(obj);
|
|
186
|
-
return addToIpfs(Buffer.from(payload, 'utf-8'), filename, mfsPath);
|
|
187
|
-
};
|
|
188
|
-
/**
|
|
189
|
-
* Compute the CID that Kubo would assign to a payload without pinning or copying it into MFS.
|
|
190
|
-
* Useful when building canonical backup manifests from the actual bytes that will be restored later.
|
|
191
|
-
*
|
|
192
|
-
* @param {Buffer|string} content
|
|
193
|
-
* @param {string} [filename='data']
|
|
194
|
-
* @returns {Promise<IpfsAddResult|null>}
|
|
195
|
-
*/
|
|
196
|
-
const hashContentForIpfs = async (content, filename = 'data') => {
|
|
197
|
-
const kuboUrl = getIpfsApiUrl();
|
|
198
|
-
const buf = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf-8');
|
|
199
|
-
const formData = new FormData();
|
|
200
|
-
formData.append('file', new Blob([buf]), filename);
|
|
201
|
-
try {
|
|
202
|
-
const res = await fetchWithTimeout(
|
|
203
|
-
`${kuboUrl}/api/v0/add?only-hash=true&pin=false&cid-version=1`,
|
|
204
|
-
{
|
|
205
|
-
method: 'POST',
|
|
206
|
-
body: formData,
|
|
207
|
-
},
|
|
208
|
-
{ kind: 'kubo', label: `IPFS Kubo only-hash ${filename}` },
|
|
209
|
-
);
|
|
210
|
-
if (!res.ok) {
|
|
211
|
-
const text = await res.text();
|
|
212
|
-
logger.error(`IPFS Kubo only-hash failed (${res.status}): ${text}`);
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
const json = await res.json();
|
|
216
|
-
return { cid: json.Hash, size: Number(json.Size) };
|
|
217
|
-
} catch (err) {
|
|
218
|
-
logger.warn(`IPFS Kubo only-hash unreachable at ${kuboUrl}: ${err.message}`);
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
/**
|
|
223
|
-
* Compute the CID for a JSON-serialisable object using the same stable stringification
|
|
224
|
-
* that the regular addJsonToIpfs path uses.
|
|
225
|
-
*
|
|
226
|
-
* @param {any} obj
|
|
227
|
-
* @param {string} [filename='data.json']
|
|
228
|
-
* @returns {Promise<IpfsAddResult|null>}
|
|
229
|
-
*/
|
|
230
|
-
const hashJsonForIpfs = async (obj, filename = 'data.json') => {
|
|
231
|
-
const payload = stringify(obj);
|
|
232
|
-
return hashContentForIpfs(Buffer.from(payload, 'utf-8'), filename);
|
|
233
|
-
};
|
|
234
|
-
/**
|
|
235
|
-
* Add a binary buffer (e.g. a PNG image) to IPFS.
|
|
236
|
-
*
|
|
237
|
-
* @param {Buffer} buffer – raw image / file bytes.
|
|
238
|
-
* @param {string} filename – e.g. `"atlas.png"`.
|
|
239
|
-
* @param {string} [mfsPath] – optional full MFS destination path.
|
|
240
|
-
* @returns {Promise<IpfsAddResult|null>}
|
|
241
|
-
*/
|
|
242
|
-
const addBufferToIpfs = async (buffer, filename, mfsPath) => {
|
|
243
|
-
return addToIpfs(buffer, filename, mfsPath);
|
|
244
|
-
};
|
|
245
|
-
/**
|
|
246
|
-
* Compute the CID for a binary buffer without pinning it.
|
|
247
|
-
*
|
|
248
|
-
* @param {Buffer} buffer
|
|
249
|
-
* @param {string} filename
|
|
250
|
-
* @returns {Promise<IpfsAddResult|null>}
|
|
251
|
-
*/
|
|
252
|
-
const hashBufferForIpfs = async (buffer, filename) => {
|
|
253
|
-
return hashContentForIpfs(buffer, filename);
|
|
254
|
-
};
|
|
255
|
-
// ─────────────────────────────────────────────────────────
|
|
256
|
-
// Pin management
|
|
257
|
-
// ─────────────────────────────────────────────────────────
|
|
258
|
-
/**
|
|
259
|
-
* Explicitly pin an existing CID on both the Kubo node and the Cluster.
|
|
260
|
-
*
|
|
261
|
-
* @param {string} cid
|
|
262
|
-
* @param {string} [type='recursive'] – `'recursive'` | `'direct'`
|
|
263
|
-
* @returns {Promise<boolean>} `true` when at least the Kubo pin succeeded.
|
|
264
|
-
*/
|
|
265
|
-
const pinCid = async (cid, type = 'recursive') => {
|
|
266
|
-
const kuboUrl = getIpfsApiUrl();
|
|
267
|
-
const clusterUrl = getClusterApiUrl();
|
|
268
|
-
let kuboOk = false;
|
|
269
|
-
// Kubo pin
|
|
270
|
-
try {
|
|
271
|
-
const res = await fetchWithTimeout(
|
|
272
|
-
`${kuboUrl}/api/v0/pin/add?arg=${encodeURIComponent(cid)}&type=${type}`,
|
|
273
|
-
{
|
|
274
|
-
method: 'POST',
|
|
275
|
-
},
|
|
276
|
-
{ kind: 'kubo', label: `IPFS Kubo pin/add ${cid}` },
|
|
277
|
-
);
|
|
278
|
-
if (!res.ok) {
|
|
279
|
-
const text = await res.text();
|
|
280
|
-
logger.error(`IPFS Kubo pin/add failed (${res.status}): ${text}`);
|
|
281
|
-
} else {
|
|
282
|
-
kuboOk = true;
|
|
283
|
-
logger.info(`IPFS Kubo pin OK – CID: ${cid} (${type})`);
|
|
284
|
-
}
|
|
285
|
-
} catch (err) {
|
|
286
|
-
logger.warn(`IPFS Kubo pin unreachable: ${err.message}`);
|
|
287
|
-
}
|
|
288
|
-
// Cluster pin
|
|
289
|
-
try {
|
|
290
|
-
const clusterRes = await fetchWithTimeout(
|
|
291
|
-
`${clusterUrl}/pins/${encodeURIComponent(cid)}`,
|
|
292
|
-
{
|
|
293
|
-
method: 'POST',
|
|
294
|
-
},
|
|
295
|
-
{ kind: 'cluster', label: `IPFS Cluster pin ${cid}` },
|
|
296
|
-
);
|
|
297
|
-
if (!clusterRes.ok) {
|
|
298
|
-
const text = await clusterRes.text();
|
|
299
|
-
logger.warn(`IPFS Cluster pin failed (${clusterRes.status}): ${text}`);
|
|
300
|
-
} else {
|
|
301
|
-
logger.info(`IPFS Cluster pin OK – CID: ${cid}`);
|
|
302
|
-
}
|
|
303
|
-
} catch (err) {
|
|
304
|
-
logger.warn(`IPFS Cluster pin unreachable: ${err.message}`);
|
|
305
|
-
}
|
|
306
|
-
return kuboOk;
|
|
307
|
-
};
|
|
308
|
-
/**
|
|
309
|
-
* Unpin a CID from both the Kubo node and the Cluster.
|
|
310
|
-
*
|
|
311
|
-
* @param {string} cid
|
|
312
|
-
* @returns {Promise<boolean>}
|
|
313
|
-
*/
|
|
314
|
-
const unpinCid = async (cid) => {
|
|
315
|
-
const kuboUrl = getIpfsApiUrl();
|
|
316
|
-
const clusterUrl = getClusterApiUrl();
|
|
317
|
-
let kuboOk = false;
|
|
318
|
-
// Cluster unpin
|
|
319
|
-
try {
|
|
320
|
-
const clusterRes = await fetchWithTimeout(
|
|
321
|
-
`${clusterUrl}/pins/${encodeURIComponent(cid)}`,
|
|
322
|
-
{
|
|
323
|
-
method: 'DELETE',
|
|
324
|
-
},
|
|
325
|
-
{ kind: 'cluster', label: `IPFS Cluster unpin ${cid}` },
|
|
326
|
-
);
|
|
327
|
-
if (!clusterRes.ok) {
|
|
328
|
-
const text = await clusterRes.text();
|
|
329
|
-
if (clusterRes.status === 404) {
|
|
330
|
-
logger.info(`IPFS Cluster unpin – CID already not pinned: ${cid}`);
|
|
331
|
-
} else {
|
|
332
|
-
logger.warn(`IPFS Cluster unpin failed (${clusterRes.status}): ${text}`);
|
|
333
|
-
}
|
|
334
|
-
} else {
|
|
335
|
-
logger.info(`IPFS Cluster unpin OK – CID: ${cid}`);
|
|
336
|
-
}
|
|
337
|
-
} catch (err) {
|
|
338
|
-
logger.warn(`IPFS Cluster unpin unreachable: ${err.message}`);
|
|
339
|
-
}
|
|
340
|
-
// Kubo unpin
|
|
341
|
-
try {
|
|
342
|
-
const res = await fetchWithTimeout(
|
|
343
|
-
`${kuboUrl}/api/v0/pin/rm?arg=${encodeURIComponent(cid)}`,
|
|
344
|
-
{
|
|
345
|
-
method: 'POST',
|
|
346
|
-
},
|
|
347
|
-
{ kind: 'kubo', label: `IPFS Kubo pin/rm ${cid}` },
|
|
348
|
-
);
|
|
349
|
-
if (!res.ok) {
|
|
350
|
-
const text = await res.text();
|
|
351
|
-
// "not pinned or pinned indirectly" means the CID is already unpinned – treat as success
|
|
352
|
-
if (text.includes('not pinned')) {
|
|
353
|
-
kuboOk = true;
|
|
354
|
-
logger.info(`IPFS Kubo unpin – CID already not pinned: ${cid}`);
|
|
355
|
-
} else {
|
|
356
|
-
logger.warn(`IPFS Kubo pin/rm failed (${res.status}): ${text}`);
|
|
357
|
-
}
|
|
358
|
-
} else {
|
|
359
|
-
kuboOk = true;
|
|
360
|
-
logger.info(`IPFS Kubo unpin OK – CID: ${cid}`);
|
|
361
|
-
}
|
|
362
|
-
} catch (err) {
|
|
363
|
-
logger.warn(`IPFS Kubo unpin unreachable: ${err.message}`);
|
|
364
|
-
}
|
|
365
|
-
return kuboOk;
|
|
366
|
-
};
|
|
367
|
-
// ─────────────────────────────────────────────────────────
|
|
368
|
-
// Retrieval
|
|
369
|
-
// ─────────────────────────────────────────────────────────
|
|
370
|
-
/**
|
|
371
|
-
* Retrieve raw bytes for a CID from the IPFS HTTP Gateway (port 8080).
|
|
372
|
-
*
|
|
373
|
-
* @param {string} cid
|
|
374
|
-
* @returns {Promise<Buffer|null>}
|
|
375
|
-
*/
|
|
376
|
-
const getFromIpfs = async (cid) => {
|
|
377
|
-
const url = getGatewayUrl();
|
|
378
|
-
try {
|
|
379
|
-
const res = await fetchWithTimeout(
|
|
380
|
-
`${url}/ipfs/${encodeURIComponent(cid)}`,
|
|
381
|
-
{},
|
|
382
|
-
{
|
|
383
|
-
kind: 'gateway',
|
|
384
|
-
label: `IPFS gateway GET ${cid}`,
|
|
385
|
-
},
|
|
386
|
-
);
|
|
387
|
-
if (!res.ok) {
|
|
388
|
-
logger.error(`IPFS gateway GET failed (${res.status}) for ${cid}`);
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
const arrayBuffer = await res.arrayBuffer();
|
|
392
|
-
return Buffer.from(arrayBuffer);
|
|
393
|
-
} catch (err) {
|
|
394
|
-
logger.warn(`IPFS gateway unreachable at ${url}: ${err.message}`);
|
|
395
|
-
return null;
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
// ─────────────────────────────────────────────────────────
|
|
399
|
-
// Diagnostics
|
|
400
|
-
// ─────────────────────────────────────────────────────────
|
|
401
|
-
/**
|
|
402
|
-
* List all pins tracked by the IPFS Cluster (port 9094).
|
|
403
|
-
* Each line in the response is a JSON object with at least a `cid` field.
|
|
404
|
-
*
|
|
405
|
-
* @returns {Promise<Array<{ cid: string, name: string, peer_map: object }>>}
|
|
406
|
-
*/
|
|
407
|
-
const listClusterPins = async () => {
|
|
408
|
-
const clusterUrl = getClusterApiUrl();
|
|
409
|
-
try {
|
|
410
|
-
const res = await fetchWithTimeout(
|
|
411
|
-
`${clusterUrl}/pins`,
|
|
412
|
-
{},
|
|
413
|
-
{
|
|
414
|
-
kind: 'cluster',
|
|
415
|
-
label: 'IPFS Cluster list pins',
|
|
416
|
-
},
|
|
417
|
-
);
|
|
418
|
-
if (res.status === 204) {
|
|
419
|
-
// 204 No Content → the cluster has no pins at all.
|
|
420
|
-
return [];
|
|
421
|
-
}
|
|
422
|
-
if (!res.ok) {
|
|
423
|
-
const text = await res.text();
|
|
424
|
-
logger.error(`IPFS Cluster list pins failed (${res.status}): ${text}`);
|
|
425
|
-
return [];
|
|
426
|
-
}
|
|
427
|
-
const text = await res.text();
|
|
428
|
-
// The cluster streams one JSON object per line (NDJSON).
|
|
429
|
-
return text
|
|
430
|
-
.split('\n')
|
|
431
|
-
.filter((line) => line.trim())
|
|
432
|
-
.map((line) => {
|
|
433
|
-
try {
|
|
434
|
-
return JSON.parse(line);
|
|
435
|
-
} catch {
|
|
436
|
-
return null;
|
|
437
|
-
}
|
|
438
|
-
})
|
|
439
|
-
.filter(Boolean);
|
|
440
|
-
} catch (err) {
|
|
441
|
-
logger.warn(`IPFS Cluster unreachable at ${clusterUrl}: ${err.message}`);
|
|
442
|
-
return [];
|
|
443
|
-
}
|
|
444
|
-
};
|
|
445
|
-
/**
|
|
446
|
-
* List pins tracked by the local Kubo node (port 5001).
|
|
447
|
-
*
|
|
448
|
-
* @param {string} [type='recursive'] – `'all'` | `'recursive'` | `'direct'` | `'indirect'`
|
|
449
|
-
* @returns {Promise<Object<string, { Type: string }>>} Map of CID → pin info.
|
|
450
|
-
*/
|
|
451
|
-
const listKuboPins = async (type = 'recursive') => {
|
|
452
|
-
const kuboUrl = getIpfsApiUrl();
|
|
453
|
-
try {
|
|
454
|
-
const res = await fetchWithTimeout(
|
|
455
|
-
`${kuboUrl}/api/v0/pin/ls?type=${type}`,
|
|
456
|
-
{
|
|
457
|
-
method: 'POST',
|
|
458
|
-
},
|
|
459
|
-
{ kind: 'kubo', label: `IPFS Kubo pin/ls type=${type}` },
|
|
460
|
-
);
|
|
461
|
-
if (!res.ok) {
|
|
462
|
-
const text = await res.text();
|
|
463
|
-
logger.error(`IPFS Kubo pin/ls failed (${res.status}): ${text}`);
|
|
464
|
-
return {};
|
|
465
|
-
}
|
|
466
|
-
const json = await res.json();
|
|
467
|
-
return json.Keys || {};
|
|
468
|
-
} catch (err) {
|
|
469
|
-
logger.warn(`IPFS Kubo pin/ls unreachable: ${err.message}`);
|
|
470
|
-
return {};
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
// ─────────────────────────────────────────────────────────
|
|
474
|
-
// MFS management
|
|
475
|
-
// ─────────────────────────────────────────────────────────
|
|
476
|
-
/**
|
|
477
|
-
* Remove a file or directory from the Kubo MFS (Mutable File System).
|
|
478
|
-
* This cleans up entries visible in the IPFS Web UI "Files" section.
|
|
479
|
-
*
|
|
480
|
-
* @param {string} mfsPath – Full MFS path to remove, e.g. `/pinned/myfile.json`
|
|
481
|
-
* or `/object-layer/itemId`.
|
|
482
|
-
* @param {boolean} [recursive=true] – When `true`, removes directories recursively.
|
|
483
|
-
* @returns {Promise<boolean>} `true` when the removal succeeded or the path didn't exist.
|
|
484
|
-
*/
|
|
485
|
-
const removeMfsPath = async (mfsPath, recursive = true) => {
|
|
486
|
-
const kuboUrl = getIpfsApiUrl();
|
|
487
|
-
try {
|
|
488
|
-
// First check if the path exists via stat; if it doesn't we can return early.
|
|
489
|
-
const statRes = await fetchWithTimeout(
|
|
490
|
-
`${kuboUrl}/api/v0/files/stat?arg=${encodeURIComponent(mfsPath)}`,
|
|
491
|
-
{ method: 'POST' },
|
|
492
|
-
{ kind: 'kubo', label: `IPFS MFS stat ${mfsPath}` },
|
|
493
|
-
);
|
|
494
|
-
if (!statRes.ok) {
|
|
495
|
-
// Path doesn't exist – nothing to remove.
|
|
496
|
-
logger.info(`IPFS MFS rm – path does not exist, skipping: ${mfsPath}`);
|
|
497
|
-
return true;
|
|
498
|
-
}
|
|
499
|
-
const rmRes = await fetchWithTimeout(
|
|
500
|
-
`${kuboUrl}/api/v0/files/rm?arg=${encodeURIComponent(mfsPath)}&force=true${recursive ? '&recursive=true' : ''}`,
|
|
501
|
-
{ method: 'POST' },
|
|
502
|
-
{ kind: 'kubo', label: `IPFS MFS rm ${mfsPath}` },
|
|
503
|
-
);
|
|
504
|
-
if (!rmRes.ok) {
|
|
505
|
-
const text = await rmRes.text();
|
|
506
|
-
logger.warn(`IPFS MFS rm failed (${rmRes.status}): ${text}`);
|
|
507
|
-
return false;
|
|
508
|
-
}
|
|
509
|
-
logger.info(`IPFS MFS rm OK – ${mfsPath}`);
|
|
510
|
-
return true;
|
|
511
|
-
} catch (err) {
|
|
512
|
-
logger.warn(`IPFS MFS rm unreachable: ${err.message}`);
|
|
513
|
-
return false;
|
|
514
|
-
}
|
|
515
|
-
};
|
|
516
|
-
/**
|
|
517
|
-
* Restore a CID into the Kubo MFS at a specific path (e.g. when re-importing a backup).
|
|
518
|
-
* Creates the parent directory if needed, removes any existing entry, then copies the CID.
|
|
519
|
-
*
|
|
520
|
-
* @param {string} cid – IPFS CID to copy into MFS.
|
|
521
|
-
* @param {string} mfsPath – Full destination MFS path, e.g. `/object-layer/sword/sword_data.json`.
|
|
522
|
-
* @returns {Promise<boolean>} `true` when the MFS entry was created successfully.
|
|
523
|
-
*/
|
|
524
|
-
const restoreMfsPath = async (cid, mfsPath) => {
|
|
525
|
-
const kuboUrl = getIpfsApiUrl();
|
|
526
|
-
const destDir = mfsPath.substring(0, mfsPath.lastIndexOf('/')) || '/';
|
|
527
|
-
try {
|
|
528
|
-
await fetchWithTimeout(
|
|
529
|
-
`${kuboUrl}/api/v0/files/mkdir?arg=${encodeURIComponent(destDir)}&parents=true`,
|
|
530
|
-
{ method: 'POST' },
|
|
531
|
-
{ kind: 'kubo', label: `IPFS MFS mkdir ${destDir}` },
|
|
532
|
-
);
|
|
533
|
-
await fetchWithTimeout(
|
|
534
|
-
`${kuboUrl}/api/v0/files/rm?arg=${encodeURIComponent(mfsPath)}&force=true`,
|
|
535
|
-
{ method: 'POST' },
|
|
536
|
-
{ kind: 'kubo', label: `IPFS MFS rm ${mfsPath}` },
|
|
537
|
-
);
|
|
538
|
-
const cpRes = await fetchWithTimeout(
|
|
539
|
-
`${kuboUrl}/api/v0/files/cp?arg=/ipfs/${encodeURIComponent(cid)}&arg=${encodeURIComponent(mfsPath)}`,
|
|
540
|
-
{ method: 'POST' },
|
|
541
|
-
{ kind: 'kubo', label: `IPFS MFS restore ${mfsPath}` },
|
|
542
|
-
);
|
|
543
|
-
if (!cpRes.ok) {
|
|
544
|
-
const text = await cpRes.text();
|
|
545
|
-
logger.warn(`IPFS MFS restore failed (${cpRes.status}): ${text} – ${mfsPath}`);
|
|
546
|
-
return false;
|
|
547
|
-
}
|
|
548
|
-
logger.info(`IPFS MFS restore OK – ${mfsPath} → ${cid}`);
|
|
549
|
-
return true;
|
|
550
|
-
} catch (err) {
|
|
551
|
-
logger.warn(`IPFS MFS restore unreachable: ${err.message}`);
|
|
552
|
-
return false;
|
|
553
|
-
}
|
|
554
|
-
};
|
|
555
|
-
// ─────────────────────────────────────────────────────────
|
|
556
|
-
// Export
|
|
557
|
-
// ─────────────────────────────────────────────────────────
|
|
558
|
-
class IpfsClient {
|
|
559
|
-
static getIpfsApiUrl = getIpfsApiUrl;
|
|
560
|
-
static getClusterApiUrl = getClusterApiUrl;
|
|
561
|
-
static getGatewayUrl = getGatewayUrl;
|
|
562
|
-
static addToIpfs = addToIpfs;
|
|
563
|
-
static addJsonToIpfs = addJsonToIpfs;
|
|
564
|
-
static addBufferToIpfs = addBufferToIpfs;
|
|
565
|
-
static hashContentForIpfs = hashContentForIpfs;
|
|
566
|
-
static hashJsonForIpfs = hashJsonForIpfs;
|
|
567
|
-
static hashBufferForIpfs = hashBufferForIpfs;
|
|
568
|
-
static pinCid = pinCid;
|
|
569
|
-
static unpinCid = unpinCid;
|
|
570
|
-
static getFromIpfs = getFromIpfs;
|
|
571
|
-
static listClusterPins = listClusterPins;
|
|
572
|
-
static listKuboPins = listKuboPins;
|
|
573
|
-
static removeMfsPath = removeMfsPath;
|
|
574
|
-
static restoreMfsPath = restoreMfsPath;
|
|
575
|
-
/**
|
|
576
|
-
* Check whether a single CID is currently pinned on the local Kubo node.
|
|
577
|
-
* Uses the pin/ls?arg=<cid> endpoint which returns only that one pin
|
|
578
|
-
* (much cheaper than fetching the full list).
|
|
579
|
-
*
|
|
580
|
-
* @param {string} cid - IPFS Content Identifier to check.
|
|
581
|
-
* @returns {Promise<boolean>} true when the CID is pinned.
|
|
582
|
-
*/
|
|
583
|
-
static isCidPinned = async (cid) => {
|
|
584
|
-
const kuboUrl = getIpfsApiUrl();
|
|
585
|
-
try {
|
|
586
|
-
const res = await fetchWithTimeout(
|
|
587
|
-
`${kuboUrl}/api/v0/pin/ls?arg=${encodeURIComponent(cid)}&type=all`,
|
|
588
|
-
{ method: 'POST' },
|
|
589
|
-
{ kind: 'kubo', label: `IPFS Kubo pin/ls ${cid}` },
|
|
590
|
-
);
|
|
591
|
-
if (!res.ok) return false;
|
|
592
|
-
const json = await res.json();
|
|
593
|
-
return !!(json.Keys && json.Keys[cid]);
|
|
594
|
-
} catch {
|
|
595
|
-
return false;
|
|
596
|
-
}
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
export { IpfsClient };
|
|
File without changes
|
|
File without changes
|