diff --git a/markup/urls.py b/markup/urls.py index b4dd0c3..9b722e8 100644 --- a/markup/urls.py +++ b/markup/urls.py @@ -1,8 +1,10 @@ from django.urls import path from . import views +from .webdav import MarkupDavView urlpatterns = [ path('submit', views.submit, name='markup_submit'), #path('fail', views.fail, name='markup_fail'), + path('dav', MarkupDavView.as_view()), ] diff --git a/markup/webdav.py b/markup/webdav.py new file mode 100644 index 0000000..82ff310 --- /dev/null +++ b/markup/webdav.py @@ -0,0 +1,319 @@ +import datetime +import logging +import os +import re +import shutil + +from os.path import dirname +from pathlib import Path +from sys import getfilesystemencoding + +from django.core.exceptions import PermissionDenied +from django.utils.decorators import method_decorator +from django.utils.functional import cached_property + +from django_http_auth.decorators import http_basic_auth + +from django.http import HttpResponseForbidden, HttpResponseNotAllowed, HttpResponseBadRequest, \ + HttpResponseRedirect, Http404, HttpResponse, FileResponse + +from djangodav.acls import DavAcl, ReadOnlyAcl, FullAcl +from djangodav.base.resources import BaseDavResource, MetaEtagMixIn +from djangodav.fs.resources import DummyFSDAVResource +from djangodav.locks import DummyLock +from djangodav.utils import url_join +from djangodav.views import DavView + +from procat2.models import Catalog +from procat2.settings import ASSET_DIR + +from .utils import clean_path, ensure_dir, set_file_perms, WORKDIR + +log = logging.getLogger(__name__) + +fs_encoding = getfilesystemencoding() + + +@method_decorator(http_basic_auth, name='dispatch') +class MarkupDavView(DavView): + + def __init__(self): + super().__init__(resource_class=resource_factory, lock_class=DummyLock, acl_class=FullAcl) + + def get_access(self, resource): + return resource.get_acl() + + +def resource_factory(**kwargs): + # log.info(f"resource_factory '{kwargs}'") + path = kwargs['path'] + if path == RootFolderResource.PATH: + return RootFolderResource(**kwargs) + elif path == CatalogFolderResource.PATH: + return CatalogFolderResource(**kwargs) + elif is_my_cats_catalog(path): + return CatalogResource(**kwargs) + elif is_markup_catalog(path): + return MarkupResource(**kwargs) + else: + return NonsensicalResource(**kwargs) + +def is_my_cats_catalog(path): + return re.search(fr'^{CatalogFolderResource.PATH}.*-([0-9]+)\.pdf', path, re.I) + +def is_markup_catalog(path): + return re.search(fr'^{MarkupResource.PATH}', path, re.I) + + +class MarkupDavResource(MetaEtagMixIn, BaseDavResource): + + def __str__(self): + return f"<{type(self).__name__} '{self.user}' '{self.path}'>" + + def get_acl(self): + return ReadOnlyAcl() + # return DavAcl(read=True, write=False, delete=False, full=None) + + +class RootFolderResource(MarkupDavResource): + NAME = '' + PATH = '/' + + def get_parent(self): + return None + + def get_children(self): + children = [CatalogFolderResource(CatalogFolderResource.PATH, self.user), + MarkupResource(MarkupResource.PATH, self.user)] + for child in children: + yield child + + @property + def getcontentlength(self): + return 2 + + def get_created(self): + return datetime.datetime.now() # TODO last created time of object in there? + + def get_modified(self): + return datetime.datetime.now() # TODO last modified time of object in there? + + @property + def is_collection(self): + return True + + @property + def is_object(self): + return False + + @property + def exists(self): + return True + + +class CatalogFolderResource(MarkupDavResource): + NAME = 'My Catalogs' + PATH = f'/{NAME}/' + MARKUP_SUBMIT_NAME = 'For Markup' + MARKUP_SUBMIT_PATH = f'/{MARKUP_SUBMIT_NAME}/' + + def get_parent(self): + return RootFolderResource(user=self.user) + + def get_children(self): + for child in self.user_catalogs: + yield CatalogResource(path=url_join(*(self.path + [child])), user=self.user) + + @cached_property + def user_catalogs(self): + cats = Catalog.objects.filter(owner=self.user).order_by('-updated') + return [c.pdf_name() for c in cats if c.pdf_exists()] + + @property + def getcontentlength(self): + return 2 # TODO based on contents + + def get_created(self): + return datetime.datetime.now() # TODO last created time of object in there? + + def get_modified(self): + return datetime.datetime.now() # TODO last modified time of object in there? + + @property + def is_collection(self): + return True + + @property + def is_object(self): + return False + + @property + def exists(self): + return True + + +class CatalogResource(MarkupDavResource): + + def get_parent(self): + return CatalogFolderResource(path=CatalogFolderResource.PATH, user=self.user) + + def get_children(self): + return + yield + + @cached_property + def catalog(self): + id = self.id_from_pdf_name(self.path) + return Catalog.objects.get(id=id) + + def id_from_pdf_name(self, path): + result = re.search(r'-([0-9]+)\.pdf', path[-1], re.I) + if result and result.groups(): + return result.group(1) + else: + return None + + @property + def getcontentlength(self): + if self.catalog.pdf_exists(): + return os.stat(self.catalog.pdf_file()).st_size + else: + return 0 + + def get_created(self): + return self.catalog.created + + def get_modified(self): + return self.catalog.updated + + @property + def is_collection(self): + return False + + @property + def is_object(self): + return True + + @property + def exists(self): + return self.catalog.pdf_exists() + + def read(self): + if self.catalog.pdf_exists(): + return open(self.catalog.pdf_file(), 'rb') + else: + return None + + def delete(self): + pass + + def copy_object(self, destination, depth=0): + dest = destination.get_abs_path() + dest_dir = dirname(dest) + Path(dest_dir).mkdir(parents=True, exist_ok=True) + shutil.copy(self.catalog.pdf_file(), dest) + + def move_object(self, destination): + os.rename(self.get_abs_path(), destination.get_abs_path()) + + def write(self, request, temp_file=None, range_start=None): + autosave_path = url_join(MarkupResource.PATH, 'Autosave', self.path[-1]) + autosave = MarkupResource(path=autosave_path, user=self.user) + autosave_dir = autosave.get_abs_path() + log.debug(f'autosave_dir: {autosave_dir}') + ensure_dir(dirname(autosave_dir)) + return autosave.write(request, temp_file=None, range_start=None) + + def get_acl(self): + return DavAcl(read=True, write=True, delete=False, full=None) + + +def get_markup_user_path(user): + return os.path.join(ASSET_DIR, 'markup', 'webdav', user.username if user else 'unknown_user') + + +class MarkupResource(MarkupDavResource, DummyFSDAVResource): + NAME = 'For Markup' + PATH = f'/{NAME}/' + SUBDIR = 'markup' + + def get_parent(self): + parent_path = self.path[:-1] + if len(parent_path): + return MarkupResource(path=self.construct_path(parent_path, True), user=self.user) + else: + return RootFolderResource(user=self.user) + + def get_children(self): + path = self.get_abs_path() + if os.path.isdir(path): + for child in os.listdir(path): + is_unicode = isinstance(child, str) + if not is_unicode: + child = child.decode(fs_encoding) + yield MarkupResource(path=url_join(*(self.path + [child])), user=self.user) + + def get_abs_path(self): + base_dir = os.path.join(get_markup_user_path(self.user), self.SUBDIR) + ensure_dir(base_dir) + path = os.path.join(base_dir, *self.path[1:]) + # log.debug(f'markup: get abs path for {self.path}: base {base_dir}') + return path + + def write(self, request, temp_file=None, range_start=None): + super().write(request, temp_file=temp_file, range_start=range_start) + + def get_acl(self): + return FullAcl() + + +class NonsensicalResource(MarkupDavResource): + NAME = 'Misc' + PATH = f'/{NAME}/' + SUBDIR = 'misc' + + def get_abs_path(self): + base_dir = os.path.join(get_markup_user_path(self.user), self.SUBDIR) + ensure_dir(base_dir) + log.debug(f'nonsense: get abs path for {self.path}: base {base_dir}') + path = os.path.join(base_dir, *self.path[1:]) + return path + + def get_parent(self): + log.debug(f'nonsense parent of {self.path} is root') + return RootFolderResource(path=RootFolderResource.PATH, user=self.user) + + def get_children(self): + return + yield + + def delete(self): + return HttpResponseForbidden() + + def move(self): + return HttpResponseForbidden() + + def copy(self, destination, depth=-1): + return HttpResponseForbidden() + + @property + def getcontentlength(self): + return 0 + + def get_created(self): + return datetime.datetime.now() + + def get_modified(self): + return datetime.datetime.now() + + @property + def is_collection(self): + return False + + @property + def is_object(self): + return True + + @property + def exists(self): + return False diff --git a/procat2/models.py b/procat2/models.py index 0e0fc3b..6d339b0 100644 --- a/procat2/models.py +++ b/procat2/models.py @@ -51,7 +51,7 @@ def unix_datetime(date): class Catalog(models.Model): - PDF_DIR = 'catalogs' + PDF_DIR = 'catalogs/user' PDF_URL = 'export/catalogs' owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) diff --git a/procat2/settings.py b/procat2/settings.py index 6908ac5..aab03e7 100644 --- a/procat2/settings.py +++ b/procat2/settings.py @@ -118,6 +118,7 @@ INSTALLED_APPS = [ 'account', 'lazysignup', 'webpack_loader', + 'djangodav', 'procat2', 'dashboard', 'products', diff --git a/requirements.txt b/requirements.txt index 9d2f11b..079282b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,21 @@ amqp==2.5.2 +asgiref==3.2.3 backcall==0.1.0 billiard==3.6.1.0 celery==4.4.0 decorator==4.4.1 +defusedxml==0.6.0 Django==2.2.9 django-appconf==1.0.3 django-celery-results==1.1.2 django-debug-toolbar==2.1 django-extensions==2.2.5 +git+https://github.com/arcli/django-http-auth.git#egg=django-http-auth django-lazysignup==2.0.0 django-settings-export==1.2.1 django-user-accounts==2.1.0 django-webpack-loader==0.6.0 +git+https://github.com/arcli/djangodav.git#egg=DjangoDav Dumper==1.2.0 et-xmlfile==1.0.1 humanize==0.5.1 @@ -23,6 +27,7 @@ ipython-genutils==0.2.0 jdcal==1.4.1 jedi==0.15.2 kombu==4.6.7 +lxml==4.5.0 more-itertools==8.1.0 numpy==1.18.1 opencv-python==4.1.2.30