From a53e30e0a5929f6aa150fc6695a0cdf4bdd113a3 Mon Sep 17 00:00:00 2001 From: Jonas Winkler Date: Thu, 5 Jul 2018 16:18:20 +0200 Subject: [PATCH] Initial support for WebDAV. Lots of stuff is not there yet and most of the stuff which is there is not really tested. But it kind of already works. --- requirements.txt | 1 + src/paperless/dav.py | 265 ++++++++++++++++++++++++++++++++++++++ src/paperless/settings.py | 4 +- src/paperless/urls.py | 6 + 4 files changed, 275 insertions(+), 1 deletion(-) create mode 100755 src/paperless/dav.py diff --git a/requirements.txt b/requirements.txt index c6cb6fd47..d53da13d7 100755 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ django-extensions==2.0.7 django-filter==1.1.0 django-flat-responsive==2.0 django==2.0.7 +djangodav==0.0.1b26 djangorestframework==3.8.2 docopt==0.6.2 execnet==1.5.0 diff --git a/src/paperless/dav.py b/src/paperless/dav.py new file mode 100755 index 000000000..2f3a74210 --- /dev/null +++ b/src/paperless/dav.py @@ -0,0 +1,265 @@ +import base64 +import binascii +import datetime +import os +import shutil +from functools import wraps +from sys import getfilesystemencoding +from urllib.parse import unquote_plus + +from django.http import HttpResponse +from django.utils.decorators import method_decorator +from django.contrib.auth import authenticate + +from djangodav.base.resources import MetaEtagMixIn, BaseDavResource +from djangodav.utils import url_join +from djangodav.views import DavView + +from documents.models import Tag, Document, Correspondent + + +def extract_basicauth(authorization_header, encoding='utf-8'): + splitted = authorization_header.split(' ') + if len(splitted) != 2: + return None + + auth_type, auth_string = splitted + + if 'basic' != auth_type.lower(): + return None + + try: + b64_decoded = base64.b64decode(auth_string) + except (TypeError, binascii.Error): + return None + try: + auth_string_decoded = b64_decoded.decode(encoding) + except UnicodeDecodeError: + return None + + splitted = auth_string_decoded.split(':') + + if len(splitted) != 2: + return None + + username, password = map(unquote_plus, splitted) + return username, password + + +def validate_request(request): + + if 'HTTP_AUTHORIZATION' not in request.META: + return False + + authorization_header = request.META['HTTP_AUTHORIZATION'] + ret = extract_basicauth(authorization_header) + if not ret: + return False + + username, password = ret + + user = authenticate(username=username, password=password) + + if user is None: + return False + + request.META['REMOTE_USER'] = username + return True + + +class HttpResponseUnauthorized(HttpResponse): + status_code = 401 + + def __init__(self): + super(HttpResponseUnauthorized, self).__init__( + """Basic auth required +

Authorization Required

""", + ) + realm = 'Paperless WebDAV' + self['WWW-Authenticate'] = 'Basic realm="{}"'.format(realm) + + +def basic_auth_required(func=None, + target_test=(lambda request: True)): + def actual_decorator(view_func): + @wraps(view_func) + def _wrapped(request, *args, **kwargs): + if target_test(request) and not validate_request(request): + return HttpResponseUnauthorized() + return view_func(request, *args, **kwargs) + return _wrapped + + if func: + return actual_decorator(func) + else: + return actual_decorator + +#@method_decorator(basic_auth_required, name='dispatch') +class SecuredDavView(DavView): + pass + +class PaperlessDavResource(MetaEtagMixIn, BaseDavResource): + + document = None + + def __init__(self, path, **kwargs): + super(PaperlessDavResource, self).__init__(path) + self.documents, self.document, self.children = parse_path(path) + + @property + def getcontentlength(self): + if self.document: + return os.path.getsize(self.document.source_path) + else: + return None + + def get_created(self): + """Return the create time as datetime object.""" + if self.document: + return self.document.created + else: + return None + + def get_modified(self): + if self.document: + return self.document.modified + else: + return None + + @property + def is_collection(self): + return not self.document + + @property + def is_object(self): + return self.document + + @property + def exists(self): + return True + + def get_children(self): + if not self.document: + for child in self.children: + yield PaperlessDavResource(url_join(*(self.path + [child]))) + + for doc in self.documents: + yield PaperlessDavResource(url_join(*(self.path + [doc.title])), document=doc) + + def write(self, content, temp_file=None): + raise NotImplementedError + + def read(self): + print("calling read!!!") + return self.document.source_file + + def delete(self): + raise NotImplementedError + + def create_collection(self): + raise NotImplementedError + + def copy_object(self, destination, depth=0): + raise NotImplementedError + + def move_object(self, destination): + raise NotImplementedError + +def parse_path(path): + used_tags = [] + used_correspondents = [] + year_selected = False + month_selected = False + day_selected = False + show_documents = True + + def get_filter_children(): + filters = [] + if not year_selected: + filters.append('year') + elif not month_selected: + filters.append('month') + elif not day_selected: + filters.append('day') + filters.append('correspondent') + filters.append('tag') + return filters + + path_queue = [x for x in path.split('/') if x] + + filter = Document.objects.all() + children = get_filter_children() + document = None + + current_rule = 'select_filter' + + print("path",path_queue) + while len(path_queue) > 0: + path_segment = path_queue.pop(0) + # 1. check if path_segment exists / is allowed + # 2. determine what path_segment does + # 3. determine + + print("segment", path_segment) + print("rule ", current_rule) + print("---") + + if current_rule == 'select_filter': + show_documents = False + if path_segment == 'year': + next_rule = 'select_year' + children = [str(d.year) for d in filter.dates('created', 'year')] + elif path_segment == 'month': + next_rule = 'select_month' + children = [str(d.month) for d in filter.dates('created', 'month')] + elif path_segment == 'day': + next_rule = 'select_day' + children = [str(d.day) for d in filter.dates('created', 'day')] + elif path_segment == 'correspondent': + next_rule = 'select_correspondent' + children = [c.name for c in Correspondent.objects.all()] + elif path_segment == 'tag': + next_rule = 'select_tag' + children = [t.name for t in Tag.objects.all()] + else: + next_rule = 'document' + children = [] + document = Document.objects.get(title=path_segment) + elif current_rule == 'select_tag': + next_rule = 'select_filter' + filter = filter.filter(tags__name=path_segment) + used_tags.append(path_segment) + children = get_filter_children() + show_documents = True + elif current_rule == 'select_correspondent': + next_rule = 'select_filter' + filter = filter.filter(correspondent__name=path_segment) + used_tags.append(path_segment) + children = get_filter_children() + show_documents = True + elif current_rule == 'select_year': + next_rule = 'select_filter' + filter = filter.filter(created__year=path_segment) + year_selected = True + children = get_filter_children() + show_documents = True + elif current_rule == 'select_month': + next_rule = 'select_filter' + filter = filter.filter(created__month=path_segment) + month_selected = True + children = get_filter_children() + show_documents = True + elif current_rule == 'select_day': + next_rule = 'select_filter' + filter = filter.filter(created__day=path_segment) + day_selected = True + children = get_filter_children() + show_documents = True + else: + raise ValueError() + + current_rule = next_rule + + print("ok") + print("children", children) + return filter if show_documents else [], document, children \ No newline at end of file diff --git a/src/paperless/settings.py b/src/paperless/settings.py index a5d1f6a2c..736c23d1a 100755 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -72,7 +72,9 @@ INSTALLED_APPS = [ "rest_framework", "crispy_forms", - "django_filters" + "django_filters", + + "djangodav" ] diff --git a/src/paperless/urls.py b/src/paperless/urls.py index 59cbd86b5..9ea81394a 100755 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -6,6 +6,9 @@ from django.views.decorators.csrf import csrf_exempt from django.views.generic import RedirectView from rest_framework.routers import DefaultRouter +from djangodav.acls import FullAcl +from djangodav.locks import DummyLock + from documents.views import ( CorrespondentViewSet, DocumentViewSet, @@ -14,6 +17,7 @@ from documents.views import ( PushView, TagViewSet ) +from paperless.dav import PaperlessDavResource, SecuredDavView from reminders.views import ReminderViewSet router = DefaultRouter() @@ -46,6 +50,8 @@ urlpatterns = [ # The Django admin url(r"admin/", admin.site.urls), + url(r'^dav(?P.*)$', SecuredDavView.as_view(resource_class=PaperlessDavResource, lock_class=DummyLock, acl_class=FullAcl)), + # Redirect / to /admin url(r"^$", RedirectView.as_view( permanent=True, url=reverse_lazy("admin:index"))),