diff --git a/api/config/urls.py b/api/config/urls.py index 99fc32f1fce09e6a3b8c227e530102ba0e9abd17..aa38cc2b9029db131670de14edd838665db348d3 100644 --- a/api/config/urls.py +++ b/api/config/urls.py @@ -13,6 +13,7 @@ urlpatterns = [ # Django Admin, use {% url 'admin:index' %} url(settings.ADMIN_URL, admin.site.urls), url(r"^api/", include(("config.api_urls", "api"), namespace="api")), + url(r"^dav/", include(("funkwhale_api.webdav.urls", "webdav"), namespace="webdav")), url( r"^", include( diff --git a/api/funkwhale_api/common/middleware.py b/api/funkwhale_api/common/middleware.py index 59d50b30ead3ea34f5219431475162889461b596..0502f960bbf1c97d7b29c63fff38b9b0e2a265ce 100644 --- a/api/funkwhale_api/common/middleware.py +++ b/api/funkwhale_api/common/middleware.py @@ -9,7 +9,7 @@ from django import urls from . import preferences from . import utils -EXCLUDED_PATHS = ["/api", "/federation", "/.well-known"] +EXCLUDED_PATHS = ["/api", "/federation", "/.well-known", "/dav", "/rest"] def should_fallback_to_spa(path): diff --git a/api/funkwhale_api/webdav/__init__.py b/api/funkwhale_api/webdav/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/funkwhale_api/webdav/renderers.py b/api/funkwhale_api/webdav/renderers.py new file mode 100644 index 0000000000000000000000000000000000000000..ac2fce675e91733feaf0773f3cd2fcb07fc715cd --- /dev/null +++ b/api/funkwhale_api/webdav/renderers.py @@ -0,0 +1,258 @@ +from __future__ import unicode_literals + + +from django.utils import six +from django.utils.xmlutils import SimplerXMLGenerator +from django.utils.six.moves import StringIO +from django.utils.encoding import smart_text +from rest_framework.renderers import BaseRenderer + +DATA = """<?xml version="1.0" encoding="utf-8"?> +<d:multistatus xmlns:d="DAV:"> + <d:response> + <d:href>https://www.ajaxbrowser.com:443/Userc97c746/Products/</d:href> + <d:propstat> + <d:status>HTTP/1.1 200 OK</d:status> + <d:prop> + <d:resourcetype> + <d:collection /> + </d:resourcetype> + <d:displayname>Products</d:displayname> + <d:creationdate>2019-02-22T14:36:36Z</d:creationdate> + <d:getlastmodified>Fri, 22 Feb 2019 14:36:36 GMT</d:getlastmodified> + <d:supportedlock> + <d:lockentry> + <d:lockscope> + <d:exclusive /> + </d:lockscope> + <d:locktype> + <d:write /> + </d:locktype> + </d:lockentry> + <d:lockentry> + <d:lockscope> + <d:shared /> + </d:lockscope> + <d:locktype> + <d:write /> + </d:locktype> + </d:lockentry> + </d:supportedlock> + <d:lockdiscovery /> + <d:quota-available-bytes>102312153088</d:quota-available-bytes> + <d:quota-used-bytes>26520371200</d:quota-used-bytes> + </d:prop> + </d:propstat> + <d:propstat> + <d:status>HTTP/1.1 404 Not Found</d:status> + <d:prop> + <d:getcontenttype /> + <d:getcontentlength /> + <d:checked-in /> + <d:checked-out /> + </d:prop> + <d:responsedescription>Property was not found</d:responsedescription> + </d:propstat> + </d:response> + <d:response> + <d:href>https://www.ajaxbrowser.com:443/Userc97c746/Products/General.doc</d:href> + <d:propstat> + <d:status>HTTP/1.1 200 OK</d:status> + <d:prop> + <d:resourcetype /> + <d:displayname>General.doc</d:displayname> + <d:creationdate>2019-02-22T14:36:36Z</d:creationdate> + <d:getlastmodified>Fri, 22 Feb 2019 14:36:36 GMT</d:getlastmodified> + <d:getcontenttype>application/msword</d:getcontenttype> + <d:getcontentlength>10752</d:getcontentlength> + <d:supportedlock> + <d:lockentry> + <d:lockscope> + <d:exclusive /> + </d:lockscope> + <d:locktype> + <d:write /> + </d:locktype> + </d:lockentry> + <d:lockentry> + <d:lockscope> + <d:shared /> + </d:lockscope> + <d:locktype> + <d:write /> + </d:locktype> + </d:lockentry> + </d:supportedlock> + <d:lockdiscovery /> + <d:checked-in> + <d:href>https://www.ajaxbrowser.com:443/Userc97c746/Products/General.doc?version=1</d:href> + </d:checked-in> + </d:prop> + </d:propstat> + <d:propstat> + <d:status>HTTP/1.1 404 Not Found</d:status> + <d:prop> + <d:quota-available-bytes /> + <d:quota-used-bytes /> + <d:checked-out /> + </d:prop> + <d:responsedescription>Property was not found</d:responsedescription> + </d:propstat> + </d:response> + <d:response> + <d:href>https://www.ajaxbrowser.com:443/Userc97c746/Products/General.vsd</d:href> + <d:propstat> + <d:status>HTTP/1.1 200 OK</d:status> + <d:prop> + <d:resourcetype /> + <d:displayname>General.vsd</d:displayname> + <d:creationdate>2019-02-22T14:36:36Z</d:creationdate> + <d:getlastmodified>Fri, 22 Feb 2019 14:36:36 GMT</d:getlastmodified> + <d:getcontenttype>application/x-visio</d:getcontenttype> + <d:getcontentlength>0</d:getcontentlength> + <d:supportedlock> + <d:lockentry> + <d:lockscope> + <d:exclusive /> + </d:lockscope> + <d:locktype> + <d:write /> + </d:locktype> + </d:lockentry> + <d:lockentry> + <d:lockscope> + <d:shared /> + </d:lockscope> + <d:locktype> + <d:write /> + </d:locktype> + </d:lockentry> + </d:supportedlock> + <d:lockdiscovery /> + <d:checked-in> + <d:href>https://www.ajaxbrowser.com:443/Userc97c746/Products/General.vsd?version=1</d:href> + </d:checked-in> + </d:prop> + </d:propstat> + <d:propstat> + <d:status>HTTP/1.1 404 Not Found</d:status> + <d:prop> + <d:quota-available-bytes /> + <d:quota-used-bytes /> + <d:checked-out /> + </d:prop> + <d:responsedescription>Property was not found</d:responsedescription> + </d:propstat> + </d:response> + <d:response> + <d:href>https://www.ajaxbrowser.com:443/Userc97c746/Products/Product.mpp</d:href> + <d:propstat> + <d:status>HTTP/1.1 200 OK</d:status> + <d:prop> + <d:resourcetype /> + <d:displayname>Product.mpp</d:displayname> + <d:creationdate>2019-02-22T14:36:36Z</d:creationdate> + <d:getlastmodified>Fri, 22 Feb 2019 14:36:36 GMT</d:getlastmodified> + <d:getcontenttype>application/vnd.ms-project</d:getcontenttype> + <d:getcontentlength>114176</d:getcontentlength> + <d:supportedlock> + <d:lockentry> + <d:lockscope> + <d:exclusive /> + </d:lockscope> + <d:locktype> + <d:write /> + </d:locktype> + </d:lockentry> + <d:lockentry> + <d:lockscope> + <d:shared /> + </d:lockscope> + <d:locktype> + <d:write /> + </d:locktype> + </d:lockentry> + </d:supportedlock> + <d:lockdiscovery /> + <d:checked-in> + <d:href>https://www.ajaxbrowser.com:443/Userc97c746/Products/Product.mpp?version=1</d:href> + </d:checked-in> + </d:prop> + </d:propstat> + <d:propstat> + <d:status>HTTP/1.1 404 Not Found</d:status> + <d:prop> + <d:quota-available-bytes /> + <d:quota-used-bytes /> + <d:checked-out /> + </d:prop> + <d:responsedescription>Property was not found</d:responsedescription> + </d:propstat> + </d:response> +</d:multistatus> +""" + + +class WebDAVXMLRenderer(BaseRenderer): + """ + Renders WebDAV-compliant XML + """ + + media_type = "application/xml" + format = "xml" + charset = "utf-8" + item_tag_name = "list-item" + root_tag_name = "multistatus" + + def render(self, data, accepted_media_type=None, renderer_context=None): + """ + Renders `data` into serialized XML. + """ + return DATA.encode("utf-8") + if data is None: + return "" + + stream = StringIO() + + xml = SimplerXMLGenerator(stream, self.charset) + xml.startDocument() + xml.startElement(self.root_tag_name, {}) + + self._to_xml(xml, data) + + xml.endElement(self.root_tag_name) + xml.endDocument() + return stream.getvalue() + + def _to_xml(self, xml, data): + if isinstance(data, (list, tuple)): + for item in data: + self._to_xml(xml, item) + + elif isinstance(data, dict): + for key, value in six.iteritems(data): + if isinstance(value, ElementList): + for innerval in value: + xml.startElement(key, {}) + self._to_xml(xml, innerval) + xml.endElement(key) + if isinstance(value, ElementGroup): + for innerkey, innerval in value.items(): + xml.startElement(key, {}) # key=<propstat> + self._to_xml(xml, innerval) + xml.startElement(innerkey[0], {}) + self._to_xml(xml, innerkey[1]) + xml.endElement(innerkey[0]) + xml.endElement(key) + + else: + xml.startElement(key, {}) + self._to_xml(xml, value) + xml.endElement(key) + + elif data is None: + # Don't output any value + pass + + else: + xml.characters(smart_text(data)) diff --git a/api/funkwhale_api/webdav/urls.py b/api/funkwhale_api/webdav/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..aaca41fc9bf2e135af611f3eedc94a4abef2b52c --- /dev/null +++ b/api/funkwhale_api/webdav/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls import url + +from . import views + + +urlpatterns = [url(r"^libraries/(?P<path>.*)$", views.LibraryViewSet.as_view())] diff --git a/api/funkwhale_api/webdav/views.py b/api/funkwhale_api/webdav/views.py new file mode 100644 index 0000000000000000000000000000000000000000..5a808f396339980d6ae0e218cdfd718c99ea80e7 --- /dev/null +++ b/api/funkwhale_api/webdav/views.py @@ -0,0 +1,58 @@ +from rest_framework import response, mixins, viewsets + +from . import renderers + +WEBDAV_METHODS = [ + "get", + "put", + "lock", + "unlock", + "propfind", + "proppatch", + "delete", + "head", + "options", + "mkcol", + "copy", + "move", +] +ACTIONS = {m: "action_{}".format(m) for m in WEBDAV_METHODS} + + +class WebDAVViewSet(viewsets.GenericViewSet): + http_method_names = WEBDAV_METHODS + renderer_classes = (renderers.WebDAVXMLRenderer,) + + def dispatch(self, request, *args, **kwargs): + print("BODY", request.body) + response = super().dispatch(request, *args, **kwargs) + response["DAV"] = "1" + return response + + @classmethod + def as_view(cls, *args, **kwargs): + actions = {} + for method in WEBDAV_METHODS: + handler_name = "action_{}".format(method) + if hasattr(cls, handler_name): + actions[method] = handler_name + + kwargs["actions"] = actions + return super().as_view(*args, **kwargs) + + +class LibraryViewSet(WebDAVViewSet): + authentication_classes = [] + permission_classes = [] + + def action_propfind(self, request, *args, **kwargs): + return response.Response({}) + + def action_options(self, request, *args, **kwargs): + return response.Response({}) + + def action_get(self, request, *args, **kwargs): + return response.Response({}) + + def action_put(self, request, *args, **kwargs): + return response.Response({})