digitaltwin-core 0.13.1 → 0.13.3
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/dist/components/tileset_manager.d.ts +45 -56
- package/dist/components/tileset_manager.d.ts.map +1 -1
- package/dist/components/tileset_manager.js +370 -497
- package/dist/components/tileset_manager.js.map +1 -1
- package/dist/database/adapters/knex_database_adapter.d.ts.map +1 -1
- package/dist/database/adapters/knex_database_adapter.js +9 -12
- package/dist/database/adapters/knex_database_adapter.js.map +1 -1
- package/dist/engine/upload_processor.d.ts +6 -0
- package/dist/engine/upload_processor.d.ts.map +1 -1
- package/dist/engine/upload_processor.js +35 -10
- package/dist/engine/upload_processor.js.map +1 -1
- package/dist/storage/adapters/local_storage_service.d.ts +14 -0
- package/dist/storage/adapters/local_storage_service.d.ts.map +1 -1
- package/dist/storage/adapters/local_storage_service.js +46 -0
- package/dist/storage/adapters/local_storage_service.js.map +1 -1
- package/dist/storage/adapters/ovh_storage_service.d.ts +15 -0
- package/dist/storage/adapters/ovh_storage_service.d.ts.map +1 -1
- package/dist/storage/adapters/ovh_storage_service.js +59 -4
- package/dist/storage/adapters/ovh_storage_service.js.map +1 -1
- package/dist/storage/storage_service.d.ts +36 -0
- package/dist/storage/storage_service.d.ts.map +1 -1
- package/dist/storage/storage_service.js.map +1 -1
- package/dist/types/data_record.d.ts +6 -15
- package/dist/types/data_record.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/map_to_data_record.d.ts.map +1 -1
- package/dist/utils/map_to_data_record.js +3 -1
- package/dist/utils/map_to_data_record.js.map +1 -1
- package/dist/utils/zip_utils.d.ts +14 -30
- package/dist/utils/zip_utils.d.ts.map +1 -1
- package/dist/utils/zip_utils.js +25 -63
- package/dist/utils/zip_utils.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,42 +3,50 @@ import { extractAndStoreArchive } from '../utils/zip_utils.js';
|
|
|
3
3
|
import { ApisixAuthParser } from '../auth/apisix_parser.js';
|
|
4
4
|
import { AuthConfig } from '../auth/auth_config.js';
|
|
5
5
|
import fs from 'fs/promises';
|
|
6
|
+
import { successResponse, errorResponse, badRequestResponse, unauthorizedResponse, notFoundResponse, forbiddenResponse } from '../utils/http_responses.js';
|
|
7
|
+
/** Threshold for async upload (50MB) */
|
|
8
|
+
const ASYNC_UPLOAD_THRESHOLD = 50 * 1024 * 1024;
|
|
6
9
|
/**
|
|
7
10
|
* Specialized Assets Manager for handling 3D Tiles tilesets.
|
|
8
11
|
*
|
|
9
|
-
* This manager extracts uploaded ZIP files and stores each file
|
|
10
|
-
* allowing Cesium and other 3D viewers to load tilesets directly via
|
|
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.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
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
|
|
15
|
+
* ## How it works
|
|
17
16
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
23
28
|
* - PUT /{endpoint}/:id - Update tileset metadata
|
|
24
|
-
* - DELETE /{endpoint}/:id - Delete tileset and all
|
|
29
|
+
* - DELETE /{endpoint}/:id - Delete tileset and all files from storage
|
|
25
30
|
*
|
|
26
31
|
* @example
|
|
27
32
|
* ```typescript
|
|
28
33
|
* class MyTilesetManager extends TilesetManager {
|
|
29
34
|
* getConfiguration() {
|
|
30
35
|
* return {
|
|
31
|
-
* name: '
|
|
36
|
+
* name: 'tilesets',
|
|
32
37
|
* description: 'Manage 3D Tiles tilesets',
|
|
33
38
|
* contentType: 'application/json',
|
|
34
39
|
* endpoint: 'api/tilesets',
|
|
35
|
-
*
|
|
40
|
+
* extension: '.zip'
|
|
36
41
|
* }
|
|
37
42
|
* }
|
|
38
43
|
* }
|
|
39
44
|
*
|
|
40
|
-
* // After upload,
|
|
41
|
-
* //
|
|
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)
|
|
42
50
|
* ```
|
|
43
51
|
*/
|
|
44
52
|
export class TilesetManager extends AssetsManager {
|
|
@@ -55,123 +63,101 @@ export class TilesetManager extends AssetsManager {
|
|
|
55
63
|
this.uploadQueue = queue;
|
|
56
64
|
}
|
|
57
65
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* When uploadQueue is available (async mode):
|
|
61
|
-
* 1. Validates request and authentication
|
|
62
|
-
* 2. Creates a database record with status 'pending'
|
|
63
|
-
* 3. Queues a job for background processing
|
|
64
|
-
* 4. Returns immediately with job ID
|
|
66
|
+
* Handle tileset upload.
|
|
65
67
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* 2. Extracts ZIP and stores files synchronously
|
|
69
|
-
* 3. Returns with completed tileset info
|
|
70
|
-
*
|
|
71
|
-
* @param req - HTTP request with ZIP file upload
|
|
72
|
-
* @returns DataResponse with upload result (async: job info, sync: tileset info)
|
|
68
|
+
* - Files < 50MB: Synchronous extraction and upload
|
|
69
|
+
* - Files >= 50MB: Queued for async processing (returns 202)
|
|
73
70
|
*/
|
|
74
71
|
async handleUpload(req) {
|
|
75
72
|
try {
|
|
76
|
-
if (!req
|
|
77
|
-
return
|
|
78
|
-
status: 400,
|
|
79
|
-
content: JSON.stringify({ error: 'Invalid request: missing request body' }),
|
|
80
|
-
headers: { 'Content-Type': 'application/json' }
|
|
81
|
-
};
|
|
73
|
+
if (!req?.body) {
|
|
74
|
+
return badRequestResponse('Invalid request: missing request body');
|
|
82
75
|
}
|
|
83
76
|
// Authenticate user
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
id: AuthConfig.getAnonymousUserId(),
|
|
88
|
-
roles: []
|
|
89
|
-
});
|
|
90
|
-
userId = userRecord.id;
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
94
|
-
return {
|
|
95
|
-
status: 401,
|
|
96
|
-
content: JSON.stringify({ error: 'Authentication required' }),
|
|
97
|
-
headers: { 'Content-Type': 'application/json' }
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
101
|
-
if (!authUser) {
|
|
102
|
-
return {
|
|
103
|
-
status: 401,
|
|
104
|
-
content: JSON.stringify({ error: 'Invalid authentication headers' }),
|
|
105
|
-
headers: { 'Content-Type': 'application/json' }
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
109
|
-
if (!userRecord.id) {
|
|
110
|
-
return {
|
|
111
|
-
status: 500,
|
|
112
|
-
content: JSON.stringify({ error: 'Failed to retrieve user information' }),
|
|
113
|
-
headers: { 'Content-Type': 'application/json' }
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
userId = userRecord.id;
|
|
77
|
+
const userId = await this.authenticateUser(req);
|
|
78
|
+
if (typeof userId !== 'number') {
|
|
79
|
+
return userId; // Returns error response
|
|
117
80
|
}
|
|
81
|
+
// Validate request
|
|
118
82
|
const { description } = req.body;
|
|
119
83
|
const filePath = req.file?.path;
|
|
120
84
|
const fileBuffer = req.file?.buffer;
|
|
121
85
|
const filename = req.file?.originalname || req.body.filename;
|
|
122
|
-
|
|
123
|
-
|
|
86
|
+
const fileSize = req.file?.size || fileBuffer?.length || 0;
|
|
87
|
+
if (!filePath && !fileBuffer) {
|
|
88
|
+
return badRequestResponse('Missing required field: ZIP file');
|
|
89
|
+
}
|
|
90
|
+
if (!description) {
|
|
124
91
|
if (filePath)
|
|
125
92
|
await fs.unlink(filePath).catch(() => { });
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
if (!
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
93
|
+
return badRequestResponse('Missing required field: description');
|
|
94
|
+
}
|
|
95
|
+
if (!filename) {
|
|
96
|
+
if (filePath)
|
|
97
|
+
await fs.unlink(filePath).catch(() => { });
|
|
98
|
+
return badRequestResponse('Filename could not be determined from uploaded file');
|
|
99
|
+
}
|
|
100
|
+
if (!filename.toLowerCase().endsWith('.zip')) {
|
|
101
|
+
if (filePath)
|
|
102
|
+
await fs.unlink(filePath).catch(() => { });
|
|
103
|
+
return badRequestResponse('Invalid file extension. Expected: .zip');
|
|
134
104
|
}
|
|
135
|
-
if (!description)
|
|
136
|
-
return cleanupAndReturn(400, 'Missing required fields: description');
|
|
137
|
-
if (!filename)
|
|
138
|
-
return cleanupAndReturn(400, 'Filename could not be determined from uploaded file');
|
|
139
|
-
if (!filename.toLowerCase().endsWith('.zip'))
|
|
140
|
-
return cleanupAndReturn(400, 'Invalid file extension. Expected: .zip');
|
|
141
105
|
const config = this.getConfiguration();
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
106
|
+
const isPublic = req.body.is_public !== undefined ? Boolean(req.body.is_public) : true;
|
|
107
|
+
// Route to async or sync based on file size and queue availability
|
|
108
|
+
if (this.uploadQueue && filePath && fileSize >= ASYNC_UPLOAD_THRESHOLD) {
|
|
109
|
+
return this.handleAsyncUpload(userId, filePath, filename, description, isPublic, config);
|
|
145
110
|
}
|
|
146
|
-
return this.handleSyncUpload(
|
|
111
|
+
return this.handleSyncUpload(userId, filePath, fileBuffer, filename, description, isPublic, config);
|
|
147
112
|
}
|
|
148
113
|
catch (error) {
|
|
149
114
|
if (req.file?.path)
|
|
150
115
|
await fs.unlink(req.file.path).catch(() => { });
|
|
151
|
-
return
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
116
|
+
return errorResponse(error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Authenticate user from request headers.
|
|
121
|
+
* Returns user ID on success, or error response on failure.
|
|
122
|
+
*/
|
|
123
|
+
async authenticateUser(req) {
|
|
124
|
+
if (AuthConfig.isAuthDisabled()) {
|
|
125
|
+
const userRecord = await this.userService.findOrCreateUser({
|
|
126
|
+
id: AuthConfig.getAnonymousUserId(),
|
|
127
|
+
roles: []
|
|
128
|
+
});
|
|
129
|
+
return userRecord.id;
|
|
130
|
+
}
|
|
131
|
+
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
132
|
+
return unauthorizedResponse();
|
|
133
|
+
}
|
|
134
|
+
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
135
|
+
if (!authUser) {
|
|
136
|
+
return unauthorizedResponse('Invalid authentication headers');
|
|
156
137
|
}
|
|
138
|
+
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
139
|
+
if (!userRecord.id) {
|
|
140
|
+
return errorResponse('Failed to retrieve user information');
|
|
141
|
+
}
|
|
142
|
+
return userRecord.id;
|
|
157
143
|
}
|
|
158
144
|
/**
|
|
159
145
|
* Queue upload for background processing. Returns HTTP 202 immediately.
|
|
160
146
|
*/
|
|
161
|
-
async handleAsyncUpload(
|
|
147
|
+
async handleAsyncUpload(userId, filePath, filename, description, isPublic, config) {
|
|
162
148
|
let recordId = null;
|
|
163
149
|
try {
|
|
164
|
-
// Create pending record
|
|
150
|
+
// Create pending record (url will be updated after extraction)
|
|
165
151
|
const metadata = {
|
|
166
152
|
name: config.name,
|
|
167
153
|
type: 'application/json',
|
|
168
154
|
url: '',
|
|
155
|
+
tileset_url: '',
|
|
169
156
|
date: new Date(),
|
|
170
157
|
description,
|
|
171
|
-
source: req.body.source || '',
|
|
172
|
-
owner_id: userId,
|
|
173
158
|
filename,
|
|
174
|
-
|
|
159
|
+
owner_id: userId,
|
|
160
|
+
is_public: isPublic,
|
|
175
161
|
upload_status: 'pending'
|
|
176
162
|
};
|
|
177
163
|
const savedRecord = await this.db.save(metadata);
|
|
@@ -204,7 +190,6 @@ export class TilesetManager extends AssetsManager {
|
|
|
204
190
|
};
|
|
205
191
|
}
|
|
206
192
|
catch (error) {
|
|
207
|
-
// Cleanup on failure: remove DB record and temp file
|
|
208
193
|
if (recordId !== null)
|
|
209
194
|
await this.db.delete(String(recordId), config.name).catch(() => { });
|
|
210
195
|
await fs.unlink(filePath).catch(() => { });
|
|
@@ -212,64 +197,55 @@ export class TilesetManager extends AssetsManager {
|
|
|
212
197
|
}
|
|
213
198
|
}
|
|
214
199
|
/**
|
|
215
|
-
* Process upload synchronously
|
|
200
|
+
* Process upload synchronously.
|
|
216
201
|
*/
|
|
217
|
-
async handleSyncUpload(
|
|
202
|
+
async handleSyncUpload(userId, filePath, fileBuffer, filename, description, isPublic, config) {
|
|
218
203
|
let zipBuffer;
|
|
219
204
|
try {
|
|
220
|
-
|
|
221
|
-
if (!
|
|
205
|
+
const readBuffer = fileBuffer || (filePath ? await fs.readFile(filePath) : null);
|
|
206
|
+
if (!readBuffer)
|
|
222
207
|
throw new Error('No file data available');
|
|
208
|
+
zipBuffer = readBuffer;
|
|
223
209
|
}
|
|
224
210
|
catch (error) {
|
|
225
|
-
return {
|
|
226
|
-
status: 500,
|
|
227
|
-
content: JSON.stringify({
|
|
228
|
-
error: `Failed to read uploaded file: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
229
|
-
}),
|
|
230
|
-
headers: { 'Content-Type': 'application/json' }
|
|
231
|
-
};
|
|
211
|
+
return errorResponse(`Failed to read uploaded file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
232
212
|
}
|
|
233
213
|
try {
|
|
234
|
-
|
|
235
|
-
const basePath = `${config.name}/${now
|
|
214
|
+
// Generate unique base path using timestamp
|
|
215
|
+
const basePath = `${config.name}/${Date.now()}`;
|
|
216
|
+
// Extract ZIP and upload all files to storage
|
|
236
217
|
const extractResult = await extractAndStoreArchive(zipBuffer, this.storage, basePath);
|
|
237
218
|
if (!extractResult.root_file) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return {
|
|
242
|
-
status: 400,
|
|
243
|
-
content: JSON.stringify({ error: 'Invalid tileset: no tileset.json found in the ZIP archive' }),
|
|
244
|
-
headers: { 'Content-Type': 'application/json' }
|
|
245
|
-
};
|
|
219
|
+
// Clean up uploaded files
|
|
220
|
+
await this.storage.deleteByPrefix(basePath).catch(() => { });
|
|
221
|
+
return badRequestResponse('Invalid tileset: no tileset.json found in the ZIP archive');
|
|
246
222
|
}
|
|
223
|
+
// Build the public URL for tileset.json
|
|
224
|
+
const tilesetPath = `${basePath}/${extractResult.root_file}`;
|
|
225
|
+
const tilesetUrl = this.storage.getPublicUrl(tilesetPath);
|
|
226
|
+
// Save metadata to database (url = basePath for deletion)
|
|
247
227
|
const metadata = {
|
|
248
228
|
name: config.name,
|
|
249
229
|
type: 'application/json',
|
|
250
230
|
url: basePath,
|
|
251
|
-
|
|
231
|
+
tileset_url: tilesetUrl,
|
|
232
|
+
date: new Date(),
|
|
252
233
|
description,
|
|
253
|
-
source: req.body.source || '',
|
|
254
|
-
owner_id: userId,
|
|
255
234
|
filename,
|
|
256
|
-
|
|
257
|
-
|
|
235
|
+
owner_id: userId,
|
|
236
|
+
is_public: isPublic,
|
|
237
|
+
upload_status: 'completed'
|
|
258
238
|
};
|
|
259
239
|
const savedRecord = await this.db.save(metadata);
|
|
240
|
+
// Clean up temp file
|
|
260
241
|
if (filePath)
|
|
261
242
|
await fs.unlink(filePath).catch(() => { });
|
|
262
|
-
return {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
root_file: extractResult.root_file,
|
|
269
|
-
tileset_url: `/${config.endpoint}/${savedRecord.id}/files/${extractResult.root_file}`
|
|
270
|
-
}),
|
|
271
|
-
headers: { 'Content-Type': 'application/json' }
|
|
272
|
-
};
|
|
243
|
+
return successResponse({
|
|
244
|
+
message: 'Tileset uploaded successfully',
|
|
245
|
+
id: savedRecord.id,
|
|
246
|
+
tileset_url: tilesetUrl,
|
|
247
|
+
file_count: extractResult.file_count
|
|
248
|
+
});
|
|
273
249
|
}
|
|
274
250
|
catch (error) {
|
|
275
251
|
if (filePath)
|
|
@@ -278,190 +254,49 @@ export class TilesetManager extends AssetsManager {
|
|
|
278
254
|
}
|
|
279
255
|
}
|
|
280
256
|
/**
|
|
281
|
-
* Get upload status for
|
|
257
|
+
* Get upload status for async uploads.
|
|
282
258
|
*/
|
|
283
259
|
async handleGetStatus(req) {
|
|
284
260
|
try {
|
|
285
261
|
const { id } = req.params || {};
|
|
286
262
|
if (!id) {
|
|
287
|
-
return
|
|
288
|
-
status: 400,
|
|
289
|
-
content: JSON.stringify({ error: 'Asset ID is required' }),
|
|
290
|
-
headers: { 'Content-Type': 'application/json' }
|
|
291
|
-
};
|
|
263
|
+
return badRequestResponse('Asset ID is required');
|
|
292
264
|
}
|
|
293
265
|
const asset = await this.getAssetById(id);
|
|
294
266
|
if (!asset) {
|
|
295
|
-
return
|
|
296
|
-
status: 404,
|
|
297
|
-
content: JSON.stringify({ error: 'Tileset not found' }),
|
|
298
|
-
headers: { 'Content-Type': 'application/json' }
|
|
299
|
-
};
|
|
267
|
+
return notFoundResponse('Tileset not found');
|
|
300
268
|
}
|
|
301
|
-
const config = this.getConfiguration();
|
|
302
269
|
const record = asset;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const isCompleted = record.upload_status === 'completed' ||
|
|
306
|
-
(!record.upload_status && fileIndex?.files && fileIndex.files.length > 0);
|
|
307
|
-
if (isCompleted) {
|
|
308
|
-
return {
|
|
309
|
-
status: 200,
|
|
310
|
-
content: JSON.stringify({
|
|
311
|
-
id: record.id,
|
|
312
|
-
status: 'completed',
|
|
313
|
-
tileset_url: `/${config.endpoint}/${record.id}/files/${fileIndex?.root_file || 'tileset.json'}`,
|
|
314
|
-
file_count: fileIndex?.files?.length || 0
|
|
315
|
-
}),
|
|
316
|
-
headers: { 'Content-Type': 'application/json' }
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
// Broken record: no status and no files
|
|
320
|
-
if (!record.upload_status) {
|
|
321
|
-
return {
|
|
322
|
-
status: 200,
|
|
323
|
-
content: JSON.stringify({
|
|
324
|
-
id: record.id,
|
|
325
|
-
status: 'failed',
|
|
326
|
-
error: 'Upload incomplete - no files found'
|
|
327
|
-
}),
|
|
328
|
-
headers: { 'Content-Type': 'application/json' }
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
return {
|
|
332
|
-
status: 200,
|
|
333
|
-
content: JSON.stringify({
|
|
270
|
+
if (record.upload_status === 'completed') {
|
|
271
|
+
return successResponse({
|
|
334
272
|
id: record.id,
|
|
335
|
-
status:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}),
|
|
339
|
-
headers: { 'Content-Type': 'application/json' }
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
catch (error) {
|
|
343
|
-
return {
|
|
344
|
-
status: 500,
|
|
345
|
-
content: JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }),
|
|
346
|
-
headers: { 'Content-Type': 'application/json' }
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
/**
|
|
351
|
-
* Serve extracted tileset file (tileset.json, .b3dm, textures, etc.)
|
|
352
|
-
*/
|
|
353
|
-
async handleGetFile(req) {
|
|
354
|
-
try {
|
|
355
|
-
const { id } = req.params || {};
|
|
356
|
-
// Extract file path from wildcard param (Express 4.x/5.x compatibility)
|
|
357
|
-
let filePath = req.params[0] || req.params['0'] || req.params['*'] || req.params.path || req.params['*0'];
|
|
358
|
-
if (!filePath && req.url) {
|
|
359
|
-
const match = req.url.match(/\/files\/(.+)$/);
|
|
360
|
-
if (match)
|
|
361
|
-
filePath = decodeURIComponent(match[1]);
|
|
362
|
-
}
|
|
363
|
-
if (!filePath && req.originalUrl) {
|
|
364
|
-
const match = req.originalUrl.match(/\/files\/(.+)$/);
|
|
365
|
-
if (match)
|
|
366
|
-
filePath = decodeURIComponent(match[1]);
|
|
367
|
-
}
|
|
368
|
-
if (!id)
|
|
369
|
-
return {
|
|
370
|
-
status: 400,
|
|
371
|
-
content: JSON.stringify({ error: 'Asset ID is required' }),
|
|
372
|
-
headers: { 'Content-Type': 'application/json' }
|
|
373
|
-
};
|
|
374
|
-
if (!filePath)
|
|
375
|
-
return {
|
|
376
|
-
status: 400,
|
|
377
|
-
content: JSON.stringify({ error: 'File path is required' }),
|
|
378
|
-
headers: { 'Content-Type': 'application/json' }
|
|
379
|
-
};
|
|
380
|
-
const asset = await this.getAssetById(id);
|
|
381
|
-
const config = this.getConfiguration();
|
|
382
|
-
if (!asset || asset.name !== config.name) {
|
|
383
|
-
return {
|
|
384
|
-
status: 404,
|
|
385
|
-
content: JSON.stringify({ error: 'Tileset not found' }),
|
|
386
|
-
headers: { 'Content-Type': 'application/json' }
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
// Check access for private assets
|
|
390
|
-
if (!asset.is_public && !ApisixAuthParser.isAdmin(req.headers || {})) {
|
|
391
|
-
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
392
|
-
return {
|
|
393
|
-
status: 401,
|
|
394
|
-
content: JSON.stringify({ error: 'Authentication required for private assets' }),
|
|
395
|
-
headers: { 'Content-Type': 'application/json' }
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
399
|
-
if (authUser) {
|
|
400
|
-
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
401
|
-
if (!userRecord.id || asset.owner_id !== userRecord.id) {
|
|
402
|
-
return {
|
|
403
|
-
status: 403,
|
|
404
|
-
content: JSON.stringify({ error: 'This asset is private' }),
|
|
405
|
-
headers: { 'Content-Type': 'application/json' }
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
const storagePath = `${asset.url}/${filePath}`;
|
|
411
|
-
let fileContent;
|
|
412
|
-
try {
|
|
413
|
-
fileContent = await this.storage.retrieve(storagePath);
|
|
273
|
+
status: 'completed',
|
|
274
|
+
tileset_url: record.tileset_url
|
|
275
|
+
});
|
|
414
276
|
}
|
|
415
|
-
|
|
416
|
-
return {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
};
|
|
277
|
+
if (record.upload_status === 'failed') {
|
|
278
|
+
return successResponse({
|
|
279
|
+
id: record.id,
|
|
280
|
+
status: 'failed',
|
|
281
|
+
error: record.upload_error || 'Upload failed'
|
|
282
|
+
});
|
|
421
283
|
}
|
|
422
|
-
return {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
};
|
|
284
|
+
return successResponse({
|
|
285
|
+
id: record.id,
|
|
286
|
+
status: record.upload_status || 'unknown',
|
|
287
|
+
job_id: record.upload_job_id
|
|
288
|
+
});
|
|
427
289
|
}
|
|
428
290
|
catch (error) {
|
|
429
|
-
return
|
|
430
|
-
status: 500,
|
|
431
|
-
content: JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }),
|
|
432
|
-
headers: { 'Content-Type': 'application/json' }
|
|
433
|
-
};
|
|
291
|
+
return errorResponse(error);
|
|
434
292
|
}
|
|
435
293
|
}
|
|
436
294
|
/**
|
|
437
|
-
*
|
|
438
|
-
*/
|
|
439
|
-
getContentTypeForFile(filePath) {
|
|
440
|
-
const ext = filePath.toLowerCase().split('.').pop();
|
|
441
|
-
const contentTypes = {
|
|
442
|
-
json: 'application/json',
|
|
443
|
-
b3dm: 'application/octet-stream',
|
|
444
|
-
i3dm: 'application/octet-stream',
|
|
445
|
-
pnts: 'application/octet-stream',
|
|
446
|
-
cmpt: 'application/octet-stream',
|
|
447
|
-
glb: 'model/gltf-binary',
|
|
448
|
-
gltf: 'model/gltf+json',
|
|
449
|
-
bin: 'application/octet-stream',
|
|
450
|
-
png: 'image/png',
|
|
451
|
-
jpg: 'image/jpeg',
|
|
452
|
-
jpeg: 'image/jpeg',
|
|
453
|
-
webp: 'image/webp',
|
|
454
|
-
ktx2: 'image/ktx2'
|
|
455
|
-
};
|
|
456
|
-
return contentTypes[ext || ''] || 'application/octet-stream';
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* Override retrieve to include tileset.json URL in the response.
|
|
295
|
+
* List all tilesets with their public URLs.
|
|
460
296
|
*/
|
|
461
297
|
async retrieve(req) {
|
|
462
298
|
try {
|
|
463
299
|
const assets = await this.getAllAssets();
|
|
464
|
-
const config = this.getConfiguration();
|
|
465
300
|
const isAdmin = req && ApisixAuthParser.isAdmin(req.headers || {});
|
|
466
301
|
// Get authenticated user ID if available
|
|
467
302
|
let authenticatedUserId = null;
|
|
@@ -476,109 +311,48 @@ export class TilesetManager extends AssetsManager {
|
|
|
476
311
|
const visibleAssets = isAdmin
|
|
477
312
|
? assets
|
|
478
313
|
: assets.filter(asset => asset.is_public || (authenticatedUserId !== null && asset.owner_id === authenticatedUserId));
|
|
479
|
-
// Transform to
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
filename: asset.filename || '',
|
|
492
|
-
is_public: asset.is_public ?? true,
|
|
493
|
-
// Tileset-specific URLs
|
|
494
|
-
file_count: fileIndex?.files?.length || 0,
|
|
495
|
-
root_file: rootFile,
|
|
496
|
-
tileset_url: `/${config.endpoint}/${asset.id}/files/${rootFile}`,
|
|
497
|
-
files_base_url: `/${config.endpoint}/${asset.id}/files`
|
|
498
|
-
};
|
|
499
|
-
});
|
|
500
|
-
return {
|
|
501
|
-
status: 200,
|
|
502
|
-
content: JSON.stringify(assetsWithUrls),
|
|
503
|
-
headers: { 'Content-Type': 'application/json' }
|
|
504
|
-
};
|
|
314
|
+
// Transform to response format
|
|
315
|
+
const response = visibleAssets.map(asset => ({
|
|
316
|
+
id: asset.id,
|
|
317
|
+
description: asset.description || '',
|
|
318
|
+
filename: asset.filename || '',
|
|
319
|
+
date: asset.date,
|
|
320
|
+
owner_id: asset.owner_id || null,
|
|
321
|
+
is_public: asset.is_public ?? true,
|
|
322
|
+
tileset_url: asset.tileset_url || '',
|
|
323
|
+
upload_status: asset.upload_status || 'completed'
|
|
324
|
+
}));
|
|
325
|
+
return successResponse(response);
|
|
505
326
|
}
|
|
506
327
|
catch (error) {
|
|
507
|
-
return
|
|
508
|
-
status: 500,
|
|
509
|
-
content: JSON.stringify({
|
|
510
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
511
|
-
}),
|
|
512
|
-
headers: { 'Content-Type': 'application/json' }
|
|
513
|
-
};
|
|
328
|
+
return errorResponse(error);
|
|
514
329
|
}
|
|
515
330
|
}
|
|
516
331
|
/**
|
|
517
|
-
* Delete tileset and all
|
|
332
|
+
* Delete tileset and all files from storage.
|
|
518
333
|
*/
|
|
519
334
|
async handleDelete(req) {
|
|
520
335
|
try {
|
|
521
336
|
// Authenticate user
|
|
522
|
-
|
|
523
|
-
if (
|
|
524
|
-
|
|
525
|
-
id: AuthConfig.getAnonymousUserId(),
|
|
526
|
-
roles: []
|
|
527
|
-
});
|
|
528
|
-
userId = userRecord.id;
|
|
529
|
-
}
|
|
530
|
-
else {
|
|
531
|
-
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
532
|
-
return {
|
|
533
|
-
status: 401,
|
|
534
|
-
content: JSON.stringify({ error: 'Authentication required' }),
|
|
535
|
-
headers: { 'Content-Type': 'application/json' }
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
539
|
-
if (!authUser) {
|
|
540
|
-
return {
|
|
541
|
-
status: 401,
|
|
542
|
-
content: JSON.stringify({ error: 'Invalid authentication headers' }),
|
|
543
|
-
headers: { 'Content-Type': 'application/json' }
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
547
|
-
if (!userRecord.id) {
|
|
548
|
-
return {
|
|
549
|
-
status: 500,
|
|
550
|
-
content: JSON.stringify({ error: 'Failed to retrieve user information' }),
|
|
551
|
-
headers: { 'Content-Type': 'application/json' }
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
userId = userRecord.id;
|
|
337
|
+
const userId = await this.authenticateUser(req);
|
|
338
|
+
if (typeof userId !== 'number') {
|
|
339
|
+
return userId;
|
|
555
340
|
}
|
|
556
341
|
const { id } = req.params || {};
|
|
557
|
-
if (!id)
|
|
558
|
-
return
|
|
559
|
-
|
|
560
|
-
content: JSON.stringify({ error: 'Asset ID is required' }),
|
|
561
|
-
headers: { 'Content-Type': 'application/json' }
|
|
562
|
-
};
|
|
342
|
+
if (!id) {
|
|
343
|
+
return badRequestResponse('Asset ID is required');
|
|
344
|
+
}
|
|
563
345
|
const asset = await this.getAssetById(id);
|
|
564
|
-
if (!asset)
|
|
565
|
-
return
|
|
566
|
-
|
|
567
|
-
content: JSON.stringify({ error: 'Tileset not found' }),
|
|
568
|
-
headers: { 'Content-Type': 'application/json' }
|
|
569
|
-
};
|
|
346
|
+
if (!asset) {
|
|
347
|
+
return notFoundResponse('Tileset not found');
|
|
348
|
+
}
|
|
570
349
|
// Check ownership (admins can delete any)
|
|
571
350
|
const isAdmin = ApisixAuthParser.isAdmin(req.headers || {});
|
|
572
351
|
if (!isAdmin && asset.owner_id !== null && asset.owner_id !== userId) {
|
|
573
|
-
return
|
|
574
|
-
status: 403,
|
|
575
|
-
content: JSON.stringify({ error: 'You can only delete your own assets' }),
|
|
576
|
-
headers: { 'Content-Type': 'application/json' }
|
|
577
|
-
};
|
|
352
|
+
return forbiddenResponse('You can only delete your own assets');
|
|
578
353
|
}
|
|
579
|
-
// Block deletion while upload in progress
|
|
580
|
-
|
|
581
|
-
if (record.upload_status === 'pending' || record.upload_status === 'processing') {
|
|
354
|
+
// Block deletion while upload in progress
|
|
355
|
+
if (asset.upload_status === 'pending' || asset.upload_status === 'processing') {
|
|
582
356
|
return {
|
|
583
357
|
status: 409,
|
|
584
358
|
content: JSON.stringify({ error: 'Cannot delete tileset while upload is in progress' }),
|
|
@@ -586,58 +360,65 @@ export class TilesetManager extends AssetsManager {
|
|
|
586
360
|
};
|
|
587
361
|
}
|
|
588
362
|
// Delete all files from storage
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
363
|
+
// Support both new format (url = basePath) and legacy format (file_index.files)
|
|
364
|
+
const legacyFileIndex = asset.file_index;
|
|
365
|
+
if (legacyFileIndex?.files && legacyFileIndex.files.length > 0) {
|
|
366
|
+
// Legacy format: delete individual files from file_index
|
|
367
|
+
console.log(`[TilesetManager] Deleting ${legacyFileIndex.files.length} files (legacy format)`);
|
|
368
|
+
for (const file of legacyFileIndex.files) {
|
|
369
|
+
await this.storage.delete(file.path).catch(() => {
|
|
370
|
+
// Ignore individual file deletion errors
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else if (asset.url) {
|
|
375
|
+
// New format: url contains basePath, use deleteByPrefix
|
|
376
|
+
const deletedCount = await this.storage.deleteByPrefix(asset.url);
|
|
377
|
+
console.log(`[TilesetManager] Deleted ${deletedCount} files from ${asset.url}`);
|
|
592
378
|
}
|
|
379
|
+
// Delete database record
|
|
593
380
|
await this.deleteAssetById(id);
|
|
594
|
-
return {
|
|
595
|
-
status: 200,
|
|
596
|
-
content: JSON.stringify({ message: 'Tileset deleted successfully' }),
|
|
597
|
-
headers: { 'Content-Type': 'application/json' }
|
|
598
|
-
};
|
|
381
|
+
return successResponse({ message: 'Tileset deleted successfully' });
|
|
599
382
|
}
|
|
600
383
|
catch (error) {
|
|
601
|
-
return
|
|
602
|
-
status: 500,
|
|
603
|
-
content: JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }),
|
|
604
|
-
headers: { 'Content-Type': 'application/json' }
|
|
605
|
-
};
|
|
384
|
+
return errorResponse(error);
|
|
606
385
|
}
|
|
607
386
|
}
|
|
608
387
|
/**
|
|
609
|
-
*
|
|
388
|
+
* Get HTTP endpoints for this manager.
|
|
610
389
|
*/
|
|
611
390
|
getEndpoints() {
|
|
612
391
|
const config = this.getConfiguration();
|
|
613
|
-
// Get base endpoints from parent but filter out ones we override
|
|
614
|
-
const baseEndpoints = super.getEndpoints().filter(ep =>
|
|
615
|
-
// Keep all except GET /:id (we don't serve the raw asset, only files)
|
|
616
|
-
// and DELETE /:id (we override to clean up files)
|
|
617
|
-
!(ep.method === 'get' && ep.path === `/${config.endpoint}/:id`) &&
|
|
618
|
-
!(ep.method === 'delete' && ep.path === `/${config.endpoint}/:id`) &&
|
|
619
|
-
// Also remove download endpoint - not applicable for extracted tilesets
|
|
620
|
-
!(ep.method === 'get' && ep.path === `/${config.endpoint}/:id/download`));
|
|
621
|
-
// IMPORTANT: More specific routes must come BEFORE less specific ones
|
|
622
|
-
// /:id/files/* and /:id/status must be registered before /:id to avoid incorrect matching
|
|
623
392
|
return [
|
|
624
|
-
// Status endpoint
|
|
393
|
+
// Status endpoint (for async upload polling)
|
|
625
394
|
{
|
|
626
395
|
method: 'get',
|
|
627
396
|
path: `/${config.endpoint}/:id/status`,
|
|
628
397
|
handler: this.handleGetStatus.bind(this),
|
|
629
398
|
responseType: 'application/json'
|
|
630
399
|
},
|
|
631
|
-
//
|
|
400
|
+
// List tilesets
|
|
632
401
|
{
|
|
633
402
|
method: 'get',
|
|
634
|
-
path: `/${config.endpoint}
|
|
635
|
-
handler: this.
|
|
636
|
-
responseType: 'application/
|
|
403
|
+
path: `/${config.endpoint}`,
|
|
404
|
+
handler: this.retrieve.bind(this),
|
|
405
|
+
responseType: 'application/json'
|
|
637
406
|
},
|
|
638
|
-
//
|
|
639
|
-
|
|
640
|
-
|
|
407
|
+
// Upload tileset
|
|
408
|
+
{
|
|
409
|
+
method: 'post',
|
|
410
|
+
path: `/${config.endpoint}`,
|
|
411
|
+
handler: this.handleUpload.bind(this),
|
|
412
|
+
responseType: 'application/json'
|
|
413
|
+
},
|
|
414
|
+
// Update metadata
|
|
415
|
+
{
|
|
416
|
+
method: 'put',
|
|
417
|
+
path: `/${config.endpoint}/:id`,
|
|
418
|
+
handler: this.handleUpdate.bind(this),
|
|
419
|
+
responseType: 'application/json'
|
|
420
|
+
},
|
|
421
|
+
// Delete tileset
|
|
641
422
|
{
|
|
642
423
|
method: 'delete',
|
|
643
424
|
path: `/${config.endpoint}/:id`,
|
|
@@ -647,99 +428,191 @@ export class TilesetManager extends AssetsManager {
|
|
|
647
428
|
];
|
|
648
429
|
}
|
|
649
430
|
/**
|
|
650
|
-
*
|
|
431
|
+
* Generate OpenAPI specification.
|
|
651
432
|
*/
|
|
652
433
|
getOpenAPISpec() {
|
|
653
|
-
const parentSpec = super.getOpenAPISpec();
|
|
654
434
|
const config = this.getConfiguration();
|
|
655
435
|
const basePath = `/${config.endpoint}`;
|
|
656
436
|
const tagName = config.tags?.[0] || config.name;
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
437
|
+
return {
|
|
438
|
+
paths: {
|
|
439
|
+
[basePath]: {
|
|
440
|
+
get: {
|
|
441
|
+
summary: 'List all tilesets',
|
|
442
|
+
description: 'Returns all tilesets with their public URLs for Cesium loading',
|
|
443
|
+
tags: [tagName],
|
|
444
|
+
responses: {
|
|
445
|
+
'200': {
|
|
446
|
+
description: 'List of tilesets',
|
|
447
|
+
content: {
|
|
448
|
+
'application/json': {
|
|
449
|
+
schema: {
|
|
450
|
+
type: 'array',
|
|
451
|
+
items: { $ref: '#/components/schemas/TilesetResponse' }
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
670
455
|
}
|
|
671
456
|
}
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
// Add file serving endpoint
|
|
677
|
-
parentSpec.paths[`${basePath}/{id}/files/{filePath}`] = {
|
|
678
|
-
get: {
|
|
679
|
-
summary: 'Get tileset file',
|
|
680
|
-
description: 'Retrieve an extracted file from the tileset (tileset.json, .b3dm, etc.)',
|
|
681
|
-
tags: [tagName],
|
|
682
|
-
parameters: [
|
|
683
|
-
{
|
|
684
|
-
name: 'id',
|
|
685
|
-
in: 'path',
|
|
686
|
-
required: true,
|
|
687
|
-
schema: { type: 'string' },
|
|
688
|
-
description: 'Tileset ID'
|
|
689
457
|
},
|
|
690
|
-
{
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
458
|
+
post: {
|
|
459
|
+
summary: 'Upload a tileset',
|
|
460
|
+
description: 'Upload a ZIP file containing a 3D Tiles tileset. Files < 50MB are processed synchronously, larger files are queued.',
|
|
461
|
+
tags: [tagName],
|
|
462
|
+
security: [{ ApiKeyAuth: [] }],
|
|
463
|
+
requestBody: {
|
|
464
|
+
required: true,
|
|
465
|
+
content: {
|
|
466
|
+
'multipart/form-data': {
|
|
467
|
+
schema: {
|
|
468
|
+
type: 'object',
|
|
469
|
+
required: ['file', 'description'],
|
|
470
|
+
properties: {
|
|
471
|
+
file: {
|
|
472
|
+
type: 'string',
|
|
473
|
+
format: 'binary',
|
|
474
|
+
description: 'ZIP file containing tileset'
|
|
475
|
+
},
|
|
476
|
+
description: { type: 'string', description: 'Tileset description' },
|
|
477
|
+
is_public: {
|
|
478
|
+
type: 'boolean',
|
|
479
|
+
description: 'Whether tileset is public (default: true)'
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
responses: {
|
|
487
|
+
'200': {
|
|
488
|
+
description: 'Tileset uploaded successfully (sync)',
|
|
489
|
+
content: {
|
|
490
|
+
'application/json': {
|
|
491
|
+
schema: {
|
|
492
|
+
type: 'object',
|
|
493
|
+
properties: {
|
|
494
|
+
message: { type: 'string' },
|
|
495
|
+
id: { type: 'integer' },
|
|
496
|
+
tileset_url: {
|
|
497
|
+
type: 'string',
|
|
498
|
+
description: 'Public URL to load in Cesium'
|
|
499
|
+
},
|
|
500
|
+
file_count: { type: 'integer' }
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
'202': {
|
|
507
|
+
description: 'Upload accepted for async processing',
|
|
508
|
+
content: {
|
|
509
|
+
'application/json': {
|
|
510
|
+
schema: {
|
|
511
|
+
type: 'object',
|
|
512
|
+
properties: {
|
|
513
|
+
message: { type: 'string' },
|
|
514
|
+
id: { type: 'integer' },
|
|
515
|
+
status: { type: 'string' },
|
|
516
|
+
status_url: { type: 'string' }
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
'400': { description: 'Bad request - missing fields or invalid file' },
|
|
523
|
+
'401': { description: 'Unauthorized' }
|
|
524
|
+
}
|
|
696
525
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
526
|
+
},
|
|
527
|
+
[`${basePath}/{id}/status`]: {
|
|
528
|
+
get: {
|
|
529
|
+
summary: 'Get upload status',
|
|
530
|
+
description: 'Poll the status of an async upload',
|
|
531
|
+
tags: [tagName],
|
|
532
|
+
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
533
|
+
responses: {
|
|
534
|
+
'200': {
|
|
535
|
+
description: 'Upload status',
|
|
536
|
+
content: {
|
|
537
|
+
'application/json': {
|
|
538
|
+
schema: {
|
|
539
|
+
type: 'object',
|
|
540
|
+
properties: {
|
|
541
|
+
id: { type: 'integer' },
|
|
542
|
+
status: {
|
|
543
|
+
type: 'string',
|
|
544
|
+
enum: ['pending', 'processing', 'completed', 'failed']
|
|
545
|
+
},
|
|
546
|
+
tileset_url: { type: 'string' },
|
|
547
|
+
error: { type: 'string' }
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
'404': { description: 'Tileset not found' }
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
[`${basePath}/{id}`]: {
|
|
558
|
+
put: {
|
|
559
|
+
summary: 'Update tileset metadata',
|
|
560
|
+
tags: [tagName],
|
|
561
|
+
security: [{ ApiKeyAuth: [] }],
|
|
562
|
+
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
563
|
+
requestBody: {
|
|
564
|
+
content: {
|
|
565
|
+
'application/json': {
|
|
566
|
+
schema: {
|
|
567
|
+
type: 'object',
|
|
568
|
+
properties: {
|
|
569
|
+
description: { type: 'string' },
|
|
570
|
+
is_public: { type: 'boolean' }
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
704
574
|
}
|
|
575
|
+
},
|
|
576
|
+
responses: {
|
|
577
|
+
'200': { description: 'Updated successfully' },
|
|
578
|
+
'401': { description: 'Unauthorized' },
|
|
579
|
+
'403': { description: 'Forbidden' },
|
|
580
|
+
'404': { description: 'Not found' }
|
|
705
581
|
}
|
|
706
582
|
},
|
|
707
|
-
|
|
583
|
+
delete: {
|
|
584
|
+
summary: 'Delete tileset',
|
|
585
|
+
description: 'Delete tileset and all files from storage',
|
|
586
|
+
tags: [tagName],
|
|
587
|
+
security: [{ ApiKeyAuth: [] }],
|
|
588
|
+
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
589
|
+
responses: {
|
|
590
|
+
'200': { description: 'Deleted successfully' },
|
|
591
|
+
'401': { description: 'Unauthorized' },
|
|
592
|
+
'403': { description: 'Forbidden' },
|
|
593
|
+
'404': { description: 'Not found' },
|
|
594
|
+
'409': { description: 'Upload in progress' }
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
tags: [{ name: tagName, description: config.description }],
|
|
600
|
+
schemas: {
|
|
601
|
+
TilesetResponse: {
|
|
602
|
+
type: 'object',
|
|
603
|
+
properties: {
|
|
604
|
+
id: { type: 'integer' },
|
|
605
|
+
description: { type: 'string' },
|
|
606
|
+
filename: { type: 'string' },
|
|
607
|
+
date: { type: 'string', format: 'date-time' },
|
|
608
|
+
owner_id: { type: 'integer', nullable: true },
|
|
609
|
+
is_public: { type: 'boolean' },
|
|
610
|
+
tileset_url: { type: 'string', description: 'Public URL to load in Cesium' },
|
|
611
|
+
upload_status: { type: 'string', enum: ['pending', 'processing', 'completed', 'failed'] }
|
|
612
|
+
}
|
|
708
613
|
}
|
|
709
614
|
}
|
|
710
615
|
};
|
|
711
|
-
// Update GET list response schema
|
|
712
|
-
if (parentSpec.schemas) {
|
|
713
|
-
parentSpec.schemas.TilesetResponse = {
|
|
714
|
-
type: 'object',
|
|
715
|
-
properties: {
|
|
716
|
-
id: { type: 'integer' },
|
|
717
|
-
name: { type: 'string' },
|
|
718
|
-
date: { type: 'string', format: 'date-time' },
|
|
719
|
-
contentType: { type: 'string' },
|
|
720
|
-
description: { type: 'string' },
|
|
721
|
-
source: { type: 'string' },
|
|
722
|
-
owner_id: { type: 'integer', nullable: true },
|
|
723
|
-
filename: { type: 'string' },
|
|
724
|
-
is_public: { type: 'boolean' },
|
|
725
|
-
file_count: { type: 'integer', description: 'Number of files in tileset' },
|
|
726
|
-
root_file: { type: 'string', description: 'Root tileset.json filename' },
|
|
727
|
-
tileset_url: { type: 'string', description: 'URL to load tileset in Cesium' },
|
|
728
|
-
files_base_url: { type: 'string', description: 'Base URL for all tileset files' }
|
|
729
|
-
}
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
if (parentSpec.paths[basePath]?.get?.responses?.['200']?.content?.['application/json']) {
|
|
733
|
-
parentSpec.paths[basePath].get.responses['200'].content['application/json'].schema = {
|
|
734
|
-
type: 'array',
|
|
735
|
-
items: { $ref: '#/components/schemas/TilesetResponse' }
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
// Remove the single asset GET endpoint since we don't serve raw assets
|
|
739
|
-
delete parentSpec.paths[`${basePath}/{id}`]?.get;
|
|
740
|
-
// Remove download endpoint
|
|
741
|
-
delete parentSpec.paths[`${basePath}/{id}/download`];
|
|
742
|
-
return parentSpec;
|
|
743
616
|
}
|
|
744
617
|
}
|
|
745
618
|
//# sourceMappingURL=tileset_manager.js.map
|