2022-01-14 16:49:26 +00:00
|
|
|
from functools import reduce
|
2022-01-07 11:55:35 +00:00
|
|
|
import requests
|
|
|
|
from os import path
|
|
|
|
from app.plugins import logger
|
2022-01-12 18:03:49 +00:00
|
|
|
from urllib.parse import urlparse
|
2022-01-07 11:55:35 +00:00
|
|
|
|
|
|
|
VALID_IMAGE_EXTENSIONS = ['.tiff', '.tif', '.png', '.jpeg', '.jpg']
|
2022-01-18 12:04:36 +00:00
|
|
|
DEFAULT_HUB_URL = 'https://hub.dronedb.app'
|
2022-01-07 11:55:35 +00:00
|
|
|
|
2022-01-12 18:03:49 +00:00
|
|
|
class DroneDB:
|
|
|
|
|
2022-01-17 14:47:37 +00:00
|
|
|
def __init__(self, registry_url, username, password, token=None, update_token=None):
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
if not self.validate_url(registry_url):
|
2022-01-12 18:03:49 +00:00
|
|
|
raise ValueError("Invalid registry URL.")
|
|
|
|
|
2022-01-07 11:55:35 +00:00
|
|
|
self.username = username
|
|
|
|
self.password = password
|
2022-01-17 14:47:37 +00:00
|
|
|
self.token = token
|
2022-01-12 18:03:49 +00:00
|
|
|
self.public = False if username else True
|
2022-01-17 14:47:37 +00:00
|
|
|
self.update_token = update_token
|
2022-01-07 11:55:35 +00:00
|
|
|
|
2022-01-12 18:03:49 +00:00
|
|
|
self.__registry_url = registry_url[:-1] if registry_url.endswith('/') else registry_url
|
|
|
|
self.__authenticate_url = self.__registry_url + "/users/authenticate"
|
|
|
|
self.__refresh_url = self.__registry_url + "/users/authenticate/refresh"
|
|
|
|
self.__get_organizations_url = self.__registry_url + "/orgs"
|
|
|
|
self.__get_datasets_url = self.__registry_url + "/orgs/{0}/ds"
|
|
|
|
self.__get_folders_url = self.__registry_url + "/orgs/{0}/ds/{1}/search"
|
|
|
|
self.__get_files_list_url = self.__registry_url + "/orgs/{0}/ds/{1}/list"
|
|
|
|
self.__download_file_url = self.__registry_url + "/orgs/{0}/ds/{1}/download?path={2}&inline=1"
|
2022-01-20 16:22:15 +00:00
|
|
|
|
2022-01-19 17:53:29 +00:00
|
|
|
self.__share_init_url = self.__registry_url + "/share/init"
|
|
|
|
self.__share_upload_url = self.__registry_url + "/share/upload/{0}"
|
|
|
|
self.__share_commit_url = self.__registry_url + "/share/commit/{0}"
|
|
|
|
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
# Validate url
|
|
|
|
def validate_url(self, url):
|
|
|
|
try:
|
|
|
|
result = urlparse(url)
|
|
|
|
return all([result.scheme, result.netloc])
|
|
|
|
except:
|
|
|
|
return False
|
|
|
|
|
2022-01-12 18:03:49 +00:00
|
|
|
def login(self):
|
|
|
|
|
|
|
|
if (self.public):
|
|
|
|
logger.info("No need to login to DroneDB.")
|
|
|
|
return True
|
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
try:
|
2022-01-07 11:55:35 +00:00
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
# Authenticate
|
|
|
|
payload = {'username': self.username, 'password': self.password}
|
|
|
|
response = requests.post(self.__authenticate_url, data=payload)
|
2022-01-07 11:55:35 +00:00
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
if response.status_code != 200:
|
|
|
|
return False
|
2022-01-07 11:55:35 +00:00
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
# Get the token
|
|
|
|
self.token = response.json()['token']
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
logger.info("Logged in to DroneDB as user " + self.username + ".")
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-17 14:47:37 +00:00
|
|
|
if (self.update_token is not None):
|
|
|
|
self.update_token(self.token)
|
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
except(Exception) as e:
|
|
|
|
logger.error(e)
|
2022-01-20 18:21:43 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
def refresh_token(self):
|
|
|
|
|
|
|
|
if (self.public):
|
|
|
|
logger.info("Cannot refresh token.")
|
|
|
|
return False
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
response = self.wrapped_call('POST', self.__refresh_url)
|
|
|
|
|
|
|
|
self.token = response.json()['token']
|
|
|
|
|
|
|
|
if (self.update_token is not None):
|
|
|
|
self.update_token(self.token)
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
raise Exception("Failed to refresh token.") from e
|
2022-01-13 12:08:12 +00:00
|
|
|
|
2022-01-20 16:22:15 +00:00
|
|
|
def wrapped_call(self, type, url, data=None, params=None, files=None, attempts=3):
|
2022-01-12 18:03:49 +00:00
|
|
|
|
|
|
|
headers = {}
|
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
cnt = attempts
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
|
|
if not self.public and self.token is None and not self.login():
|
|
|
|
raise ValueError("Could not authenticate to DroneDB.")
|
|
|
|
|
|
|
|
if self.token is not None:
|
|
|
|
headers = {'Authorization': 'Bearer ' + self.token }
|
2022-01-20 16:22:15 +00:00
|
|
|
|
|
|
|
response = requests.request(type, url, data=data, params=params, headers=headers, files=files)
|
2022-01-13 12:08:12 +00:00
|
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
return response
|
|
|
|
|
|
|
|
if response.status_code == 401:
|
|
|
|
if (self.public):
|
2022-01-20 16:22:15 +00:00
|
|
|
raise DroneDBException("Failed to call '" + url + "': unauthorized.")
|
2022-01-13 12:08:12 +00:00
|
|
|
|
|
|
|
if not self.login():
|
2022-01-20 16:22:15 +00:00
|
|
|
raise DroneDBException("Failed to re-authenticate to DroneDB, cannot call '" + url + "'.")
|
2022-01-13 12:08:12 +00:00
|
|
|
else:
|
|
|
|
cnt -= 1
|
|
|
|
if cnt == 0:
|
2022-01-20 16:22:15 +00:00
|
|
|
raise DroneDBException("Failed all attempts to re-authenticate to DroneDB, cannot call '" + url + "'.")
|
|
|
|
else:
|
|
|
|
res = response.json()
|
|
|
|
raise DroneDBException("Failed to call '" + url + "'.", res)
|
2022-01-12 18:03:49 +00:00
|
|
|
|
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
def get_organizations(self):
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
response = self.wrapped_call('GET', self.__get_organizations_url)
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
return [{'slug': o['slug'], 'name': o['name']} for o in response.json()]
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
except Exception as e:
|
|
|
|
raise Exception("Failed to get organizations.") from e
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
def get_datasets(self, orgSlug):
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
try:
|
|
|
|
|
|
|
|
response = self.wrapped_call('GET', self.__get_datasets_url.format(orgSlug))
|
2022-01-14 16:49:26 +00:00
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
return [
|
|
|
|
{'slug': o['slug'],
|
2022-04-30 10:07:46 +00:00
|
|
|
'name': o['properties'].get('meta', {}).get('name', {}).get('data', o['slug']),
|
2022-01-13 12:08:12 +00:00
|
|
|
'public': o['properties'].get('public'),
|
|
|
|
'size': o['size'],
|
|
|
|
'entries': o['properties'].get('entries')
|
|
|
|
} for o in response.json()]
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
except Exception as e:
|
|
|
|
raise Exception("Failed to get datasets.") from e
|
|
|
|
|
2022-01-12 18:03:49 +00:00
|
|
|
|
|
|
|
def get_folders(self, orgSlug, dsSlug):
|
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
try:
|
|
|
|
|
|
|
|
# Type 1 is folder
|
|
|
|
payload = {'query': '*', 'recursive': True, 'type': 1}
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
response = self.wrapped_call('POST', self.__get_folders_url.format(orgSlug, dsSlug), data=payload)
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
return [o['path'] for o in response.json()]
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
raise Exception("Failed to get folders.") from e
|
2022-01-12 18:03:49 +00:00
|
|
|
|
|
|
|
def get_files_list(self, orgSlug, dsSlug, folder=None):
|
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
try:
|
|
|
|
|
|
|
|
# Type 1 is folder
|
|
|
|
params = {'path': '' if folder is None else folder}
|
|
|
|
|
2022-01-18 10:55:28 +00:00
|
|
|
logger.info(self.__get_files_list_url.format(orgSlug, dsSlug))
|
|
|
|
|
2022-01-13 12:08:12 +00:00
|
|
|
# Get the folders
|
|
|
|
response = self.wrapped_call('GET', self.__get_files_list_url.format(orgSlug, dsSlug), params=params)
|
|
|
|
|
2022-01-24 09:54:45 +00:00
|
|
|
# Exclude folders
|
|
|
|
files = filter(lambda itm: itm['type'] != 1, response.json())
|
|
|
|
|
2022-01-17 10:06:50 +00:00
|
|
|
return [
|
|
|
|
{'path': o['path'],
|
|
|
|
# extract name from path
|
|
|
|
'name': o['path'].split('/')[-1],
|
2022-01-13 12:08:12 +00:00
|
|
|
'type': o['type'],
|
|
|
|
'size': o['size'],
|
|
|
|
'url': self.__download_file_url.format(orgSlug, dsSlug, o['path'])
|
2022-01-24 09:54:45 +00:00
|
|
|
} for o in files]
|
2022-01-13 12:08:12 +00:00
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
raise Exception("Failed to get files list.") from e
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-19 17:53:29 +00:00
|
|
|
def share_init(self, tag=None):
|
|
|
|
try:
|
|
|
|
|
2022-01-20 16:22:15 +00:00
|
|
|
data = {'tag': tag} if tag is not None else None
|
2022-01-19 17:53:29 +00:00
|
|
|
|
2022-01-20 16:22:15 +00:00
|
|
|
response = self.wrapped_call('POST', self.__share_init_url, data=data)
|
2022-01-19 17:53:29 +00:00
|
|
|
|
|
|
|
return response.json()['token']
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
raise Exception("Failed to initialize share.") from e
|
2022-01-20 16:22:15 +00:00
|
|
|
|
2022-01-20 18:21:43 +00:00
|
|
|
def share_upload(self, token, path, name):
|
2022-01-20 16:22:15 +00:00
|
|
|
try:
|
|
|
|
|
|
|
|
# Get file name
|
2022-01-20 18:21:43 +00:00
|
|
|
files = { 'file': open(path, 'rb') }
|
2022-01-20 16:22:15 +00:00
|
|
|
data = {'path': name}
|
|
|
|
|
|
|
|
response = self.wrapped_call('POST', self.__share_upload_url.format(token), files=files, data=data)
|
|
|
|
|
|
|
|
return response.json()
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
raise Exception("Failed to upload file.") from e
|
|
|
|
|
|
|
|
def share_commit(self, token):
|
|
|
|
try:
|
|
|
|
|
|
|
|
response = self.wrapped_call('POST', self.__share_commit_url.format(token))
|
|
|
|
|
|
|
|
return response.json()
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
raise Exception("Failed to commit share.") from e
|
2022-01-19 17:53:29 +00:00
|
|
|
|
2022-01-12 18:03:49 +00:00
|
|
|
def verify_url(url, username=None, password=None):
|
|
|
|
try:
|
|
|
|
|
2022-01-17 17:54:44 +00:00
|
|
|
registryUrl, orgSlug, dsSlug, folder = parse_url(url).values()
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-17 17:54:44 +00:00
|
|
|
ddb = DroneDB(registryUrl, username, password)
|
|
|
|
files = ddb.get_files_list(orgSlug, dsSlug, folder)
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-17 17:54:44 +00:00
|
|
|
# return some info
|
2022-01-18 10:55:28 +00:00
|
|
|
return {
|
|
|
|
'success': True,
|
|
|
|
'orgSlug': orgSlug,
|
|
|
|
'dsSlug': dsSlug,
|
|
|
|
'folder': folder,
|
|
|
|
'count': len(files),
|
|
|
|
'size': sum(i['size'] for i in files)
|
|
|
|
}
|
2022-01-12 18:03:49 +00:00
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
logger.error(e)
|
2022-01-18 10:55:28 +00:00
|
|
|
return {
|
|
|
|
'success': False,
|
|
|
|
'orgSlug': None,
|
|
|
|
'dsSlug': None,
|
|
|
|
'folder': None,
|
|
|
|
'count': None,
|
|
|
|
'size': None
|
|
|
|
}
|
2022-01-12 18:03:49 +00:00
|
|
|
|
|
|
|
def parse_url(url):
|
|
|
|
|
|
|
|
# Check if the url is valid
|
|
|
|
# Root folder of dataset: ddb://localhost:5001/admin/4uyyyaxcbvahd7qb
|
|
|
|
# 'test' folder of dataset: ddb://localhost:5001/admin/4uyyyaxcbvahd7qb/test
|
2022-01-18 10:55:28 +00:00
|
|
|
# using http instead of https: ddb+unsafe://localhost:5000/admin/4uyyyaxcbvahd7qb
|
2022-01-12 18:03:49 +00:00
|
|
|
# using hub url: https://localhost:5001/r/admin/4uyyyaxcbvahd7qb
|
2022-01-13 14:35:21 +00:00
|
|
|
# using hub url without /r/ http://localhost:5000/admin/4uyyyaxcbvahd7qb/test
|
2022-01-12 18:03:49 +00:00
|
|
|
|
|
|
|
p = urlparse(url)
|
|
|
|
segments = p.path.split('/')
|
|
|
|
|
|
|
|
if p.scheme not in ['ddb', 'ddb+unsafe', 'http', 'https']:
|
|
|
|
raise ValueError("Invalid URL scheme.")
|
|
|
|
|
|
|
|
if p.netloc == '':
|
|
|
|
raise ValueError("Invalid URL.")
|
|
|
|
|
|
|
|
scheme = p.scheme
|
|
|
|
|
|
|
|
# used to skip the /r/: if ddb url we have no /r/ instead if http we have it
|
|
|
|
if p.scheme == 'ddb':
|
2022-01-13 14:35:21 +00:00
|
|
|
scheme = 'https'
|
2022-01-12 18:03:49 +00:00
|
|
|
elif p.scheme == 'ddb+unsafe':
|
2022-01-13 14:35:21 +00:00
|
|
|
scheme = 'http'
|
2022-01-12 18:03:49 +00:00
|
|
|
|
2022-01-13 14:35:21 +00:00
|
|
|
offset = 1 if segments[1] == 'r' else 0
|
|
|
|
|
2022-01-12 18:03:49 +00:00
|
|
|
if (len(segments) < offset + 3):
|
|
|
|
raise ValueError("Invalid URL.")
|
|
|
|
|
|
|
|
return {
|
|
|
|
'registryUrl': scheme + '://' + p.netloc,
|
|
|
|
'orgSlug': segments[1 + offset],
|
|
|
|
'dsSlug': segments[2 + offset],
|
|
|
|
'folder': '/'.join(segments[3 + offset:])
|
|
|
|
}
|
2022-01-13 12:08:12 +00:00
|
|
|
|
2022-01-20 16:22:15 +00:00
|
|
|
class DroneDBException(Exception):
|
|
|
|
def __init__(self, message, res=None):
|
|
|
|
super().__init__(message)
|
|
|
|
self.response = res
|
|
|
|
|