digitaltwin-core 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/assets_manager.d.ts +9 -1
- package/dist/components/assets_manager.d.ts.map +1 -1
- package/dist/components/assets_manager.js +24 -11
- package/dist/components/assets_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 +510 -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,196 @@ 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/ultimate-express captures wildcard path in different ways:
|
|
261
|
+
// - Express 4.x: params[0] or params['0']
|
|
262
|
+
// - Express 5.x / ultimate-express: params['*'] or via URL parsing
|
|
263
|
+
let filePath = req.params[0] || req.params['0'] || req.params['*'] || req.params.path || req.params['*0'];
|
|
264
|
+
// Fallback: extract from URL if params didn't capture it
|
|
265
|
+
if (!filePath && req.url) {
|
|
266
|
+
const match = req.url.match(/\/files\/(.+)$/);
|
|
267
|
+
if (match) {
|
|
268
|
+
filePath = decodeURIComponent(match[1]);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Another fallback: use originalUrl
|
|
272
|
+
if (!filePath && req.originalUrl) {
|
|
273
|
+
const match = req.originalUrl.match(/\/files\/(.+)$/);
|
|
274
|
+
if (match) {
|
|
275
|
+
filePath = decodeURIComponent(match[1]);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (!id) {
|
|
279
|
+
return {
|
|
280
|
+
status: 400,
|
|
281
|
+
content: JSON.stringify({ error: 'Asset ID is required' }),
|
|
282
|
+
headers: { 'Content-Type': 'application/json' }
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (!filePath) {
|
|
286
|
+
return {
|
|
287
|
+
status: 400,
|
|
288
|
+
content: JSON.stringify({ error: 'File path is required' }),
|
|
289
|
+
headers: { 'Content-Type': 'application/json' }
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// Get asset metadata
|
|
293
|
+
const asset = await this.getAssetById(id);
|
|
294
|
+
if (!asset) {
|
|
295
|
+
return {
|
|
296
|
+
status: 404,
|
|
297
|
+
content: JSON.stringify({ error: 'Tileset not found' }),
|
|
298
|
+
headers: { 'Content-Type': 'application/json' }
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// Verify this asset belongs to our component
|
|
302
|
+
const config = this.getConfiguration();
|
|
303
|
+
if (asset.name !== config.name) {
|
|
304
|
+
return {
|
|
305
|
+
status: 404,
|
|
306
|
+
content: JSON.stringify({ error: 'Tileset not found' }),
|
|
307
|
+
headers: { 'Content-Type': 'application/json' }
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
// Check access permissions for private assets
|
|
311
|
+
if (!asset.is_public) {
|
|
312
|
+
if (ApisixAuthParser.isAdmin(req.headers || {})) {
|
|
313
|
+
// Admin can access everything
|
|
314
|
+
}
|
|
315
|
+
else if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
316
|
+
return {
|
|
317
|
+
status: 401,
|
|
318
|
+
content: JSON.stringify({ error: 'Authentication required for private assets' }),
|
|
319
|
+
headers: { 'Content-Type': 'application/json' }
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
324
|
+
if (authUser) {
|
|
325
|
+
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
326
|
+
if (!userRecord.id || asset.owner_id !== userRecord.id) {
|
|
327
|
+
return {
|
|
328
|
+
status: 403,
|
|
329
|
+
content: JSON.stringify({ error: 'This asset is private' }),
|
|
330
|
+
headers: { 'Content-Type': 'application/json' }
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Build storage path using asset.url as base path
|
|
337
|
+
// asset.url contains the base path (e.g., 'tilesets_manager/1733064000000')
|
|
338
|
+
const storagePath = `${asset.url}/${filePath}`;
|
|
339
|
+
// Retrieve file from storage
|
|
340
|
+
let fileContent;
|
|
341
|
+
try {
|
|
342
|
+
fileContent = await this.storage.retrieve(storagePath);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return {
|
|
346
|
+
status: 404,
|
|
347
|
+
content: JSON.stringify({ error: 'File not found' }),
|
|
348
|
+
headers: { 'Content-Type': 'application/json' }
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
// Determine content type based on file extension
|
|
352
|
+
const contentType = this.getContentTypeForFile(filePath);
|
|
353
|
+
return {
|
|
354
|
+
status: 200,
|
|
355
|
+
content: fileContent,
|
|
356
|
+
headers: {
|
|
357
|
+
'Content-Type': contentType,
|
|
358
|
+
'Access-Control-Allow-Origin': '*'
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
return {
|
|
364
|
+
status: 500,
|
|
365
|
+
content: JSON.stringify({
|
|
366
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
367
|
+
}),
|
|
368
|
+
headers: { 'Content-Type': 'application/json' }
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Determines the MIME content type based on file extension.
|
|
374
|
+
*/
|
|
375
|
+
getContentTypeForFile(filePath) {
|
|
376
|
+
const ext = filePath.toLowerCase().split('.').pop();
|
|
377
|
+
const contentTypes = {
|
|
378
|
+
json: 'application/json',
|
|
379
|
+
b3dm: 'application/octet-stream',
|
|
380
|
+
i3dm: 'application/octet-stream',
|
|
381
|
+
pnts: 'application/octet-stream',
|
|
382
|
+
cmpt: 'application/octet-stream',
|
|
383
|
+
glb: 'model/gltf-binary',
|
|
384
|
+
gltf: 'model/gltf+json',
|
|
385
|
+
bin: 'application/octet-stream',
|
|
386
|
+
png: 'image/png',
|
|
387
|
+
jpg: 'image/jpeg',
|
|
388
|
+
jpeg: 'image/jpeg',
|
|
389
|
+
webp: 'image/webp',
|
|
390
|
+
ktx2: 'image/ktx2'
|
|
391
|
+
};
|
|
392
|
+
return contentTypes[ext || ''] || 'application/octet-stream';
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Override retrieve to include tileset.json URL in the response.
|
|
180
396
|
*/
|
|
181
|
-
async retrieve() {
|
|
397
|
+
async retrieve(req) {
|
|
182
398
|
try {
|
|
183
399
|
const assets = await this.getAllAssets();
|
|
184
400
|
const config = this.getConfiguration();
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
401
|
+
const isAdmin = req && ApisixAuthParser.isAdmin(req.headers || {});
|
|
402
|
+
// Get authenticated user ID if available
|
|
403
|
+
let authenticatedUserId = null;
|
|
404
|
+
if (req && ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
405
|
+
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
406
|
+
if (authUser) {
|
|
407
|
+
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
408
|
+
authenticatedUserId = userRecord.id || null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Filter to visible assets only (unless admin)
|
|
412
|
+
const visibleAssets = isAdmin
|
|
413
|
+
? assets
|
|
414
|
+
: assets.filter(asset => asset.is_public || (authenticatedUserId !== null && asset.owner_id === authenticatedUserId));
|
|
415
|
+
// Transform to include tileset URLs
|
|
416
|
+
const assetsWithUrls = visibleAssets.map(asset => {
|
|
417
|
+
const fileIndex = asset.file_index;
|
|
418
|
+
const rootFile = fileIndex?.root_file || 'tileset.json';
|
|
419
|
+
return {
|
|
420
|
+
id: asset.id,
|
|
421
|
+
name: asset.name,
|
|
422
|
+
date: asset.date,
|
|
423
|
+
contentType: asset.contentType,
|
|
424
|
+
description: asset.description || '',
|
|
425
|
+
source: asset.source || '',
|
|
426
|
+
owner_id: asset.owner_id || null,
|
|
427
|
+
filename: asset.filename || '',
|
|
428
|
+
is_public: asset.is_public ?? true,
|
|
429
|
+
// Tileset-specific URLs
|
|
430
|
+
file_count: fileIndex?.files?.length || 0,
|
|
431
|
+
root_file: rootFile,
|
|
432
|
+
tileset_url: `/${config.endpoint}/${asset.id}/files/${rootFile}`,
|
|
433
|
+
files_base_url: `/${config.endpoint}/${asset.id}/files`
|
|
434
|
+
};
|
|
435
|
+
});
|
|
204
436
|
return {
|
|
205
437
|
status: 200,
|
|
206
|
-
content: JSON.stringify(
|
|
438
|
+
content: JSON.stringify(assetsWithUrls),
|
|
207
439
|
headers: { 'Content-Type': 'application/json' }
|
|
208
440
|
};
|
|
209
441
|
}
|
|
@@ -218,36 +450,196 @@ export class TilesetManager extends AssetsManager {
|
|
|
218
450
|
}
|
|
219
451
|
}
|
|
220
452
|
/**
|
|
221
|
-
* Override
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
453
|
+
* Override delete to clean up all extracted files.
|
|
454
|
+
*/
|
|
455
|
+
async handleDelete(req) {
|
|
456
|
+
try {
|
|
457
|
+
// Check authentication (skip if auth is disabled)
|
|
458
|
+
let userId;
|
|
459
|
+
if (AuthConfig.isAuthDisabled()) {
|
|
460
|
+
const anonymousUser = {
|
|
461
|
+
id: AuthConfig.getAnonymousUserId(),
|
|
462
|
+
roles: []
|
|
463
|
+
};
|
|
464
|
+
const userRecord = await this.userService.findOrCreateUser(anonymousUser);
|
|
465
|
+
userId = userRecord.id;
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
469
|
+
return {
|
|
470
|
+
status: 401,
|
|
471
|
+
content: JSON.stringify({ error: 'Authentication required' }),
|
|
472
|
+
headers: { 'Content-Type': 'application/json' }
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
476
|
+
if (!authUser) {
|
|
477
|
+
return {
|
|
478
|
+
status: 401,
|
|
479
|
+
content: JSON.stringify({ error: 'Invalid authentication headers' }),
|
|
480
|
+
headers: { 'Content-Type': 'application/json' }
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
484
|
+
if (!userRecord.id) {
|
|
485
|
+
return {
|
|
486
|
+
status: 500,
|
|
487
|
+
content: JSON.stringify({ error: 'Failed to retrieve user information' }),
|
|
488
|
+
headers: { 'Content-Type': 'application/json' }
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
userId = userRecord.id;
|
|
492
|
+
}
|
|
493
|
+
const { id } = req.params || {};
|
|
494
|
+
if (!id) {
|
|
495
|
+
return {
|
|
496
|
+
status: 400,
|
|
497
|
+
content: JSON.stringify({ error: 'Asset ID is required' }),
|
|
498
|
+
headers: { 'Content-Type': 'application/json' }
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
// Get asset metadata
|
|
502
|
+
const asset = await this.getAssetById(id);
|
|
503
|
+
if (!asset) {
|
|
504
|
+
return {
|
|
505
|
+
status: 404,
|
|
506
|
+
content: JSON.stringify({ error: 'Tileset not found' }),
|
|
507
|
+
headers: { 'Content-Type': 'application/json' }
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
// Check ownership (admins can delete any asset)
|
|
511
|
+
const isAdmin = ApisixAuthParser.isAdmin(req.headers || {});
|
|
512
|
+
if (!isAdmin && asset.owner_id !== null && asset.owner_id !== userId) {
|
|
513
|
+
return {
|
|
514
|
+
status: 403,
|
|
515
|
+
content: JSON.stringify({ error: 'You can only delete your own assets' }),
|
|
516
|
+
headers: { 'Content-Type': 'application/json' }
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
// Delete all extracted files from storage
|
|
520
|
+
const fileIndex = asset.file_index;
|
|
521
|
+
if (fileIndex?.files) {
|
|
522
|
+
for (const file of fileIndex.files) {
|
|
523
|
+
await this.storage.delete(file.path).catch(() => {
|
|
524
|
+
// Ignore individual file deletion errors
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Delete database record
|
|
529
|
+
await this.deleteAssetById(id);
|
|
530
|
+
return {
|
|
531
|
+
status: 200,
|
|
532
|
+
content: JSON.stringify({ message: 'Tileset deleted successfully' }),
|
|
533
|
+
headers: { 'Content-Type': 'application/json' }
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
return {
|
|
538
|
+
status: 500,
|
|
539
|
+
content: JSON.stringify({
|
|
540
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
541
|
+
}),
|
|
542
|
+
headers: { 'Content-Type': 'application/json' }
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Override getEndpoints to add the file serving endpoint.
|
|
548
|
+
*/
|
|
549
|
+
getEndpoints() {
|
|
550
|
+
const config = this.getConfiguration();
|
|
551
|
+
// Get base endpoints from parent but filter out ones we override
|
|
552
|
+
const baseEndpoints = super.getEndpoints().filter(ep =>
|
|
553
|
+
// Keep all except GET /:id (we don't serve the raw asset, only files)
|
|
554
|
+
// and DELETE /:id (we override to clean up files)
|
|
555
|
+
!(ep.method === 'get' && ep.path === `/${config.endpoint}/:id`) &&
|
|
556
|
+
!(ep.method === 'delete' && ep.path === `/${config.endpoint}/:id`) &&
|
|
557
|
+
// Also remove download endpoint - not applicable for extracted tilesets
|
|
558
|
+
!(ep.method === 'get' && ep.path === `/${config.endpoint}/:id/download`));
|
|
559
|
+
// IMPORTANT: More specific routes must come BEFORE less specific ones
|
|
560
|
+
// /:id/files/* must be registered before /:id to avoid incorrect matching
|
|
561
|
+
return [
|
|
562
|
+
// File serving endpoint FIRST (most specific)
|
|
563
|
+
{
|
|
564
|
+
method: 'get',
|
|
565
|
+
path: `/${config.endpoint}/:id/files/*`,
|
|
566
|
+
handler: this.handleGetFile.bind(this),
|
|
567
|
+
responseType: 'application/octet-stream'
|
|
568
|
+
},
|
|
569
|
+
// Then base endpoints (list, create, update, etc.)
|
|
570
|
+
...baseEndpoints,
|
|
571
|
+
// Override delete to clean up files (after base to override)
|
|
572
|
+
{
|
|
573
|
+
method: 'delete',
|
|
574
|
+
path: `/${config.endpoint}/:id`,
|
|
575
|
+
handler: this.handleDelete.bind(this),
|
|
576
|
+
responseType: 'application/json'
|
|
577
|
+
}
|
|
578
|
+
];
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Override OpenAPI specification to include tileset-specific endpoints.
|
|
227
582
|
*/
|
|
228
583
|
getOpenAPISpec() {
|
|
229
584
|
const parentSpec = super.getOpenAPISpec();
|
|
230
585
|
const config = this.getConfiguration();
|
|
231
586
|
const basePath = `/${config.endpoint}`;
|
|
232
|
-
|
|
587
|
+
const tagName = config.tags?.[0] || config.name;
|
|
588
|
+
// Update POST response
|
|
233
589
|
if (parentSpec.paths[basePath]?.post?.responses?.['200']) {
|
|
234
590
|
parentSpec.paths[basePath].post.responses['200'] = {
|
|
235
|
-
description: 'Tileset uploaded successfully',
|
|
591
|
+
description: 'Tileset uploaded and extracted successfully',
|
|
236
592
|
content: {
|
|
237
593
|
'application/json': {
|
|
238
594
|
schema: {
|
|
239
595
|
type: 'object',
|
|
240
596
|
properties: {
|
|
241
597
|
message: { type: 'string' },
|
|
242
|
-
|
|
243
|
-
|
|
598
|
+
file_count: { type: 'integer', description: 'Number of files extracted' },
|
|
599
|
+
root_file: { type: 'string', description: 'Root tileset.json path' },
|
|
600
|
+
tileset_url: { type: 'string', description: 'URL to load in Cesium' }
|
|
244
601
|
}
|
|
245
602
|
}
|
|
246
603
|
}
|
|
247
604
|
}
|
|
248
605
|
};
|
|
249
606
|
}
|
|
250
|
-
// Add
|
|
607
|
+
// Add file serving endpoint
|
|
608
|
+
parentSpec.paths[`${basePath}/{id}/files/{filePath}`] = {
|
|
609
|
+
get: {
|
|
610
|
+
summary: 'Get tileset file',
|
|
611
|
+
description: 'Retrieve an extracted file from the tileset (tileset.json, .b3dm, etc.)',
|
|
612
|
+
tags: [tagName],
|
|
613
|
+
parameters: [
|
|
614
|
+
{
|
|
615
|
+
name: 'id',
|
|
616
|
+
in: 'path',
|
|
617
|
+
required: true,
|
|
618
|
+
schema: { type: 'string' },
|
|
619
|
+
description: 'Tileset ID'
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
name: 'filePath',
|
|
623
|
+
in: 'path',
|
|
624
|
+
required: true,
|
|
625
|
+
schema: { type: 'string' },
|
|
626
|
+
description: 'Path to file within tileset (e.g., tileset.json, tiles/tile_0.b3dm)'
|
|
627
|
+
}
|
|
628
|
+
],
|
|
629
|
+
responses: {
|
|
630
|
+
'200': {
|
|
631
|
+
description: 'File content',
|
|
632
|
+
content: {
|
|
633
|
+
'application/octet-stream': {
|
|
634
|
+
schema: { type: 'string', format: 'binary' }
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
'404': { description: 'Tileset or file not found' }
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
// Update GET list response schema
|
|
251
643
|
if (parentSpec.schemas) {
|
|
252
644
|
parentSpec.schemas.TilesetResponse = {
|
|
253
645
|
type: 'object',
|
|
@@ -260,26 +652,24 @@ export class TilesetManager extends AssetsManager {
|
|
|
260
652
|
source: { type: 'string' },
|
|
261
653
|
owner_id: { type: 'integer', nullable: true },
|
|
262
654
|
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' }
|
|
655
|
+
is_public: { type: 'boolean' },
|
|
656
|
+
file_count: { type: 'integer', description: 'Number of files in tileset' },
|
|
657
|
+
root_file: { type: 'string', description: 'Root tileset.json filename' },
|
|
658
|
+
tileset_url: { type: 'string', description: 'URL to load tileset in Cesium' },
|
|
659
|
+
files_base_url: { type: 'string', description: 'Base URL for all tileset files' }
|
|
273
660
|
}
|
|
274
661
|
};
|
|
275
662
|
}
|
|
276
|
-
// Update the GET list response to use tileset schema
|
|
277
663
|
if (parentSpec.paths[basePath]?.get?.responses?.['200']?.content?.['application/json']) {
|
|
278
664
|
parentSpec.paths[basePath].get.responses['200'].content['application/json'].schema = {
|
|
279
665
|
type: 'array',
|
|
280
666
|
items: { $ref: '#/components/schemas/TilesetResponse' }
|
|
281
667
|
};
|
|
282
668
|
}
|
|
669
|
+
// Remove the single asset GET endpoint since we don't serve raw assets
|
|
670
|
+
delete parentSpec.paths[`${basePath}/{id}`]?.get;
|
|
671
|
+
// Remove download endpoint
|
|
672
|
+
delete parentSpec.paths[`${basePath}/{id}/download`];
|
|
283
673
|
return parentSpec;
|
|
284
674
|
}
|
|
285
675
|
}
|