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