diff --git a/procat2/resource.py b/procat2/resource.py new file mode 100644 index 0000000..c850386 --- /dev/null +++ b/procat2/resource.py @@ -0,0 +1,369 @@ +import datetime +import os +import shutil +from sys import getfilesystemencoding + +import logging + +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt + +from django.core.exceptions import PermissionDenied, ValidationError +from djangodav.utils import WEBDAV_NSMAP, D, url_join, get_property_tag_list, rfc1123_date, rfc5987_content_disposition +from django.utils.timezone import now + +from djangodav.responses import HttpResponsePreconditionFailed, HttpResponseCreated, HttpResponseNoContent, \ + HttpResponseConflict, HttpResponseMediatypeNotSupported, HttpResponseBadGateway, HttpResponseMultiStatus, \ + HttpResponseLocked, ResponseException + +#from djangodav.fs.resources import DummyFSDAVResource +from djangodav.acls import FullAcl +from djangodav.base.resources import BaseDavResource +from djangodav.base.resources import MetaEtagMixIn +from djangodav.locks import DummyLock +from djangodav.utils import url_join +from djangodav.views import DavView + +# import lxml xml parser +from lxml import etree +# use defusedxmls parse function +from defusedxml.lxml import parse + +from rest_framework.authentication import SessionAuthentication, BasicAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from .models import Catalog + +log = logging.getLogger(__name__) + +fs_encoding = getfilesystemencoding() + + +class ExampleView(APIView): + authentication_classes = [SessionAuthentication, BasicAuthentication] + permission_classes = [IsAuthenticated] + + def get(self, request, format=None): + content = { + 'user': str(request.user), # `django.contrib.auth.User` instance. + 'auth': str(request.auth), # None + } + return Response(content) + + +class AuthDavView(DavView, APIView): + authentication_classes = [BasicAuthentication] + #authentication_classes = [SessionAuthentication, BasicAuthentication] + permission_classes = [IsAuthenticated] + auth_user = None + path = "" + + def __init__(self): + log.info(f"AuthDavView init") + super(DavView, self).__init__(resource_class=MarkupDavResource, lock_class=DummyLock, acl_class=FullAcl) + + # def get(self, request, format=None): + def get(self, request, head=False, *args, **kwargs): + # content = { + # 'user': str(request.user), # `django.contrib.auth.User` instance. + # 'auth': str(request.auth), # None + # } + # return Response(content) + self.auth_user = request.user + log.info(f'AuthDavView GET auth user: {request.user}') + path = "" + return super(DavView, self).get(request, path, head=False, *args, **kwargs) + + def dispatch(self, request, path, *args, **kwargs): + log.info(f'AuthDavView dispatch 0') + if path: + self.path = path + self.base_url = request.META['PATH_INFO'][:-len(self.path)] + else: + self.path = '/' + self.base_url = request.META['PATH_INFO'] + + # log.info(f'AuthDavView dispatch 1') + # super(APIView, self).dispatch(request, *args, **kwargs) + # log.info(f'AuthDavView dispatch 2') + # super(DavView, self).dispatch(request, *args, **kwargs) + # log.info(f'AuthDavView dispatch 3') + + # FROM APIView + self.args = args + self.kwargs = kwargs + request = self.initialize_request(request, *args, **kwargs) + self.request = request + self.headers = self.default_response_headers # deprecate? + + try: + self.initial(request, *args, **kwargs) + + # TODO HERE? + + meta = request.META.get + self.xbody = kwargs['xbody'] = None + if (request.method.lower() != 'put' + and "/xml" in meta('CONTENT_TYPE', '') + and meta('CONTENT_LENGTH', 0) != '' + and int(meta('CONTENT_LENGTH', 0)) > 0): + + # parse XML using defusedxmls parse function + self.xbody = kwargs['xbody'] = etree.XPathDocumentEvaluator( + parse(request, etree.XMLParser(ns_clean=True, resolve_entities=True)), + namespaces=WEBDAV_NSMAP + ) + + if request.method.upper() in self._allowed_methods(): + handler = getattr(self, request.method.lower(), self.http_method_not_allowed) + else: + handler = self.http_method_not_allowed + #try: + log.info(f"AuthDavView DISPATCH {handler} for {path}") + resp = handler(request, self.path, *args, **kwargs) + except ResponseException as e: + print(e) + resp = e.response + except PermissionDenied as pe: + print(pe) + resp = HttpResponseForbidden() + except ValidationError as ve: + print(ve) + resp = HttpResponseBadRequest() + except Exception as exc: + resp = self.handle_exception(exc) + self.response = self.finalize_response(request, resp, *args, **kwargs) + return self.response + + if not 'Allow' in resp: + methods = self._allowed_methods() + if methods: + resp['Allow'] = ", ".join(methods) + if not 'Date' in resp: + resp['Date'] = rfc1123_date(now()) + if self.server_header: + resp['Server'] = self.server_header + return resp + + + # TODO END HERE + + # Get the appropriate handler method + # if request.method.lower() in self.http_method_names: + # handler = getattr(self, request.method.lower(), + # self.http_method_not_allowed) + # else: + # handler = self.http_method_not_allowed + + # response = handler(request, *args, **kwargs) + + # except Exception as exc: + # response = self.handle_exception(exc) + + # self.response = self.finalize_response(request, response, *args, **kwargs) + # return self.response + + + def options(self, request, path, *args, **kwargs): + """ + Handler method for HTTP 'OPTIONS' request. + """ + log.info('AuthDavView OPTIONS') + # FROM APIView + # if self.metadata_class is None: + # return self.http_method_not_allowed(request, *args, **kwargs) + # data = self.metadata_class().determine_metadata(request, self) + # return Response(data, status=status.HTTP_200_OK) + + # FROM DavView + if not self.has_access(self.resource, 'read'): + return self.no_access() + response = self.build_xml_response() + response['DAV'] = '1,2' + response['Content-Length'] = '0' + if self.path in ('/', '*'): + return response + response['Allow'] = ", ".join(self._allowed_methods()) + if self.resource.exists and self.resource.is_object: + response['Accept-Ranges'] = 'bytes' + return response + + # @method_decorator(csrf_exempt) + # def dispatch(self, request, path, *args, **kwargs): + # self.auth_user = request.user + # log.info(f'auth user: {request.user}') + # return super().dispatch(request, path, *args, **kwargs) + + +class MarkupDavResource(MetaEtagMixIn, BaseDavResource): + root = '/opt/imagebank/mkbeta/webdav' # TODO replace with settings var + + def __init__(self, path): + log.info(f"MarkupDavResource INIT {path}") + super().__init__(path) + + def __str__(self): + return f"" + + def get_children(self): + """Return an iterator of all direct children of this resource.""" + # make sure the current object is a directory + path = self.get_path() + log.info(f"get_children of {path}") + + children = [] + if path == '/': + #children = ['My Catalogs', 'For Markup'] + children = ['MyCatalogs'] + elif path == '/MyCatalogs': + # TODO return user's catalogs + # get User + children = None + elif path == '/For Markup': + children = None + # else: + # path = self.get_abs_path() + # if os.path.isdir(path): + # children = os.listdir(path) + + for child in children: + try: + is_unicode = isinstance(child, str) + except NameError: # Python 3 fix + is_unicode = isinstance(child, str) + if not is_unicode: + child = child.decode(fs_encoding) + log.info(f'returning kid {self.clone(url_join(*(self.path + [child])))}') + yield self.clone(url_join(*(self.path + [child]))) + + + def get_abs_path(self): + """Return the absolute path of the resource. Used internally to interface with + an actual file system. If you override all other methods, this one will not + be used.""" + return os.path.join(self.root, *self.path) + + @property + def getcontentlength(self): + """Return the size of the resource in bytes.""" + if self.is_collection: + return 0 + else: + return os.path.getsize(self.get_abs_path()) + + def get_created(self): + """Return the create time as datetime object.""" + if self.is_collection: + return datetime.datetime.now() # TODO last created time of object in there? + else: + return datetime.datetime.fromtimestamp(os.stat(self.get_abs_path()).st_ctime) + + def get_modified(self): + """Return the modified time as datetime object.""" + if self.is_collection: + return datetime.datetime.now() # TODO last modified time of object in there? + else: + return datetime.datetime.fromtimestamp(os.stat(self.get_abs_path()).st_mtime) + + @property + def is_collection(self): + """Return True if this resource is a directory (collection in WebDAV parlance).""" + path = '/'.join(self.path) + # log.info(f"is_collection {self.path}") + + if path == '': + return True + elif path == 'MyCatalogs': + return True + elif path == 'For Markup': + return True + + log.info(f"is_collection {self.path} FALLTHROUGH") + return False + # return os.path.isdir(self.get_abs_path()) + + @property + def is_object(self): + """Return True if this resource is a file (resource in WebDAV parlance).""" + #path = self.get_path() + path = '/'.join(self.path) + # log.info(f"is_object {path}") + + if path == '': + return False + elif path == 'MyCatalogs': + return False + elif path == 'For Markup': + return False + + return True + #return os.path.isfile(self.get_abs_path()) + + @property + def exists(self): + """Return True if this resource exists.""" + path = '/'.join(self.path) + log.info(f"exists {path}") + if path in ['', 'MyCatalogs', 'For Markup']: + return True + else: + return os.path.exists(self.get_abs_path()) + + # def get_children(self): + # """Return an iterator of all direct children of this resource.""" + # # make sure the current object is a directory + # path = self.get_abs_path() + # log.info(f"get_children {path}") + # if os.path.isdir(path): + # for child in os.listdir(path): + # try: + # is_unicode = isinstance(child, str) + # except NameError: # Python 3 fix + # is_unicode = isinstance(child, str) + # if not is_unicode: + # child = child.decode(fs_encoding) + # yield self.clone(url_join(*(self.path + [child]))) + + # def write(self, content, temp_file=None, range_start=None): + # raise NotImplementedError + + # def read(self): + # raise NotImplementedError + + # def delete(self): + # """Delete the resource, recursive is implied.""" + # if self.is_collection: + # for child in self.get_children(): + # child.delete() + # os.rmdir(self.get_abs_path()) + # elif self.is_object: + # os.remove(self.get_abs_path()) + + # def create_collection(self): + # """Create a directory in the location of this resource.""" + # os.mkdir(self.get_abs_path()) + + # def copy_object(self, destination, depth=0): + # shutil.copy(self.get_abs_path(), destination.get_abs_path()) + + # def move_object(self, destination): + # os.rename(self.get_abs_path(), destination.get_abs_path()) + + # def read(self): + # return open(self.get_abs_path(), 'rb') + + # def write(self, request, temp_file=None, range_start=None): + # if temp_file: + # # move temp file (e.g., coming from nginx) + # shutil.move(temp_file, self.get_abs_path()) + # elif range_start == None: + # # open binary file and write to disk + # with open(self.get_abs_path(), 'wb') as dst: + # shutil.copyfileobj(request, dst) + # else: + # # open binary file and write to disk + # with open(self.get_abs_path(), 'r+b') as dst: + # dst.seek(range_start) + # shutil.copyfileobj(request, dst) diff --git a/procat2/settings.py b/procat2/settings.py index 6908ac5..fb0ee68 100644 --- a/procat2/settings.py +++ b/procat2/settings.py @@ -118,6 +118,8 @@ INSTALLED_APPS = [ 'account', 'lazysignup', 'webpack_loader', + 'djangodav', + 'rest_framework', 'procat2', 'dashboard', 'products', diff --git a/procat2/urls.py b/procat2/urls.py index d4539d4..c546eb7 100644 --- a/procat2/urls.py +++ b/procat2/urls.py @@ -26,6 +26,7 @@ from products.views import search_products, all_models from .forms import UserCreationForm from .views import login_guest, lazy_convert_done +from .resource import AuthDavView, ExampleView urlpatterns = [ @@ -53,6 +54,10 @@ urlpatterns = [ path('quickinfo/', include('quickinfo.urls')), path('markup/', include('markup.urls')), + + #path('dav', DavView.as_view(resource_class=MarkupDavResource, lock_class=DummyLock, acl_class=FullAcl)), + path('dav', AuthDavView.as_view()), + path('example', ExampleView.as_view()), ] if settings.DJDT: diff --git a/requirements.txt b/requirements.txt index 9d2f11b..4d17982 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,10 @@ 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 @@ -12,6 +14,8 @@ 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/abend/djangodav.git#egg=DjangoDav +djangorestframework==3.11.0 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