Source code for ewoksserver.app.backends.binary_backend
import base64
import logging
import mimetypes
import re
from pathlib import Path
from typing import Iterator
ResourceIdentifierType = str
ResourceUrlType = Path
ResourceContentType = dict
_logger = logging.getLogger(__name__)
[docs]
def root_url(root_url: str | Path | None, category: str) -> ResourceUrlType:
if not root_url:
root_url = Path(".")
elif isinstance(root_url, str):
root_url = Path(root_url)
return root_url / category
[docs]
def resource_identifiers(root: ResourceUrlType) -> Iterator[ResourceIdentifierType]:
for url in _resource_urls(root):
yield _url_to_identifier(url)
[docs]
def resources(root: ResourceUrlType) -> Iterator[ResourceContentType]:
for url in _resource_urls(root):
yield _load_url(url)
[docs]
def resource_exists(root: ResourceUrlType, identifier: ResourceIdentifierType) -> bool:
return _identifier_to_url(root, identifier).exists()
def _resource_urls(root: ResourceUrlType) -> Iterator[ResourceUrlType]:
for url in root.iterdir():
if _is_resource(url):
yield url
def _is_resource(url: ResourceUrlType) -> bool:
return (
url.is_file() and not url.name.startswith(".") and not url.name.endswith(".py")
)
[docs]
def save_resource(
root: ResourceUrlType,
identifier: ResourceIdentifierType,
resource: ResourceContentType,
):
url = _identifier_to_url(root, identifier)
_save_url(url, resource)
[docs]
def load_resource(
root: ResourceUrlType, identifier: ResourceIdentifierType
) -> ResourceContentType:
url = _identifier_to_url(root, identifier)
return _load_url(url)
[docs]
def delete_resource(root: ResourceUrlType, identifier: ResourceIdentifierType) -> None:
url = _identifier_to_url(root, identifier)
_delete_url(url)
def _delete_url(url: ResourceUrlType) -> ResourceContentType:
_logger.debug("Delete file '%s'", url)
url.unlink()
def _identifier_to_url(
root: ResourceUrlType, identifier: ResourceIdentifierType
) -> ResourceUrlType:
url = (root / identifier).resolve()
if not url.is_relative_to(root.resolve()):
raise ValueError(f"Invalid resource identifier: {identifier!r}")
return url
def _url_to_identifier(url: ResourceUrlType) -> ResourceIdentifierType:
return url.name
def _load_url(url: ResourceUrlType) -> ResourceContentType:
mimetype, encoding = mimetypes.guess_type(url, strict=True)
if mimetype is None:
raise ValueError(f"Cannot derive mime type from '{url}'")
try:
with open(url, "rb") as f:
data = f.read()
except FileNotFoundError:
_logger.error(f"'{url}' not found")
raise
if not encoding:
encoding = "base64"
data = base64.b64encode(data).decode()
return {"data_url": f"data:{mimetype};{encoding},{data}"}
def _save_url(url: ResourceUrlType, resource: ResourceContentType) -> None:
_logger.debug("Save file '%s'", url)
url.parent.mkdir(parents=True, exist_ok=True)
data = _decode_data_url(resource["data_url"])
with open(url, "wb") as f:
f.write(data)
# Matches the "data:<mimetype>;base64," prefix produced by `_load_url` above.
# Exposed so API models (e.g. EwoksIcon) can validate this at the request boundary.
DATA_URL_PREFIX = re.compile(r"^data:[^;,]*;base64,", re.IGNORECASE)
def _decode_data_url(data_url: str) -> bytes:
# Decode directly instead of using `urllib.request.urlopen`, which would
# also follow "file:", "http:" and other schemes for client-provided input.
match = DATA_URL_PREFIX.match(data_url)
if not match:
raise ValueError("Only base64-encoded 'data:' URLs are supported")
return base64.b64decode(data_url[match.end() :])