webdav: initial ugly but mostly working proof of concept
This commit is contained in:
369
procat2/resource.py
Normal file
369
procat2/resource.py
Normal file
@ -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"<MarkupDavResource {self.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_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)
|
||||||
@ -118,6 +118,8 @@ INSTALLED_APPS = [
|
|||||||
'account',
|
'account',
|
||||||
'lazysignup',
|
'lazysignup',
|
||||||
'webpack_loader',
|
'webpack_loader',
|
||||||
|
'djangodav',
|
||||||
|
'rest_framework',
|
||||||
'procat2',
|
'procat2',
|
||||||
'dashboard',
|
'dashboard',
|
||||||
'products',
|
'products',
|
||||||
|
|||||||
@ -26,6 +26,7 @@ from products.views import search_products, all_models
|
|||||||
|
|
||||||
from .forms import UserCreationForm
|
from .forms import UserCreationForm
|
||||||
from .views import login_guest, lazy_convert_done
|
from .views import login_guest, lazy_convert_done
|
||||||
|
from .resource import AuthDavView, ExampleView
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -53,6 +54,10 @@ urlpatterns = [
|
|||||||
path('quickinfo/', include('quickinfo.urls')),
|
path('quickinfo/', include('quickinfo.urls')),
|
||||||
|
|
||||||
path('markup/', include('markup.urls')),
|
path('markup/', include('markup.urls')),
|
||||||
|
|
||||||
|
#path('dav<path:path>', DavView.as_view(resource_class=MarkupDavResource, lock_class=DummyLock, acl_class=FullAcl)),
|
||||||
|
path('dav<path:path>', AuthDavView.as_view()),
|
||||||
|
path('example', ExampleView.as_view()),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DJDT:
|
if settings.DJDT:
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
amqp==2.5.2
|
amqp==2.5.2
|
||||||
|
asgiref==3.2.3
|
||||||
backcall==0.1.0
|
backcall==0.1.0
|
||||||
billiard==3.6.1.0
|
billiard==3.6.1.0
|
||||||
celery==4.4.0
|
celery==4.4.0
|
||||||
decorator==4.4.1
|
decorator==4.4.1
|
||||||
|
defusedxml==0.6.0
|
||||||
Django==2.2.9
|
Django==2.2.9
|
||||||
django-appconf==1.0.3
|
django-appconf==1.0.3
|
||||||
django-celery-results==1.1.2
|
django-celery-results==1.1.2
|
||||||
@ -12,6 +14,8 @@ django-lazysignup==2.0.0
|
|||||||
django-settings-export==1.2.1
|
django-settings-export==1.2.1
|
||||||
django-user-accounts==2.1.0
|
django-user-accounts==2.1.0
|
||||||
django-webpack-loader==0.6.0
|
django-webpack-loader==0.6.0
|
||||||
|
git+https://github.com/abend/djangodav.git#egg=DjangoDav
|
||||||
|
djangorestframework==3.11.0
|
||||||
Dumper==1.2.0
|
Dumper==1.2.0
|
||||||
et-xmlfile==1.0.1
|
et-xmlfile==1.0.1
|
||||||
humanize==0.5.1
|
humanize==0.5.1
|
||||||
@ -23,6 +27,7 @@ ipython-genutils==0.2.0
|
|||||||
jdcal==1.4.1
|
jdcal==1.4.1
|
||||||
jedi==0.15.2
|
jedi==0.15.2
|
||||||
kombu==4.6.7
|
kombu==4.6.7
|
||||||
|
lxml==4.5.0
|
||||||
more-itertools==8.1.0
|
more-itertools==8.1.0
|
||||||
numpy==1.18.1
|
numpy==1.18.1
|
||||||
opencv-python==4.1.2.30
|
opencv-python==4.1.2.30
|
||||||
|
|||||||
Reference in New Issue
Block a user