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