From 9405f2e3bb4f4504ca6d2a80855f402fb9a274f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jagie=C5=82=C5=82o?= Date: Sun, 26 Mar 2023 00:45:24 +0100 Subject: [PATCH 1/3] fix: gzip responses --- pywebdav/lib/WebDAVServer.py | 179 +++++++++++++---------------------- 1 file changed, 67 insertions(+), 112 deletions(-) diff --git a/pywebdav/lib/WebDAVServer.py b/pywebdav/lib/WebDAVServer.py index 42dcf7d..6969867 100644 --- a/pywebdav/lib/WebDAVServer.py +++ b/pywebdav/lib/WebDAVServer.py @@ -56,6 +56,59 @@ class DAVRequestHandler(AuthServer.AuthRequestHandler, LockManager): server_version = "DAV/" + __version__ encode_threshold = 1400 # common MTU + def data_to_bytes_iterator(self, data): + if isinstance(data, (six.string_types, six.binary_type)): + log.debug("Not using iterator, string type") + data = (data,) + elif self._config.DAV.getboolean('http_response_use_iterator'): + # Use iterator to reduce using memory + log.debug("Use iterator") + else: + # Don't use iterator, it's a compatibility option + data = (data.read(),) + log.debug("Don't use iterator") + + for buf in data: + yield buf if isinstance(buf, six.binary_type) else str(buf).encode('utf-8') + + def send_body_encoded(self, headers, data, content_type): + self.send_header('Date', rfc1123_date()) + self._send_dav_version() + + for a, v in headers.items(): + self.send_header(a, v) + + if not data: + self.send_header('Content-Length', "0") + self.end_headers() + return True + + try: + is_gzip = ('gzip' in self.headers.get('Accept-Encoding', '').split(',') + and len(data) > self.encode_threshold) + if is_gzip: + buffer = io.BytesIO() + output = gzip.GzipFile(mode='wb', fileobj=buffer) + for buf in self.data_to_bytes_iterator(data): + output.write(buf) + output.close() + buffer.seek(0) + data = buffer.getvalue() + except Exception as ex: + is_gzip = False + log.exception(ex) + + self.send_header('Content-Length', str(len(data))) + self.send_header('Content-Type', content_type) + + if is_gzip: + self.send_header('Content-Encoding', 'gzip') + self.end_headers() + if data: + self.wfile.write(data) + + return is_gzip + def send_body(self, DATA, code=None, msg=None, desc=None, ctype='application/octet-stream', headers={}): """ send a body in one part """ @@ -64,59 +117,12 @@ def send_body(self, DATA, code=None, msg=None, desc=None, self.send_response(code, message=msg) self.send_header("Connection", "close") self.send_header("Accept-Ranges", "bytes") - self.send_header('Date', rfc1123_date()) - - self._send_dav_version() - - for a, v in headers.items(): - self.send_header(a, v) - if DATA: - try: - if 'gzip' in self.headers.get('Accept-Encoding', '').split(',') \ - and len(DATA) > self.encode_threshold: - buffer = io.BytesIO() - output = gzip.GzipFile(mode='wb', fileobj=buffer) - if isinstance(DATA, str) or isinstance(DATA, six.text_type): - output.write(DATA) - else: - for buf in DATA: - output.write(buf) - output.close() - buffer.seek(0) - DATA = buffer.getvalue() - self.send_header('Content-Encoding', 'gzip') - - self.send_header('Content-Length', len(DATA)) - self.send_header('Content-Type', ctype) - except Exception as ex: - log.exception(ex) - else: - self.send_header('Content-Length', 0) - - self.end_headers() - if DATA: - if isinstance(DATA, str): - DATA = DATA.encode('utf-8') - if isinstance(DATA, six.text_type) or isinstance(DATA, bytes): - log.debug("Don't use iterator") - self.wfile.write(DATA) - else: - if self._config.DAV.getboolean('http_response_use_iterator'): - # Use iterator to reduce using memory - log.debug("Use iterator") - for buf in DATA: - self.wfile.write(buf) - self.wfile.flush() - else: - # Don't use iterator, it's a compatibility option - log.debug("Don't use iterator") - res = DATA.read() - if isinstance(res,bytes): - self.wfile.write(res) - else: - self.wfile.write(res.encode('utf8')) - return None + if not self.send_body_encoded(headers, DATA, ctype): + self.end_headers() + for buf in self.data_to_bytes_iterator(DATA): + self.wfile.write(buf) + self.wfile.flush() def send_body_chunks_if_http11(self, DATA, code, msg=None, desc=None, ctype='text/xml; encoding="utf-8"', @@ -133,69 +139,18 @@ def send_body_chunks(self, DATA, code, msg=None, desc=None, self.responses[207] = (msg, desc) self.send_response(code, message=msg) - self.send_header("Content-type", ctype) - self.send_header("Transfer-Encoding", "chunked") - self.send_header('Date', rfc1123_date()) - self._send_dav_version() - - for a, v in headers.items(): - self.send_header(a, v) + if not self.send_body_encoded(headers, DATA, ctype): + self.send_header("Transfer-Encoding", "chunked") + self.end_headers() - GZDATA = None - if DATA: - if ('gzip' in self.headers.get('Accept-Encoding', '').split(',') - and len(DATA) > self.encode_threshold): - buffer = io.BytesIO() - output = gzip.GzipFile(mode='wb', fileobj=buffer) - if isinstance(DATA, bytes): - output.write(DATA) - else: - for buf in DATA: - buf = buf.encode() if isinstance(buf, six.text_type) else buf - output.write(buf) - output.close() - buffer.seek(0) - GZDATA = buffer.getvalue() - self.send_header('Content-Encoding', 'gzip') - - self.send_header('Content-Length', len(DATA)) - self.send_header('Content-Type', ctype) - - else: - self.send_header('Content-Length', 0) - - self.end_headers() - - if GZDATA: - self.wfile.write(GZDATA) - - elif DATA: - DATA = DATA.encode() if isinstance(DATA, six.text_type) else DATA - if isinstance(DATA, six.binary_type): - self.wfile.write(b"%s\r\n" % hex(len(DATA))[2:].encode()) - self.wfile.write(DATA) - self.wfile.write(b"\r\n") - self.wfile.write(b"0\r\n") + for buf in self.data_to_bytes_iterator(DATA): + self.wfile.write(b"%x\r\n" % len(buf)) + self.wfile.write(buf) self.wfile.write(b"\r\n") - else: - if self._config.DAV.getboolean('http_response_use_iterator'): - # Use iterator to reduce using memory - for buf in DATA: - buf = buf.encode() if isinstance(buf, six.text_type) else buf - self.wfile.write((hex(len(buf))[2:] + "\r\n").encode()) - self.wfile.write(buf) - self.wfile.write(b"\r\n") - - self.wfile.write(b"0\r\n") - self.wfile.write(b"\r\n") - else: - # Don't use iterator, it's a compatibility option - self.wfile.write((hex(len(DATA))[2:] + "\r\n").encode()) - self.wfile.write(DATA.read()) - self.wfile.write(b"\r\n") - self.wfile.write(b"0\r\n") - self.wfile.write(b"\r\n") + + self.wfile.write(b"0\r\n") + self.wfile.write(b"\r\n") def _send_dav_version(self): if self._config.DAV.getboolean('lockemulation'): From 3ab2ea4213ef681aa99ce36925ba59336b8830aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jagie=C5=82=C5=82o?= Date: Sun, 26 Mar 2023 01:15:07 +0100 Subject: [PATCH 2/3] fix: do not use 'httpd/unix-directory' as a content type for GET responses --- pywebdav/lib/WebDAVServer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pywebdav/lib/WebDAVServer.py b/pywebdav/lib/WebDAVServer.py index 6969867..19b87d5 100644 --- a/pywebdav/lib/WebDAVServer.py +++ b/pywebdav/lib/WebDAVServer.py @@ -205,6 +205,8 @@ def _HEAD_GET(self, with_body=False): content_type = 'text/html;charset=utf-8' else: content_type = dc.get_prop(uri, "DAV:", "getcontenttype") + if content_type == 'httpd/unix-directory': + content_type = 'text/html;charset=utf-8' except DAV_NotFound: content_type = "application/octet-stream" From 1fa02aa98e540c39876fdd41c20779e9406f4531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jagie=C5=82=C5=82o?= Date: Sun, 26 Mar 2023 03:27:51 +0200 Subject: [PATCH 3/3] feat: allow using index.html files --- pywebdav/server/fshandler.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pywebdav/server/fshandler.py b/pywebdav/server/fshandler.py index 4b059dd..054c419 100644 --- a/pywebdav/server/fshandler.py +++ b/pywebdav/server/fshandler.py @@ -18,7 +18,7 @@ log = logging.getLogger(__name__) -BUFFER_SIZE = 128 * 1000 +BUFFER_SIZE = 128 * 1000 # include magic support to correctly determine mimetypes MAGIC_AVAILABLE = False try: @@ -53,10 +53,10 @@ def read(self, length = 0): data = self.__fp.read(length) return data - + class FilesystemHandler(dav_interface): - """ + """ Model a filesystem for DAV This class models a regular filesystem for the DAV server @@ -68,6 +68,7 @@ class FilesystemHandler(dav_interface): to /tmp/gfx/pix """ + index_files = () def __init__(self, directory, uri, verbose=False): self.setDirectory(directory) @@ -155,6 +156,16 @@ def get_data(self,uri, range = None): path=self.uri2local(uri) if os.path.exists(path): + if os.path.isdir(path): + for filename in self.index_files: + new_path = os.path.join(path, filename) + if os.path.isfile(new_path): + path = new_path + break + else: + msg = self._get_listing(path) + return Resource(StringIO(msg), len(msg)) + if os.path.isfile(path): file_size = os.path.getsize(path) if range is None: @@ -182,9 +193,6 @@ def get_data(self,uri, range = None): fp.seek(range[0]) log.info('Serving range %s -> %s content of %s' % (range[0], range[1], uri)) return Resource(fp, range[1] - range[0]) - elif os.path.isdir(path): - msg = self._get_listing(path) - return Resource(StringIO(msg), len(msg)) else: # also raise an error for collections # don't know what should happen then.. @@ -319,7 +327,7 @@ def rmcol(self,uri): shutil.rmtree(path) except OSError: raise DAV_Forbidden # forbidden - + return 204 def rm(self,uri): @@ -353,7 +361,7 @@ def delone(self,uri): return delone(self,uri) def deltree(self,uri): - """ delete a collection + """ delete a collection You have to return a result dict of the form uri:error_code