digitaltwin-core 0.10.3 → 0.11.1

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.
Files changed (40) hide show
  1. package/dist/components/assets_manager.d.ts +9 -1
  2. package/dist/components/assets_manager.d.ts.map +1 -1
  3. package/dist/components/assets_manager.js +26 -13
  4. package/dist/components/assets_manager.js.map +1 -1
  5. package/dist/components/global_assets_handler.js +2 -2
  6. package/dist/components/global_assets_handler.js.map +1 -1
  7. package/dist/components/map_manager.js +2 -2
  8. package/dist/components/map_manager.js.map +1 -1
  9. package/dist/components/tileset_manager.d.ts +84 -31
  10. package/dist/components/tileset_manager.d.ts.map +1 -1
  11. package/dist/components/tileset_manager.js +491 -120
  12. package/dist/components/tileset_manager.js.map +1 -1
  13. package/dist/database/adapters/knex_database_adapter.d.ts.map +1 -1
  14. package/dist/database/adapters/knex_database_adapter.js +17 -0
  15. package/dist/database/adapters/knex_database_adapter.js.map +1 -1
  16. package/dist/storage/adapters/local_storage_service.d.ts +8 -0
  17. package/dist/storage/adapters/local_storage_service.d.ts.map +1 -1
  18. package/dist/storage/adapters/local_storage_service.js +14 -0
  19. package/dist/storage/adapters/local_storage_service.js.map +1 -1
  20. package/dist/storage/adapters/ovh_storage_service.d.ts +8 -0
  21. package/dist/storage/adapters/ovh_storage_service.d.ts.map +1 -1
  22. package/dist/storage/adapters/ovh_storage_service.js +16 -0
  23. package/dist/storage/adapters/ovh_storage_service.js.map +1 -1
  24. package/dist/storage/storage_service.d.ts +20 -0
  25. package/dist/storage/storage_service.d.ts.map +1 -1
  26. package/dist/storage/storage_service.js.map +1 -1
  27. package/dist/types/data_record.d.ts +30 -0
  28. package/dist/types/data_record.d.ts.map +1 -1
  29. package/dist/utils/index.d.ts +2 -1
  30. package/dist/utils/index.d.ts.map +1 -1
  31. package/dist/utils/index.js +1 -1
  32. package/dist/utils/index.js.map +1 -1
  33. package/dist/utils/map_to_data_record.d.ts.map +1 -1
  34. package/dist/utils/map_to_data_record.js +7 -1
  35. package/dist/utils/map_to_data_record.js.map +1 -1
  36. package/dist/utils/zip_utils.d.ts +58 -0
  37. package/dist/utils/zip_utils.d.ts.map +1 -1
  38. package/dist/utils/zip_utils.js +100 -0
  39. package/dist/utils/zip_utils.js.map +1 -1
  40. package/package.json +1 -1
@@ -1,34 +1,60 @@
1
1
  import { AssetsManager } from './assets_manager.js';
2
- import { zipToDict, analyzeTilesetContent } from '../utils/zip_utils.js';
2
+ import { extractAndStoreArchive } from '../utils/zip_utils.js';
3
3
  import { ApisixAuthParser } from '../auth/apisix_parser.js';
4
+ import { AuthConfig } from '../auth/auth_config.js';
4
5
  import fs from 'fs/promises';
5
6
  /**
6
- * Specialized Assets Manager for handling tileset ZIP files.
7
+ * Specialized Assets Manager for handling 3D Tiles tilesets.
7
8
  *
8
- * Extends the base AssetsManager with specialized logic for:
9
- * - Processing ZIP files containing tileset data
10
- * - Extracting and analyzing tileset metadata
11
- * - Storing tileset-specific information
9
+ * This manager extracts uploaded ZIP files and stores each file individually,
10
+ * allowing Cesium and other 3D viewers to load tilesets directly via URL.
12
11
  *
13
- * Inherits all CRUD endpoints from AssetsManager:
14
- * - GET /{name} - List all tilesets
15
- * - POST /{name}/upload - Upload tileset ZIP (overridden)
16
- * - GET /{name}/:id - Get tileset file
17
- * - PUT /{name}/:id - Update tileset metadata
18
- * - DELETE /{name}/:id - Delete tileset
19
- * - GET /{name}/:id/download - Download tileset ZIP
12
+ * Key features:
13
+ * - Extracts ZIP archives on upload (no ZIP stored, only extracted files)
14
+ * - Detects and tracks the root tileset.json file
15
+ * - Serves individual files via dedicated endpoint
16
+ * - Returns tileset.json URL for direct Cesium loading
17
+ *
18
+ * Endpoints:
19
+ * - GET /{endpoint} - List all tilesets with tileset.json URLs
20
+ * - POST /{endpoint} - Upload tileset ZIP (extracts and stores files)
21
+ * - GET /{endpoint}/:id - Get tileset metadata
22
+ * - GET /{endpoint}/:id/files/* - Serve extracted files (tileset.json, .b3dm, etc.)
23
+ * - PUT /{endpoint}/:id - Update tileset metadata
24
+ * - DELETE /{endpoint}/:id - Delete tileset and all extracted files
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * class MyTilesetManager extends TilesetManager {
29
+ * getConfiguration() {
30
+ * return {
31
+ * name: 'tilesets_manager',
32
+ * description: 'Manage 3D Tiles tilesets',
33
+ * contentType: 'application/json',
34
+ * endpoint: 'api/tilesets',
35
+ * tags: ['Tileset']
36
+ * }
37
+ * }
38
+ * }
39
+ *
40
+ * // After upload, Cesium can load:
41
+ * // Cesium3DTileset.fromUrl('/api/tilesets/123/files/tileset.json')
42
+ * ```
20
43
  */
21
44
  export class TilesetManager extends AssetsManager {
22
45
  /**
23
- * Override the upload handler to process ZIP files containing tilesets.
46
+ * Override the upload handler to extract ZIP files and store individual files.
24
47
  *
25
- * Processes the uploaded ZIP file:
26
- * 1. Extracts and analyzes the ZIP content
27
- * 2. Stores tileset-specific metadata
28
- * 3. Saves the ZIP file using the storage service
48
+ * Flow:
49
+ * 1. Validates request and authentication
50
+ * 2. Reads uploaded ZIP file
51
+ * 3. Extracts all files and stores them individually
52
+ * 4. Detects the root tileset.json file
53
+ * 5. Saves metadata with file_index containing all file paths
54
+ * 6. Deletes the temporary ZIP file
29
55
  *
30
56
  * @param req - HTTP request with ZIP file upload
31
- * @returns DataResponse with upload result
57
+ * @returns DataResponse with upload result including tileset.json URL
32
58
  */
33
59
  async handleUpload(req) {
34
60
  try {
@@ -41,46 +67,67 @@ export class TilesetManager extends AssetsManager {
41
67
  headers: { 'Content-Type': 'application/json' }
42
68
  };
43
69
  }
44
- // Check authentication
45
- if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
46
- return {
47
- status: 401,
48
- content: JSON.stringify({
49
- error: 'Authentication required'
50
- }),
51
- headers: { 'Content-Type': 'application/json' }
70
+ // Check authentication (skip if auth is disabled)
71
+ let userId;
72
+ if (AuthConfig.isAuthDisabled()) {
73
+ const anonymousUser = {
74
+ id: AuthConfig.getAnonymousUserId(),
75
+ roles: []
52
76
  };
77
+ const userRecord = await this.userService.findOrCreateUser(anonymousUser);
78
+ userId = userRecord.id;
53
79
  }
54
- // Parse authenticated user
55
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
56
- if (!authUser) {
57
- return {
58
- status: 401,
59
- content: JSON.stringify({
60
- error: 'Invalid authentication headers'
61
- }),
62
- headers: { 'Content-Type': 'application/json' }
63
- };
80
+ else {
81
+ if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
82
+ return {
83
+ status: 401,
84
+ content: JSON.stringify({
85
+ error: 'Authentication required'
86
+ }),
87
+ headers: { 'Content-Type': 'application/json' }
88
+ };
89
+ }
90
+ const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
91
+ if (!authUser) {
92
+ return {
93
+ status: 401,
94
+ content: JSON.stringify({
95
+ error: 'Invalid authentication headers'
96
+ }),
97
+ headers: { 'Content-Type': 'application/json' }
98
+ };
99
+ }
100
+ const userRecord = await this.userService.findOrCreateUser(authUser);
101
+ if (!userRecord.id) {
102
+ return {
103
+ status: 500,
104
+ content: JSON.stringify({
105
+ error: 'Failed to retrieve user information'
106
+ }),
107
+ headers: { 'Content-Type': 'application/json' }
108
+ };
109
+ }
110
+ userId = userRecord.id;
64
111
  }
65
- // Find or create user in database
66
- const userRecord = await this.userService.findOrCreateUser(authUser);
67
- if (!userRecord.id) {
112
+ const { description } = req.body;
113
+ const filePath = req.file?.path;
114
+ const fileBuffer = req.file?.buffer;
115
+ const filename = req.file?.originalname || req.body.filename;
116
+ // Support both disk storage (filePath) and memory storage (fileBuffer)
117
+ if (!filePath && !fileBuffer) {
68
118
  return {
69
- status: 500,
119
+ status: 400,
70
120
  content: JSON.stringify({
71
- error: 'Failed to retrieve user information'
121
+ error: 'Missing required fields: ZIP file'
72
122
  }),
73
123
  headers: { 'Content-Type': 'application/json' }
74
124
  };
75
125
  }
76
- const { description } = req.body;
77
- const filePath = req.file?.path;
78
- const filename = req.file?.originalname || req.body.filename;
79
- if (!filePath || !description) {
126
+ if (!description) {
80
127
  return {
81
128
  status: 400,
82
129
  content: JSON.stringify({
83
- error: 'Missing required fields: description, ZIP file'
130
+ error: 'Missing required fields: description'
84
131
  }),
85
132
  headers: { 'Content-Type': 'application/json' }
86
133
  };
@@ -104,10 +151,15 @@ export class TilesetManager extends AssetsManager {
104
151
  headers: { 'Content-Type': 'application/json' }
105
152
  };
106
153
  }
107
- // Read and process ZIP file
108
- let fileBuffer;
154
+ // Read ZIP file content
155
+ let zipBuffer;
109
156
  try {
110
- fileBuffer = await fs.readFile(filePath);
157
+ if (fileBuffer) {
158
+ zipBuffer = fileBuffer;
159
+ }
160
+ else {
161
+ zipBuffer = await fs.readFile(filePath);
162
+ }
111
163
  }
112
164
  catch (error) {
113
165
  return {
@@ -119,49 +171,67 @@ export class TilesetManager extends AssetsManager {
119
171
  };
120
172
  }
121
173
  try {
122
- // Extract and analyze tileset content
123
- const zipDict = await zipToDict(fileBuffer);
124
- const tilesetInfo = analyzeTilesetContent(zipDict);
125
- // Use the analyzed name or fallback to filename
126
- const tilesetName = tilesetInfo.name || filename.replace('.zip', '');
127
174
  const config = this.getConfiguration();
128
175
  const now = new Date();
129
- // Store ZIP file using framework pattern
130
- const url = await this.storage.save(fileBuffer, config.name, filename);
131
- // Create extended metadata with tileset-specific fields
176
+ // Generate unique ID for this tileset (timestamp-based)
177
+ const tilesetId = now.getTime().toString();
178
+ // Base path for storing extracted files: componentName/tilesetId
179
+ const basePath = `${config.name}/${tilesetId}`;
180
+ // Extract ZIP and store all files individually
181
+ const extractResult = await extractAndStoreArchive(zipBuffer, this.storage, basePath);
182
+ // Validate that we found a tileset.json
183
+ if (!extractResult.root_file) {
184
+ // Clean up stored files if no tileset.json found
185
+ for (const file of extractResult.files) {
186
+ await this.storage.delete(file.path).catch(() => { });
187
+ }
188
+ return {
189
+ status: 400,
190
+ content: JSON.stringify({
191
+ error: 'Invalid tileset: no tileset.json found in the ZIP archive'
192
+ }),
193
+ headers: { 'Content-Type': 'application/json' }
194
+ };
195
+ }
196
+ // Create metadata with file_index
197
+ // url stores the base path for all extracted files
132
198
  const metadata = {
133
199
  name: config.name,
134
- type: config.contentType || 'application/zip',
135
- url,
200
+ type: 'application/json', // tileset.json content type
201
+ url: basePath, // Base path for all files (componentName/timestamp)
136
202
  date: now,
137
203
  description,
138
- source: req.body.source || 'uploaded',
139
- owner_id: userRecord.id,
140
- filename,
141
- // Tileset-specific metadata
142
- tileset_name: tilesetName,
143
- file_count: tilesetInfo.fileCount,
144
- has_metadata: tilesetInfo.hasMetadata,
145
- main_files: tilesetInfo.mainFiles.join(',')
204
+ source: req.body.source || '',
205
+ owner_id: userId,
206
+ filename: filename,
207
+ is_public: req.body.is_public !== undefined ? Boolean(req.body.is_public) : true,
208
+ file_index: {
209
+ files: extractResult.files,
210
+ root_file: extractResult.root_file
211
+ }
146
212
  };
147
- await this.db.save(metadata);
213
+ const savedRecord = await this.db.save(metadata);
148
214
  // Clean up temporary file after successful upload
149
- await fs.unlink(filePath).catch(() => {
150
- // Ignore cleanup errors
151
- });
215
+ if (filePath) {
216
+ await fs.unlink(filePath).catch(() => { });
217
+ }
152
218
  return {
153
219
  status: 200,
154
220
  content: JSON.stringify({
155
- message: 'Tileset uploaded successfully',
156
- tileset_name: tilesetName,
157
- file_count: tilesetInfo.fileCount
221
+ message: 'Tileset uploaded and extracted successfully',
222
+ id: savedRecord.id,
223
+ file_count: extractResult.file_count,
224
+ root_file: extractResult.root_file,
225
+ tileset_url: `/${config.endpoint}/${savedRecord.id}/files/${extractResult.root_file}`
158
226
  }),
159
227
  headers: { 'Content-Type': 'application/json' }
160
228
  };
161
229
  }
162
230
  catch (error) {
163
231
  // Clean up temporary file even on error
164
- await fs.unlink(filePath).catch(() => { });
232
+ if (filePath) {
233
+ await fs.unlink(filePath).catch(() => { });
234
+ }
165
235
  throw error;
166
236
  }
167
237
  }
@@ -176,34 +246,180 @@ export class TilesetManager extends AssetsManager {
176
246
  }
177
247
  }
178
248
  /**
179
- * Override retrieve to include tileset-specific metadata in the response
249
+ * Serve an extracted file from the tileset.
250
+ *
251
+ * This endpoint allows Cesium to load tileset.json and all referenced files
252
+ * (tiles, textures, etc.) using relative paths.
253
+ *
254
+ * @param req - HTTP request with params.id and params['0'] (file path)
255
+ * @returns DataResponse with file content
256
+ */
257
+ async handleGetFile(req) {
258
+ try {
259
+ const { id } = req.params || {};
260
+ // Express captures wildcard path as params[0] or params['0']
261
+ const filePath = req.params[0] || req.params['0'] || req.params.path;
262
+ if (!id) {
263
+ return {
264
+ status: 400,
265
+ content: JSON.stringify({ error: 'Asset ID is required' }),
266
+ headers: { 'Content-Type': 'application/json' }
267
+ };
268
+ }
269
+ if (!filePath) {
270
+ return {
271
+ status: 400,
272
+ content: JSON.stringify({ error: 'File path is required' }),
273
+ headers: { 'Content-Type': 'application/json' }
274
+ };
275
+ }
276
+ // Get asset metadata
277
+ const asset = await this.getAssetById(id);
278
+ if (!asset) {
279
+ return {
280
+ status: 404,
281
+ content: JSON.stringify({ error: 'Tileset not found' }),
282
+ headers: { 'Content-Type': 'application/json' }
283
+ };
284
+ }
285
+ // Verify this asset belongs to our component
286
+ const config = this.getConfiguration();
287
+ if (asset.name !== config.name) {
288
+ return {
289
+ status: 404,
290
+ content: JSON.stringify({ error: 'Tileset not found' }),
291
+ headers: { 'Content-Type': 'application/json' }
292
+ };
293
+ }
294
+ // Check access permissions for private assets
295
+ if (!asset.is_public) {
296
+ if (ApisixAuthParser.isAdmin(req.headers || {})) {
297
+ // Admin can access everything
298
+ }
299
+ else if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
300
+ return {
301
+ status: 401,
302
+ content: JSON.stringify({ error: 'Authentication required for private assets' }),
303
+ headers: { 'Content-Type': 'application/json' }
304
+ };
305
+ }
306
+ else {
307
+ const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
308
+ if (authUser) {
309
+ const userRecord = await this.userService.findOrCreateUser(authUser);
310
+ if (!userRecord.id || asset.owner_id !== userRecord.id) {
311
+ return {
312
+ status: 403,
313
+ content: JSON.stringify({ error: 'This asset is private' }),
314
+ headers: { 'Content-Type': 'application/json' }
315
+ };
316
+ }
317
+ }
318
+ }
319
+ }
320
+ // Build storage path using asset.url as base path
321
+ // asset.url contains the base path (e.g., 'tilesets_manager/1733064000000')
322
+ const storagePath = `${asset.url}/${filePath}`;
323
+ // Retrieve file from storage
324
+ let fileContent;
325
+ try {
326
+ fileContent = await this.storage.retrieve(storagePath);
327
+ }
328
+ catch {
329
+ return {
330
+ status: 404,
331
+ content: JSON.stringify({ error: 'File not found' }),
332
+ headers: { 'Content-Type': 'application/json' }
333
+ };
334
+ }
335
+ // Determine content type based on file extension
336
+ const contentType = this.getContentTypeForFile(filePath);
337
+ return {
338
+ status: 200,
339
+ content: fileContent,
340
+ headers: {
341
+ 'Content-Type': contentType,
342
+ 'Access-Control-Allow-Origin': '*'
343
+ }
344
+ };
345
+ }
346
+ catch (error) {
347
+ return {
348
+ status: 500,
349
+ content: JSON.stringify({
350
+ error: error instanceof Error ? error.message : 'Unknown error'
351
+ }),
352
+ headers: { 'Content-Type': 'application/json' }
353
+ };
354
+ }
355
+ }
356
+ /**
357
+ * Determines the MIME content type based on file extension.
180
358
  */
181
- async retrieve() {
359
+ getContentTypeForFile(filePath) {
360
+ const ext = filePath.toLowerCase().split('.').pop();
361
+ const contentTypes = {
362
+ json: 'application/json',
363
+ b3dm: 'application/octet-stream',
364
+ i3dm: 'application/octet-stream',
365
+ pnts: 'application/octet-stream',
366
+ cmpt: 'application/octet-stream',
367
+ glb: 'model/gltf-binary',
368
+ gltf: 'model/gltf+json',
369
+ bin: 'application/octet-stream',
370
+ png: 'image/png',
371
+ jpg: 'image/jpeg',
372
+ jpeg: 'image/jpeg',
373
+ webp: 'image/webp',
374
+ ktx2: 'image/ktx2'
375
+ };
376
+ return contentTypes[ext || ''] || 'application/octet-stream';
377
+ }
378
+ /**
379
+ * Override retrieve to include tileset.json URL in the response.
380
+ */
381
+ async retrieve(req) {
182
382
  try {
183
383
  const assets = await this.getAllAssets();
184
384
  const config = this.getConfiguration();
185
- // Transform to include tileset metadata
186
- const assetsWithMetadata = assets.map(asset => ({
187
- id: asset.id,
188
- name: asset.name,
189
- date: asset.date,
190
- contentType: asset.contentType,
191
- description: asset.description || '',
192
- source: asset.source || '',
193
- owner_id: asset.owner_id || null,
194
- filename: asset.filename || '',
195
- // Tileset-specific fields
196
- tileset_name: asset.tileset_name || '',
197
- file_count: asset.file_count || 0,
198
- has_metadata: asset.has_metadata || false,
199
- main_files: asset.main_files ? asset.main_files.split(',') : [],
200
- // URLs for frontend
201
- url: `/${config.name}/${asset.id}`,
202
- download_url: `/${config.name}/${asset.id}/download`
203
- }));
385
+ const isAdmin = req && ApisixAuthParser.isAdmin(req.headers || {});
386
+ // Get authenticated user ID if available
387
+ let authenticatedUserId = null;
388
+ if (req && ApisixAuthParser.hasValidAuth(req.headers || {})) {
389
+ const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
390
+ if (authUser) {
391
+ const userRecord = await this.userService.findOrCreateUser(authUser);
392
+ authenticatedUserId = userRecord.id || null;
393
+ }
394
+ }
395
+ // Filter to visible assets only (unless admin)
396
+ const visibleAssets = isAdmin
397
+ ? assets
398
+ : assets.filter(asset => asset.is_public || (authenticatedUserId !== null && asset.owner_id === authenticatedUserId));
399
+ // Transform to include tileset URLs
400
+ const assetsWithUrls = visibleAssets.map(asset => {
401
+ const fileIndex = asset.file_index;
402
+ const rootFile = fileIndex?.root_file || 'tileset.json';
403
+ return {
404
+ id: asset.id,
405
+ name: asset.name,
406
+ date: asset.date,
407
+ contentType: asset.contentType,
408
+ description: asset.description || '',
409
+ source: asset.source || '',
410
+ owner_id: asset.owner_id || null,
411
+ filename: asset.filename || '',
412
+ is_public: asset.is_public ?? true,
413
+ // Tileset-specific URLs
414
+ file_count: fileIndex?.files?.length || 0,
415
+ root_file: rootFile,
416
+ tileset_url: `/${config.endpoint}/${asset.id}/files/${rootFile}`,
417
+ files_base_url: `/${config.endpoint}/${asset.id}/files`
418
+ };
419
+ });
204
420
  return {
205
421
  status: 200,
206
- content: JSON.stringify(assetsWithMetadata),
422
+ content: JSON.stringify(assetsWithUrls),
207
423
  headers: { 'Content-Type': 'application/json' }
208
424
  };
209
425
  }
@@ -218,36 +434,193 @@ export class TilesetManager extends AssetsManager {
218
434
  }
219
435
  }
220
436
  /**
221
- * Override OpenAPI specification to include tileset-specific fields.
222
- *
223
- * Extends the parent AssetsManager spec with additional properties
224
- * specific to tileset uploads (ZIP file analysis metadata).
225
- *
226
- * @returns {OpenAPIComponentSpec} OpenAPI spec with tileset extensions
437
+ * Override delete to clean up all extracted files.
438
+ */
439
+ async handleDelete(req) {
440
+ try {
441
+ // Check authentication (skip if auth is disabled)
442
+ let userId;
443
+ if (AuthConfig.isAuthDisabled()) {
444
+ const anonymousUser = {
445
+ id: AuthConfig.getAnonymousUserId(),
446
+ roles: []
447
+ };
448
+ const userRecord = await this.userService.findOrCreateUser(anonymousUser);
449
+ userId = userRecord.id;
450
+ }
451
+ else {
452
+ if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
453
+ return {
454
+ status: 401,
455
+ content: JSON.stringify({ error: 'Authentication required' }),
456
+ headers: { 'Content-Type': 'application/json' }
457
+ };
458
+ }
459
+ const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
460
+ if (!authUser) {
461
+ return {
462
+ status: 401,
463
+ content: JSON.stringify({ error: 'Invalid authentication headers' }),
464
+ headers: { 'Content-Type': 'application/json' }
465
+ };
466
+ }
467
+ const userRecord = await this.userService.findOrCreateUser(authUser);
468
+ if (!userRecord.id) {
469
+ return {
470
+ status: 500,
471
+ content: JSON.stringify({ error: 'Failed to retrieve user information' }),
472
+ headers: { 'Content-Type': 'application/json' }
473
+ };
474
+ }
475
+ userId = userRecord.id;
476
+ }
477
+ const { id } = req.params || {};
478
+ if (!id) {
479
+ return {
480
+ status: 400,
481
+ content: JSON.stringify({ error: 'Asset ID is required' }),
482
+ headers: { 'Content-Type': 'application/json' }
483
+ };
484
+ }
485
+ // Get asset metadata
486
+ const asset = await this.getAssetById(id);
487
+ if (!asset) {
488
+ return {
489
+ status: 404,
490
+ content: JSON.stringify({ error: 'Tileset not found' }),
491
+ headers: { 'Content-Type': 'application/json' }
492
+ };
493
+ }
494
+ // Check ownership (admins can delete any asset)
495
+ const isAdmin = ApisixAuthParser.isAdmin(req.headers || {});
496
+ if (!isAdmin && asset.owner_id !== null && asset.owner_id !== userId) {
497
+ return {
498
+ status: 403,
499
+ content: JSON.stringify({ error: 'You can only delete your own assets' }),
500
+ headers: { 'Content-Type': 'application/json' }
501
+ };
502
+ }
503
+ // Delete all extracted files from storage
504
+ const fileIndex = asset.file_index;
505
+ if (fileIndex?.files) {
506
+ for (const file of fileIndex.files) {
507
+ await this.storage.delete(file.path).catch(() => {
508
+ // Ignore individual file deletion errors
509
+ });
510
+ }
511
+ }
512
+ // Delete database record
513
+ await this.deleteAssetById(id);
514
+ return {
515
+ status: 200,
516
+ content: JSON.stringify({ message: 'Tileset deleted successfully' }),
517
+ headers: { 'Content-Type': 'application/json' }
518
+ };
519
+ }
520
+ catch (error) {
521
+ return {
522
+ status: 500,
523
+ content: JSON.stringify({
524
+ error: error instanceof Error ? error.message : 'Unknown error'
525
+ }),
526
+ headers: { 'Content-Type': 'application/json' }
527
+ };
528
+ }
529
+ }
530
+ /**
531
+ * Override getEndpoints to add the file serving endpoint.
532
+ */
533
+ getEndpoints() {
534
+ const config = this.getConfiguration();
535
+ // Get base endpoints from parent but filter out ones we override
536
+ const baseEndpoints = super.getEndpoints().filter(ep =>
537
+ // Keep all except GET /:id (we don't serve the raw asset, only files)
538
+ // and DELETE /:id (we override to clean up files)
539
+ !(ep.method === 'get' && ep.path === `/${config.endpoint}/:id`) &&
540
+ !(ep.method === 'delete' && ep.path === `/${config.endpoint}/:id`) &&
541
+ // Also remove download endpoint - not applicable for extracted tilesets
542
+ !(ep.method === 'get' && ep.path === `/${config.endpoint}/:id/download`));
543
+ return [
544
+ ...baseEndpoints,
545
+ // Add file serving endpoint for extracted tileset files
546
+ {
547
+ method: 'get',
548
+ path: `/${config.endpoint}/:id/files/*`,
549
+ handler: this.handleGetFile.bind(this),
550
+ responseType: 'application/octet-stream'
551
+ },
552
+ // Override delete to clean up files
553
+ {
554
+ method: 'delete',
555
+ path: `/${config.endpoint}/:id`,
556
+ handler: this.handleDelete.bind(this),
557
+ responseType: 'application/json'
558
+ }
559
+ ];
560
+ }
561
+ /**
562
+ * Override OpenAPI specification to include tileset-specific endpoints.
227
563
  */
228
564
  getOpenAPISpec() {
229
565
  const parentSpec = super.getOpenAPISpec();
230
566
  const config = this.getConfiguration();
231
567
  const basePath = `/${config.endpoint}`;
232
- // Extend the POST response to include tileset-specific fields
568
+ const tagName = config.tags?.[0] || config.name;
569
+ // Update POST response
233
570
  if (parentSpec.paths[basePath]?.post?.responses?.['200']) {
234
571
  parentSpec.paths[basePath].post.responses['200'] = {
235
- description: 'Tileset uploaded successfully',
572
+ description: 'Tileset uploaded and extracted successfully',
236
573
  content: {
237
574
  'application/json': {
238
575
  schema: {
239
576
  type: 'object',
240
577
  properties: {
241
578
  message: { type: 'string' },
242
- tileset_name: { type: 'string', description: 'Extracted tileset name' },
243
- file_count: { type: 'integer', description: 'Number of files in the tileset' }
579
+ file_count: { type: 'integer', description: 'Number of files extracted' },
580
+ root_file: { type: 'string', description: 'Root tileset.json path' },
581
+ tileset_url: { type: 'string', description: 'URL to load in Cesium' }
244
582
  }
245
583
  }
246
584
  }
247
585
  }
248
586
  };
249
587
  }
250
- // Add tileset-specific schema extending the base AssetResponse
588
+ // Add file serving endpoint
589
+ parentSpec.paths[`${basePath}/{id}/files/{filePath}`] = {
590
+ get: {
591
+ summary: 'Get tileset file',
592
+ description: 'Retrieve an extracted file from the tileset (tileset.json, .b3dm, etc.)',
593
+ tags: [tagName],
594
+ parameters: [
595
+ {
596
+ name: 'id',
597
+ in: 'path',
598
+ required: true,
599
+ schema: { type: 'string' },
600
+ description: 'Tileset ID'
601
+ },
602
+ {
603
+ name: 'filePath',
604
+ in: 'path',
605
+ required: true,
606
+ schema: { type: 'string' },
607
+ description: 'Path to file within tileset (e.g., tileset.json, tiles/tile_0.b3dm)'
608
+ }
609
+ ],
610
+ responses: {
611
+ '200': {
612
+ description: 'File content',
613
+ content: {
614
+ 'application/octet-stream': {
615
+ schema: { type: 'string', format: 'binary' }
616
+ }
617
+ }
618
+ },
619
+ '404': { description: 'Tileset or file not found' }
620
+ }
621
+ }
622
+ };
623
+ // Update GET list response schema
251
624
  if (parentSpec.schemas) {
252
625
  parentSpec.schemas.TilesetResponse = {
253
626
  type: 'object',
@@ -260,26 +633,24 @@ export class TilesetManager extends AssetsManager {
260
633
  source: { type: 'string' },
261
634
  owner_id: { type: 'integer', nullable: true },
262
635
  filename: { type: 'string' },
263
- tileset_name: { type: 'string', description: 'Extracted tileset name from ZIP' },
264
- file_count: { type: 'integer', description: 'Number of files in the tileset' },
265
- has_metadata: { type: 'boolean', description: 'Whether tileset has metadata file' },
266
- main_files: {
267
- type: 'array',
268
- items: { type: 'string' },
269
- description: 'Main tileset files (e.g., tileset.json)'
270
- },
271
- url: { type: 'string' },
272
- download_url: { type: 'string' }
636
+ is_public: { type: 'boolean' },
637
+ file_count: { type: 'integer', description: 'Number of files in tileset' },
638
+ root_file: { type: 'string', description: 'Root tileset.json filename' },
639
+ tileset_url: { type: 'string', description: 'URL to load tileset in Cesium' },
640
+ files_base_url: { type: 'string', description: 'Base URL for all tileset files' }
273
641
  }
274
642
  };
275
643
  }
276
- // Update the GET list response to use tileset schema
277
644
  if (parentSpec.paths[basePath]?.get?.responses?.['200']?.content?.['application/json']) {
278
645
  parentSpec.paths[basePath].get.responses['200'].content['application/json'].schema = {
279
646
  type: 'array',
280
647
  items: { $ref: '#/components/schemas/TilesetResponse' }
281
648
  };
282
649
  }
650
+ // Remove the single asset GET endpoint since we don't serve raw assets
651
+ delete parentSpec.paths[`${basePath}/{id}`]?.get;
652
+ // Remove download endpoint
653
+ delete parentSpec.paths[`${basePath}/{id}/download`];
283
654
  return parentSpec;
284
655
  }
285
656
  }