cyberia 2.99.8 → 3.0.2
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/.env.production +1 -0
- package/.github/workflows/engine-cyberia.cd.yml +1 -0
- package/.github/workflows/gitlab.ci.yml +20 -0
- package/.github/workflows/publish.ci.yml +18 -38
- package/.github/workflows/publish.cyberia.ci.yml +18 -38
- package/.vscode/extensions.json +8 -50
- package/.vscode/settings.json +0 -77
- package/CHANGELOG.md +171 -1
- package/{cli.md → CLI-HELP.md} +49 -44
- package/README.md +139 -0
- package/bin/build.js +7 -15
- package/bin/cyberia.js +385 -71
- package/bin/deploy.js +14 -151
- package/bin/file.js +13 -8
- package/bin/index.js +385 -71
- package/bin/zed.js +63 -2
- package/conf.js +32 -3
- package/deployment.yaml +2 -2
- package/jsdoc.json +1 -2
- 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/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/manifests/deployment/fastapi/initial_data.sh +4 -52
- package/manifests/ipfs/configmap.yaml +64 -0
- package/manifests/ipfs/headless-service.yaml +35 -0
- package/manifests/ipfs/kustomization.yaml +8 -0
- package/manifests/ipfs/statefulset.yaml +149 -0
- package/manifests/ipfs/storage-class.yaml +9 -0
- package/package.json +15 -11
- package/scripts/k3s-node-setup.sh +89 -0
- package/scripts/lxd-vm-setup.sh +23 -0
- package/scripts/rocky-setup.sh +1 -13
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +2 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +7 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +93 -2
- package/src/api/file/file.controller.js +3 -13
- package/src/api/file/file.ref.json +0 -21
- package/src/api/ipfs/ipfs.controller.js +104 -0
- package/src/api/ipfs/ipfs.model.js +71 -0
- package/src/api/ipfs/ipfs.router.js +31 -0
- package/src/api/ipfs/ipfs.service.js +193 -0
- package/src/api/object-layer/README.md +139 -0
- package/src/api/object-layer/object-layer.controller.js +3 -0
- package/src/api/object-layer/object-layer.model.js +15 -1
- package/src/api/object-layer/object-layer.router.js +6 -10
- package/src/api/object-layer/object-layer.service.js +311 -182
- package/src/api/user/user.router.js +0 -47
- package/src/cli/baremetal.js +7 -9
- package/src/cli/cluster.js +95 -152
- package/src/cli/deploy.js +8 -5
- package/src/cli/index.js +31 -31
- package/src/cli/ipfs.js +184 -0
- package/src/cli/lxd.js +192 -237
- package/src/cli/repository.js +4 -1
- package/src/cli/run.js +17 -2
- package/src/client/components/core/Docs.js +92 -6
- package/src/client/components/core/LoadingAnimation.js +2 -3
- package/src/client/components/core/Modal.js +1 -1
- package/src/client/components/core/VanillaJs.js +36 -25
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +4 -5
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +280 -29
- package/src/client/services/ipfs/ipfs.service.js +144 -0
- package/src/client/services/object-layer/object-layer.management.js +161 -8
- package/src/client/services/user/user.management.js +0 -5
- package/src/client/services/user/user.service.js +1 -1
- package/src/index.js +12 -1
- package/src/runtime/express/Express.js +4 -3
- package/src/server/auth.js +18 -18
- package/src/server/client-build-docs.js +178 -41
- package/src/server/conf.js +1 -1
- package/src/server/ipfs-client.js +433 -0
- package/src/server/logger.js +22 -10
- package/src/server/object-layer.js +649 -18
- package/src/server/semantic-layer-generator.js +1083 -0
- package/src/server/shape-generator.js +952 -0
- package/test/shape-generator.test.js +457 -0
- package/.vscode/zed.keymap.json +0 -39
- package/.vscode/zed.settings.json +0 -20
- package/bin/ssl.js +0 -63
- package/manifests/lxd/underpost-setup.sh +0 -163
package/scripts/rocky-setup.sh
CHANGED
|
@@ -103,16 +103,4 @@ echo "[+] Cleanup: remove unnecessary packages and old metadata"
|
|
|
103
103
|
dnf autoremove -y
|
|
104
104
|
dnf clean all
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Installation complete.
|
|
109
|
-
- To allow SSH access (if this is a VM or server), open port 22 in firewalld:
|
|
110
|
-
sudo firewall-cmd --add-service=ssh --permanent && sudo firewall-cmd --reload
|
|
111
|
-
- If you installed Development Tools, you will have gcc, make, etc.
|
|
112
|
-
|
|
113
|
-
Examples:
|
|
114
|
-
sudo ./scripts/rocky-setup.sh --install-dev
|
|
115
|
-
INSTALL_DEV=1 sudo ./scripts/rocky-setup.sh
|
|
116
|
-
|
|
117
|
-
Customize PACKAGES=(...) inside this script according to your needs (docker, podman, kube, mssql-tools, etc.).
|
|
118
|
-
EOF
|
|
106
|
+
echo "[+] Setup complete! Rocky Linux is now configured with the recommended base packages."
|
|
@@ -51,6 +51,8 @@ const AtlasSpriteSheetController = {
|
|
|
51
51
|
},
|
|
52
52
|
get: async (req, res, options) => {
|
|
53
53
|
try {
|
|
54
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
55
|
+
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
54
56
|
const { page, limit } = req.query;
|
|
55
57
|
const result = await AtlasSpriteSheetService.get(
|
|
56
58
|
{ ...req, query: { ...req.query, page: parseInt(page), limit: parseInt(limit) } },
|
|
@@ -75,6 +75,7 @@ const DirectionFramesSchema = new Schema(
|
|
|
75
75
|
/**
|
|
76
76
|
* @typedef {Object} AtlasSpriteSheet
|
|
77
77
|
* @property {Types.ObjectId} fileId - Reference to File document (consolidated PNG)
|
|
78
|
+
* @property {string} [cid] - IPFS Content Identifier for the atlas PNG
|
|
78
79
|
* @property {Object} metadata - Atlas sprite sheet metadata
|
|
79
80
|
* @property {string} metadata.itemKey - Item identifier key for texture reference
|
|
80
81
|
* @property {number} metadata.atlasWidth - Total atlas width in pixels
|
|
@@ -92,6 +93,11 @@ const AtlasSpriteSheetSchema = new Schema(
|
|
|
92
93
|
ref: 'File',
|
|
93
94
|
required: true,
|
|
94
95
|
},
|
|
96
|
+
cid: {
|
|
97
|
+
type: String,
|
|
98
|
+
default: '',
|
|
99
|
+
trim: true,
|
|
100
|
+
},
|
|
95
101
|
metadata: {
|
|
96
102
|
itemKey: { type: String, required: true, trim: true },
|
|
97
103
|
atlasWidth: { type: Number, required: true, min: 1 },
|
|
@@ -129,6 +135,7 @@ const AtlasSpriteSheetDto = {
|
|
|
129
135
|
return {
|
|
130
136
|
_id: 1,
|
|
131
137
|
fileId: 1,
|
|
138
|
+
cid: 1,
|
|
132
139
|
metadata: 1,
|
|
133
140
|
createdAt: 1,
|
|
134
141
|
updatedAt: 1,
|
|
@@ -4,6 +4,8 @@ import { DataQuery } from '../../server/data-query.js';
|
|
|
4
4
|
import { AtlasSpriteSheetGenerator } from '../../server/atlas-sprite-sheet-generator.js';
|
|
5
5
|
import { FileFactory } from '../file/file.service.js';
|
|
6
6
|
import { AtlasSpriteSheetDto } from './atlas-sprite-sheet.model.js';
|
|
7
|
+
import { IpfsClient } from '../../server/ipfs-client.js';
|
|
8
|
+
import { createPinRecord, removePinRecordsAndUnpin } from '../ipfs/ipfs.service.js';
|
|
7
9
|
|
|
8
10
|
const logger = loggerFactory(import.meta);
|
|
9
11
|
|
|
@@ -39,6 +41,27 @@ const AtlasSpriteSheetService = {
|
|
|
39
41
|
|
|
40
42
|
const fileDoc = await new File(FileFactory.create(buffer, `${itemKey}.png`)).save();
|
|
41
43
|
|
|
44
|
+
// Add atlas PNG to IPFS and obtain its CID
|
|
45
|
+
let atlasCid = '';
|
|
46
|
+
try {
|
|
47
|
+
const ipfsResult = await IpfsClient.addBufferToIpfs(
|
|
48
|
+
buffer,
|
|
49
|
+
`${itemKey}_atlas_sprite_sheet.png`,
|
|
50
|
+
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
51
|
+
);
|
|
52
|
+
if (ipfsResult) {
|
|
53
|
+
atlasCid = ipfsResult.cid;
|
|
54
|
+
// Create pin record for the authenticated user (when available)
|
|
55
|
+
const userId = req.auth && req.auth.user ? req.auth.user._id : undefined;
|
|
56
|
+
if (userId) {
|
|
57
|
+
await createPinRecord({ cid: atlasCid, userId, options });
|
|
58
|
+
}
|
|
59
|
+
logger.info(`Atlas sprite sheet pinned to IPFS – CID: ${atlasCid}`);
|
|
60
|
+
}
|
|
61
|
+
} catch (ipfsError) {
|
|
62
|
+
logger.warn('Failed to add atlas sprite sheet to IPFS:', ipfsError.message);
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
let atlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
|
|
43
66
|
|
|
44
67
|
if (atlasDoc) {
|
|
@@ -47,16 +70,20 @@ const AtlasSpriteSheetService = {
|
|
|
47
70
|
await File.findByIdAndDelete(atlasDoc.fileId);
|
|
48
71
|
}
|
|
49
72
|
atlasDoc.fileId = fileDoc._id;
|
|
73
|
+
atlasDoc.cid = atlasCid;
|
|
50
74
|
atlasDoc.metadata = metadata;
|
|
51
75
|
await atlasDoc.save();
|
|
52
76
|
} else {
|
|
53
77
|
atlasDoc = await new AtlasSpriteSheet({
|
|
54
78
|
fileId: fileDoc._id,
|
|
79
|
+
cid: atlasCid,
|
|
55
80
|
metadata,
|
|
56
81
|
}).save();
|
|
57
82
|
}
|
|
58
83
|
|
|
59
84
|
objectLayer.atlasSpriteSheetId = atlasDoc._id;
|
|
85
|
+
objectLayer.data.atlasSpriteSheetCid = atlasCid;
|
|
86
|
+
objectLayer.markModified('data.atlasSpriteSheetCid');
|
|
60
87
|
await objectLayer.save();
|
|
61
88
|
|
|
62
89
|
return atlasDoc;
|
|
@@ -78,12 +105,30 @@ const AtlasSpriteSheetService = {
|
|
|
78
105
|
if (objectLayer.atlasSpriteSheetId) {
|
|
79
106
|
const atlasDoc = await AtlasSpriteSheet.findById(objectLayer.atlasSpriteSheetId);
|
|
80
107
|
if (atlasDoc) {
|
|
108
|
+
// Remove pin records and unpin atlas CID from IPFS
|
|
109
|
+
const atlasCid = atlasDoc.cid || objectLayer.data.atlasSpriteSheetCid;
|
|
110
|
+
if (atlasCid) {
|
|
111
|
+
try {
|
|
112
|
+
await removePinRecordsAndUnpin(atlasCid, options);
|
|
113
|
+
// Remove the MFS entry for the atlas sprite sheet PNG
|
|
114
|
+
const itemId = objectLayer.data.item.id;
|
|
115
|
+
await IpfsClient.removeMfsPath(`/object-layer/${itemId}/${itemId}_atlas_sprite_sheet.png`);
|
|
116
|
+
logger.info(`Cleaned up IPFS atlas CID ${atlasCid} for ObjectLayer ${objectLayer._id}`);
|
|
117
|
+
} catch (ipfsErr) {
|
|
118
|
+
logger.warn(`Failed to clean up IPFS atlas CID ${atlasCid}: ${ipfsErr.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Delete the atlas File document from MongoDB
|
|
81
123
|
if (atlasDoc.fileId) {
|
|
82
124
|
await File.findByIdAndDelete(atlasDoc.fileId);
|
|
83
125
|
}
|
|
126
|
+
// Delete the AtlasSpriteSheet document itself
|
|
84
127
|
await AtlasSpriteSheet.findByIdAndDelete(atlasDoc._id);
|
|
85
128
|
}
|
|
86
129
|
objectLayer.atlasSpriteSheetId = undefined;
|
|
130
|
+
objectLayer.data.atlasSpriteSheetCid = '';
|
|
131
|
+
objectLayer.markModified('data.atlasSpriteSheetCid');
|
|
87
132
|
await objectLayer.save();
|
|
88
133
|
}
|
|
89
134
|
|
|
@@ -130,8 +175,54 @@ const AtlasSpriteSheetService = {
|
|
|
130
175
|
/** @type {import('./atlas-sprite-sheet.model.js').AtlasSpriteSheetModel} */
|
|
131
176
|
const AtlasSpriteSheet =
|
|
132
177
|
DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.AtlasSpriteSheet;
|
|
133
|
-
|
|
134
|
-
|
|
178
|
+
/** @type {import('../file/file.model.js').FileModel} */
|
|
179
|
+
const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
|
|
180
|
+
|
|
181
|
+
if (req.params.id) {
|
|
182
|
+
const atlasDoc = await AtlasSpriteSheet.findById(req.params.id);
|
|
183
|
+
if (!atlasDoc) return null;
|
|
184
|
+
|
|
185
|
+
// Remove pin records and unpin atlas CID from IPFS
|
|
186
|
+
if (atlasDoc.cid) {
|
|
187
|
+
try {
|
|
188
|
+
await removePinRecordsAndUnpin(atlasDoc.cid, options);
|
|
189
|
+
if (atlasDoc.metadata?.itemKey) {
|
|
190
|
+
const itemKey = atlasDoc.metadata.itemKey;
|
|
191
|
+
await IpfsClient.removeMfsPath(`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`);
|
|
192
|
+
}
|
|
193
|
+
logger.info(`Cleaned up IPFS atlas CID ${atlasDoc.cid} for AtlasSpriteSheet ${atlasDoc._id}`);
|
|
194
|
+
} catch (ipfsErr) {
|
|
195
|
+
logger.warn(`Failed to clean up IPFS atlas CID ${atlasDoc.cid}: ${ipfsErr.message}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Delete the referenced File document (the atlas PNG blob)
|
|
200
|
+
if (atlasDoc.fileId) {
|
|
201
|
+
await File.findByIdAndDelete(atlasDoc.fileId);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return await AtlasSpriteSheet.findByIdAndDelete(req.params.id);
|
|
205
|
+
} else {
|
|
206
|
+
// Bulk delete: iterate each atlas to clean up File, IPFS pins, and pin records
|
|
207
|
+
const allAtlases = await AtlasSpriteSheet.find({});
|
|
208
|
+
for (const atlasDoc of allAtlases) {
|
|
209
|
+
try {
|
|
210
|
+
if (atlasDoc.cid) {
|
|
211
|
+
await removePinRecordsAndUnpin(atlasDoc.cid, options);
|
|
212
|
+
if (atlasDoc.metadata?.itemKey) {
|
|
213
|
+
const itemKey = atlasDoc.metadata.itemKey;
|
|
214
|
+
await IpfsClient.removeMfsPath(`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (atlasDoc.fileId) {
|
|
218
|
+
await File.findByIdAndDelete(atlasDoc.fileId);
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
logger.error(`Failed to clean up AtlasSpriteSheet ${atlasDoc._id} during bulk delete: ${err.message}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return await AtlasSpriteSheet.deleteMany();
|
|
225
|
+
}
|
|
135
226
|
},
|
|
136
227
|
};
|
|
137
228
|
|
|
@@ -19,22 +19,12 @@ const FileController = {
|
|
|
19
19
|
},
|
|
20
20
|
get: async (req, res, options) => {
|
|
21
21
|
try {
|
|
22
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
23
|
+
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
22
24
|
const result = await FileService.get(req, res, options);
|
|
23
25
|
if (result instanceof Buffer) {
|
|
24
|
-
|
|
25
|
-
process.env.NODE_ENV === 'development' ||
|
|
26
|
-
req.hostname === options.host ||
|
|
27
|
-
(options.origins && options.origins.find((o) => o.match(req.hostname)))
|
|
28
|
-
) {
|
|
29
|
-
res.set('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
30
|
-
return res.status(200).end(result);
|
|
31
|
-
}
|
|
32
|
-
return res.status(403).json({
|
|
33
|
-
status: 'error',
|
|
34
|
-
message: 'Forbidden',
|
|
35
|
-
});
|
|
26
|
+
return res.status(200).end(result);
|
|
36
27
|
}
|
|
37
|
-
|
|
38
28
|
return res.status(200).json({
|
|
39
29
|
status: 'success',
|
|
40
30
|
data: result,
|
|
@@ -5,27 +5,6 @@
|
|
|
5
5
|
"logo": true
|
|
6
6
|
}
|
|
7
7
|
},
|
|
8
|
-
{
|
|
9
|
-
"api": "cyberia-biome",
|
|
10
|
-
"model": {
|
|
11
|
-
"fileId": true,
|
|
12
|
-
"topLevelColorFileId": true
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"api": "cyberia-tile",
|
|
17
|
-
"model": {
|
|
18
|
-
"fileId": true
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
"api": "cyberia-world",
|
|
23
|
-
"model": {
|
|
24
|
-
"adjacentFace": {
|
|
25
|
-
"fileId": true
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
8
|
{
|
|
30
9
|
"api": "document",
|
|
31
10
|
"model": {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { loggerFactory } from '../../server/logger.js';
|
|
2
|
+
import { IpfsService } from './ipfs.service.js';
|
|
3
|
+
|
|
4
|
+
const logger = loggerFactory(import.meta);
|
|
5
|
+
|
|
6
|
+
const IpfsController = {
|
|
7
|
+
post: async (req, res, options) => {
|
|
8
|
+
try {
|
|
9
|
+
const result = await IpfsService.post(req, res, options);
|
|
10
|
+
return res.status(200).json({
|
|
11
|
+
status: 'success',
|
|
12
|
+
data: result,
|
|
13
|
+
});
|
|
14
|
+
} catch (error) {
|
|
15
|
+
logger.error(error, error.stack);
|
|
16
|
+
return res.status(400).json({
|
|
17
|
+
status: 'error',
|
|
18
|
+
message: error.message,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
get: async (req, res, options) => {
|
|
23
|
+
try {
|
|
24
|
+
const { page, limit } = req.query;
|
|
25
|
+
const result = await IpfsService.get(
|
|
26
|
+
{ ...req, query: { ...req.query, page: parseInt(page), limit: parseInt(limit) } },
|
|
27
|
+
res,
|
|
28
|
+
options,
|
|
29
|
+
);
|
|
30
|
+
return res.status(200).json({
|
|
31
|
+
status: 'success',
|
|
32
|
+
data: result,
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
logger.error(error, error.stack);
|
|
36
|
+
return res.status(400).json({
|
|
37
|
+
status: 'error',
|
|
38
|
+
message: error.message,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
put: async (req, res, options) => {
|
|
43
|
+
try {
|
|
44
|
+
const result = await IpfsService.put(req, res, options);
|
|
45
|
+
return res.status(200).json({
|
|
46
|
+
status: 'success',
|
|
47
|
+
data: result,
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logger.error(error, error.stack);
|
|
51
|
+
return res.status(400).json({
|
|
52
|
+
status: 'error',
|
|
53
|
+
message: error.message,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
delete: async (req, res, options) => {
|
|
58
|
+
try {
|
|
59
|
+
const result = await IpfsService.delete(req, res, options);
|
|
60
|
+
return res.status(200).json({
|
|
61
|
+
status: 'success',
|
|
62
|
+
data: result,
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logger.error(error, error.stack);
|
|
66
|
+
return res.status(400).json({
|
|
67
|
+
status: 'error',
|
|
68
|
+
message: error.message,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
pin: async (req, res, options) => {
|
|
73
|
+
try {
|
|
74
|
+
const result = await IpfsService.pin(req, res, options);
|
|
75
|
+
return res.status(200).json({
|
|
76
|
+
status: 'success',
|
|
77
|
+
data: result,
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
logger.error(error, error.stack);
|
|
81
|
+
return res.status(400).json({
|
|
82
|
+
status: 'error',
|
|
83
|
+
message: error.message,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
unpin: async (req, res, options) => {
|
|
88
|
+
try {
|
|
89
|
+
const result = await IpfsService.unpin(req, res, options);
|
|
90
|
+
return res.status(200).json({
|
|
91
|
+
status: 'success',
|
|
92
|
+
data: result,
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
logger.error(error, error.stack);
|
|
96
|
+
return res.status(400).json({
|
|
97
|
+
status: 'error',
|
|
98
|
+
message: error.message,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export { IpfsController };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mongoose model for IPFS API – a general-purpose pin record that relates
|
|
3
|
+
* a user to a CID.
|
|
4
|
+
*
|
|
5
|
+
* @module src/api/ipfs/ipfs.model.js
|
|
6
|
+
* @namespace IpfsModel
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Schema, model } from 'mongoose';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} Ipfs
|
|
13
|
+
* @property {string} cid – IPFS Content Identifier (CIDv0 or CIDv1).
|
|
14
|
+
* @property {Types.ObjectId} userId – Reference to the User who owns / requested the pin.
|
|
15
|
+
* @property {Date} createdAt – Auto-managed by Mongoose.
|
|
16
|
+
* @property {Date} updatedAt – Auto-managed by Mongoose.
|
|
17
|
+
* @memberof IpfsModel
|
|
18
|
+
*/
|
|
19
|
+
const IpfsSchema = new Schema(
|
|
20
|
+
{
|
|
21
|
+
cid: {
|
|
22
|
+
type: String,
|
|
23
|
+
required: true,
|
|
24
|
+
trim: true,
|
|
25
|
+
},
|
|
26
|
+
userId: {
|
|
27
|
+
type: Schema.Types.ObjectId,
|
|
28
|
+
ref: 'User',
|
|
29
|
+
required: true,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
timestamps: true,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Compound index: one pin record per user + CID pair.
|
|
38
|
+
IpfsSchema.index({ cid: 1, userId: 1 }, { unique: true });
|
|
39
|
+
|
|
40
|
+
// Fast look-ups by user.
|
|
41
|
+
IpfsSchema.index({ userId: 1 });
|
|
42
|
+
|
|
43
|
+
// Fast look-ups by CID.
|
|
44
|
+
IpfsSchema.index({ cid: 1 });
|
|
45
|
+
|
|
46
|
+
const IpfsModel = model('Ipfs', IpfsSchema);
|
|
47
|
+
|
|
48
|
+
const ProviderSchema = IpfsSchema;
|
|
49
|
+
|
|
50
|
+
const IpfsDto = {
|
|
51
|
+
select: {
|
|
52
|
+
get: () => {
|
|
53
|
+
return {
|
|
54
|
+
_id: 1,
|
|
55
|
+
cid: 1,
|
|
56
|
+
userId: 1,
|
|
57
|
+
createdAt: 1,
|
|
58
|
+
updatedAt: 1,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
populate: {
|
|
63
|
+
user: () => ({
|
|
64
|
+
path: 'userId',
|
|
65
|
+
model: 'User',
|
|
66
|
+
select: '_id username email role',
|
|
67
|
+
}),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export { IpfsSchema, IpfsModel, ProviderSchema, IpfsDto };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { adminGuard } from '../../server/auth.js';
|
|
2
|
+
import { loggerFactory } from '../../server/logger.js';
|
|
3
|
+
import { IpfsController } from './ipfs.controller.js';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
|
|
6
|
+
const logger = loggerFactory(import.meta);
|
|
7
|
+
|
|
8
|
+
const IpfsRouter = (options) => {
|
|
9
|
+
const router = express.Router();
|
|
10
|
+
const authMiddleware = options.authMiddleware;
|
|
11
|
+
router.post(`/pin`, authMiddleware, adminGuard, async (req, res) => await IpfsController.pin(req, res, options));
|
|
12
|
+
router.delete(
|
|
13
|
+
`/pin/:cid`,
|
|
14
|
+
authMiddleware,
|
|
15
|
+
adminGuard,
|
|
16
|
+
async (req, res) => await IpfsController.unpin(req, res, options),
|
|
17
|
+
);
|
|
18
|
+
router.post(`/:id`, authMiddleware, async (req, res) => await IpfsController.post(req, res, options));
|
|
19
|
+
router.post(`/`, authMiddleware, async (req, res) => await IpfsController.post(req, res, options));
|
|
20
|
+
router.get(`/:id`, authMiddleware, async (req, res) => await IpfsController.get(req, res, options));
|
|
21
|
+
router.get(`/`, authMiddleware, async (req, res) => await IpfsController.get(req, res, options));
|
|
22
|
+
router.put(`/:id`, authMiddleware, async (req, res) => await IpfsController.put(req, res, options));
|
|
23
|
+
router.put(`/`, authMiddleware, async (req, res) => await IpfsController.put(req, res, options));
|
|
24
|
+
router.delete(`/:id`, authMiddleware, adminGuard, async (req, res) => await IpfsController.delete(req, res, options));
|
|
25
|
+
router.delete(`/`, authMiddleware, adminGuard, async (req, res) => await IpfsController.delete(req, res, options));
|
|
26
|
+
return router;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ApiRouter = IpfsRouter;
|
|
30
|
+
|
|
31
|
+
export { ApiRouter, IpfsRouter };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { DataBaseProvider } from '../../db/DataBaseProvider.js';
|
|
2
|
+
import { loggerFactory } from '../../server/logger.js';
|
|
3
|
+
import { DataQuery } from '../../server/data-query.js';
|
|
4
|
+
import { IpfsClient } from '../../server/ipfs-client.js';
|
|
5
|
+
import { IpfsDto } from './ipfs.model.js';
|
|
6
|
+
|
|
7
|
+
const logger = loggerFactory(import.meta);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create (or upsert) an IPFS pin record for a given user + CID pair.
|
|
11
|
+
* This is a helper consumed by other services (ObjectLayer, AtlasSpriteSheet, …)
|
|
12
|
+
* so they don't need to know about the Ipfs model directly.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} opts
|
|
15
|
+
* @param {string} opts.cid – IPFS Content Identifier.
|
|
16
|
+
* @param {string} opts.userId – Mongoose ObjectId string of the owning user.
|
|
17
|
+
* @param {object} opts.options – Router options ({ host, path }) for DB lookup.
|
|
18
|
+
* @returns {Promise<import('mongoose').Document>}
|
|
19
|
+
*/
|
|
20
|
+
const createPinRecord = async ({ cid, userId, options }) => {
|
|
21
|
+
const Ipfs = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Ipfs;
|
|
22
|
+
|
|
23
|
+
// Upsert: if a record for this user + CID already exists, just touch it.
|
|
24
|
+
const record = await Ipfs.findOneAndUpdate(
|
|
25
|
+
{ cid, userId },
|
|
26
|
+
{ cid, userId },
|
|
27
|
+
{ upsert: true, new: true, setDefaultsOnInsert: true },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
logger.info(`IPFS pin record upserted – CID: ${cid}, userId: ${userId}`);
|
|
31
|
+
return record;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Remove all DB pin records for a CID, then best-effort unpin from IPFS node/cluster.
|
|
36
|
+
* Always deletes the DB records first so that even if the IPFS node is unreachable
|
|
37
|
+
* the database stays clean.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} cid – IPFS Content Identifier to clean up.
|
|
40
|
+
* @param {object} options – Router options ({ host, path }) for DB lookup.
|
|
41
|
+
* @returns {Promise<void>}
|
|
42
|
+
*/
|
|
43
|
+
const removePinRecordsAndUnpin = async (cid, options) => {
|
|
44
|
+
const Ipfs = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Ipfs;
|
|
45
|
+
|
|
46
|
+
// 1. Remove all DB pin records for this CID first
|
|
47
|
+
await Ipfs.deleteMany({ cid });
|
|
48
|
+
logger.info(`Removed all IPFS pin records for CID: ${cid}`);
|
|
49
|
+
|
|
50
|
+
// 2. Best-effort unpin from IPFS node/cluster (ignore "not pinned" errors)
|
|
51
|
+
try {
|
|
52
|
+
await IpfsClient.unpinCid(cid);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
logger.warn(`Best-effort IPFS unpin failed for CID ${cid}: ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const IpfsService = {
|
|
59
|
+
/** Expose helpers so other modules can import them directly. */
|
|
60
|
+
createPinRecord,
|
|
61
|
+
removePinRecordsAndUnpin,
|
|
62
|
+
|
|
63
|
+
// ──────────────────────────────────────────────
|
|
64
|
+
// Standard CRUD
|
|
65
|
+
// ──────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
post: async (req, res, options) => {
|
|
68
|
+
/** @type {import('./ipfs.model.js').IpfsModel} */
|
|
69
|
+
const Ipfs = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Ipfs;
|
|
70
|
+
|
|
71
|
+
// Accept { cid, userId? } in body.
|
|
72
|
+
// If userId is omitted, fall back to the authenticated user.
|
|
73
|
+
const body = { ...req.body };
|
|
74
|
+
if (!body.userId && req.auth && req.auth.user) {
|
|
75
|
+
body.userId = req.auth.user._id;
|
|
76
|
+
}
|
|
77
|
+
// Strip pinType if sent by legacy clients
|
|
78
|
+
delete body.pinType;
|
|
79
|
+
|
|
80
|
+
return await new Ipfs(body).save();
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
get: async (req, res, options) => {
|
|
84
|
+
/** @type {import('./ipfs.model.js').IpfsModel} */
|
|
85
|
+
const Ipfs = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Ipfs;
|
|
86
|
+
|
|
87
|
+
if (req.params.id) {
|
|
88
|
+
return await Ipfs.findById(req.params.id).select(IpfsDto.select.get()).populate(IpfsDto.populate.user());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { query, sort, skip, limit, page } = DataQuery.parse(req.query);
|
|
92
|
+
|
|
93
|
+
const [data, total] = await Promise.all([
|
|
94
|
+
Ipfs.find(query)
|
|
95
|
+
.select(IpfsDto.select.get())
|
|
96
|
+
.sort(sort)
|
|
97
|
+
.limit(limit)
|
|
98
|
+
.skip(skip)
|
|
99
|
+
.populate(IpfsDto.populate.user()),
|
|
100
|
+
Ipfs.countDocuments(query),
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
const totalPages = Math.ceil(total / limit);
|
|
104
|
+
return { data, total, page, totalPages };
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
put: async (req, res, options) => {
|
|
108
|
+
/** @type {import('./ipfs.model.js').IpfsModel} */
|
|
109
|
+
const Ipfs = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Ipfs;
|
|
110
|
+
return await Ipfs.findByIdAndUpdate(req.params.id, req.body, { new: true });
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
delete: async (req, res, options) => {
|
|
114
|
+
/** @type {import('./ipfs.model.js').IpfsModel} */
|
|
115
|
+
const Ipfs = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Ipfs;
|
|
116
|
+
|
|
117
|
+
if (req.params.id) {
|
|
118
|
+
const record = await Ipfs.findById(req.params.id);
|
|
119
|
+
if (record) {
|
|
120
|
+
// Remove DB record first, then best-effort unpin
|
|
121
|
+
await Ipfs.findByIdAndDelete(req.params.id);
|
|
122
|
+
try {
|
|
123
|
+
// Only unpin from IPFS if no other records reference this CID
|
|
124
|
+
const remaining = await Ipfs.countDocuments({ cid: record.cid });
|
|
125
|
+
if (remaining === 0) {
|
|
126
|
+
await IpfsClient.unpinCid(record.cid);
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
logger.warn(`Failed to unpin CID ${record.cid} from IPFS node: ${err.message}`);
|
|
130
|
+
}
|
|
131
|
+
return record;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return await Ipfs.deleteMany();
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// ──────────────────────────────────────────────
|
|
140
|
+
// Pin / Unpin helpers (called via controller)
|
|
141
|
+
// ──────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* POST /ipfs/pin – add content to IPFS, pin it, and create a DB record.
|
|
145
|
+
* Body: { data: <string|object>, userId? }
|
|
146
|
+
*/
|
|
147
|
+
pin: async (req, res, options) => {
|
|
148
|
+
const userId = req.body.userId || (req.auth && req.auth.user ? req.auth.user._id : undefined);
|
|
149
|
+
if (!userId) throw new Error('userId is required to create a pin record');
|
|
150
|
+
|
|
151
|
+
const content = typeof req.body.data === 'string' ? req.body.data : JSON.stringify(req.body.data);
|
|
152
|
+
const result = await IpfsClient.addToIpfs(Buffer.from(content, 'utf-8'), req.body.filename || 'data');
|
|
153
|
+
|
|
154
|
+
if (!result) throw new Error('IPFS node is unreachable – content was not pinned');
|
|
155
|
+
|
|
156
|
+
const record = await createPinRecord({
|
|
157
|
+
cid: result.cid,
|
|
158
|
+
userId,
|
|
159
|
+
options,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return { cid: result.cid, size: result.size, record };
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* DELETE /ipfs/pin/:cid – unpin a CID and remove the DB record for the current user.
|
|
167
|
+
*/
|
|
168
|
+
unpin: async (req, res, options) => {
|
|
169
|
+
const Ipfs = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Ipfs;
|
|
170
|
+
const userId = req.auth && req.auth.user ? req.auth.user._id : undefined;
|
|
171
|
+
const cid = req.params.cid || req.params.id;
|
|
172
|
+
|
|
173
|
+
const record = await Ipfs.findOne({ cid, ...(userId ? { userId } : {}) });
|
|
174
|
+
if (!record) throw new Error(`No pin record found for CID ${cid}`);
|
|
175
|
+
|
|
176
|
+
// Remove DB record first
|
|
177
|
+
await Ipfs.findByIdAndDelete(record._id);
|
|
178
|
+
|
|
179
|
+
// Only unpin from the IPFS node when nobody else has a record for this CID
|
|
180
|
+
const remaining = await Ipfs.countDocuments({ cid });
|
|
181
|
+
if (remaining === 0) {
|
|
182
|
+
try {
|
|
183
|
+
await IpfsClient.unpinCid(cid);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
logger.warn(`Best-effort IPFS unpin failed for CID ${cid}: ${err.message}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { success: true, cid };
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export { IpfsService, createPinRecord, removePinRecordsAndUnpin };
|