zhongrj
2025-11-24 276323dce9613867abb3f58a4cc2abbfb2fd0dea
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
from genericpath import isfile
import importlib
import json
from posixpath import join
import time
import requests
import os
from os import listdir, path
 
from app import models, pending_actions
from app.security import path_traversal_check
from app.plugins.views import TaskView
from app.plugins.worker import run_function_async, task
from app.plugins import get_current_plugin
from app.plugins import GlobalDataStore, get_site_settings, signals as plugin_signals
 
from coreplugins.dronedb.ddb import DEFAULT_HUB_URL, DroneDB, parse_url, verify_url
 
from django.dispatch import receiver
 
from worker.celery import app
from rest_framework.response import Response
from rest_framework import status
 
VALID_IMAGE_EXTENSIONS = ['.tiff', '.tif', '.png', '.jpeg', '.jpg']
 
def is_valid(file):
    _, file_extension = path.splitext(file)
    return file_extension.lower() in VALID_IMAGE_EXTENSIONS or file == 'gcp_list.txt' or file == 'geo.txt'
 
def get_settings(request):
    ds = get_current_plugin().get_user_data_store(request.user)
    
    registry_url = ds.get_string('registry_url') or DEFAULT_HUB_URL
    username = ds.get_string('username') or None
    password = ds.get_string('password') or None
    token = ds.get_string('token') or None
 
    return registry_url, username, password, token
    
 
def update_token(request, token):
    ds = get_current_plugin().get_user_data_store(request.user)
    ds.set_string('token', token)
 
def get_ddb(request):
    registry_url, username, password, token = get_settings(request)
 
    if registry_url == None or username == None or password == None:
        raise ValueError('Credentials must be set.')
 
    return DroneDB(registry_url, username, password, token, lambda token: update_token(request, token))
 
def to_web_protocol(registry_url):
    return registry_url.replace('ddb+unsafe://', 'http://').replace('ddb://', 'https://').rstrip('/')
 
class CheckCredentialsTaskView(TaskView):
    def post(self, request):
 
        # Read form data
        hub_url = request.data.get('hubUrl', None)
        username = request.data.get('userName', None)
        password = request.data.get('password', None)
 
        # Make sure both values are set
        if hub_url == None or username == None or password == None:
            return Response({'error': 'All fields must be set.'}, status=status.HTTP_400_BAD_REQUEST)
 
        try:
 
            ddb = DroneDB(hub_url, username, password)
 
            return Response({'success': ddb.login()}, status=status.HTTP_200_OK)      
 
        except(Exception) as e:
            return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
 
class OrganizationsTaskView(TaskView):
    def get(self, request):
 
        try:
 
            ddb = get_ddb(request)
 
            orgs = ddb.get_organizations()
 
            return Response(orgs, status=status.HTTP_200_OK)
 
        except Exception as e:
            return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
 
 
class DatasetsTaskView(TaskView):
    def get(self, request, org=None):
 
        if org == None:
            return Response({'error': 'Organization must be set.'}, status=status.HTTP_400_BAD_REQUEST)
 
        try:
 
            ddb = get_ddb(request)
 
            dss = ddb.get_datasets(org)
 
            return Response(dss, status=status.HTTP_200_OK)
 
        except Exception as e:
            return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
 
class FoldersTaskView(TaskView):
    def get(self, request, org=None, ds=None):
 
        if org == None or ds == None:
            return Response({'error': 'Organization and dataset must be set.'}, status=status.HTTP_400_BAD_REQUEST)
 
        try:
 
            ddb = get_ddb(request)
 
            folders = ddb.get_folders(org, ds)
 
            return Response(folders, status=status.HTTP_200_OK)
 
        except Exception as e:
            return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
 
class VerifyUrlTaskView(TaskView):
    def post(self, request):
 
        # Read form data
        url = request.data.get('url', None)
 
        if url == None:
            return Response({'error': 'Url must be set.'}, status=status.HTTP_400_BAD_REQUEST)
 
        _, username, password, _ = get_settings(request)
 
        try:
 
            result, org, ds, folder, count, size = verify_url(url, username, password).values() 
 
            if (not result):
                return Response({'error': 'Invalid url.'}, status=status.HTTP_400_BAD_REQUEST)         
 
            return Response({'count': count, 'success': result, 'ds' : ds, 'org': org, 'folder': folder or None, 'size': size} 
                    if org else {'success': False}, status=status.HTTP_200_OK)
 
        except Exception as e:
            return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)    
 
class InfoTaskView(TaskView):
    def get(self, request):
            
        registry_url, username, _, _ = get_settings(request)
     
        return Response({ 'hubUrl': registry_url, 'username': username }, status=status.HTTP_200_OK)
           
 
class ImportDatasetTaskView(TaskView):
    def post(self, request, project_pk=None, pk=None):
                        
        task = self.get_and_check_task(request, pk)
 
        # Read form data
        ddb_url = request.data.get('ddb_url', None)
                      
        if ddb_url == None:
            return Response({'error': 'DroneDB url must be set.'}, status=status.HTTP_400_BAD_REQUEST)
        
        registry_url, orgSlug, dsSlug, folder = parse_url(ddb_url).values()
 
        _, username, password, token = get_settings(request)
        ddb = DroneDB(registry_url, username, password, token, lambda token: update_token(request, token))
 
        # Get the files from the folder
        rawfiles = ddb.get_files_list(orgSlug, dsSlug, folder)
        files = [file for file in rawfiles if is_valid(file['path'])]
                        
        # Verify that the folder url is valid    
        if len(files) == 0:
            return Response({'error': 'Empty dataset or folder.'}, status=status.HTTP_400_BAD_REQUEST)
              
        # Update the task with the new information
        task.console += "Importing {} images...\n".format(len(files))
        task.images_count = len(files)
        task.pending_action = pending_actions.IMPORT
        task.save()
        
        # Associate the folder url with the project and task
        combined_id = "{}_{}".format(project_pk, pk)
        
        datastore = get_current_plugin().get_global_data_store()
        datastore.set_json(combined_id, {
            "ddbUrl": ddb_url, 
            "token": ddb.token, 
            "ddbWebUrl": "{}/r/{}/{}/{}".format(to_web_protocol(registry_url), orgSlug, dsSlug, folder.rstrip('/'))
        })
        
        #ddb.refresh_token()
 
        # Start importing the files in the background
        serialized = {'token': ddb.token, 'files': files}
        run_function_async(import_files, task.id, serialized)
 
        return Response({}, status=status.HTTP_200_OK)
 
def import_files(task_id, carrier):
    import requests
    from app import models
    from app.plugins import logger
    from app.security import path_traversal_check
 
    files = carrier['files']
    
    headers = {}
 
    if carrier['token'] != None:
        headers['Authorization'] = 'Bearer ' + carrier['token']
 
    def download_file(task, file):
        path = path_traversal_check(task.task_path(file['name']), task.task_path())
        logger.info("Downloading file: " + file['url'])
        download_stream = requests.get(file['url'], stream=True, timeout=60, headers=headers)
 
        with open(path, 'wb') as fd:
            for chunk in download_stream.iter_content(4096):
                fd.write(chunk)
        
    logger.info("Will import {} files".format(len(files)))
    task = models.Task.objects.get(pk=task_id)
    task.create_task_directories()
    task.save()
    
    try:
        downloaded_total = 0
        for file in files: 
            download_file(task, file)
            task.check_if_canceled()
            models.Task.objects.filter(pk=task.id).update(upload_progress=(float(downloaded_total) / float(len(files))))
            downloaded_total += 1
 
    except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
        raise NodeServerError(e)
 
    task.refresh_from_db()
    task.pending_action = None
    task.processing_time = 0
    task.partial = False
    task.save()
 
class CheckUrlTaskView(TaskView):
    def get(self, request, project_pk=None, pk=None):
 
        # Assert that task exists
        self.get_and_check_task(request, pk)
 
        # Check if there is an imported url associated with the project and task
        combined_id = "{}_{}".format(project_pk, pk)
        data = get_current_plugin().get_global_data_store().get_json(combined_id, default = None)
 
        if data == None or 'ddbWebUrl' not in data:
            return Response({'ddbWebUrl': None}, status=status.HTTP_200_OK)
        else:
            return Response({'ddbUrl': data['ddbWebUrl']}, status=status.HTTP_200_OK)
 
def get_status_key(task_id):
    return '{}_ddb'.format(task_id)
 
@receiver(plugin_signals.task_removed, dispatch_uid="ddb_on_task_removed")
@receiver(plugin_signals.task_completed, dispatch_uid="ddb_on_task_completed")
def ddb_cleanup(sender, task_id, **kwargs):
 
    from app.plugins import logger
 
    # When a task is removed, simply remove clutter
    # When a task is re-processed, make sure we can re-share it if we shared a task previously
 
    logger.info("Cleaning up DroneDB datastore for task {}".format(str(task_id)))
 
    datastore = get_current_plugin().get_global_data_store()
    status_key = get_status_key(task_id)
 
    logger.info("Info task {0} ({1})".format(str(task_id), status_key))
 
    datastore.del_key(status_key)
    
 
class StatusTaskView(TaskView):
    def get(self, request, pk):
 
        task = self.get_and_check_task(request, pk)
 
        # Associate the folder url with the project and task
        status_key = get_status_key(pk)
        
        datastore = get_current_plugin().get_global_data_store()
 
        task_info = datastore.get_json(status_key, {
            'status': 0, # Idle
            'shareUrl': None,
            'uploadedFiles': 0,
            'totalFiles': 0,
            'uploadedSize': 0,
            'totalSize': 0,
            'error': None
        })
 
        return Response(task_info, status=status.HTTP_200_OK)
 
DRONEDB_ASSETS = [
    'orthophoto.tif', 
    'orthophoto.png',
    'georeferenced_model.laz',
    'dtm.tif',
    'dsm.tif',
    'shots.geojson',
    'report.pdf',
    'ground_control_points.geojson'
]
 
class ShareTaskView(TaskView): 
    def post(self, request, pk):
 
        from app.plugins import logger
 
        task = self.get_and_check_task(request, pk)
 
        status_key = get_status_key(pk)
        
        datastore = get_current_plugin().get_global_data_store()
 
        data = {
            'status': 1, # Running
            'shareUrl': None,
            'uploadedFiles': 0,
            'totalFiles': 0,
            'uploadedSize': 0,
            'totalSize': 0,
            'error': None
        }
 
        datastore.set_json(status_key, data)
 
        settings = get_settings(request)
 
        available_assets = [task.get_asset_file_or_stream(f) for f in list(set(task.available_assets) & set(DRONEDB_ASSETS))]
 
        if 'textured_model.zip' in task.available_assets:            
            texture_files = [join(task.assets_path('odm_texturing'), f) for f in listdir(task.assets_path('odm_texturing')) if isfile(join(task.assets_path('odm_texturing'), f))]
            available_assets.extend(texture_files)
 
        assets_path = task.assets_path()
 
        files = [{'path': f, 'name': f[len(assets_path)+1:], 'size': os.path.getsize(f)} for f in available_assets]
 
        share_to_ddb.delay(pk, settings, files)
 
        return Response(data, status=status.HTTP_200_OK)        
 
 
@task
def share_to_ddb(pk, settings, files):
    
    from app.plugins import logger
  
    status_key = get_status_key(pk)        
    datastore = get_current_plugin().get_global_data_store()
 
    registry_url, username, password, token = settings
    
    ddb = DroneDB(registry_url, username, password, token)
 
    # Init share (to check)
    share_token = ddb.share_init()
 
    status = datastore.get_json(status_key)
 
    status['totalFiles'] = len(files)
    status['totalSize'] = sum(i['size'] for i in files)
 
    datastore.set_json(status_key, status)
 
    for file in files:
 
        # check that file exists
        if not os.path.exists(file['path']):
            logger.info("File {} does not exist".format(file['path']))
            continue
 
        attempt = 0
 
        while attempt < 3:
            try:
                
                attempt += 1
                
                up = ddb.share_upload(share_token, file['path'], file['name'])
 
                logger.info("Uploaded " + file['name'] + " to Dronedb (hash: " + up['hash'] + ")")
 
                status['uploadedFiles'] += 1
                status['uploadedSize'] += file['size']
 
                datastore.set_json(status_key, status)
            
                break
 
            except Exception as e:
 
                if (attempt == 3):
                    logger.error("Error uploading file {}: {}".format(file['name'], str(e)))
                    status['error'] = "Error uploading file {}: {}".format(file['name'], str(e))
                    status['status'] = 2 # Error
                    datastore.set_json(status_key, status)
                    return
                else:
                    logger.info("Error uploading file {}: {}. Retrying...".format(file['name'], str(e)))
                    time.sleep(5)
                    continue
 
 
    res = ddb.share_commit(share_token)
    
    status['status'] = 3 # Done
    status['shareUrl'] = registry_url + res['url']
 
    logger.info("Shared on url " + status['shareUrl'])
 
    datastore.set_json(status_key, status)