You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

106 lines
4.2 KiB

  1. # Simplified version of aiohttp's StaticResource with support for index.html
  2. # https://github.com/aio-libs/aiohttp/blob/v3.6.2/aiohttp/web_urldispatcher.py#L496-L678
  3. # Licensed under Apache 2.0
  4. from typing import Callable, Awaitable, Tuple, Optional, Union, Dict, Set, Iterator, Any
  5. from pathlib import Path, PurePath
  6. from aiohttp.web import (Request, StreamResponse, FileResponse, ResourceRoute, AbstractResource,
  7. AbstractRoute, UrlMappingMatchInfo, HTTPNotFound, HTTPForbidden)
  8. from aiohttp.abc import AbstractMatchInfo
  9. from yarl import URL
  10. Handler = Callable[[Request], Awaitable[StreamResponse]]
  11. class StaticResource(AbstractResource):
  12. def __init__(self, prefix: str, directory: Union[str, PurePath], *, name: Optional[str] = None,
  13. error_path: Optional[str] = "index.html", chunk_size: int = 256 * 1024) -> None:
  14. super().__init__(name=name)
  15. try:
  16. directory = Path(directory).resolve()
  17. if not directory.is_dir():
  18. raise ValueError("Not a directory")
  19. except (FileNotFoundError, ValueError) as error:
  20. raise ValueError(f"No directory exists at '{directory}'") from error
  21. self._directory = directory
  22. self._chunk_size = chunk_size
  23. self._prefix = prefix
  24. self._error_file = (directory / error_path) if error_path else None
  25. self._routes = {
  26. "GET": ResourceRoute("GET", self._handle, self),
  27. "HEAD": ResourceRoute("HEAD", self._handle, self),
  28. }
  29. @property
  30. def canonical(self) -> str:
  31. return self._prefix
  32. def add_prefix(self, prefix: str) -> None:
  33. assert prefix.startswith("/")
  34. assert not prefix.endswith("/")
  35. assert len(prefix) > 1
  36. self._prefix = prefix + self._prefix
  37. def raw_match(self, prefix: str) -> bool:
  38. return False
  39. def url_for(self, *, filename: Union[str, Path]) -> URL:
  40. if isinstance(filename, Path):
  41. filename = str(filename)
  42. while filename.startswith("/"):
  43. filename = filename[1:]
  44. return URL.build(path=f"{self._prefix}/{filename}")
  45. def get_info(self) -> Dict[str, Any]:
  46. return {
  47. "directory": self._directory,
  48. "prefix": self._prefix,
  49. }
  50. def set_options_route(self, handler: Handler) -> None:
  51. if "OPTIONS" in self._routes:
  52. raise RuntimeError("OPTIONS route was set already")
  53. self._routes["OPTIONS"] = ResourceRoute("OPTIONS", handler, self)
  54. async def resolve(self, request: Request) -> Tuple[Optional[AbstractMatchInfo], Set[str]]:
  55. path = request.rel_url.raw_path
  56. method = request.method
  57. allowed_methods = set(self._routes)
  58. if not path.startswith(self._prefix):
  59. return None, set()
  60. if method not in allowed_methods:
  61. return None, allowed_methods
  62. return UrlMappingMatchInfo({
  63. "filename": URL.build(path=path[len(self._prefix):], encoded=True).path
  64. }, self._routes[method]), allowed_methods
  65. def __len__(self) -> int:
  66. return len(self._routes)
  67. def __iter__(self) -> Iterator[AbstractRoute]:
  68. return iter(self._routes.values())
  69. async def _handle(self, request: Request) -> StreamResponse:
  70. try:
  71. filename = Path(request.match_info["filename"])
  72. if not filename.anchor:
  73. filepath = (self._directory / filename).resolve()
  74. if filepath.is_file():
  75. return FileResponse(filepath, chunk_size=self._chunk_size)
  76. index_path = (self._directory / filename / "index.html").resolve()
  77. if index_path.is_file():
  78. return FileResponse(index_path, chunk_size=self._chunk_size)
  79. except (ValueError, FileNotFoundError) as error:
  80. raise HTTPNotFound() from error
  81. except HTTPForbidden:
  82. raise
  83. except Exception as error:
  84. request.app.logger.exception("Error while trying to serve static file")
  85. raise HTTPNotFound() from error
  86. def __repr__(self) -> str:
  87. name = f"'{self.name}'" if self.name is not None else ""
  88. return f"<StaticResource {name} {self._prefix} -> {self._directory!r}>"