From c4f92794aaaee797fa9d6df04743980b2d478dcc Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Wed, 25 Nov 2015 23:22:35 +0200 Subject: [PATCH] added support for uploading media files to Dropbox --- extra/motioneye-256x256.png | Bin 0 -> 11165 bytes extra/motioneye-64x64.png | Bin 0 -> 2475 bytes extra/motioneye.svg | 67 +++++++ motioneye/config.py | 3 + motioneye/handlers.py | 16 +- motioneye/static/js/main.js | 16 ++ motioneye/templates/main.html | 9 +- motioneye/uploadservices.py | 362 ++++++++++++++++++++++++++++++---- 8 files changed, 430 insertions(+), 43 deletions(-) create mode 100644 extra/motioneye-256x256.png create mode 100644 extra/motioneye-64x64.png create mode 100644 extra/motioneye.svg diff --git a/extra/motioneye-256x256.png b/extra/motioneye-256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..e2dde4c72f611b7461fa4ce3522cdab216bd2ace GIT binary patch literal 11165 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 QlolW-tt?e0VHEWL0KUNCO8@`> literal 0 HcmV?d00001 diff --git a/extra/motioneye-64x64.png b/extra/motioneye-64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..6aedd844d5f737e986e132341bf15240e59c14fc GIT binary patch literal 2475 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 literal 0 HcmV?d00001 diff --git a/extra/motioneye.svg b/extra/motioneye.svg new file mode 100644 index 0000000..69f3c8b --- /dev/null +++ b/extra/motioneye.svg @@ -0,0 +1,67 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/motioneye/config.py b/motioneye/config.py index 5646ed5..69a4ae1 100644 --- a/motioneye/config.py +++ b/motioneye/config.py @@ -643,6 +643,7 @@ def motion_camera_ui_to_dict(ui, old_config=None): '@upload_port': ui['upload_port'], '@upload_method': ui['upload_method'], '@upload_location': ui['upload_location'], + '@upload_subfolders': ui['upload_subfolders'], '@upload_username': ui['upload_username'], '@upload_password': ui['upload_password'], '@upload_authorization_key': ui['upload_authorization_key'], @@ -959,6 +960,7 @@ def motion_camera_dict_to_ui(data): 'upload_port': data['@upload_port'], 'upload_method': data['@upload_method'], 'upload_location': data['@upload_location'], + 'upload_subfolders': data['@upload_subfolders'], 'upload_username': data['@upload_username'], 'upload_password': data['@upload_password'], 'upload_authorization_key': data['@upload_authorization_key'], @@ -1655,6 +1657,7 @@ def _set_default_motion_camera(camera_id, data): data.setdefault('@upload_port', '') data.setdefault('@upload_method', 'POST') data.setdefault('@upload_location', '') + data.setdefault('@upload_subfolders', True) data.setdefault('@upload_username', '') data.setdefault('@upload_password', '') data.setdefault('@upload_authorization_key', '') diff --git a/motioneye/handlers.py b/motioneye/handlers.py index 3042f7e..bd59d8f 100644 --- a/motioneye/handlers.py +++ b/motioneye/handlers.py @@ -1492,23 +1492,27 @@ class RelayEventHandler(BaseHandler): # upload to external service if camera_config['@upload_enabled']: - service_name = camera_config['@upload_service'] - tasks.add(5, uploadservices.upload_media_file, tag='upload_media_file(%s)' % filename, - camera_id=camera_id, service_name=service_name, filename=filename) + self.upload_media_file(filename, camera_id, camera_config) elif event == 'picture_save': filename = self.get_argument('filename') # upload to external service if camera_config['@upload_enabled']: - service_name = camera_config['@upload_service'] - tasks.add(5, uploadservices.upload_media_file, tag='upload_media_file(%s)' % filename, - camera_id=camera_id, service_name=service_name, filename=filename) + self.upload_media_file(filename, camera_id, camera_config) else: logging.warn('unknown event %s' % event) self.finish_json() + + def upload_media_file(self, filename, camera_id, camera_config): + service_name = camera_config['@upload_service'] + + tasks.add(5, uploadservices.upload_media_file, tag='upload_media_file(%s)' % filename, async=True, + camera_id=camera_id, service_name=service_name, + target_dir=camera_config['@upload_subfolders'] and camera_config['target_dir'], + filename=filename) class LogHandler(BaseHandler): diff --git a/motioneye/static/js/main.js b/motioneye/static/js/main.js index 5174eea..e6f9cc0 100644 --- a/motioneye/static/js/main.js +++ b/motioneye/static/js/main.js @@ -1370,6 +1370,7 @@ function cameraUi2Dict() { 'upload_port': $('#uploadPortEntry').val(), 'upload_method': $('#uploadMethodSelect').val(), 'upload_location': $('#uploadLocationEntry').val(), + 'upload_subfolders': $('#uploadSubfoldersSwitch')[0].checked, 'upload_username': $('#uploadUsernameEntry').val(), 'upload_password': $('#uploadPasswordEntry').val(), 'upload_authorization_key': $('#uploadAuthorizationKeyEntry').val(), @@ -1656,6 +1657,7 @@ function dict2CameraUi(dict) { $('#uploadPortEntry').val(dict['upload_port']); markHideIfNull('upload_port', 'uploadPortEntry'); $('#uploadMethodSelect').val(dict['upload_method']); markHideIfNull('upload_method', 'uploadMethodSelect'); $('#uploadLocationEntry').val(dict['upload_location']); markHideIfNull('upload_location', 'uploadLocationEntry'); + $('#uploadSubfoldersSwitch')[0].checked = dict['upload_subfolders']; markHideIfNull('upload_subfolders', 'uploadSubfoldersSwitch'); $('#uploadUsernameEntry').val(dict['upload_username']); markHideIfNull('upload_username', 'uploadUsernameEntry'); $('#uploadPasswordEntry').val(dict['upload_password']); markHideIfNull('upload_password', 'uploadPasswordEntry'); $('#uploadAuthorizationKeyEntry').val(dict['upload_authorization_key']); markHideIfNull('upload_authorization_key', 'uploadAuthorizationKeyEntry'); @@ -2304,6 +2306,19 @@ function doRestore() { } function doTestUpload() { + var q = $('#uploadPortEntry, #uploadLocationEntry, #uploadServerEntry'); + var valid = true; + q.each(function() { + this.validate(); + if (this.invalid) { + valid = false; + } + }); + + if (!valid) { + return runAlertDialog('Make sure all the configuration options are valid!'); + } + showModalDialog('', null, null, true); var data = { @@ -2313,6 +2328,7 @@ function doTestUpload() { port: $('#uploadPortEntry').val(), method: $('#uploadMethodSelect').val(), location: $('#uploadLocationEntry').val(), + subfolders: $('#uploadSubfoldersSwitch')[0].checked, username: $('#uploadUsernameEntry').val(), password: $('#uploadPasswordEntry').val(), authorization_key: $('#uploadAuthorizationKeyEntry').val() diff --git a/motioneye/templates/main.html b/motioneye/templates/main.html index 1a9c2e4..6daef16 100644 --- a/motioneye/templates/main.html +++ b/motioneye/templates/main.html @@ -349,7 +349,7 @@ - + ? @@ -377,7 +377,12 @@ Location - ? + ? + + + Include Subfolders + + ? Username diff --git a/motioneye/uploadservices.py b/motioneye/uploadservices.py index 3f520d4..f0cc0c4 100644 --- a/motioneye/uploadservices.py +++ b/motioneye/uploadservices.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import httplib2 import json import logging import mimetypes @@ -23,14 +22,12 @@ import os.path import urllib import urllib2 -from oauth2client.client import OAuth2WebServerFlow, Credentials - import settings _STATE_FILE_NAME = 'uploadservices.json' -_services = {} +_services = None class UploadService(object): @@ -48,8 +45,20 @@ class UploadService(object): def test_access(self): return True - def upload_file(self, filename): - self.debug('uploading file "%s" to %s' % (filename, self)) + def upload_file(self, target_dir, filename): + if target_dir: + target_dir = os.path.realpath(target_dir) + rel_filename = filename[len(target_dir):] + + while rel_filename.startswith('/'): + rel_filename = rel_filename[1:] + + self.debug('uploading file "[%s]/%s" to %s' % (target_dir, rel_filename, self)) + + else: + rel_filename = os.path.basename(filename) + + self.debug('uploading file "%s" to %s' % (filename, self)) try: st = os.stat(filename) @@ -78,7 +87,9 @@ class UploadService(object): mime_type = mimetypes.guess_type(filename)[0] or 'image/jpeg' self.debug('mime type of "%s" is "%s"' % (filename, mime_type)) - self.upload_data(filename, mime_type, data) + self.upload_data(rel_filename, mime_type, data) + + self.debug('file "%s" successfully uploaded' % filename) def upload_data(self, filename, mime_type, data): pass @@ -108,12 +119,18 @@ class UploadService(object): class GoogleDrive(UploadService): NAME = 'gdrive' + + AUTH_URL = 'https://accounts.google.com/o/oauth2/auth' + TOKEN_URL = 'https://accounts.google.com/o/oauth2/token' + CLIENT_ID = '349038943026-m16svdadjrqc0c449u4qv71v1m1niu5o.apps.googleusercontent.com' CLIENT_NOT_SO_SECRET = 'jjqbWmICpA0GvbhsJB3okX7s' + SCOPE = 'https://www.googleapis.com/auth/drive' CHILDREN_URL = 'https://www.googleapis.com/drive/v2/files/%(parent_id)s/children?q=%(query)s' CHILDREN_QUERY = "'%(parent_id)s' in parents and title = '%(child_name)s'" UPLOAD_URL = 'https://www.googleapis.com/upload/drive/v2/files?uploadType=multipart' + BOUNDARY = 'motioneye_multipart_boundary' MAX_FILE_SIZE = 128 * 1024 * 1024 # 128 MB @@ -124,10 +141,15 @@ class GoogleDrive(UploadService): self._folder_id = None def get_authorize_url(self): - flow = OAuth2WebServerFlow(client_id=self.CLIENT_ID, client_secret=self.CLIENT_NOT_SO_SECRET, - scope='https://www.googleapis.com/auth/drive', redirect_uri='urn:ietf:wg:oauth:2.0:oob') + query = { + 'scope': self.SCOPE, + 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob', + 'response_type': 'code', + 'client_id': self.CLIENT_ID, + 'access_type': 'offline' + } - return flow.step1_get_authorize_url() + return self.AUTH_URL + '?' + urllib.urlencode(query) def test_access(self): try: @@ -168,7 +190,7 @@ class GoogleDrive(UploadService): def dump(self): return { 'location': self._location, - 'credentials': self._credentials and json.loads(self._credentials.to_json()), + 'credentials': self._credentials, 'authorization_key': self._authorization_key, 'folder_id': self._folder_id } @@ -178,7 +200,7 @@ class GoogleDrive(UploadService): self._location = data['location'] self._folder_id = None # invalidate the folder if 'credentials' in data: - self._credentials = Credentials.new_from_json(json.dumps(data['credentials'])) + self._credentials = data['credentials'] if 'authorization_key' in data: self._authorization_key = data['authorization_key'] if 'folder_id' in data: @@ -193,17 +215,29 @@ class GoogleDrive(UploadService): return self._folder_id def _get_folder_id_by_path(self, path): - path = [p.strip() for p in path.split('/') if p.strip()] - - parent_id = 'root' - for name in path: - parent_id = self._get_folder_id_by_name(parent_id, name) + if path and path != '/': + path = [p.strip() for p in path.split('/') if p.strip()] + parent_id = 'root' + for name in path: + parent_id = self._get_folder_id_by_name(parent_id, name) + + return parent_id - return parent_id + else: # root folder + return self._get_folder_id_by_name(None, 'root') def _get_folder_id_by_name(self, parent_id, child_name): - query = self.CHILDREN_QUERY % {'parent_id': parent_id, 'child_name': child_name} - query = urllib.quote(query) + if parent_id: + query = self.CHILDREN_QUERY % {'parent_id': parent_id, 'child_name': child_name} + query = urllib.quote(query) + + else: + query = '' + + parent_id = parent_id or 'root' + # when requesting the id of the root folder, we perform a dummy request, + # event though we already know the id (which is "root"), to test the request + url = self.CHILDREN_URL % {'parent_id': parent_id, 'query': query} response = self._request(url) try: @@ -212,6 +246,9 @@ class GoogleDrive(UploadService): except Exception: self.error("response doesn't seem to be a valid json") raise + + if parent_id == 'root' and child_name == 'root': + return 'root' items = response.get('items') if not items: @@ -228,19 +265,229 @@ class GoogleDrive(UploadService): raise Exception(msg) if not self._credentials: - self.debug('requesting access token') - flow = self._get_oauth2_flow() + self.debug('requesting credentials') try: - self._credentials = flow.step2_exchange(self._authorization_key) + self._credentials = self._request_credentials(self._authorization_key) save() except Exception as e: - self.error('failed to obtain access token: %s' % e) + self.error('failed to obtain credentials: %s' % e) raise headers = headers or {} - headers['Authorization'] = 'Bearer %s' % self._credentials.access_token + headers['Authorization'] = 'Bearer %s' % self._credentials['access_token'] + + self.debug('requesting %s' % url) + request = urllib2.Request(url, data=body, headers=headers) + try: + response = urllib2.urlopen(request) + + except urllib2.HTTPError as e: + if e.code == 401 and retry_auth: # unauthorized, access token may have expired + try: + self.debug('credentials have probably expired, refreshing them') + self._credentials = self._refresh_credentials(self._credentials['refresh_token']) + save() + + # retry the request with refreshed credentials + return self._request(url, body, headers, retry_auth=False) + + except Exception as e: + self.error('refreshing credentials failed') + raise + + else: + try: + e = json.load(e) + msg = e['error']['message'] + + except Exception: + msg = str(e) + + self.error('request failed: %s' % msg) + raise Exception(msg) + + except Exception as e: + self.error('request failed: %s' % e) + raise + + return response.read() + + def _request_credentials(self, authorization_key): + headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + + body = { + 'code': authorization_key, + 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob', + 'client_id': self.CLIENT_ID, + 'client_secret': self.CLIENT_NOT_SO_SECRET, + 'scope': self.SCOPE, + 'grant_type': 'authorization_code' + } + body = urllib.urlencode(body) + + request = urllib2.Request(self.TOKEN_URL, data=body, headers=headers) + + try: + response = urllib2.urlopen(request) + + except urllib2.HTTPError as e: + error = json.load(e) + raise Exception(error.get('error_description') or error.get('error') or str(e)) + + data = json.load(response) + + return { + 'access_token': data['access_token'], + 'refresh_token': data['refresh_token'] + } + + def _refresh_credentials(self, refresh_token): + headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + + body = { + 'refresh_token': refresh_token, + 'client_id': self.CLIENT_ID, + 'client_secret': self.CLIENT_NOT_SO_SECRET, + 'grant_type': 'refresh_token' + } + body = urllib.urlencode(body) + request = urllib2.Request(self.TOKEN_URL, data=body, headers=headers) + + try: + response = urllib2.urlopen(request) + + except urllib2.HTTPError as e: + error = json.load(e) + raise Exception(error.get('error_description') or error.get('error') or str(e)) + + data = json.load(response) + + return { + 'access_token': data['access_token'], + 'refresh_token': data.get('refresh_token', refresh_token) + } + + +class Dropbox(UploadService): + NAME = 'dropbox' + + AUTH_URL = 'https://www.dropbox.com/1/oauth2/authorize' + TOKEN_URL = 'https://api.dropboxapi.com/1/oauth2/token' + + CLIENT_ID = 'dwiw710jz6r60pq' + CLIENT_NOT_SO_SECRET = '8jz75qo405ritd5' + + LIST_FOLDER_URL = 'https://api.dropboxapi.com/2/files/list_folder' + UPLOAD_URL = 'https://content.dropboxapi.com/2/files/upload' + + MAX_FILE_SIZE = 128 * 1024 * 1024 # 128 MB + + def __init__(self, location=None, subfolders=True, authorization_key=None, credentials=None, **kwargs): + self._location = location + self._subfolders = subfolders + self._authorization_key = authorization_key + self._credentials = credentials + + def get_authorize_url(self): + query = { + 'response_type': 'code', + 'client_id': self.CLIENT_ID + } + + return self.AUTH_URL + '?' + urllib.urlencode(query) + + def test_access(self): + body = { + 'path': self._clean_location(), + 'recursive': False, + 'include_media_info': False, + 'include_deleted': False + } + + body = json.dumps(body) + headers = {'Content-Type': 'application/json'} + + try: + self._request(self.LIST_FOLDER_URL, body, headers) + return True + + except Exception as e: + msg = str(e) + + # remove trailing punctuation + while msg and not msg[-1].isalnum(): + msg = msg[:-1] + + return msg + + def upload_data(self, filename, mime_type, data): + metadata = { + 'path': os.path.join(self._clean_location(), filename), + 'mode': 'add', + 'autorename': True, + 'mute': False + } + + headers = { + 'Content-Type': 'application/octet-stream', + 'Dropbox-API-Arg': json.dumps(metadata) + } + + self._request(self.UPLOAD_URL, data, headers) + + def dump(self): + return { + 'location': self._location, + 'subfolders': self._subfolders, + 'credentials': self._credentials, + 'authorization_key': self._authorization_key + } + + def load(self, data): + if 'location' in data: + self._location = data['location'] + if 'subfolders' in data: + self._subfolders = data['subfolders'] + if 'credentials' in data: + self._credentials = data['credentials'] + if 'authorization_key' in data: + self._authorization_key = data['authorization_key'] + + def _clean_location(self): + location = self._location + if location == '/': + return '' + + if not location.startswith('/'): + location = '/' + location + + return location + + def _request(self, url, body=None, headers=None, retry_auth=True): + if not self._authorization_key: + msg = 'missing authorization key' + self.error(msg) + raise Exception(msg) + + if not self._credentials: + self.debug('requesting credentials') + try: + self._credentials = self._request_credentials(self._authorization_key) + save() + + except Exception as e: + self.error('failed to obtain credentials: %s' % e) + raise + + headers = headers or {} + headers['Authorization'] = 'Bearer %s' % self._credentials['access_token'] + self.debug('requesting %s' % url) request = urllib2.Request(url, data=body, headers=headers) try: @@ -249,15 +496,22 @@ class GoogleDrive(UploadService): except urllib2.HTTPError as e: if e.code == 401 and retry_auth: # unauthorized, access token may have expired try: - self.debug('access token has probably expired, refreshing it') - self._credentials.refresh(httplib2.Http()) + self.debug('credentials have probably expired, refreshing them') + self._credentials = self._refresh_credentials(self._credentials['refresh_token']) save() + + # retry the request with refreshed credentials self._request(url, body, headers, retry_auth=False) except Exception as e: - self.error('refreshing access token failed') + self.error('refreshing credentials failed') raise - + + elif str(e).count('not_found'): + msg = 'folder "%s" not found' % self._location + self.error(msg) + raise Exception(msg) + else: self.error('request failed: %s' % e) raise @@ -267,13 +521,40 @@ class GoogleDrive(UploadService): raise return response.read() + + def _request_credentials(self, authorization_key): + headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + + body = { + 'code': authorization_key, + 'client_id': self.CLIENT_ID, + 'client_secret': self.CLIENT_NOT_SO_SECRET, + 'grant_type': 'authorization_code' + } + body = urllib.urlencode(body) - def _get_oauth2_flow(self): - return OAuth2WebServerFlow(client_id=self.CLIENT_ID, client_secret=self.CLIENT_NOT_SO_SECRET, - scope=self.SCOPE, redirect_uri='urn:ietf:wg:oauth:2.0:oob') - + request = urllib2.Request(self.TOKEN_URL, data=body, headers=headers) + + try: + response = urllib2.urlopen(request) + + except urllib2.HTTPError as e: + error = json.load(e) + raise Exception(error.get('error_description') or error.get('error') or str(e)) + + data = json.load(response) + + return { + 'access_token': data['access_token'] + } + def get(camera_id, name, create=True): + if _services is None: + load() + camera_id = str(camera_id) service = _services.get(camera_id, {}).get(name) if not service and create: @@ -288,6 +569,10 @@ def get(camera_id, name, create=True): def load(): + global _services + + _services = {} + file_path = os.path.join(settings.CONF_PATH, _STATE_FILE_NAME) if os.path.exists(file_path): @@ -324,6 +609,9 @@ def load(): def save(): + if _services is None: + return + file_path = os.path.join(settings.CONF_PATH, _STATE_FILE_NAME) logging.debug('saving upload services state to "%s"...' % file_path) @@ -351,13 +639,17 @@ def save(): file.close() -def upload_media_file(camera_id, service_name, filename): +def upload_media_file(camera_id, target_dir, service_name, filename): + # force a load from file with every upload, + # as settings might have changed + load() + service = get(camera_id, service_name, create=False) if not service: return logging.error('service "%s" not initialized for camera with id %s' % (service_name, camera_id)) try: - service.upload_file(filename) + service.upload_file(target_dir, filename) except Exception as e: logging.error('failed to upload file "%s" with service %s: %s' % (filename, service, e)) -- 2.39.5