@cepseudo/assets 1.0.0

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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +204 -0
  3. package/dist/assets_manager.d.ts +643 -0
  4. package/dist/assets_manager.d.ts.map +1 -0
  5. package/dist/assets_manager.js +1217 -0
  6. package/dist/assets_manager.js.map +1 -0
  7. package/dist/assets_openapi.d.ts +9 -0
  8. package/dist/assets_openapi.d.ts.map +1 -0
  9. package/dist/assets_openapi.js +391 -0
  10. package/dist/assets_openapi.js.map +1 -0
  11. package/dist/async_upload.d.ts +20 -0
  12. package/dist/async_upload.d.ts.map +1 -0
  13. package/dist/async_upload.js +10 -0
  14. package/dist/async_upload.js.map +1 -0
  15. package/dist/index.d.ts +18 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +17 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/map_manager.d.ts +61 -0
  20. package/dist/map_manager.d.ts.map +1 -0
  21. package/dist/map_manager.js +217 -0
  22. package/dist/map_manager.js.map +1 -0
  23. package/dist/presigned_upload_service.d.ts +52 -0
  24. package/dist/presigned_upload_service.d.ts.map +1 -0
  25. package/dist/presigned_upload_service.js +152 -0
  26. package/dist/presigned_upload_service.js.map +1 -0
  27. package/dist/tileset_manager.d.ts +128 -0
  28. package/dist/tileset_manager.d.ts.map +1 -0
  29. package/dist/tileset_manager.js +705 -0
  30. package/dist/tileset_manager.js.map +1 -0
  31. package/dist/upload_processor.d.ts +42 -0
  32. package/dist/upload_processor.d.ts.map +1 -0
  33. package/dist/upload_processor.js +138 -0
  34. package/dist/upload_processor.js.map +1 -0
  35. package/dist/upload_reconciler.d.ts +44 -0
  36. package/dist/upload_reconciler.d.ts.map +1 -0
  37. package/dist/upload_reconciler.js +140 -0
  38. package/dist/upload_reconciler.js.map +1 -0
  39. package/dist/utils/zip_utils.d.ts +66 -0
  40. package/dist/utils/zip_utils.d.ts.map +1 -0
  41. package/dist/utils/zip_utils.js +169 -0
  42. package/dist/utils/zip_utils.js.map +1 -0
  43. package/package.json +72 -0
@@ -0,0 +1,705 @@
1
+ import { AssetsManager } from './assets_manager.js';
2
+ import { successResponse, errorResponse, badRequestResponse, notFoundResponse, forbiddenResponse, safeAsync, safeCleanup, Logger } from '@cepseudo/shared';
3
+ import { ApisixAuthParser } from '@cepseudo/auth';
4
+ import { extractAndStoreArchive } from './utils/zip_utils.js';
5
+ import fs from 'fs/promises';
6
+ const logger = new Logger('TilesetManager');
7
+ /** Threshold for async upload (50MB) */
8
+ const ASYNC_UPLOAD_THRESHOLD = 50 * 1024 * 1024;
9
+ /**
10
+ * Specialized Assets Manager for handling 3D Tiles tilesets.
11
+ *
12
+ * This manager extracts uploaded ZIP files and stores each file in cloud storage (OVH S3),
13
+ * allowing Cesium and other 3D viewers to load tilesets directly via public URLs.
14
+ *
15
+ * ## How it works
16
+ *
17
+ * 1. User uploads a ZIP containing a 3D Tiles tileset
18
+ * 2. ZIP is extracted and all files are stored in OVH with public-read ACL
19
+ * 3. Database stores only the tileset.json URL and base path
20
+ * 4. Cesium loads tileset.json directly from OVH
21
+ * 5. Cesium fetches tiles using relative paths in tileset.json (directly from OVH)
22
+ *
23
+ * ## Endpoints
24
+ *
25
+ * - GET /{endpoint} - List all tilesets with their public URLs
26
+ * - POST /{endpoint} - Upload tileset ZIP (sync < 50MB, async >= 50MB)
27
+ * - GET /{endpoint}/:id/status - Poll async upload status
28
+ * - PUT /{endpoint}/:id - Update tileset metadata
29
+ * - DELETE /{endpoint}/:id - Delete tileset and all files from storage
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * class MyTilesetManager extends TilesetManager {
34
+ * getConfiguration() {
35
+ * return {
36
+ * name: 'tilesets',
37
+ * description: 'Manage 3D Tiles tilesets',
38
+ * contentType: 'application/json',
39
+ * endpoint: 'api/tilesets',
40
+ * extension: '.zip'
41
+ * }
42
+ * }
43
+ * }
44
+ *
45
+ * // After upload, response contains:
46
+ * // { tileset_url: 'https://bucket.s3.../tilesets/123/tileset.json' }
47
+ * //
48
+ * // Cesium loads directly:
49
+ * // Cesium.Cesium3DTileset.fromUrl(tileset_url)
50
+ * ```
51
+ */
52
+ export class TilesetManager extends AssetsManager {
53
+ constructor() {
54
+ super(...arguments);
55
+ /** Upload queue for async processing (injected by engine) */
56
+ this.uploadQueue = null;
57
+ }
58
+ /**
59
+ * Set the upload queue for async job processing.
60
+ * Called by DigitalTwinEngine during initialization.
61
+ */
62
+ setUploadQueue(queue) {
63
+ this.uploadQueue = queue;
64
+ }
65
+ /**
66
+ * Handle tileset upload.
67
+ *
68
+ * - Files < 50MB: Synchronous extraction and upload
69
+ * - Files >= 50MB: Queued for async processing (returns 202)
70
+ */
71
+ async handleUpload(req) {
72
+ try {
73
+ if (!req?.body) {
74
+ return badRequestResponse('Invalid request: missing request body');
75
+ }
76
+ // Authenticate user
77
+ const userId = await this.authenticateUser(req);
78
+ if (typeof userId !== 'number') {
79
+ return userId; // Returns error response
80
+ }
81
+ // Validate request
82
+ const body = req.body;
83
+ const description = body.description;
84
+ const filePath = req.file?.path;
85
+ const fileBuffer = req.file?.buffer;
86
+ const filename = (req.file?.originalname || body.filename);
87
+ const fileSize = req.file?.size || fileBuffer?.length || 0;
88
+ if (!filePath && !fileBuffer) {
89
+ return badRequestResponse('Missing required field: ZIP file');
90
+ }
91
+ if (!description) {
92
+ if (filePath)
93
+ await safeAsync(() => fs.unlink(filePath), 'cleanup temp file on validation error', logger);
94
+ return badRequestResponse('Missing required field: description');
95
+ }
96
+ if (!filename) {
97
+ if (filePath)
98
+ await safeAsync(() => fs.unlink(filePath), 'cleanup temp file on validation error', logger);
99
+ return badRequestResponse('Filename could not be determined from uploaded file');
100
+ }
101
+ if (!filename.toLowerCase().endsWith('.zip')) {
102
+ if (filePath)
103
+ await safeAsync(() => fs.unlink(filePath), 'cleanup temp file on validation error', logger);
104
+ return badRequestResponse('Invalid file extension. Expected: .zip');
105
+ }
106
+ const config = this.getConfiguration();
107
+ const isPublic = body.is_public !== undefined ? Boolean(body.is_public) : true;
108
+ // Route to async or sync based on file size and queue availability
109
+ if (this.uploadQueue && filePath && fileSize >= ASYNC_UPLOAD_THRESHOLD) {
110
+ return this.handleAsyncUpload(userId, filePath, filename, description, isPublic, config);
111
+ }
112
+ return this.handleSyncUpload(userId, filePath, fileBuffer, filename, description, isPublic, config);
113
+ }
114
+ catch (error) {
115
+ if (req.file?.path)
116
+ await safeAsync(() => fs.unlink(req.file.path), 'cleanup temp file on error', logger);
117
+ return errorResponse(error);
118
+ }
119
+ }
120
+ /**
121
+ * Authenticate user from request headers.
122
+ * Returns user ID on success, or error response on failure.
123
+ */
124
+ async authenticateUser(req) {
125
+ const authResult = await this.authMiddleware.authenticate(req);
126
+ if (!authResult.success) {
127
+ return authResult.response;
128
+ }
129
+ return authResult.userRecord.id;
130
+ }
131
+ /**
132
+ * Queue upload for background processing. Returns HTTP 202 immediately.
133
+ */
134
+ async handleAsyncUpload(userId, filePath, filename, description, isPublic, config) {
135
+ let recordId = null;
136
+ try {
137
+ // Create pending record (url will be updated after extraction)
138
+ const metadata = {
139
+ name: config.name,
140
+ type: 'application/json',
141
+ url: '',
142
+ tileset_url: '',
143
+ date: new Date(),
144
+ description,
145
+ filename,
146
+ owner_id: userId,
147
+ is_public: isPublic,
148
+ upload_status: 'pending'
149
+ };
150
+ const savedRecord = await this.db.save(metadata);
151
+ recordId = savedRecord.id;
152
+ const jobData = {
153
+ type: 'tileset',
154
+ recordId,
155
+ tempFilePath: filePath,
156
+ componentName: config.name,
157
+ userId,
158
+ filename,
159
+ description
160
+ };
161
+ const job = await this.uploadQueue?.add(`tileset-${recordId}`, jobData, {
162
+ jobId: `tileset-upload-${recordId}`
163
+ });
164
+ if (!job)
165
+ throw new Error('Failed to queue upload job');
166
+ await this.db.updateById(config.name, recordId, { upload_job_id: job.id });
167
+ return {
168
+ status: 202,
169
+ content: JSON.stringify({
170
+ message: 'Tileset upload accepted, processing in background',
171
+ id: recordId,
172
+ job_id: job.id,
173
+ status: 'pending',
174
+ status_url: `/${config.endpoint}/${recordId}/status`
175
+ }),
176
+ headers: { 'Content-Type': 'application/json' }
177
+ };
178
+ }
179
+ catch (error) {
180
+ await safeCleanup([
181
+ {
182
+ operation: () => recordId !== null ? this.db.delete(String(recordId), config.name) : Promise.resolve(),
183
+ context: 'delete record on async upload error'
184
+ },
185
+ { operation: () => fs.unlink(filePath), context: 'cleanup temp file on async upload error' }
186
+ ], logger);
187
+ throw error;
188
+ }
189
+ }
190
+ /**
191
+ * Process upload synchronously.
192
+ */
193
+ async handleSyncUpload(userId, filePath, fileBuffer, filename, description, isPublic, config) {
194
+ let zipBuffer;
195
+ try {
196
+ const readBuffer = fileBuffer || (filePath ? await fs.readFile(filePath) : null);
197
+ if (!readBuffer)
198
+ throw new Error('No file data available');
199
+ zipBuffer = readBuffer;
200
+ }
201
+ catch (error) {
202
+ return errorResponse(`Failed to read uploaded file: ${error instanceof Error ? error.message : 'Unknown error'}`);
203
+ }
204
+ try {
205
+ // Generate unique base path using timestamp
206
+ const basePath = `${config.name}/${Date.now()}`;
207
+ // Extract ZIP and upload all files to storage
208
+ const extractResult = await extractAndStoreArchive(zipBuffer, this.storage, basePath);
209
+ if (!extractResult.root_file) {
210
+ // Clean up uploaded files
211
+ await safeAsync(() => this.storage.deleteByPrefix(basePath), 'cleanup storage on invalid tileset', logger);
212
+ return badRequestResponse('Invalid tileset: no tileset.json found in the ZIP archive');
213
+ }
214
+ // Build the public URL for tileset.json
215
+ const tilesetPath = `${basePath}/${extractResult.root_file}`;
216
+ const tilesetUrl = this.storage.getPublicUrl(tilesetPath);
217
+ // Save metadata to database (url = basePath for deletion)
218
+ const metadata = {
219
+ name: config.name,
220
+ type: 'application/json',
221
+ url: basePath,
222
+ tileset_url: tilesetUrl,
223
+ date: new Date(),
224
+ description,
225
+ filename,
226
+ owner_id: userId,
227
+ is_public: isPublic,
228
+ upload_status: 'completed'
229
+ };
230
+ const savedRecord = await this.db.save(metadata);
231
+ // Clean up temp file
232
+ if (filePath)
233
+ await safeAsync(() => fs.unlink(filePath), 'cleanup temp file after sync upload', logger);
234
+ return successResponse({
235
+ message: 'Tileset uploaded successfully',
236
+ id: savedRecord.id,
237
+ tileset_url: tilesetUrl,
238
+ file_count: extractResult.file_count
239
+ });
240
+ }
241
+ catch (error) {
242
+ if (filePath)
243
+ await safeAsync(() => fs.unlink(filePath), 'cleanup temp file on sync upload error', logger);
244
+ throw error;
245
+ }
246
+ }
247
+ /**
248
+ * Get upload status for async uploads.
249
+ */
250
+ async handleGetStatus(req) {
251
+ try {
252
+ const { id } = req.params || {};
253
+ if (!id) {
254
+ return badRequestResponse('Asset ID is required');
255
+ }
256
+ const asset = await this.getAssetById(id);
257
+ if (!asset) {
258
+ return notFoundResponse('Tileset not found');
259
+ }
260
+ const record = asset;
261
+ if (record.upload_status === 'completed') {
262
+ return successResponse({
263
+ id: record.id,
264
+ status: 'completed',
265
+ tileset_url: record.tileset_url
266
+ });
267
+ }
268
+ if (record.upload_status === 'failed') {
269
+ return successResponse({
270
+ id: record.id,
271
+ status: 'failed',
272
+ error: record.upload_error || 'Upload failed'
273
+ });
274
+ }
275
+ return successResponse({
276
+ id: record.id,
277
+ status: record.upload_status || 'unknown',
278
+ job_id: record.upload_job_id
279
+ });
280
+ }
281
+ catch (error) {
282
+ return errorResponse(error);
283
+ }
284
+ }
285
+ /**
286
+ * Override presigned upload confirmation for tilesets.
287
+ * Instead of marking as completed, queue a BullMQ job for ZIP extraction.
288
+ */
289
+ async handleUploadConfirm(req) {
290
+ try {
291
+ // Authenticate user
292
+ const authResult = await this.authMiddleware.authenticate(req);
293
+ if (!authResult.success) {
294
+ return authResult.response;
295
+ }
296
+ const userId = authResult.userRecord.id;
297
+ if (!userId) {
298
+ return errorResponse('Failed to retrieve user information');
299
+ }
300
+ const fileId = req.params?.fileId;
301
+ if (!fileId) {
302
+ return badRequestResponse('File ID is required');
303
+ }
304
+ const config = this.getConfiguration();
305
+ const asset = await this.getAssetById(fileId);
306
+ if (!asset) {
307
+ return notFoundResponse('Tileset not found');
308
+ }
309
+ // Check ownership
310
+ const ownershipError = this.validateOwnership(asset, userId, req.headers);
311
+ if (ownershipError) {
312
+ return ownershipError;
313
+ }
314
+ // Check status
315
+ if (asset.upload_status !== 'pending') {
316
+ return {
317
+ status: 409,
318
+ content: JSON.stringify({ error: `Upload is not pending (current status: ${asset.upload_status || 'completed'})` }),
319
+ headers: { 'Content-Type': 'application/json' }
320
+ };
321
+ }
322
+ // Verify file exists on storage
323
+ if (!asset.presigned_key) {
324
+ return badRequestResponse('No presigned key found for this record');
325
+ }
326
+ const existsResult = await this.storage.objectExists(asset.presigned_key);
327
+ if (!existsResult.exists) {
328
+ return badRequestResponse('File not found on storage. Please upload the file using the presigned URL first.');
329
+ }
330
+ // Queue extraction job instead of marking completed
331
+ if (!this.uploadQueue) {
332
+ return errorResponse('Upload processing queue is not available');
333
+ }
334
+ const jobData = {
335
+ type: 'tileset',
336
+ recordId: asset.id,
337
+ tempFilePath: '', // Not used for presigned uploads
338
+ componentName: config.name,
339
+ userId,
340
+ filename: asset.filename || 'tileset.zip',
341
+ description: asset.description || '',
342
+ presignedKey: asset.presigned_key
343
+ };
344
+ const job = await this.uploadQueue.add(`tileset-presigned-${asset.id}`, jobData, {
345
+ jobId: `tileset-presigned-${asset.id}`
346
+ });
347
+ if (!job) {
348
+ return errorResponse('Failed to queue extraction job');
349
+ }
350
+ await this.db.updateById(config.name, asset.id, {
351
+ upload_status: 'processing',
352
+ upload_job_id: job.id
353
+ });
354
+ return {
355
+ status: 202,
356
+ content: JSON.stringify({
357
+ message: 'Upload confirmed, extraction queued',
358
+ id: asset.id,
359
+ job_id: job.id,
360
+ status: 'processing',
361
+ status_url: `/${config.endpoint}/${asset.id}/status`
362
+ }),
363
+ headers: { 'Content-Type': 'application/json' }
364
+ };
365
+ }
366
+ catch (error) {
367
+ return errorResponse(error);
368
+ }
369
+ }
370
+ /**
371
+ * List all tilesets with their public URLs.
372
+ */
373
+ async retrieve(req) {
374
+ try {
375
+ const assets = await this.getAllAssets();
376
+ const isAdmin = req && ApisixAuthParser.isAdmin(req.headers || {});
377
+ // Get authenticated user ID if available
378
+ let authenticatedUserId = null;
379
+ if (req) {
380
+ const authResult = await this.authMiddleware.authenticate(req);
381
+ if (authResult.success) {
382
+ authenticatedUserId = authResult.userRecord.id || null;
383
+ }
384
+ }
385
+ // Filter to visible assets only (unless admin)
386
+ const visibleAssets = isAdmin
387
+ ? assets
388
+ : assets.filter(asset => asset.is_public || (authenticatedUserId !== null && asset.owner_id === authenticatedUserId));
389
+ // Transform to response format
390
+ const response = visibleAssets.map(asset => ({
391
+ id: asset.id,
392
+ description: asset.description || '',
393
+ filename: asset.filename || '',
394
+ date: asset.date,
395
+ owner_id: asset.owner_id || null,
396
+ is_public: asset.is_public ?? true,
397
+ tileset_url: asset.tileset_url || '',
398
+ upload_status: asset.upload_status || 'completed'
399
+ }));
400
+ return successResponse(response);
401
+ }
402
+ catch (error) {
403
+ return errorResponse(error);
404
+ }
405
+ }
406
+ /**
407
+ * Delete tileset and all files from storage.
408
+ */
409
+ async handleDelete(req) {
410
+ try {
411
+ // Authenticate user
412
+ const userId = await this.authenticateUser(req);
413
+ if (typeof userId !== 'number') {
414
+ return userId;
415
+ }
416
+ const { id } = req.params || {};
417
+ if (!id) {
418
+ return badRequestResponse('Asset ID is required');
419
+ }
420
+ const asset = await this.getAssetById(id);
421
+ if (!asset) {
422
+ return notFoundResponse('Tileset not found');
423
+ }
424
+ // Check ownership (admins can delete any)
425
+ const isAdmin = ApisixAuthParser.isAdmin(req.headers || {});
426
+ if (!isAdmin && asset.owner_id !== null && asset.owner_id !== userId) {
427
+ return forbiddenResponse('You can only delete your own assets');
428
+ }
429
+ // Block deletion while upload in progress
430
+ if (asset.upload_status === 'pending' || asset.upload_status === 'processing') {
431
+ return {
432
+ status: 409,
433
+ content: JSON.stringify({ error: 'Cannot delete tileset while upload is in progress' }),
434
+ headers: { 'Content-Type': 'application/json' }
435
+ };
436
+ }
437
+ // Delete all files from storage
438
+ // Support both new format (url = basePath) and legacy format (file_index.files)
439
+ const legacyFileIndex = asset.file_index;
440
+ if (legacyFileIndex?.files && legacyFileIndex.files.length > 0) {
441
+ // Legacy format: delete individual files from file_index
442
+ logger.info(`Deleting ${legacyFileIndex.files.length} files (legacy format)`);
443
+ for (const file of legacyFileIndex.files) {
444
+ await safeAsync(() => this.storage.delete(file.path), `delete legacy file ${file.path}`, logger);
445
+ }
446
+ }
447
+ else if (asset.url) {
448
+ // New format: url contains basePath, use deleteByPrefix
449
+ const deletedCount = await this.storage.deleteByPrefix(asset.url);
450
+ logger.info(`Deleted ${deletedCount} files from ${asset.url}`);
451
+ }
452
+ // Delete database record
453
+ await this.deleteAssetById(id);
454
+ return successResponse({ message: 'Tileset deleted successfully' });
455
+ }
456
+ catch (error) {
457
+ return errorResponse(error);
458
+ }
459
+ }
460
+ /**
461
+ * Get HTTP endpoints for this manager.
462
+ */
463
+ getEndpoints() {
464
+ const config = this.getConfiguration();
465
+ return [
466
+ // Status endpoint (for async upload polling)
467
+ {
468
+ method: 'get',
469
+ path: `/${config.endpoint}/:id/status`,
470
+ handler: this.handleGetStatus.bind(this),
471
+ responseType: 'application/json'
472
+ },
473
+ // List tilesets
474
+ {
475
+ method: 'get',
476
+ path: `/${config.endpoint}`,
477
+ handler: this.retrieve.bind(this),
478
+ responseType: 'application/json'
479
+ },
480
+ // Upload tileset (multipart)
481
+ {
482
+ method: 'post',
483
+ path: `/${config.endpoint}`,
484
+ handler: this.handleUpload.bind(this),
485
+ responseType: 'application/json'
486
+ },
487
+ // Presigned upload request
488
+ {
489
+ method: 'post',
490
+ path: `/${config.endpoint}/upload-request`,
491
+ handler: this.handlePresignedUploadRequest.bind(this),
492
+ responseType: 'application/json'
493
+ },
494
+ // Presigned upload confirm (triggers extraction)
495
+ {
496
+ method: 'post',
497
+ path: `/${config.endpoint}/confirm/:fileId`,
498
+ handler: this.handleUploadConfirm.bind(this),
499
+ responseType: 'application/json'
500
+ },
501
+ // Update metadata
502
+ {
503
+ method: 'put',
504
+ path: `/${config.endpoint}/:id`,
505
+ handler: this.handleUpdate.bind(this),
506
+ responseType: 'application/json'
507
+ },
508
+ // Delete tileset
509
+ {
510
+ method: 'delete',
511
+ path: `/${config.endpoint}/:id`,
512
+ handler: this.handleDelete.bind(this),
513
+ responseType: 'application/json'
514
+ }
515
+ ];
516
+ }
517
+ /**
518
+ * Generate OpenAPI specification.
519
+ */
520
+ getOpenAPISpec() {
521
+ const config = this.getConfiguration();
522
+ const basePath = `/${config.endpoint}`;
523
+ const tagName = config.tags?.[0] || config.name;
524
+ return {
525
+ paths: {
526
+ [basePath]: {
527
+ get: {
528
+ summary: 'List all tilesets',
529
+ description: 'Returns all tilesets with their public URLs for Cesium loading',
530
+ tags: [tagName],
531
+ responses: {
532
+ '200': {
533
+ description: 'List of tilesets',
534
+ content: {
535
+ 'application/json': {
536
+ schema: {
537
+ type: 'array',
538
+ items: { $ref: '#/components/schemas/TilesetResponse' }
539
+ }
540
+ }
541
+ }
542
+ }
543
+ }
544
+ },
545
+ post: {
546
+ summary: 'Upload a tileset',
547
+ description: 'Upload a ZIP file containing a 3D Tiles tileset. Files < 50MB are processed synchronously, larger files are queued.',
548
+ tags: [tagName],
549
+ security: [{ ApiKeyAuth: [] }],
550
+ requestBody: {
551
+ required: true,
552
+ content: {
553
+ 'multipart/form-data': {
554
+ schema: {
555
+ type: 'object',
556
+ required: ['file', 'description'],
557
+ properties: {
558
+ file: {
559
+ type: 'string',
560
+ format: 'binary',
561
+ description: 'ZIP file containing tileset'
562
+ },
563
+ description: { type: 'string', description: 'Tileset description' },
564
+ is_public: {
565
+ type: 'boolean',
566
+ description: 'Whether tileset is public (default: true)'
567
+ }
568
+ }
569
+ }
570
+ }
571
+ }
572
+ },
573
+ responses: {
574
+ '200': {
575
+ description: 'Tileset uploaded successfully (sync)',
576
+ content: {
577
+ 'application/json': {
578
+ schema: {
579
+ type: 'object',
580
+ properties: {
581
+ message: { type: 'string' },
582
+ id: { type: 'integer' },
583
+ tileset_url: {
584
+ type: 'string',
585
+ description: 'Public URL to load in Cesium'
586
+ },
587
+ file_count: { type: 'integer' }
588
+ }
589
+ }
590
+ }
591
+ }
592
+ },
593
+ '202': {
594
+ description: 'Upload accepted for async processing',
595
+ content: {
596
+ 'application/json': {
597
+ schema: {
598
+ type: 'object',
599
+ properties: {
600
+ message: { type: 'string' },
601
+ id: { type: 'integer' },
602
+ status: { type: 'string' },
603
+ status_url: { type: 'string' }
604
+ }
605
+ }
606
+ }
607
+ }
608
+ },
609
+ '400': { description: 'Bad request - missing fields or invalid file' },
610
+ '401': { description: 'Unauthorized' }
611
+ }
612
+ }
613
+ },
614
+ [`${basePath}/{id}/status`]: {
615
+ get: {
616
+ summary: 'Get upload status',
617
+ description: 'Poll the status of an async upload',
618
+ tags: [tagName],
619
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
620
+ responses: {
621
+ '200': {
622
+ description: 'Upload status',
623
+ content: {
624
+ 'application/json': {
625
+ schema: {
626
+ type: 'object',
627
+ properties: {
628
+ id: { type: 'integer' },
629
+ status: {
630
+ type: 'string',
631
+ enum: ['pending', 'processing', 'completed', 'failed']
632
+ },
633
+ tileset_url: { type: 'string' },
634
+ error: { type: 'string' }
635
+ }
636
+ }
637
+ }
638
+ }
639
+ },
640
+ '404': { description: 'Tileset not found' }
641
+ }
642
+ }
643
+ },
644
+ [`${basePath}/{id}`]: {
645
+ put: {
646
+ summary: 'Update tileset metadata',
647
+ tags: [tagName],
648
+ security: [{ ApiKeyAuth: [] }],
649
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
650
+ requestBody: {
651
+ content: {
652
+ 'application/json': {
653
+ schema: {
654
+ type: 'object',
655
+ properties: {
656
+ description: { type: 'string' },
657
+ is_public: { type: 'boolean' }
658
+ }
659
+ }
660
+ }
661
+ }
662
+ },
663
+ responses: {
664
+ '200': { description: 'Updated successfully' },
665
+ '401': { description: 'Unauthorized' },
666
+ '403': { description: 'Forbidden' },
667
+ '404': { description: 'Not found' }
668
+ }
669
+ },
670
+ delete: {
671
+ summary: 'Delete tileset',
672
+ description: 'Delete tileset and all files from storage',
673
+ tags: [tagName],
674
+ security: [{ ApiKeyAuth: [] }],
675
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
676
+ responses: {
677
+ '200': { description: 'Deleted successfully' },
678
+ '401': { description: 'Unauthorized' },
679
+ '403': { description: 'Forbidden' },
680
+ '404': { description: 'Not found' },
681
+ '409': { description: 'Upload in progress' }
682
+ }
683
+ }
684
+ }
685
+ },
686
+ tags: [{ name: tagName, description: config.description }],
687
+ schemas: {
688
+ TilesetResponse: {
689
+ type: 'object',
690
+ properties: {
691
+ id: { type: 'integer' },
692
+ description: { type: 'string' },
693
+ filename: { type: 'string' },
694
+ date: { type: 'string', format: 'date-time' },
695
+ owner_id: { type: 'integer', nullable: true },
696
+ is_public: { type: 'boolean' },
697
+ tileset_url: { type: 'string', description: 'Public URL to load in Cesium' },
698
+ upload_status: { type: 'string', enum: ['pending', 'processing', 'completed', 'failed'] }
699
+ }
700
+ }
701
+ }
702
+ };
703
+ }
704
+ }
705
+ //# sourceMappingURL=tileset_manager.js.map