cyberia 3.0.1 → 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/.github/workflows/engine-cyberia.cd.yml +1 -0
- package/CHANGELOG.md +56 -1
- package/CLI-HELP.md +2 -4
- package/README.md +139 -0
- package/bin/build.js +5 -0
- package/bin/cyberia.js +385 -71
- package/bin/deploy.js +18 -26
- package/bin/file.js +3 -0
- package/bin/index.js +385 -71
- package/conf.js +32 -3
- package/deployment.yaml +2 -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/ipfs/configmap.yaml +7 -0
- package/package.json +8 -8
- 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/cli/cluster.js +30 -38
- package/src/cli/index.js +0 -1
- package/src/cli/run.js +14 -0
- package/src/client/components/core/LoadingAnimation.js +2 -3
- package/src/client/components/core/Modal.js +1 -1
- 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/index.js +1 -1
- package/src/runtime/express/Express.js +1 -1
- package/src/server/auth.js +18 -18
- package/src/server/ipfs-client.js +433 -0
- 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/bin/ssl.js +0 -63
|
@@ -1,18 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object Layer service module for handling CRUD operations on object layer assets.
|
|
3
|
+
* Provides REST API handlers for creating, reading, updating, and deleting object layers
|
|
4
|
+
* including frame image management, metadata processing, and WebP animation generation.
|
|
5
|
+
*
|
|
6
|
+
* Delegates shared object layer creation and update logic to {@link ObjectLayerEngine} in
|
|
7
|
+
* `src/server/object-layer.js` to keep a single source of truth shared with the Cyberia CLI.
|
|
8
|
+
*
|
|
9
|
+
* @module src/api/object-layer/object-layer.service.js
|
|
10
|
+
* @namespace CyberiaObjectLayerService
|
|
11
|
+
*/
|
|
12
|
+
|
|
1
13
|
import { DataBaseProvider } from '../../db/DataBaseProvider.js';
|
|
2
14
|
import { loggerFactory } from '../../server/logger.js';
|
|
15
|
+
import { ObjectLayerRenderFramesDto } from '../object-layer-render-frames/object-layer-render-frames.model.js';
|
|
3
16
|
import { FileFactory } from '../file/file.service.js';
|
|
4
17
|
import fs from 'fs-extra';
|
|
5
|
-
import crypto from 'crypto';
|
|
6
|
-
import stringify from 'fast-json-stable-stringify';
|
|
7
18
|
import { ObjectLayerDto } from './object-layer.model.js';
|
|
8
19
|
import { ObjectLayerEngine } from '../../server/object-layer.js';
|
|
9
20
|
import { shellExec } from '../../server/process.js';
|
|
10
21
|
import { DataQuery } from '../../server/data-query.js';
|
|
11
22
|
import { AtlasSpriteSheetService } from '../atlas-sprite-sheet/atlas-sprite-sheet.service.js';
|
|
12
|
-
|
|
23
|
+
import { IpfsClient } from '../../server/ipfs-client.js';
|
|
24
|
+
import { createPinRecord, removePinRecordsAndUnpin } from '../ipfs/ipfs.service.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Logger instance for this module.
|
|
28
|
+
* @type {Function}
|
|
29
|
+
* @memberof CyberiaObjectLayerService
|
|
30
|
+
* @private
|
|
31
|
+
*/
|
|
13
32
|
const logger = loggerFactory(import.meta);
|
|
14
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Object Layer Service providing REST API handlers for object layer operations.
|
|
36
|
+
* @namespace CyberiaObjectLayerService.ObjectLayerService
|
|
37
|
+
* @memberof CyberiaObjectLayerService
|
|
38
|
+
*/
|
|
15
39
|
const ObjectLayerService = {
|
|
40
|
+
/**
|
|
41
|
+
* POST handler for creating object layers and uploading frame images.
|
|
42
|
+
*
|
|
43
|
+
* Supports three sub-routes:
|
|
44
|
+
* - `/frame-image/:itemType/:itemId/:directionCode` — Upload PNG frame images for a direction.
|
|
45
|
+
* - `/metadata/:itemType/:itemId` — Create an object layer from uploaded frames and metadata.
|
|
46
|
+
* - Default — Create an object layer directly from the request body.
|
|
47
|
+
*
|
|
48
|
+
* The `/metadata` and default routes delegate to {@link ObjectLayerEngine.createObjectLayerDocuments}
|
|
49
|
+
* for centralized document creation, atlas generation, SHA-256 computation, and IPFS pinning.
|
|
50
|
+
*
|
|
51
|
+
* @async
|
|
52
|
+
* @function post
|
|
53
|
+
* @memberof CyberiaObjectLayerService.ObjectLayerService
|
|
54
|
+
* @param {Object} req - Express request object.
|
|
55
|
+
* @param {Object} res - Express response object.
|
|
56
|
+
* @param {Object} options - Server options containing host and path.
|
|
57
|
+
* @param {string} options.host - The deployment host.
|
|
58
|
+
* @param {string} options.path - The deployment path.
|
|
59
|
+
* @returns {Promise<Object>} The created object layer document or frame upload result.
|
|
60
|
+
* @throws {Error} If file validation fails or required parameters are missing.
|
|
61
|
+
*/
|
|
16
62
|
post: async (req, res, options) => {
|
|
17
63
|
/** @type {import('./object-layer.model.js').ObjectLayerModel} */
|
|
18
64
|
|
|
@@ -88,8 +134,10 @@ const ObjectLayerService = {
|
|
|
88
134
|
}
|
|
89
135
|
|
|
90
136
|
if (req.path.startsWith('/metadata')) {
|
|
91
|
-
const
|
|
92
|
-
const
|
|
137
|
+
const itemType = req.params.itemType;
|
|
138
|
+
const itemId = req.params.itemId;
|
|
139
|
+
const folder = `./src/client/public/cyberia/assets/${itemType}/${itemId}`;
|
|
140
|
+
const publicFolder = `./public/${options.host}${options.path}/assets/${itemType}/${itemId}`;
|
|
93
141
|
|
|
94
142
|
// Ensure both folders exist
|
|
95
143
|
if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
|
|
@@ -100,113 +148,90 @@ const ObjectLayerService = {
|
|
|
100
148
|
fs.writeFileSync(`${folder}/metadata.json`, metadataContent);
|
|
101
149
|
fs.writeFileSync(`${publicFolder}/metadata.json`, metadataContent);
|
|
102
150
|
|
|
103
|
-
//
|
|
151
|
+
// Build object layer data from the asset directory using centralized logic
|
|
104
152
|
const ObjectLayer = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayer;
|
|
105
153
|
const ObjectLayerRenderFrames =
|
|
106
154
|
DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayerRenderFrames;
|
|
107
155
|
|
|
108
|
-
const objectLayerRenderFramesData =
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const objectLayerData = {
|
|
116
|
-
data: {
|
|
117
|
-
item: req.body.data.item,
|
|
118
|
-
stats: req.body.data.stats,
|
|
119
|
-
seed: crypto.randomUUID(),
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// Process all PNG files to generate frames and colors
|
|
124
|
-
const directionFolders = await fs.readdir(folder);
|
|
125
|
-
for (const directionCode of directionFolders) {
|
|
126
|
-
const directionPath = `${folder}/${directionCode}`;
|
|
127
|
-
if (!fs.statSync(directionPath).isDirectory()) continue;
|
|
128
|
-
|
|
129
|
-
const frameFiles = await fs.readdir(directionPath);
|
|
130
|
-
// Sort frame files numerically
|
|
131
|
-
frameFiles.sort((a, b) => {
|
|
132
|
-
const numA = parseInt(a.split('.')[0]);
|
|
133
|
-
const numB = parseInt(b.split('.')[0]);
|
|
134
|
-
return numA - numB;
|
|
156
|
+
const { objectLayerRenderFramesData, objectLayerData } =
|
|
157
|
+
await ObjectLayerEngine.buildObjectLayerDataFromDirectory({
|
|
158
|
+
folder,
|
|
159
|
+
objectLayerType: itemType,
|
|
160
|
+
objectLayerId: itemId,
|
|
161
|
+
metadataOverride: req.body,
|
|
135
162
|
});
|
|
136
163
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// Create ObjectLayerRenderFrames document
|
|
148
|
-
const objectLayerRenderFramesDoc = await ObjectLayerRenderFrames.create(objectLayerRenderFramesData);
|
|
149
|
-
|
|
150
|
-
// Add reference to render frames (top-level, not in data)
|
|
151
|
-
objectLayerData.objectLayerRenderFramesId = objectLayerRenderFramesDoc._id;
|
|
152
|
-
|
|
153
|
-
// Generate SHA256 hash using fast-json-stable-stringify (seed is part of data)
|
|
154
|
-
objectLayerData.sha256 = crypto.createHash('sha256').update(stringify(objectLayerData.data)).digest('hex');
|
|
155
|
-
|
|
156
|
-
// Save to MongoDB
|
|
157
|
-
try {
|
|
158
|
-
const existingObjectLayer = await ObjectLayer.findOne({ sha256: objectLayerData.sha256 });
|
|
159
|
-
if (existingObjectLayer) {
|
|
160
|
-
logger.info(`ObjectLayer with sha256 ${objectLayerData.sha256} already exists, updating...`);
|
|
161
|
-
const updated = await ObjectLayer.findByIdAndUpdate(existingObjectLayer._id, objectLayerData, {
|
|
162
|
-
new: true,
|
|
163
|
-
}).populate('objectLayerRenderFramesId');
|
|
164
|
-
|
|
165
|
-
// Update atlas sprite sheet
|
|
166
|
-
try {
|
|
167
|
-
await AtlasSpriteSheetService.generate({ params: { id: updated._id }, objectLayer: updated }, res, options);
|
|
168
|
-
} catch (atlasError) {
|
|
169
|
-
logger.error('Failed to auto-update atlas for ObjectLayer:', atlasError);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return updated;
|
|
173
|
-
}
|
|
174
|
-
const newObjectLayer = await (await ObjectLayer.create(objectLayerData)).populate('objectLayerRenderFramesId');
|
|
175
|
-
logger.info(`ObjectLayer created successfully with id: ${newObjectLayer._id}`);
|
|
176
|
-
|
|
177
|
-
// Generate atlas sprite sheet
|
|
178
|
-
try {
|
|
179
|
-
await AtlasSpriteSheetService.generate(
|
|
180
|
-
{ params: { id: newObjectLayer._id }, objectLayer: newObjectLayer },
|
|
164
|
+
// Create documents using centralized engine method (with atlas generation)
|
|
165
|
+
const { objectLayer } = await ObjectLayerEngine.createObjectLayerDocuments({
|
|
166
|
+
ObjectLayer,
|
|
167
|
+
ObjectLayerRenderFrames,
|
|
168
|
+
objectLayerRenderFramesData,
|
|
169
|
+
objectLayerData,
|
|
170
|
+
createOptions: {
|
|
171
|
+
generateAtlas: true,
|
|
172
|
+
atlasServiceContext: {
|
|
173
|
+
req,
|
|
181
174
|
res,
|
|
182
175
|
options,
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
176
|
+
AtlasSpriteSheetService,
|
|
177
|
+
IpfsClient,
|
|
178
|
+
createPinRecord,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
});
|
|
187
182
|
|
|
188
|
-
|
|
189
|
-
} catch (error) {
|
|
190
|
-
logger.error('Error creating ObjectLayer:', error);
|
|
191
|
-
throw error;
|
|
192
|
-
}
|
|
183
|
+
return objectLayer;
|
|
193
184
|
}
|
|
194
185
|
|
|
195
186
|
// create object layer from body
|
|
196
187
|
const ObjectLayer = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayer;
|
|
197
188
|
const ObjectLayerRenderFrames =
|
|
198
189
|
DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayerRenderFrames;
|
|
199
|
-
|
|
190
|
+
let newObjectLayer = await new ObjectLayer(req.body).save();
|
|
200
191
|
|
|
201
|
-
// Generate atlas sprite sheet
|
|
192
|
+
// Generate atlas sprite sheet – this sets data.atlasSpriteSheetCid and saves
|
|
202
193
|
try {
|
|
203
|
-
await AtlasSpriteSheetService.generate({ params: { id: newObjectLayer._id } }, res, options);
|
|
194
|
+
await AtlasSpriteSheetService.generate({ params: { id: newObjectLayer._id }, auth: req.auth }, res, options);
|
|
204
195
|
} catch (atlasError) {
|
|
205
196
|
logger.error('Failed to auto-generate atlas for new ObjectLayer:', atlasError);
|
|
206
197
|
}
|
|
207
198
|
|
|
199
|
+
// Re-read so data.atlasSpriteSheetCid is up-to-date, then recompute SHA-256 & IPFS CID
|
|
200
|
+
newObjectLayer = await ObjectLayer.findById(newObjectLayer._id).populate('objectLayerRenderFramesId');
|
|
201
|
+
if (newObjectLayer) {
|
|
202
|
+
newObjectLayer = await ObjectLayerEngine.computeAndSaveFinalSha256({
|
|
203
|
+
objectLayer: newObjectLayer,
|
|
204
|
+
ipfsClient: IpfsClient,
|
|
205
|
+
createPinRecord,
|
|
206
|
+
userId: req.auth && req.auth.user ? req.auth.user._id : undefined,
|
|
207
|
+
options,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
208
211
|
return newObjectLayer;
|
|
209
212
|
},
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* GET handler for retrieving object layers.
|
|
216
|
+
*
|
|
217
|
+
* Supports multiple sub-routes:
|
|
218
|
+
* - `/frame-counts/:id` — Get frame counts for each direction using numeric codes.
|
|
219
|
+
* - `/render/:id` — Get only render data (populated ObjectLayerRenderFrames) for a specific object layer.
|
|
220
|
+
* - `/metadata/:id` — Get metadata (no full render frames/colors) with atlas sprite sheet validation.
|
|
221
|
+
* - `/:id` — Get a single object layer by ID.
|
|
222
|
+
* - `/` — Get a paginated list of object layers.
|
|
223
|
+
*
|
|
224
|
+
* @async
|
|
225
|
+
* @function get
|
|
226
|
+
* @memberof CyberiaObjectLayerService.ObjectLayerService
|
|
227
|
+
* @param {Object} req - Express request object.
|
|
228
|
+
* @param {Object} res - Express response object.
|
|
229
|
+
* @param {Object} options - Server options containing host and path.
|
|
230
|
+
* @param {string} options.host - The deployment host.
|
|
231
|
+
* @param {string} options.path - The deployment path.
|
|
232
|
+
* @returns {Promise<Object>} The requested object layer data, list, or frame counts.
|
|
233
|
+
* @throws {Error} If the requested object layer is not found.
|
|
234
|
+
*/
|
|
210
235
|
get: async (req, res, options) => {
|
|
211
236
|
/** @type {import('./object-layer.model.js').ObjectLayerModel} */
|
|
212
237
|
const ObjectLayer = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayer;
|
|
@@ -271,7 +296,9 @@ const ObjectLayerService = {
|
|
|
271
296
|
|
|
272
297
|
// GET /metadata/:id - Get only metadata (no render frames/colors) for specific object layer
|
|
273
298
|
if (req.path.startsWith('/metadata/')) {
|
|
274
|
-
const objectLayer = await ObjectLayer.findById(req.params.id)
|
|
299
|
+
const objectLayer = await ObjectLayer.findById(req.params.id)
|
|
300
|
+
.select(ObjectLayerDto.select.getMetadata())
|
|
301
|
+
.populate('objectLayerRenderFramesId', ObjectLayerRenderFramesDto.select.get());
|
|
275
302
|
if (!objectLayer) {
|
|
276
303
|
throw new Error('ObjectLayer not found');
|
|
277
304
|
}
|
|
@@ -326,7 +353,10 @@ const ObjectLayerService = {
|
|
|
326
353
|
logger.info(`ObjectLayerService.get - filtering check - id: ${id}`);
|
|
327
354
|
if (id && id !== 'undefined' && !['render', 'metadata', 'frame-counts'].includes(id)) {
|
|
328
355
|
try {
|
|
329
|
-
const objectLayer = await ObjectLayer.findById(id)
|
|
356
|
+
const objectLayer = await ObjectLayer.findById(id)
|
|
357
|
+
.select(ObjectLayerDto.select.get())
|
|
358
|
+
.populate('atlasSpriteSheetId', 'cid')
|
|
359
|
+
.populate('objectLayerRenderFramesId', ObjectLayerRenderFramesDto.select.get());
|
|
330
360
|
if (objectLayer) {
|
|
331
361
|
logger.info(`ObjectLayerService.get - found record by id: ${id}`);
|
|
332
362
|
return { data: [objectLayer], total: 1, page: 1, totalPages: 1 };
|
|
@@ -344,11 +374,29 @@ const ObjectLayerService = {
|
|
|
344
374
|
.sort(sort)
|
|
345
375
|
.limit(limit)
|
|
346
376
|
.skip(skip)
|
|
347
|
-
.select(ObjectLayerDto.select.get())
|
|
377
|
+
.select(ObjectLayerDto.select.get())
|
|
378
|
+
.populate('atlasSpriteSheetId', 'cid')
|
|
379
|
+
.populate('objectLayerRenderFramesId', ObjectLayerRenderFramesDto.select.get()),
|
|
348
380
|
ObjectLayer.countDocuments(query), // { userId: req.auth.user._id }
|
|
349
381
|
]);
|
|
350
382
|
return { data, total, page, totalPages: Math.ceil(total / limit) };
|
|
351
383
|
},
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Generates a WebP animation from PNG frame images for a specific direction of an object layer.
|
|
387
|
+
* Uses the `img2webp` CLI tool to assemble the animation.
|
|
388
|
+
*
|
|
389
|
+
* @async
|
|
390
|
+
* @function generateWebp
|
|
391
|
+
* @memberof CyberiaObjectLayerService.ObjectLayerService
|
|
392
|
+
* @param {Object} req - Express request object with params: itemType, itemId, directionCode.
|
|
393
|
+
* @param {Object} res - Express response object.
|
|
394
|
+
* @param {Object} options - Server options containing host and path.
|
|
395
|
+
* @param {string} options.host - The deployment host.
|
|
396
|
+
* @param {string} options.path - The deployment path.
|
|
397
|
+
* @returns {Promise<Buffer>} The generated WebP animation as a Buffer.
|
|
398
|
+
* @throws {Error} If required parameters are missing, frames directory is not found, or img2webp fails.
|
|
399
|
+
*/
|
|
352
400
|
generateWebp: async (req, res, options) => {
|
|
353
401
|
/** @type {import('./object-layer.model.js').ObjectLayerModel} */
|
|
354
402
|
const ObjectLayer = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayer;
|
|
@@ -461,6 +509,29 @@ const ObjectLayerService = {
|
|
|
461
509
|
throw error;
|
|
462
510
|
}
|
|
463
511
|
},
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* PUT handler for updating object layers, their frame images, and metadata.
|
|
515
|
+
*
|
|
516
|
+
* Supports three sub-routes:
|
|
517
|
+
* - `/:id/frame-image/:itemType/:itemId/:directionCode` — Replace frame images for a direction.
|
|
518
|
+
* - `/:id/metadata/:itemType/:itemId` — Update metadata and reprocess all frames.
|
|
519
|
+
* - `/:id` — Standard update from request body.
|
|
520
|
+
*
|
|
521
|
+
* The `/metadata` route delegates to {@link ObjectLayerEngine.updateObjectLayerDocuments}
|
|
522
|
+
* for centralized document update, atlas regeneration, SHA-256 computation, and IPFS pinning.
|
|
523
|
+
*
|
|
524
|
+
* @async
|
|
525
|
+
* @function put
|
|
526
|
+
* @memberof CyberiaObjectLayerService.ObjectLayerService
|
|
527
|
+
* @param {Object} req - Express request object.
|
|
528
|
+
* @param {Object} res - Express response object.
|
|
529
|
+
* @param {Object} options - Server options containing host and path.
|
|
530
|
+
* @param {string} options.host - The deployment host.
|
|
531
|
+
* @param {string} options.path - The deployment path.
|
|
532
|
+
* @returns {Promise<Object>} The updated object layer document or frame upload result.
|
|
533
|
+
* @throws {Error} If file validation fails, object layer is not found, or required parameters are missing.
|
|
534
|
+
*/
|
|
464
535
|
put: async (req, res, options) => {
|
|
465
536
|
/** @type {import('./object-layer.model.js').ObjectLayerModel} */
|
|
466
537
|
const ObjectLayer = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayer;
|
|
@@ -565,125 +636,183 @@ const ObjectLayerService = {
|
|
|
565
636
|
const ObjectLayerRenderFrames =
|
|
566
637
|
DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayerRenderFrames;
|
|
567
638
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const objectLayerData = {
|
|
576
|
-
data: {
|
|
577
|
-
item: req.body.data.item,
|
|
578
|
-
stats: req.body.data.stats,
|
|
579
|
-
seed: req.body.data.seed || crypto.randomUUID(),
|
|
580
|
-
},
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
// Process all PNG files from direction folders (uploaded via /frame-image/)
|
|
584
|
-
const directionFolders = await fs.readdir(folder);
|
|
585
|
-
for (const directionCode of directionFolders) {
|
|
586
|
-
const directionPath = `${folder}/${directionCode}`;
|
|
587
|
-
|
|
588
|
-
// Skip non-directories (like metadata.json)
|
|
589
|
-
try {
|
|
590
|
-
const stat = await fs.stat(directionPath);
|
|
591
|
-
if (!stat.isDirectory()) continue;
|
|
592
|
-
} catch (error) {
|
|
593
|
-
logger.warn(`Skipping ${directionCode}: ${error.message}`);
|
|
594
|
-
continue;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const frameFiles = await fs.readdir(directionPath);
|
|
598
|
-
// Sort frame files numerically
|
|
599
|
-
frameFiles.sort((a, b) => {
|
|
600
|
-
const numA = parseInt(a.split('.')[0]);
|
|
601
|
-
const numB = parseInt(b.split('.')[0]);
|
|
602
|
-
return numA - numB;
|
|
639
|
+
// Build object layer data from the asset directory using centralized logic
|
|
640
|
+
const { objectLayerRenderFramesData, objectLayerData } =
|
|
641
|
+
await ObjectLayerEngine.buildObjectLayerDataFromDirectory({
|
|
642
|
+
folder,
|
|
643
|
+
objectLayerType: itemType,
|
|
644
|
+
objectLayerId: itemId,
|
|
645
|
+
metadataOverride: req.body,
|
|
603
646
|
});
|
|
604
647
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const framePath = `${directionPath}/${frameFile}`;
|
|
609
|
-
|
|
610
|
-
// Process image and push frame to render data with color palette management
|
|
611
|
-
await ObjectLayerEngine.processAndPushFrame(objectLayerRenderFramesData, framePath, directionCode);
|
|
612
|
-
}
|
|
648
|
+
// Preserve the existing seed if provided in the request body
|
|
649
|
+
if (req.body.data && req.body.data.seed) {
|
|
650
|
+
objectLayerData.data.seed = req.body.data.seed;
|
|
613
651
|
}
|
|
614
652
|
|
|
615
|
-
// Update
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const objectLayerRenderFramesDoc = await ObjectLayerRenderFrames.create(objectLayerRenderFramesData);
|
|
627
|
-
objectLayerData.objectLayerRenderFramesId = objectLayerRenderFramesDoc._id;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Generate SHA256 hash using fast-json-stable-stringify (seed is part of data)
|
|
631
|
-
objectLayerData.sha256 = crypto.createHash('sha256').update(stringify(objectLayerData.data)).digest('hex');
|
|
632
|
-
|
|
633
|
-
// Save to MongoDB
|
|
634
|
-
try {
|
|
635
|
-
const updatedObjectLayer = await ObjectLayer.findByIdAndUpdate(objectLayerId, objectLayerData, {
|
|
636
|
-
new: true,
|
|
637
|
-
}).populate('objectLayerRenderFramesId');
|
|
638
|
-
if (!updatedObjectLayer) {
|
|
639
|
-
throw new Error('ObjectLayer not found for update');
|
|
640
|
-
}
|
|
641
|
-
logger.info(`ObjectLayer updated successfully with id: ${objectLayerId}`);
|
|
642
|
-
|
|
643
|
-
// Update atlas sprite sheet
|
|
644
|
-
try {
|
|
645
|
-
await AtlasSpriteSheetService.generate(
|
|
646
|
-
{ params: { id: objectLayerId }, objectLayer: updatedObjectLayer },
|
|
653
|
+
// Update documents using centralized engine method (with atlas generation)
|
|
654
|
+
const { objectLayer } = await ObjectLayerEngine.updateObjectLayerDocuments({
|
|
655
|
+
objectLayerId,
|
|
656
|
+
ObjectLayer,
|
|
657
|
+
ObjectLayerRenderFrames,
|
|
658
|
+
objectLayerRenderFramesData,
|
|
659
|
+
objectLayerData,
|
|
660
|
+
updateOptions: {
|
|
661
|
+
generateAtlas: true,
|
|
662
|
+
atlasServiceContext: {
|
|
663
|
+
req,
|
|
647
664
|
res,
|
|
648
665
|
options,
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
666
|
+
AtlasSpriteSheetService,
|
|
667
|
+
IpfsClient,
|
|
668
|
+
createPinRecord,
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
});
|
|
653
672
|
|
|
654
|
-
|
|
655
|
-
} catch (error) {
|
|
656
|
-
logger.error('Error updating ObjectLayer:', error);
|
|
657
|
-
throw error;
|
|
658
|
-
}
|
|
673
|
+
return objectLayer;
|
|
659
674
|
}
|
|
660
675
|
|
|
661
676
|
// PUT /:id - Standard update
|
|
662
|
-
|
|
677
|
+
let updatedObjectLayer = await ObjectLayer.findByIdAndUpdate(req.params.id, req.body, { new: true });
|
|
663
678
|
|
|
664
679
|
if (updatedObjectLayer) {
|
|
665
|
-
//
|
|
680
|
+
// Generate atlas sprite sheet – this sets data.atlasSpriteSheetCid and saves
|
|
666
681
|
try {
|
|
667
|
-
await AtlasSpriteSheetService.generate({ params: { id: req.params.id } }, res, options);
|
|
682
|
+
await AtlasSpriteSheetService.generate({ params: { id: req.params.id }, auth: req.auth }, res, options);
|
|
668
683
|
} catch (atlasError) {
|
|
669
684
|
logger.error('Failed to auto-update atlas for ObjectLayer:', atlasError);
|
|
670
685
|
}
|
|
686
|
+
|
|
687
|
+
// Re-read so data.atlasSpriteSheetCid is up-to-date, then recompute SHA-256 & IPFS CID
|
|
688
|
+
updatedObjectLayer = await ObjectLayer.findById(req.params.id).populate('objectLayerRenderFramesId');
|
|
689
|
+
if (updatedObjectLayer) {
|
|
690
|
+
updatedObjectLayer = await ObjectLayerEngine.computeAndSaveFinalSha256({
|
|
691
|
+
objectLayer: updatedObjectLayer,
|
|
692
|
+
ipfsClient: IpfsClient,
|
|
693
|
+
createPinRecord,
|
|
694
|
+
userId: req.auth && req.auth.user ? req.auth.user._id : undefined,
|
|
695
|
+
options,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
671
698
|
}
|
|
672
699
|
|
|
673
700
|
return updatedObjectLayer;
|
|
674
701
|
},
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* DELETE handler for removing object layers and all associated resources.
|
|
705
|
+
*
|
|
706
|
+
* When a specific ID is provided, performs a cascading delete that removes:
|
|
707
|
+
* 1. The AtlasSpriteSheet and its File document.
|
|
708
|
+
* 2. The ObjectLayerRenderFrames document.
|
|
709
|
+
* 3. IPFS pin records and CIDs (both data JSON and atlas PNG).
|
|
710
|
+
* 4. The MFS directory for this object layer.
|
|
711
|
+
* 5. Static asset files from disk.
|
|
712
|
+
* 6. The ObjectLayer document itself.
|
|
713
|
+
*
|
|
714
|
+
* When no ID is provided, performs a bulk delete of all object layers.
|
|
715
|
+
*
|
|
716
|
+
* @async
|
|
717
|
+
* @function delete
|
|
718
|
+
* @memberof CyberiaObjectLayerService.ObjectLayerService
|
|
719
|
+
* @param {Object} req - Express request object.
|
|
720
|
+
* @param {Object} res - Express response object.
|
|
721
|
+
* @param {Object} options - Server options containing host and path.
|
|
722
|
+
* @param {string} options.host - The deployment host.
|
|
723
|
+
* @param {string} options.path - The deployment path.
|
|
724
|
+
* @returns {Promise<Object>} The deleted object layer document or bulk delete count.
|
|
725
|
+
* @throws {Error} If the object layer is not found.
|
|
726
|
+
*/
|
|
675
727
|
delete: async (req, res, options) => {
|
|
676
728
|
/** @type {import('./object-layer.model.js').ObjectLayerModel} */
|
|
677
729
|
const ObjectLayer = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayer;
|
|
730
|
+
const ObjectLayerRenderFrames =
|
|
731
|
+
DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayerRenderFrames;
|
|
732
|
+
|
|
678
733
|
if (req.params.id) {
|
|
679
|
-
//
|
|
734
|
+
// Load the full object layer so we can access all references
|
|
735
|
+
const objectLayer = await ObjectLayer.findById(req.params.id);
|
|
736
|
+
if (!objectLayer) {
|
|
737
|
+
throw new Error('ObjectLayer not found');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const itemType = objectLayer.data?.item?.type;
|
|
741
|
+
const itemId = objectLayer.data?.item?.id;
|
|
742
|
+
|
|
743
|
+
// ── 1. Clean up AtlasSpriteSheet, its File, and atlas IPFS CID ──
|
|
680
744
|
try {
|
|
681
745
|
await AtlasSpriteSheetService.deleteByObjectLayerId(req, res, options);
|
|
682
746
|
} catch (atlasError) {
|
|
683
747
|
logger.error('Failed to clean up atlas during ObjectLayer deletion:', atlasError);
|
|
684
748
|
}
|
|
685
|
-
|
|
686
|
-
|
|
749
|
+
|
|
750
|
+
// ── 2. Clean up ObjectLayerRenderFrames document ──
|
|
751
|
+
if (objectLayer.objectLayerRenderFramesId) {
|
|
752
|
+
try {
|
|
753
|
+
await ObjectLayerRenderFrames.findByIdAndDelete(objectLayer.objectLayerRenderFramesId);
|
|
754
|
+
logger.info(
|
|
755
|
+
`Deleted ObjectLayerRenderFrames ${objectLayer.objectLayerRenderFramesId} for ObjectLayer ${req.params.id}`,
|
|
756
|
+
);
|
|
757
|
+
} catch (renderFramesError) {
|
|
758
|
+
logger.error('Failed to delete ObjectLayerRenderFrames:', renderFramesError);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ── 3. Remove pin records and unpin object layer data JSON CID from IPFS ──
|
|
763
|
+
if (objectLayer.cid) {
|
|
764
|
+
try {
|
|
765
|
+
await removePinRecordsAndUnpin(objectLayer.cid, options);
|
|
766
|
+
logger.info(`Cleaned up IPFS data CID ${objectLayer.cid} for ObjectLayer ${req.params.id}`);
|
|
767
|
+
} catch (ipfsError) {
|
|
768
|
+
logger.warn(`Failed to clean up IPFS data CID ${objectLayer.cid}: ${ipfsError.message}`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// ── 4. Remove MFS directory for this object layer (covers both data JSON and atlas PNG) ──
|
|
773
|
+
if (itemId) {
|
|
774
|
+
try {
|
|
775
|
+
await IpfsClient.removeMfsPath(`/object-layer/${itemId}`);
|
|
776
|
+
logger.info(`Removed MFS directory /object-layer/${itemId}`);
|
|
777
|
+
} catch (mfsError) {
|
|
778
|
+
logger.warn(`Failed to remove MFS path /object-layer/${itemId}: ${mfsError.message}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ── 5. Remove static asset files from disk ──
|
|
783
|
+
if (itemType && itemId) {
|
|
784
|
+
const staticPaths = [
|
|
785
|
+
`./src/client/public/cyberia/assets/${itemType}/${itemId}`,
|
|
786
|
+
`./public/${options.host}${options.path}/assets/${itemType}/${itemId}`,
|
|
787
|
+
];
|
|
788
|
+
for (const assetDir of staticPaths) {
|
|
789
|
+
try {
|
|
790
|
+
if (fs.existsSync(assetDir)) {
|
|
791
|
+
await fs.remove(assetDir);
|
|
792
|
+
logger.info(`Removed static asset directory: ${assetDir}`);
|
|
793
|
+
}
|
|
794
|
+
} catch (fsError) {
|
|
795
|
+
logger.warn(`Failed to remove static asset directory ${assetDir}: ${fsError.message}`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ── 6. Delete the ObjectLayer document itself ──
|
|
801
|
+
const deleted = await ObjectLayer.findByIdAndDelete(req.params.id);
|
|
802
|
+
logger.info(`ObjectLayer ${req.params.id} and all associated resources deleted successfully`);
|
|
803
|
+
return deleted;
|
|
804
|
+
} else {
|
|
805
|
+
// Bulk delete: clean up all object layers
|
|
806
|
+
const allObjectLayers = await ObjectLayer.find({});
|
|
807
|
+
for (const ol of allObjectLayers) {
|
|
808
|
+
try {
|
|
809
|
+
await ObjectLayerService.delete({ params: { id: ol._id.toString() }, auth: req.auth }, res, options);
|
|
810
|
+
} catch (err) {
|
|
811
|
+
logger.error(`Failed to delete ObjectLayer ${ol._id} during bulk delete: ${err.message}`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return { deletedCount: allObjectLayers.length };
|
|
815
|
+
}
|
|
687
816
|
},
|
|
688
817
|
};
|
|
689
818
|
|