parent
a97093d001
commit
2b8f4b9419
@ -29,13 +29,11 @@ import os
|
||||
import threading
|
||||
from typing import Iterable, Optional, cast
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from radicale import config, log, types
|
||||
from radicale import config, log, types, utils
|
||||
from radicale.app import Application
|
||||
from radicale.log import logger
|
||||
|
||||
VERSION: str = pkg_resources.get_distribution("radicale").version
|
||||
VERSION: str = utils.package_version("radicale")
|
||||
|
||||
_application_instance: Optional[Application] = None
|
||||
_application_config_path: Optional[str] = None
|
||||
|
@ -24,13 +24,25 @@ Helper functions for HTTP.
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import time
|
||||
from http import client
|
||||
from typing import List, Mapping, cast
|
||||
from typing import List, Mapping, Union, cast
|
||||
|
||||
from radicale import config, pathutils, types
|
||||
from radicale.log import logger
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
import pkg_resources
|
||||
|
||||
_TRAVERSABLE_LIKE_TYPE = pathlib.Path
|
||||
else:
|
||||
import importlib.abc
|
||||
from importlib import resources
|
||||
|
||||
_TRAVERSABLE_LIKE_TYPE = Union[importlib.abc.Traversable, pathlib.Path]
|
||||
|
||||
NOT_ALLOWED: types.WSGIResponse = (
|
||||
client.FORBIDDEN, (("Content-Type", "text/plain"),),
|
||||
"Access to the requested resource forbidden.")
|
||||
@ -140,36 +152,63 @@ def redirect(location: str, status: int = client.FOUND) -> types.WSGIResponse:
|
||||
"Redirected to %s" % location)
|
||||
|
||||
|
||||
def serve_folder(folder: str, base_prefix: str, path: str,
|
||||
path_prefix: str = "/.web", index_file: str = "index.html",
|
||||
mimetypes: Mapping[str, str] = MIMETYPES,
|
||||
fallback_mimetype: str = FALLBACK_MIMETYPE,
|
||||
) -> types.WSGIResponse:
|
||||
def _serve_traversable(
|
||||
traversable: _TRAVERSABLE_LIKE_TYPE, base_prefix: str, path: str,
|
||||
path_prefix: str, index_file: str, mimetypes: Mapping[str, str],
|
||||
fallback_mimetype: str) -> types.WSGIResponse:
|
||||
if path != path_prefix and not path.startswith(path_prefix):
|
||||
raise ValueError("path must start with path_prefix: %r --> %r" %
|
||||
(path_prefix, path))
|
||||
assert pathutils.sanitize_path(path) == path
|
||||
try:
|
||||
filesystem_path = pathutils.path_to_filesystem(
|
||||
folder, path[len(path_prefix):].strip("/"))
|
||||
except ValueError as e:
|
||||
logger.debug("Web content with unsafe path %r requested: %s",
|
||||
path, e, exc_info=True)
|
||||
parts_path = path[len(path_prefix):].strip('/')
|
||||
parts = parts_path.split("/") if parts_path else []
|
||||
for part in parts:
|
||||
if not pathutils.is_safe_filesystem_path_component(part):
|
||||
logger.debug("Web content with unsafe path %r requested", path)
|
||||
return NOT_FOUND
|
||||
if os.path.isdir(filesystem_path) and not path.endswith("/"):
|
||||
if (not traversable.is_dir() or
|
||||
all(part != entry.name for entry in traversable.iterdir())):
|
||||
return NOT_FOUND
|
||||
traversable = traversable.joinpath(part)
|
||||
if traversable.is_dir():
|
||||
if not path.endswith("/"):
|
||||
return redirect(base_prefix + path + "/")
|
||||
if os.path.isdir(filesystem_path) and index_file:
|
||||
filesystem_path = os.path.join(filesystem_path, index_file)
|
||||
if not os.path.isfile(filesystem_path):
|
||||
if not index_file:
|
||||
return NOT_FOUND
|
||||
traversable = traversable.joinpath(index_file)
|
||||
if not traversable.is_file():
|
||||
return NOT_FOUND
|
||||
content_type = MIMETYPES.get(
|
||||
os.path.splitext(filesystem_path)[1].lower(), FALLBACK_MIMETYPE)
|
||||
with open(filesystem_path, "rb") as f:
|
||||
answer = f.read()
|
||||
last_modified = time.strftime(
|
||||
os.path.splitext(traversable.name)[1].lower(), FALLBACK_MIMETYPE)
|
||||
headers = {"Content-Type": content_type}
|
||||
if isinstance(traversable, pathlib.Path):
|
||||
headers["Last-Modified"] = time.strftime(
|
||||
"%a, %d %b %Y %H:%M:%S GMT",
|
||||
time.gmtime(os.fstat(f.fileno()).st_mtime))
|
||||
headers = {
|
||||
"Content-Type": content_type,
|
||||
"Last-Modified": last_modified}
|
||||
time.gmtime(traversable.stat().st_mtime))
|
||||
answer = traversable.read_bytes()
|
||||
return client.OK, headers, answer
|
||||
|
||||
|
||||
def serve_resource(
|
||||
package: str, resource: str, base_prefix: str, path: str,
|
||||
path_prefix: str = "/.web", index_file: str = "index.html",
|
||||
mimetypes: Mapping[str, str] = MIMETYPES,
|
||||
fallback_mimetype: str = FALLBACK_MIMETYPE) -> types.WSGIResponse:
|
||||
if sys.version_info < (3, 9):
|
||||
traversable = pathlib.Path(
|
||||
pkg_resources.resource_filename(package, resource))
|
||||
else:
|
||||
traversable = resources.files(package).joinpath(resource)
|
||||
return _serve_traversable(traversable, base_prefix, path, path_prefix,
|
||||
index_file, mimetypes, fallback_mimetype)
|
||||
|
||||
|
||||
def serve_folder(
|
||||
folder: str, base_prefix: str, path: str,
|
||||
path_prefix: str = "/.web", index_file: str = "index.html",
|
||||
mimetypes: Mapping[str, str] = MIMETYPES,
|
||||
fallback_mimetype: str = FALLBACK_MIMETYPE) -> types.WSGIResponse:
|
||||
# deprecated: use `serve_resource` instead
|
||||
traversable = pathlib.Path(folder)
|
||||
return _serve_traversable(traversable, base_prefix, path, path_prefix,
|
||||
index_file, mimetypes, fallback_mimetype)
|
||||
|
@ -29,7 +29,6 @@ from hashlib import sha256
|
||||
from typing import (Iterable, Iterator, Mapping, Optional, Sequence, Set,
|
||||
Tuple, Union, overload)
|
||||
|
||||
import pkg_resources
|
||||
import vobject
|
||||
|
||||
from radicale import config
|
||||
@ -41,7 +40,7 @@ INTERNAL_TYPES: Sequence[str] = ("multifilesystem", "multifilesystem_nolock",)
|
||||
|
||||
CACHE_DEPS: Sequence[str] = ("radicale", "vobject", "python-dateutil",)
|
||||
CACHE_VERSION: bytes = "".join(
|
||||
"%s=%s;" % (pkg, pkg_resources.get_distribution(pkg).version)
|
||||
"%s=%s;" % (pkg, utils.package_version(pkg))
|
||||
for pkg in CACHE_DEPS).encode()
|
||||
|
||||
|
||||
|
@ -16,12 +16,18 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
from importlib import import_module
|
||||
from typing import Callable, Sequence, Type, TypeVar, Union
|
||||
|
||||
from radicale import config
|
||||
from radicale.log import logger
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
import pkg_resources
|
||||
else:
|
||||
from importlib import metadata
|
||||
|
||||
_T_co = TypeVar("_T_co", covariant=True)
|
||||
|
||||
|
||||
@ -43,3 +49,9 @@ def load_plugin(internal_types: Sequence[str], module_name: str,
|
||||
(module_name, module, e)) from e
|
||||
logger.info("%s type is %r", module_name, module)
|
||||
return class_(configuration)
|
||||
|
||||
|
||||
def package_version(name):
|
||||
if sys.version_info < (3, 8):
|
||||
return pkg_resources.get_distribution(name).version
|
||||
return metadata.version(name)
|
||||
|
@ -25,9 +25,7 @@ Features:
|
||||
|
||||
"""
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from radicale import config, httputils, types, web
|
||||
from radicale import httputils, types, web
|
||||
|
||||
MIMETYPES = httputils.MIMETYPES # deprecated
|
||||
FALLBACK_MIMETYPE = httputils.FALLBACK_MIMETYPE # deprecated
|
||||
@ -35,13 +33,7 @@ FALLBACK_MIMETYPE = httputils.FALLBACK_MIMETYPE # deprecated
|
||||
|
||||
class Web(web.BaseWeb):
|
||||
|
||||
folder: str
|
||||
|
||||
def __init__(self, configuration: config.Configuration) -> None:
|
||||
super().__init__(configuration)
|
||||
self.folder = pkg_resources.resource_filename(
|
||||
__name__, "internal_data")
|
||||
|
||||
def get(self, environ: types.WSGIEnviron, base_prefix: str, path: str,
|
||||
user: str) -> types.WSGIResponse:
|
||||
return httputils.serve_folder(self.folder, base_prefix, path)
|
||||
return httputils.serve_resource("radicale.web", "internal_data",
|
||||
base_prefix, path)
|
||||
|
7
setup.py
7
setup.py
@ -49,6 +49,10 @@ WEB_FILES = ["web/internal_data/css/icon.png",
|
||||
"web/internal_data/fn.js",
|
||||
"web/internal_data/index.html"]
|
||||
|
||||
install_requires = ["defusedxml", "passlib", "vobject>=0.9.6",
|
||||
"python-dateutil>=2.7.3"]
|
||||
if sys.version_info < (3, 9):
|
||||
install_requires.append("setuptools")
|
||||
setup_requires = []
|
||||
if {"pytest", "test", "ptr"}.intersection(sys.argv):
|
||||
setup_requires.append("pytest-runner")
|
||||
@ -76,8 +80,7 @@ setup(
|
||||
exclude=["*.tests", "*.tests.*", "tests.*", "tests"]),
|
||||
package_data={"radicale": [*WEB_FILES, "py.typed"]},
|
||||
entry_points={"console_scripts": ["radicale = radicale.__main__:run"]},
|
||||
install_requires=["defusedxml", "passlib", "vobject>=0.9.6",
|
||||
"python-dateutil>=2.7.3", "setuptools"],
|
||||
install_requires=install_requires,
|
||||
setup_requires=setup_requires,
|
||||
tests_require=tests_require,
|
||||
extras_require={"test": tests_require,
|
||||
|
Loading…
Reference in New Issue
Block a user