2023-10-26 20:42:26 +00:00
|
|
|
import functools
|
2023-10-18 16:55:54 +00:00
|
|
|
import logging
|
2023-10-26 20:42:26 +00:00
|
|
|
import operator
|
2023-10-18 16:55:54 +00:00
|
|
|
import re
|
|
|
|
from io import BytesIO
|
|
|
|
|
2023-10-26 16:21:07 +00:00
|
|
|
import requests
|
2023-10-18 16:55:54 +00:00
|
|
|
from asyncify import asyncify
|
2023-10-26 20:42:26 +00:00
|
|
|
from cachetools import TTLCache, cachedmethod
|
|
|
|
from cachetools.keys import hashkey
|
2023-10-18 16:55:54 +00:00
|
|
|
from webdav3.client import Client as WebDAVclient
|
|
|
|
|
|
|
|
from .settings import SETTINGS
|
|
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2023-10-26 20:42:26 +00:00
|
|
|
def davkey(first, _, *args, **kwargs):
|
|
|
|
"""Return a cache key for use with cached methods."""
|
|
|
|
|
|
|
|
return hashkey(first, *args, **kwargs)
|
|
|
|
|
|
|
|
|
2023-10-18 16:55:54 +00:00
|
|
|
class WebDAV:
|
2023-10-22 10:49:04 +00:00
|
|
|
class __WebDAVclient(WebDAVclient):
|
|
|
|
def execute_request(
|
|
|
|
self,
|
|
|
|
action,
|
|
|
|
path,
|
|
|
|
data=None,
|
|
|
|
headers_ext=None,
|
2023-10-26 16:21:07 +00:00
|
|
|
) -> requests.Response:
|
2023-10-22 10:49:04 +00:00
|
|
|
res = super().execute_request(action, path, data, headers_ext)
|
|
|
|
|
|
|
|
# the "Content-Length" header can randomly be missing on txt files,
|
|
|
|
# this should fix that (probably serverside bug)
|
|
|
|
if action == "download" and "Content-Length" not in res.headers:
|
|
|
|
res.headers["Content-Length"] = str(len(res.text))
|
|
|
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
_webdav_client = __WebDAVclient(
|
2023-10-18 16:55:54 +00:00
|
|
|
{
|
|
|
|
"webdav_hostname": SETTINGS.webdav.url,
|
|
|
|
"webdav_login": SETTINGS.webdav.username,
|
|
|
|
"webdav_password": SETTINGS.webdav.password,
|
|
|
|
"disable_check": SETTINGS.webdav.disable_check,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2023-10-26 20:42:26 +00:00
|
|
|
_cache = TTLCache(
|
|
|
|
ttl=SETTINGS.webdav.cache_ttl,
|
2023-10-18 16:55:54 +00:00
|
|
|
maxsize=SETTINGS.webdav.cache_size,
|
|
|
|
)
|
2023-10-26 20:42:26 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
@asyncify
|
|
|
|
@cachedmethod(
|
|
|
|
cache=operator.attrgetter("_cache"),
|
|
|
|
key=functools.partial(davkey, "list_files"),
|
|
|
|
)
|
|
|
|
def list_files(
|
2023-10-18 16:55:54 +00:00
|
|
|
cls,
|
|
|
|
directory: str = "",
|
|
|
|
*,
|
|
|
|
regex: re.Pattern[str] = re.compile(""),
|
|
|
|
) -> list[str]:
|
|
|
|
"""
|
2023-10-20 11:01:48 +00:00
|
|
|
List files in directory `directory` matching RegEx `regex`
|
2023-10-18 16:55:54 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
_logger.debug(f"list_files {directory!r}")
|
2023-10-26 20:42:26 +00:00
|
|
|
ls = cls._webdav_client.list(directory)
|
2023-10-18 16:55:54 +00:00
|
|
|
|
2023-10-22 10:49:34 +00:00
|
|
|
return [path for path in ls if regex.search(path)]
|
2023-10-18 16:55:54 +00:00
|
|
|
|
|
|
|
@classmethod
|
2023-10-26 20:42:26 +00:00
|
|
|
@asyncify
|
|
|
|
@cachedmethod(
|
|
|
|
cache=operator.attrgetter("_cache"),
|
|
|
|
key=functools.partial(davkey, "exists"),
|
2023-10-18 16:55:54 +00:00
|
|
|
)
|
2023-10-26 20:42:26 +00:00
|
|
|
def exists(cls, path: str) -> bool:
|
2023-10-18 16:55:54 +00:00
|
|
|
"""
|
2023-10-20 11:01:48 +00:00
|
|
|
`True` iff there is a WebDAV resource at `path`
|
2023-10-18 16:55:54 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
_logger.debug(f"file_exists {path!r}")
|
2023-10-26 20:42:26 +00:00
|
|
|
return cls._webdav_client.check(path)
|
2023-10-18 16:55:54 +00:00
|
|
|
|
|
|
|
@classmethod
|
2023-10-26 20:42:26 +00:00
|
|
|
@asyncify
|
|
|
|
@cachedmethod(
|
|
|
|
cache=operator.attrgetter("_cache"),
|
|
|
|
key=functools.partial(davkey, "read_bytes"),
|
2023-10-18 16:55:54 +00:00
|
|
|
)
|
2023-10-26 20:42:26 +00:00
|
|
|
def read_bytes(cls, path: str) -> bytes:
|
2023-10-18 16:55:54 +00:00
|
|
|
"""
|
2023-10-20 11:01:48 +00:00
|
|
|
Load WebDAV file from `path` as bytes
|
2023-10-18 16:55:54 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
_logger.debug(f"read_bytes {path!r}")
|
|
|
|
buffer = BytesIO()
|
2023-10-26 20:42:26 +00:00
|
|
|
cls._webdav_client.download_from(buffer, path)
|
2023-10-22 10:49:04 +00:00
|
|
|
buffer.seek(0)
|
2023-10-18 16:55:54 +00:00
|
|
|
|
|
|
|
return buffer.read()
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
async def read_str(cls, path: str, encoding="utf-8") -> str:
|
|
|
|
"""
|
2023-10-20 11:01:48 +00:00
|
|
|
Load WebDAV file from `path` as string
|
2023-10-18 16:55:54 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
_logger.debug(f"read_str {path!r}")
|
|
|
|
return (await cls.read_bytes(path)).decode(encoding=encoding).strip()
|
|
|
|
|
|
|
|
@classmethod
|
2023-10-26 20:42:26 +00:00
|
|
|
@asyncify
|
|
|
|
def write_bytes(cls, path: str, buffer: bytes) -> None:
|
2023-10-18 16:55:54 +00:00
|
|
|
"""
|
2023-10-20 11:01:48 +00:00
|
|
|
Write bytes from `buffer` into WebDAV file at `path`
|
2023-10-18 16:55:54 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
_logger.debug(f"write_bytes {path!r}")
|
2023-10-26 20:42:26 +00:00
|
|
|
cls._webdav_client.upload_to(buffer, path)
|
2023-10-18 16:55:54 +00:00
|
|
|
|
2023-10-26 20:42:26 +00:00
|
|
|
# invalidate cache entry
|
|
|
|
cls._cache.pop(hashkey("read_bytes", path))
|
2023-10-18 16:55:54 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
async def write_str(cls, path: str, content: str, encoding="utf-8") -> None:
|
|
|
|
"""
|
2023-10-20 11:01:48 +00:00
|
|
|
Write string from `content` into WebDAV file at `path`
|
2023-10-18 16:55:54 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
_logger.debug(f"write_str {path!r}")
|
|
|
|
await cls.write_bytes(path, content.encode(encoding=encoding))
|