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.
- package/dist/components/assets_manager.d.ts +9 -1
- package/dist/components/assets_manager.d.ts.map +1 -1
- package/dist/components/assets_manager.js +26 -13
- package/dist/components/assets_manager.js.map +1 -1
- package/dist/components/global_assets_handler.js +2 -2
- package/dist/components/global_assets_handler.js.map +1 -1
- package/dist/components/map_manager.js +2 -2
- package/dist/components/map_manager.js.map +1 -1
- package/dist/components/tileset_manager.d.ts +84 -31
- package/dist/components/tileset_manager.d.ts.map +1 -1
- package/dist/components/tileset_manager.js +491 -120
- 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 +17 -0
- package/dist/database/adapters/knex_database_adapter.js.map +1 -1
- package/dist/storage/adapters/local_storage_service.d.ts +8 -0
- package/dist/storage/adapters/local_storage_service.d.ts.map +1 -1
- package/dist/storage/adapters/local_storage_service.js +14 -0
- package/dist/storage/adapters/local_storage_service.js.map +1 -1
- package/dist/storage/adapters/ovh_storage_service.d.ts +8 -0
- package/dist/storage/adapters/ovh_storage_service.d.ts.map +1 -1
- package/dist/storage/adapters/ovh_storage_service.js +16 -0
- package/dist/storage/adapters/ovh_storage_service.js.map +1 -1
- package/dist/storage/storage_service.d.ts +20 -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 +30 -0
- package/dist/types/data_record.d.ts.map +1 -1
- package/dist/utils/index.d.ts +2 -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 +7 -1
- package/dist/utils/map_to_data_record.js.map +1 -1
- package/dist/utils/zip_utils.d.ts +58 -0
- package/dist/utils/zip_utils.d.ts.map +1 -1
- package/dist/utils/zip_utils.js +100 -0
- package/dist/utils/zip_utils.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,34 +1,60 @@
|
|
|
1
1
|
import { AssetsManager } from './assets_manager.js';
|
|
2
|
-
import {
|
|
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
|
|
7
|
+
* Specialized Assets Manager for handling 3D Tiles tilesets.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
46
|
+
* Override the upload handler to extract ZIP files and store individual files.
|
|
24
47
|
*
|
|
25
|
-
*
|
|
26
|
-
* 1.
|
|
27
|
-
* 2.
|
|
28
|
-
* 3.
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
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:
|
|
119
|
+
status: 400,
|
|
70
120
|
content: JSON.stringify({
|
|
71
|
-
error: '
|
|
121
|
+
error: 'Missing required fields: ZIP file'
|
|
72
122
|
}),
|
|
73
123
|
headers: { 'Content-Type': 'application/json' }
|
|
74
124
|
};
|
|
75
125
|
}
|
|
76
|
-
|
|
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
|
|
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
|
|
108
|
-
let
|
|
154
|
+
// Read ZIP file content
|
|
155
|
+
let zipBuffer;
|
|
109
156
|
try {
|
|
110
|
-
|
|
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
|
-
//
|
|
130
|
-
const
|
|
131
|
-
//
|
|
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:
|
|
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 || '
|
|
139
|
-
owner_id:
|
|
140
|
-
filename,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
157
|
-
file_count:
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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(
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
|
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
|
-
|
|
264
|
-
file_count: { type: 'integer', description: 'Number of files in
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
}
|