Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add file upload support for webdav #1743

Merged
merged 6 commits into from
May 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions motioneye/templates/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@
<option value="gphoto">Google Photo</option>
<option value="dropbox">Dropbox</option>
<option value="s3">S3 (AWS/MinIO/...)</option>
<option value="webdav">WebDAV</option>
</select>
</td>
<td><span class="help-mark" title="{{ _("elektu servon, al kiu la mediadosieroj estu alŝutitaj") }}">?</span></td>
Expand All @@ -478,6 +479,13 @@
</td>
<td><span class="help-mark" title="{{ _("la HTTP-metodo uzi por alŝuti dosierojn") }}">?</span></td>
</tr>
<tr class="settings-item" depends="uploadEnabled uploadService=(s3|webdav)">
<td class="settings-item-label"><span class="settings-item-label">{{ _("Endpoint URL") }}</span></td>
<td class="settings-item-value"><input type="text" class="styled storage camera-config" id="uploadEndpointUrlEntry"></td>
<td><span class="help-mark" title="{{ _("The complete URL to the upload server endpoint:") }}
{{ _("For S3: E.g. http://my.minio:9000. Leave it empty for AWS!") }}
{{ _("For WebDAV: E.g. https://my.cloud/remote.php/dav/files/Me/") }}">?</span></td>
</tr>
<tr class="settings-item" required="true" depends="uploadEnabled uploadService!=(s3)">
<td class="settings-item-label"><span class="settings-item-label">{{ _("Loko") }}</span></td>
<td class="settings-item-value"><input type="text" class="styled storage camera-config" id="uploadLocationEntry"></td>
Expand All @@ -493,12 +501,12 @@
<td class="settings-item-value"><input type="checkbox" class="styled storage camera-config" id="cleanCloudEnabledSwitch"></td>
<td><span class="help-mark" title="{{ _("ebligu ĉi tion forigi amaskomunikilaĵojn dosierojn alŝutitajn al la nubo ankaŭ kiam loka amaskomunikilaro estas forigita konforme al la agordo de persistemo de dosieroj. Nuntempe ĉi tiu opcio nur haveblas por gdrivo.") }}">?</span></td>
</tr>
<tr class="settings-item" depends="uploadEnabled uploadService=(ftp|sftp|http|https)">
<tr class="settings-item" depends="uploadEnabled uploadService=(ftp|sftp|http|https|webdav)">
<td class="settings-item-label"><span class="settings-item-label">{{ _("Uzantnomo") }}</span></td>
<td class="settings-item-value"><input type="text" autocapitalize="none" class="styled storage camera-config" id="uploadUsernameEntry"></td>
<td><span class="help-mark" title="{{ _("la uzantnomo por la alŝuta servo-konto") }}">?</span></td>
</tr>
<tr class="settings-item" depends="uploadEnabled uploadService=(ftp|sftp|http|https)">
<tr class="settings-item" depends="uploadEnabled uploadService=(ftp|sftp|http|https|webdav)">
<td class="settings-item-label"><span class="settings-item-label">{{ _("Pasvorto") }}</span></td>
<td class="settings-item-value"><input type="password" autocomplete="new-password" class="styled storage camera-config" id="uploadPasswordEntry"></td>
<td><span class="help-mark" title="{{ _("la pasvorto por la alŝuta servo-konto") }}">?</span></td>
Expand All @@ -517,11 +525,6 @@
</td>
<td><span class="help-mark" title="{{ _("alklaku ĉi tie por akiri la ŝlosilan rajtigan servon") }}">?</span></td>
</tr>
<tr class="settings-item" depends="uploadEnabled uploadService=(s3)">
<td class="settings-item-label"><span class="settings-item-label">{{ _("Endpoint URL") }}</span></td>
<td class="settings-item-value"><input type="text" class="styled storage camera-config" id="uploadEndpointUrlEntry"></td>
<td><span class="help-mark" title="{{ _("The complete URL to the S3 server endpoint, e.g. http://my.minio:9000. Leave this empty for AWS!") }}">?</span></td>
</tr>
<tr class="settings-item" depends="uploadEnabled uploadService=(s3)">
<td class="settings-item-label"><span class="settings-item-label">{{ _("Access Key") }}</span></td>
<td class="settings-item-value"><input type="password" autocomplete="new-password" class="styled storage camera-config" id="uploadAccessKeyEntry"></td>
Expand Down Expand Up @@ -1107,8 +1110,8 @@
<td><span class="help-mark" title="{{ _("difinas la bildan serĉtempan intervalon por krei telegramajn aldonojn (pli altaj valoroj generas pli da bildoj koste de pliigita sciiga prokrasto); agordi al 0 por malebligi bildajn aldonaĵojn; vi devas ankaŭ ebligi Senmovajn Bildojn por ke ĉi tio funkciu; vi volos ludi per ĉi tiu numero ĝis bildo estos sendita. Bona komenca numero estas 30 se vi agordas senmovajn bildojn al unu bildmoviĝo ekigita. Por norma movado ekigita, starigu ĉi tion multe pli malalte.") }}">?</span></td>
</tr>
<tr class="settings-item" required="true" depends="telegramNotificationsEnabled motionDetectionEnabled" strip="true">
<td class="settings-item-label"><a href="https://core.telegram.org/bots#6-botfather" target="_blank"><div class="button normal-button test-button" id="apiInstructionButton" style="opacity: 1; display: inline-block;">{{ _("API-Informoj") }}</div></a></td>
<td class="settings-item-label"><div class="button normal-button test-button" id="telegramTestButton" style="opacity: 1; display: inline-block;">{{ _("Testo") }}</div></td>
<td class="settings-item-label"><a href="https://core.telegram.org/bots#6-botfather" target="_blank"><div class="button normal-button test-button" id="apiInstructionButton" style="opacity: 1; display: inline-block;">{{ _("API-Informoj") }}</div></a></td>
<td class="settings-item-label"><div class="button normal-button test-button" id="telegramTestButton" style="opacity: 1; display: inline-block;">{{ _("Testo") }}</div></td>
</tr>
<tr class="settings-item" depends="motionDetectionEnabled">
<td colspan="100"><div class="settings-item-separator"></div></td>
Expand Down Expand Up @@ -1300,7 +1303,6 @@
</table>
{% endif %}
{% endfor %}

<div class="settings-progress"></div>
</form>
</div>
Expand All @@ -1313,9 +1315,8 @@
</div>
{% else %}
<div class="camera-frame" id="camera{{camera_id}}"
streaming_framerate="{{camera_config['stream_maxrate']}}" streaming_server_resize="{{camera_config['@webcam_server_resize']|string|lower}}"
proto="{{camera_config['@proto']}}" url="{{camera_config['@url']}}">

streaming_framerate="{{camera_config['stream_maxrate']}}" streaming_server_resize="{{camera_config['@webcam_server_resize']|string|lower}}"
proto="{{camera_config['@proto']}}" url="{{camera_config['@url']}}">
<div class="camera-container">
<div class="camera-placeholder"><img class="no-camera" src="{{static_path}}img/no-camera.svg"></div>
<img class="camera">
Expand Down
79 changes: 79 additions & 0 deletions motioneye/uploadservices.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,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
Expand Down Expand Up @@ -881,6 +882,84 @@ def _refresh_credentials(self, refresh_token):
}


class Webdav(UploadService):
NAME = 'webdav'

def __init__(self, camera_id):
self._endpoint_url = None
self._username = None
self._password = None
self._location = None

UploadService.__init__(self, camera_id)

def _request(self, url, method, body=None):
base64string = b64encode(f'{self._username}:{self._password}'.encode()).decode(
'ascii'
)
headers = {'Authorization': f'Basic {base64string}'}
if body is not None:
headers.update({'Content-Length': len(body)})
self.debug(f'request: {method} {url}')
request = Request(url, data=body, headers=headers)
request.get_method = lambda: method
try:
utils.urlopen(request)
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'
)
else:
raise e

def _make_dirs(self, path):
dir_url = self._endpoint_url.rstrip('/') + '/'
for folder in path.split('/'):
dir_url = dir_url + folder + '/'
self._request(dir_url, 'MKCOL')

def test_access(self):
MichaIng marked this conversation as resolved.
Show resolved Hide resolved
try:
path = self._location.strip('/') + '/' + str(time.time())
self._make_dirs(path)
self._request(self._endpoint_url.rstrip('/') + '/' + path, 'DELETE')
return True
except Exception as e:
self.error(str(e), exc_info=True)
return str(e)
MichaIng marked this conversation as resolved.
Show resolved Hide resolved

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(f'uploading {filename} of {len(data)} bytes')
self._request(
self._endpoint_url.rstrip('/') + '/' + path + '/' + filename,
'PUT',
bytearray(data),
)
self.debug('upload done')

def dump(self):
return {
'endpoint_url': self._endpoint_url,
'username': self._username,
'password': self._password,
'location': self._location,
}

def load(self, data):
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:
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
Expand Down