diff --git a/api/.devcontainer/Dockerfile b/api/.devcontainer/Dockerfile index c637a23..b36479d 100644 --- a/api/.devcontainer/Dockerfile +++ b/api/.devcontainer/Dockerfile @@ -22,6 +22,7 @@ RUN set -ex; \ export DEBIAN_FRONTEND=noninteractive; \ apt-get update; apt-get -y install --no-install-recommends \ git-flow \ + libmagic1 \ ; rm -rf /var/lib/apt/lists/*; # [Optional] Uncomment this line to install global node packages. diff --git a/api/ovdashboard_api/config.py b/api/ovdashboard_api/config.py index 72597fa..6093806 100644 --- a/api/ovdashboard_api/config.py +++ b/api/ovdashboard_api/config.py @@ -102,6 +102,7 @@ class Config(BaseModel): image_dir: str = "image" text_dir: str = "text" + file_dir: str = "file" logo: LogoUIConfig = LogoUIConfig() image: ImageConfig = ImageConfig() diff --git a/api/ovdashboard_api/routers/v1/__init__.py b/api/ovdashboard_api/routers/v1/__init__.py index 11ffd1e..cc4fec3 100644 --- a/api/ovdashboard_api/routers/v1/__init__.py +++ b/api/ovdashboard_api/routers/v1/__init__.py @@ -6,7 +6,7 @@ This file: Main API router definition. from fastapi import APIRouter -from . import aggregate, calendar, image, misc, text, ticker +from . import aggregate, calendar, file, image, misc, text, ticker router = APIRouter(prefix="/api/v1") @@ -15,6 +15,7 @@ router.include_router(misc.router) router.include_router(text.router) router.include_router(ticker.router) router.include_router(image.router) +router.include_router(file.router) router.include_router(calendar.router) router.include_router(aggregate.router) diff --git a/api/ovdashboard_api/routers/v1/file.py b/api/ovdashboard_api/routers/v1/file.py new file mode 100644 index 0000000..d64b2f7 --- /dev/null +++ b/api/ovdashboard_api/routers/v1/file.py @@ -0,0 +1,89 @@ +""" +Router "file" provides: + +- listing files +- finding files by name prefix +- getting files by name prefix +""" + +import re +from io import BytesIO +from logging import getLogger +from typing import Iterator + +from fastapi import APIRouter, Depends +from fastapi.responses import StreamingResponse +from magic import Magic + +from ...dav_common import webdav_ensure_path +from ...dav_file import DavFile +from ._common import FileNameLister, PrefixFinder, PrefixUnique + +_logger = getLogger(__name__) +_magic = Magic(mime=True) + +router = APIRouter(prefix="/file", tags=["file"]) + +file_lister = FileNameLister( + path_name="file_dir", + re=re.compile( + r"[^/]$", + flags=re.IGNORECASE, + ), +) + +file_finder = PrefixFinder(file_lister) +file_unique = PrefixUnique(file_finder) + + +@router.on_event("startup") +async def start_router() -> None: + _logger.debug(f"{router.prefix} router starting.") + + webdav_ensure_path(await file_lister.remote_path) + + +@router.get( + "/list", + response_model=list[str], + responses=file_lister.responses, +) +async def list_files( + names: Iterator[str] = Depends(file_lister), +) -> list[str]: + return list(names) + + +@router.get( + "/find/{prefix}", + response_model=list[str], + responses=file_finder.responses, +) +async def find_files( + names: Iterator[str] = Depends(file_finder), +) -> list[str]: + return list(names) + + +@router.get( + "/get/{prefix}", + response_class=StreamingResponse, + responses=file_unique.responses, +) +async def get_file( + prefix: str, + name: str = Depends(file_unique), +) -> StreamingResponse: + dav_file = DavFile(f"{await file_lister.remote_path}/{name}") + buffer = BytesIO(await dav_file.as_bytes) + + mime = _magic.from_buffer(buffer.read(2048)) + buffer.seek(0) + + return StreamingResponse( + content=buffer, + media_type=mime, + headers={ + "Content-Disposition": f"filename={prefix}" + }, + ) diff --git a/api/poetry.lock b/api/poetry.lock index 8947f53..bbccdd1 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -138,7 +138,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" [package.extras] cssselect = ["cssselect (>=0.7)"] html5 = ["html5lib"] -htmlsoup = ["beautifulsoup4"] +htmlsoup = ["BeautifulSoup4"] source = ["Cython (>=0.29.7)"] [[package]] @@ -205,6 +205,14 @@ python-versions = ">=3.7" [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-magic" +version = "0.4.27" +description = "File type identification using libmagic" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "pytz" version = "2022.2.1" @@ -388,7 +396,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "f31c08e6b5aabf05d1d144a083abbfb76c8708ef23f6d99122c646126c9f7bcd" +content-hash = "b25d37c0bf9187d599709a66e05d21c0df7da2b140373f6cd50b070ae2f073ab" [metadata.files] anyio = [ @@ -617,6 +625,10 @@ python-dotenv = [ {file = "python-dotenv-0.21.0.tar.gz", hash = "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"}, {file = "python_dotenv-0.21.0-py3-none-any.whl", hash = "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5"}, ] +python-magic = [ + {file = "python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b"}, + {file = "python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3"}, +] pytz = [ {file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"}, {file = "pytz-2022.2.1.tar.gz", hash = "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"}, diff --git a/api/pyproject.toml b/api/pyproject.toml index a1c296d..b0ecdeb 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -16,6 +16,7 @@ tomli = "^2.0.1" tomli-w = "^1.0.0" uvicorn = "^0.18.3" webdavclient3 = "3.14.5" +python-magic = "^0.4.27" [tool.poetry.dev-dependencies] # pytest = "^5.2"