From 822a6181a22f6e9ee968e0f8857e2e795eb57653 Mon Sep 17 00:00:00 2001 From: manzari Date: Sun, 10 May 2020 12:13:02 +0200 Subject: [PATCH 1/6] add file upload support for nextcloud --- motioneye/templates/main.html | 9 ++-- motioneye/uploadservices.py | 82 +++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/motioneye/templates/main.html b/motioneye/templates/main.html index 05202a911..6a9e5c7f0 100644 --- a/motioneye/templates/main.html +++ b/motioneye/templates/main.html @@ -454,16 +454,17 @@ + ? - + {{ _("Servila Adreso") }} ? - + {{ _("Servila haveno") }} ? @@ -493,12 +494,12 @@ ? - + {{ _("Uzantnomo") }} ? - + {{ _("Pasvorto") }} ? diff --git a/motioneye/uploadservices.py b/motioneye/uploadservices.py index 5669afb8b..d4db14e1e 100644 --- a/motioneye/uploadservices.py +++ b/motioneye/uploadservices.py @@ -29,6 +29,7 @@ import boto3 import pycurl +import base64 from motioneye import settings, utils @@ -881,6 +882,87 @@ def _refresh_credentials(self, refresh_token): } +class Nextcloud(UploadService): + NAME = 'nextcloud' + + def __init__(self, camera_id): + self._server = None + self._port = None + self._username = None + self._password = None + self._location = None + + UploadService.__init__(self, camera_id) + + def _get_base_url(self): + scheme = 'http://' + if self._port == 443: + scheme = 'https://' + url = scheme + self._server + ':' + str(self._port) + '/remote.php/dav/files/' + self._username + '/' + return url + + def _request(self, url, method, data=None): + self.debug('request: ' + method + ' ' + url) + request = urllib2.Request(url, data=data) + request.get_method = lambda: method + base64string = base64.b64encode('%s:%s' % (self._username, self._password)) + request.add_header("Authorization", "Basic %s" % base64string) + if data is not None: + request.add_header('Content-Length', '%d' % len(data)) + try: + utils.urlopen(request) + except urllib2.HTTPError as e: + if method == 'MKCOL' and e.code == 405: + self.debug('MKCOL failed with code 405, this is normal if the folder exists') + else: + raise e + + def _make_dirs(self, path): + dir_url = self._get_base_url() + for folder in path.strip('/').split('/'): + dir_url = dir_url + folder + '/' + self._request(dir_url, 'MKCOL') + + def test_access(self): + try: + test_path = self._location.strip('/') + '/' + str(time.time()) + self._make_dirs(test_path) + self._request(self._get_base_url() + test_path, 'DELETE') + return True + except Exception as e: + self.error(str(e), exc_info=True) + return str(e) + + def upload_data(self, filename, mime_type, data, ctime, camera_name): + path = self._location.strip('/') + '/' + os.path.dirname(filename) + '/' + filename = os.path.basename(filename) + self._make_dirs(path) + self.debug('uploading %s of %s bytes' % (filename, len(data))) + self._request(self._get_base_url() + path + filename, 'PUT', bytearray(data)) + self.debug('upload done') + + def dump(self): + return { + 'server': self._server, + 'port': self._port, + 'username': self._username, + 'password': self._password, + 'location': self._location + } + + def load(self, data): + if data.get('server') is not None: + self._server = data['server'] + if data.get('port') is not None: + self._port = int(data['port']) + if data.get('username') is not None: + self._username = data['username'] + if data.get('password') is not None: + self._password = data['password'] + if data.get('location'): + self._location = data['location'] + + class FTP(UploadService): NAME = 'ftp' CONN_LIFE_TIME = 60 # don't keep an FTP connection for more than 1 minute From 921e7591fe4da0c113801e3f6ca542e7fed00e67 Mon Sep 17 00:00:00 2001 From: manzari <22736528+manzari@users.noreply.github.com> Date: Thu, 24 Mar 2022 14:58:20 +0530 Subject: [PATCH 2/6] nextcloud to webdav --- motioneye/templates/main.html | 10 +++++----- motioneye/uploadservices.py | 36 +++++++++++++---------------------- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/motioneye/templates/main.html b/motioneye/templates/main.html index 6a9e5c7f0..6767c9f38 100644 --- a/motioneye/templates/main.html +++ b/motioneye/templates/main.html @@ -454,17 +454,17 @@ - + ? - + {{ _("Servila Adreso") }} ? - + {{ _("Servila haveno") }} ? @@ -494,12 +494,12 @@ ? - + {{ _("Uzantnomo") }} ? - + {{ _("Pasvorto") }} ? diff --git a/motioneye/uploadservices.py b/motioneye/uploadservices.py index d4db14e1e..6047e38a6 100644 --- a/motioneye/uploadservices.py +++ b/motioneye/uploadservices.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from base64 import b64encode import datetime import ftplib import io @@ -29,7 +30,6 @@ import boto3 import pycurl -import base64 from motioneye import settings, utils @@ -882,8 +882,8 @@ def _refresh_credentials(self, refresh_token): } -class Nextcloud(UploadService): - NAME = 'nextcloud' +class Webdav(UploadService): + NAME = 'webdav' def __init__(self, camera_id): self._server = None @@ -894,31 +894,24 @@ def __init__(self, camera_id): UploadService.__init__(self, camera_id) - def _get_base_url(self): - scheme = 'http://' - if self._port == 443: - scheme = 'https://' - url = scheme + self._server + ':' + str(self._port) + '/remote.php/dav/files/' + self._username + '/' - return url - - def _request(self, url, method, data=None): + def _request(self, url, method, body=None): + base64string = b64encode(f'{self._username}:{self._password}') + headers = { 'Authorization' : 'Basic %s' % base64string } + if body is not None: + headers.update('Content-Length', '%d' % len(body)) self.debug('request: ' + method + ' ' + url) - request = urllib2.Request(url, data=data) + request = urllib.request.Request(url, data=body, headers=headers) request.get_method = lambda: method - base64string = base64.b64encode('%s:%s' % (self._username, self._password)) - request.add_header("Authorization", "Basic %s" % base64string) - if data is not None: - request.add_header('Content-Length', '%d' % len(data)) try: utils.urlopen(request) - except urllib2.HTTPError as e: + except urllib.HTTPError as e: if method == 'MKCOL' and e.code == 405: self.debug('MKCOL failed with code 405, this is normal if the folder exists') else: raise e def _make_dirs(self, path): - dir_url = self._get_base_url() + dir_url = self._server for folder in path.strip('/').split('/'): dir_url = dir_url + folder + '/' self._request(dir_url, 'MKCOL') @@ -927,7 +920,7 @@ def test_access(self): try: test_path = self._location.strip('/') + '/' + str(time.time()) self._make_dirs(test_path) - self._request(self._get_base_url() + test_path, 'DELETE') + self._request(self._server + test_path, 'DELETE') return True except Exception as e: self.error(str(e), exc_info=True) @@ -938,13 +931,12 @@ def upload_data(self, filename, mime_type, data, ctime, camera_name): filename = os.path.basename(filename) self._make_dirs(path) self.debug('uploading %s of %s bytes' % (filename, len(data))) - self._request(self._get_base_url() + path + filename, 'PUT', bytearray(data)) + self._request(self._server + path + filename, 'PUT', bytearray(data)) self.debug('upload done') def dump(self): return { 'server': self._server, - 'port': self._port, 'username': self._username, 'password': self._password, 'location': self._location @@ -953,8 +945,6 @@ def dump(self): def load(self, data): if data.get('server') is not None: self._server = data['server'] - if data.get('port') is not None: - self._port = int(data['port']) if data.get('username') is not None: self._username = data['username'] if data.get('password') is not None: From 1ba800ee3f571983bb3991f10b554f308ea378c4 Mon Sep 17 00:00:00 2001 From: manzari <22736528+manzari@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:07:20 +0530 Subject: [PATCH 3/6] nextcloud to webdav --- motioneye/uploadservices.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/motioneye/uploadservices.py b/motioneye/uploadservices.py index 6047e38a6..ff0945ab6 100644 --- a/motioneye/uploadservices.py +++ b/motioneye/uploadservices.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from base64 import b64encode import datetime import ftplib import io @@ -24,6 +23,7 @@ import os import os.path import time +from base64 import b64encode from urllib.error import HTTPError from urllib.parse import quote, urlencode from urllib.request import Request @@ -896,7 +896,7 @@ def __init__(self, camera_id): def _request(self, url, method, body=None): base64string = b64encode(f'{self._username}:{self._password}') - headers = { 'Authorization' : 'Basic %s' % base64string } + headers = {'Authorization': 'Basic %s' % base64string} if body is not None: headers.update('Content-Length', '%d' % len(body)) self.debug('request: ' + method + ' ' + url) @@ -906,7 +906,9 @@ def _request(self, url, method, body=None): utils.urlopen(request) except urllib.HTTPError as e: if method == 'MKCOL' and e.code == 405: - self.debug('MKCOL failed with code 405, this is normal if the folder exists') + self.debug( + 'MKCOL failed with code 405, this is normal if the folder exists' + ) else: raise e @@ -930,7 +932,7 @@ def upload_data(self, filename, mime_type, data, ctime, camera_name): path = self._location.strip('/') + '/' + os.path.dirname(filename) + '/' filename = os.path.basename(filename) self._make_dirs(path) - self.debug('uploading %s of %s bytes' % (filename, len(data))) + self.debug(f'uploading {filename} of {len(data)} bytes') self._request(self._server + path + filename, 'PUT', bytearray(data)) self.debug('upload done') @@ -939,7 +941,7 @@ def dump(self): 'server': self._server, 'username': self._username, 'password': self._password, - 'location': self._location + 'location': self._location, } def load(self, data): From b313382294e61eb3d22a35a8babafe54fb05a5d9 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Tue, 12 Apr 2022 15:09:35 +0200 Subject: [PATCH 4/6] Fix base64 encoding b64encode takes and returns only bytes, so the credentials need to be byte-encoded first and the returned bytes object decoded afterwards (ASCII works for base64 strings). Additionally some %-formattings have been replaced with f-strings, fix some syntax errors and satisfy pre-commit black checks. --- motioneye/uploadservices.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/motioneye/uploadservices.py b/motioneye/uploadservices.py index ff0945ab6..a487720a8 100644 --- a/motioneye/uploadservices.py +++ b/motioneye/uploadservices.py @@ -895,16 +895,18 @@ def __init__(self, camera_id): UploadService.__init__(self, camera_id) def _request(self, url, method, body=None): - base64string = b64encode(f'{self._username}:{self._password}') - headers = {'Authorization': 'Basic %s' % base64string} + base64string = b64encode(f'{self._username}:{self._password}'.encode()).decode( + 'ascii' + ) + headers = {'Authorization': f'Basic {base64string}'} if body is not None: - headers.update('Content-Length', '%d' % len(body)) - self.debug('request: ' + method + ' ' + url) + headers.update({'Content-Length': len(body)}) + self.debug(f'request: {method} {url}') request = urllib.request.Request(url, data=body, headers=headers) request.get_method = lambda: method try: utils.urlopen(request) - except urllib.HTTPError as e: + except urllib.error.HTTPError as e: if method == 'MKCOL' and e.code == 405: self.debug( 'MKCOL failed with code 405, this is normal if the folder exists' From 43cd07d104c540b6e90ddd706df053356035fe4f Mon Sep 17 00:00:00 2001 From: MichaIng Date: Wed, 20 Apr 2022 19:59:48 +0200 Subject: [PATCH 5/6] Fix and harden WebDAV upload Remove unused port variable and GUI input. Update urllib calls for new selective imports. Always strip and re-add slashes from path elements so assure there is always exactly one slash between them. Signed-off-by: MichaIng --- motioneye/templates/main.html | 2 +- motioneye/uploadservices.py | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/motioneye/templates/main.html b/motioneye/templates/main.html index 6767c9f38..5e63ab62a 100644 --- a/motioneye/templates/main.html +++ b/motioneye/templates/main.html @@ -464,7 +464,7 @@ ? - + {{ _("Servila haveno") }} ? diff --git a/motioneye/uploadservices.py b/motioneye/uploadservices.py index a487720a8..e3d1da15d 100644 --- a/motioneye/uploadservices.py +++ b/motioneye/uploadservices.py @@ -887,7 +887,6 @@ class Webdav(UploadService): def __init__(self, camera_id): self._server = None - self._port = None self._username = None self._password = None self._location = None @@ -902,11 +901,11 @@ def _request(self, url, method, body=None): if body is not None: headers.update({'Content-Length': len(body)}) self.debug(f'request: {method} {url}') - request = urllib.request.Request(url, data=body, headers=headers) + request = Request(url, data=body, headers=headers) request.get_method = lambda: method try: utils.urlopen(request) - except urllib.error.HTTPError as e: + except HTTPError as e: if method == 'MKCOL' and e.code == 405: self.debug( 'MKCOL failed with code 405, this is normal if the folder exists' @@ -915,27 +914,31 @@ def _request(self, url, method, body=None): raise e def _make_dirs(self, path): - dir_url = self._server - for folder in path.strip('/').split('/'): + dir_url = self._server.rstrip('/') + '/' + for folder in path.split('/'): dir_url = dir_url + folder + '/' self._request(dir_url, 'MKCOL') def test_access(self): try: - test_path = self._location.strip('/') + '/' + str(time.time()) - self._make_dirs(test_path) - self._request(self._server + test_path, 'DELETE') + path = self._location.strip('/') + '/' + str(time.time()) + self._make_dirs(path) + self._request(self._server.rstrip('/') + '/' + path, 'DELETE') return True except Exception as e: self.error(str(e), exc_info=True) return str(e) def upload_data(self, filename, mime_type, data, ctime, camera_name): - path = self._location.strip('/') + '/' + os.path.dirname(filename) + '/' + path = self._location.strip('/') + '/' + os.path.dirname(filename) filename = os.path.basename(filename) self._make_dirs(path) self.debug(f'uploading {filename} of {len(data)} bytes') - self._request(self._server + path + filename, 'PUT', bytearray(data)) + self._request( + self._server.rstrip('/') + '/' + path + '/' + filename, + 'PUT', + bytearray(data), + ) self.debug('upload done') def dump(self): From e4d907e70eed5171d838de6d28f905cbd2b0c0d1 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Sun, 1 May 2022 15:08:44 +0200 Subject: [PATCH 6/6] Show endpoint URL instead of server address input for WebDAV since the whole URL is required, not only the IP or hostname. Merge with S3 endpoint URL setting and extend the tooltip to show examples for both upload services. --- motioneye/templates/main.html | 24 ++++++++++++------------ motioneye/uploadservices.py | 14 +++++++------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/motioneye/templates/main.html b/motioneye/templates/main.html index 5e63ab62a..8dc0de814 100644 --- a/motioneye/templates/main.html +++ b/motioneye/templates/main.html @@ -459,7 +459,7 @@ ? - + {{ _("Servila Adreso") }} ? @@ -479,6 +479,13 @@ ? + + {{ _("Endpoint URL") }} + + ? + {{ _("Loko") }} @@ -518,11 +525,6 @@ ? - - {{ _("Endpoint URL") }} - - ? - {{ _("Access Key") }} @@ -1108,8 +1110,8 @@ ? -
{{ _("API-Informoj") }}
-
{{ _("Testo") }}
+
{{ _("API-Informoj") }}
+
{{ _("Testo") }}
@@ -1301,7 +1303,6 @@ {% endif %} {% endfor %} -
@@ -1314,9 +1315,8 @@ {% else %}
- + streaming_framerate="{{camera_config['stream_maxrate']}}" streaming_server_resize="{{camera_config['@webcam_server_resize']|string|lower}}" + proto="{{camera_config['@proto']}}" url="{{camera_config['@url']}}">
diff --git a/motioneye/uploadservices.py b/motioneye/uploadservices.py index e3d1da15d..4998ac256 100644 --- a/motioneye/uploadservices.py +++ b/motioneye/uploadservices.py @@ -886,7 +886,7 @@ class Webdav(UploadService): NAME = 'webdav' def __init__(self, camera_id): - self._server = None + self._endpoint_url = None self._username = None self._password = None self._location = None @@ -914,7 +914,7 @@ def _request(self, url, method, body=None): raise e def _make_dirs(self, path): - dir_url = self._server.rstrip('/') + '/' + dir_url = self._endpoint_url.rstrip('/') + '/' for folder in path.split('/'): dir_url = dir_url + folder + '/' self._request(dir_url, 'MKCOL') @@ -923,7 +923,7 @@ def test_access(self): try: path = self._location.strip('/') + '/' + str(time.time()) self._make_dirs(path) - self._request(self._server.rstrip('/') + '/' + path, 'DELETE') + self._request(self._endpoint_url.rstrip('/') + '/' + path, 'DELETE') return True except Exception as e: self.error(str(e), exc_info=True) @@ -935,7 +935,7 @@ def upload_data(self, filename, mime_type, data, ctime, camera_name): self._make_dirs(path) self.debug(f'uploading {filename} of {len(data)} bytes') self._request( - self._server.rstrip('/') + '/' + path + '/' + filename, + self._endpoint_url.rstrip('/') + '/' + path + '/' + filename, 'PUT', bytearray(data), ) @@ -943,15 +943,15 @@ def upload_data(self, filename, mime_type, data, ctime, camera_name): def dump(self): return { - 'server': self._server, + 'endpoint_url': self._endpoint_url, 'username': self._username, 'password': self._password, 'location': self._location, } def load(self, data): - if data.get('server') is not None: - self._server = data['server'] + if data.get('endpoint_url') is not None: + self._endpoint_url = data['endpoint_url'] if data.get('username') is not None: self._username = data['username'] if data.get('password') is not None: