366 lines
12 KiB
Python
366 lines
12 KiB
Python
import argparse
|
|
import ctypes
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import webbrowser
|
|
from typing import Dict, Optional
|
|
|
|
from django.conf import ENVIRONMENT_VARIABLE
|
|
from django.core.exceptions import ImproperlyConfigured
|
|
from django.utils.crypto import get_random_string
|
|
from mypy_extensions import NoReturn
|
|
|
|
|
|
DEVELOPMENT_VERSION = "Development Version"
|
|
UNIX_VERSION = "Unix Version"
|
|
WINDOWS_VERSION = "Windows Version"
|
|
WINDOWS_PORTABLE_VERSION = "Windows Portable Version"
|
|
|
|
|
|
class PortableDirNotWritable(Exception):
|
|
pass
|
|
|
|
|
|
class PortIsBlockedError(Exception):
|
|
pass
|
|
|
|
|
|
class DatabaseInSettingsError(Exception):
|
|
pass
|
|
|
|
|
|
class UnknownCommand(Exception):
|
|
pass
|
|
|
|
|
|
class ExceptionArgumentParser(argparse.ArgumentParser):
|
|
def error(self, message: str) -> NoReturn:
|
|
raise UnknownCommand(message)
|
|
|
|
|
|
def detect_openslides_type() -> str:
|
|
"""
|
|
Returns the type of this OpenSlides version.
|
|
"""
|
|
if sys.platform == "win32":
|
|
if os.path.basename(sys.executable).lower() == "openslides.exe":
|
|
# Note: sys.executable is the path of the *interpreter*
|
|
# the portable version embeds python so it *is* the interpreter.
|
|
# The wrappers generated by pip and co. will spawn the usual
|
|
# python(w).exe, so there is no danger of mistaking them
|
|
# for the portable even though they may also be called
|
|
# openslides.exe
|
|
openslides_type = WINDOWS_PORTABLE_VERSION
|
|
else:
|
|
openslides_type = WINDOWS_VERSION
|
|
else:
|
|
openslides_type = UNIX_VERSION
|
|
return openslides_type
|
|
|
|
|
|
def get_default_settings_dir(openslides_type: str = None) -> str:
|
|
"""
|
|
Returns the default settings path according to the OpenSlides type.
|
|
|
|
The argument 'openslides_type' has to be one of the three types mentioned in
|
|
openslides.utils.main.
|
|
"""
|
|
if openslides_type is None:
|
|
openslides_type = detect_openslides_type()
|
|
|
|
if openslides_type == UNIX_VERSION:
|
|
parent_directory = os.environ.get(
|
|
"XDG_CONFIG_HOME", os.path.expanduser("~/.config")
|
|
)
|
|
elif openslides_type == WINDOWS_VERSION:
|
|
parent_directory = get_win32_app_data_dir()
|
|
elif openslides_type == WINDOWS_PORTABLE_VERSION:
|
|
parent_directory = get_win32_portable_dir()
|
|
else:
|
|
raise TypeError(f"{openslides_type} is not a valid OpenSlides type.")
|
|
return os.path.join(parent_directory, "openslides")
|
|
|
|
|
|
def get_local_settings_dir() -> str:
|
|
"""
|
|
Returns the path to a local settings.
|
|
|
|
On Unix systems: 'personal_data/var/'
|
|
"""
|
|
return os.path.join("personal_data", "var")
|
|
|
|
|
|
def setup_django_settings_module(
|
|
settings_path: str = None, local_installation: bool = False
|
|
) -> None:
|
|
"""
|
|
Sets the environment variable ENVIRONMENT_VARIABLE, that means
|
|
'DJANGO_SETTINGS_MODULE', to the given settings.
|
|
|
|
If no settings_path is given and the environment variable is already set,
|
|
then this function does nothing.
|
|
|
|
If the argument settings_path is set, then the environment variable is
|
|
always overwritten.
|
|
"""
|
|
if settings_path is None and os.environ.get(ENVIRONMENT_VARIABLE, ""):
|
|
return
|
|
|
|
if settings_path is None:
|
|
if local_installation:
|
|
settings_dir = get_local_settings_dir()
|
|
else:
|
|
settings_dir = get_default_settings_dir()
|
|
settings_path = os.path.join(settings_dir, "settings.py")
|
|
|
|
settings_file = os.path.basename(settings_path)
|
|
settings_module_name = ".".join(settings_file.split(".")[:-1])
|
|
if "." in settings_module_name:
|
|
raise ImproperlyConfigured(
|
|
"'.' is not an allowed character in the settings-file"
|
|
)
|
|
|
|
# Change the python path. Also set the environment variable python path, so
|
|
# change of the python path also works after a reload
|
|
settings_module_dir = os.path.abspath(os.path.dirname(settings_path))
|
|
sys.path.insert(0, settings_module_dir)
|
|
try:
|
|
os.environ["PYTHONPATH"] = os.pathsep.join(
|
|
(settings_module_dir, os.environ["PYTHONPATH"])
|
|
)
|
|
except KeyError:
|
|
# The environment variable is empty
|
|
os.environ["PYTHONPATH"] = settings_module_dir
|
|
|
|
# Set the environment variable to the settings module
|
|
os.environ[ENVIRONMENT_VARIABLE] = settings_module_name
|
|
|
|
|
|
def get_default_settings_context(user_data_dir: str = None) -> Dict[str, str]:
|
|
"""
|
|
Returns the default context values for the settings template:
|
|
'openslides_user_data_path', 'import_function' and 'debug'.
|
|
|
|
The argument 'user_data_path' is a given path for user specific data or None.
|
|
"""
|
|
# Setup path for user specific data (SQLite3 database, media, ...):
|
|
# Take it either from command line or get default path
|
|
default_context = {}
|
|
if user_data_dir:
|
|
default_context["openslides_user_data_dir"] = repr(user_data_dir)
|
|
default_context["import_function"] = ""
|
|
else:
|
|
openslides_type = detect_openslides_type()
|
|
if openslides_type == WINDOWS_PORTABLE_VERSION:
|
|
default_context[
|
|
"openslides_user_data_dir"
|
|
] = "get_win32_portable_user_data_dir()"
|
|
default_context[
|
|
"import_function"
|
|
] = "from openslides.utils.main import get_win32_portable_user_data_dir"
|
|
else:
|
|
data_dir = get_default_user_data_dir(openslides_type)
|
|
default_context["openslides_user_data_dir"] = repr(
|
|
os.path.join(data_dir, "openslides")
|
|
)
|
|
default_context["import_function"] = ""
|
|
default_context["debug"] = "False"
|
|
return default_context
|
|
|
|
|
|
def get_default_user_data_dir(openslides_type: str) -> str:
|
|
"""
|
|
Returns the default directory for user specific data according to the OpenSlides
|
|
type.
|
|
|
|
The argument 'openslides_type' has to be one of the three types mentioned
|
|
in openslides.utils.main.
|
|
"""
|
|
if openslides_type == UNIX_VERSION:
|
|
default_user_data_dir = os.environ.get(
|
|
"XDG_DATA_HOME", os.path.expanduser("~/.local/share")
|
|
)
|
|
elif openslides_type == WINDOWS_VERSION:
|
|
default_user_data_dir = get_win32_app_data_dir()
|
|
elif openslides_type == WINDOWS_PORTABLE_VERSION:
|
|
default_user_data_dir = get_win32_portable_dir()
|
|
else:
|
|
raise TypeError(f"{openslides_type} is not a valid OpenSlides type.")
|
|
return default_user_data_dir
|
|
|
|
|
|
def get_win32_app_data_dir() -> str:
|
|
"""
|
|
Returns the directory of Windows' AppData directory.
|
|
"""
|
|
shell32 = ctypes.WinDLL("shell32.dll") # type: ignore
|
|
SHGetFolderPath = shell32.SHGetFolderPathW
|
|
SHGetFolderPath.argtypes = (
|
|
ctypes.c_void_p,
|
|
ctypes.c_int,
|
|
ctypes.c_void_p,
|
|
ctypes.c_uint32,
|
|
ctypes.c_wchar_p,
|
|
)
|
|
SHGetFolderPath.restype = ctypes.c_uint32
|
|
|
|
CSIDL_LOCAL_APPDATA = 0x001C
|
|
MAX_PATH = 260
|
|
|
|
buf = ctypes.create_unicode_buffer(MAX_PATH)
|
|
res = SHGetFolderPath(0, CSIDL_LOCAL_APPDATA, 0, 0, buf)
|
|
if res != 0:
|
|
# TODO: Write other exception
|
|
raise Exception("Could not determine Windows' APPDATA path")
|
|
|
|
return buf.value # type: ignore
|
|
|
|
|
|
def get_win32_portable_dir() -> str:
|
|
"""
|
|
Returns the directory of the Windows portable version.
|
|
"""
|
|
# NOTE: sys.executable will be the path to openslides.exe
|
|
# since it is essentially a small wrapper that embeds the
|
|
# python interpreter
|
|
portable_dir = os.path.dirname(os.path.abspath(sys.executable))
|
|
try:
|
|
fd, test_file = tempfile.mkstemp(dir=portable_dir)
|
|
except OSError:
|
|
raise PortableDirNotWritable(
|
|
"Portable directory is not writeable. "
|
|
"Please choose another directory for settings and data files."
|
|
)
|
|
else:
|
|
os.close(fd)
|
|
os.unlink(test_file)
|
|
return portable_dir
|
|
|
|
|
|
def get_win32_portable_user_data_dir() -> str:
|
|
"""
|
|
Returns the user data directory to the Windows portable version.
|
|
"""
|
|
return os.path.join(get_win32_portable_dir(), "openslides")
|
|
|
|
|
|
def write_settings(
|
|
settings_dir: str = None,
|
|
settings_filename: str = "settings.py",
|
|
template: str = None,
|
|
**context: str,
|
|
) -> str:
|
|
"""
|
|
Creates the settings file at the given dir using the given values for the
|
|
file template.
|
|
|
|
Retuns the path to the created settings.
|
|
"""
|
|
if settings_dir is None:
|
|
settings_dir = get_default_settings_dir()
|
|
settings_path = os.path.join(settings_dir, settings_filename)
|
|
|
|
if template is None:
|
|
with open(
|
|
os.path.join(os.path.dirname(__file__), "settings.py.tpl")
|
|
) as template_file:
|
|
template = template_file.read()
|
|
|
|
# Create a random SECRET_KEY to put it in the settings.
|
|
# from django.core.management.commands.startproject
|
|
chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
|
|
context.setdefault("secret_key", get_random_string(50, chars))
|
|
for key, value in get_default_settings_context().items():
|
|
context.setdefault(key, value)
|
|
|
|
content = template % context
|
|
settings_module = os.path.realpath(settings_dir)
|
|
if not os.path.exists(settings_module):
|
|
os.makedirs(settings_module)
|
|
with open(settings_path, "w") as settings_file:
|
|
settings_file.write(content)
|
|
|
|
if context["openslides_user_data_dir"] == "get_win32_portable_user_data_dir()":
|
|
openslides_user_data_dir = get_win32_portable_user_data_dir()
|
|
else:
|
|
openslides_user_data_dir = context["openslides_user_data_dir"].strip("'")
|
|
os.makedirs(os.path.join(openslides_user_data_dir, "static"), exist_ok=True)
|
|
return os.path.realpath(settings_path)
|
|
|
|
|
|
def open_browser(host: str, port: int) -> None:
|
|
"""
|
|
Launches the default web browser at the given host and port and opens
|
|
the webinterface. Uses start_browser internally.
|
|
"""
|
|
if host == "0.0.0.0":
|
|
# Windows does not support 0.0.0.0, so use 'localhost' instead
|
|
start_browser(f"http://localhost:{port}")
|
|
else:
|
|
start_browser(f"http://{host}:{port}")
|
|
|
|
|
|
def start_browser(browser_url: str) -> None:
|
|
"""
|
|
Launches the default web browser at the given url and opens the
|
|
webinterface.
|
|
"""
|
|
try:
|
|
browser = webbrowser.get()
|
|
except webbrowser.Error:
|
|
print("Could not locate runnable browser: Skipping start")
|
|
else:
|
|
|
|
def function() -> None:
|
|
# TODO: Use a nonblocking sleep event here.
|
|
time.sleep(1)
|
|
browser.open(browser_url)
|
|
|
|
thread = threading.Thread(target=function)
|
|
thread.start()
|
|
|
|
|
|
def get_database_path_from_settings() -> Optional[str]:
|
|
"""
|
|
Retrieves the database path out of the settings file. Returns None,
|
|
if it is not a SQLite3 database.
|
|
|
|
Needed for the backupdb command.
|
|
"""
|
|
from django.conf import settings as django_settings
|
|
from django.db import DEFAULT_DB_ALIAS
|
|
|
|
db_settings = django_settings.DATABASES
|
|
default = db_settings.get(DEFAULT_DB_ALIAS)
|
|
if not default:
|
|
raise DatabaseInSettingsError("Default databases is not configured")
|
|
database_path = default.get("NAME")
|
|
if not database_path:
|
|
raise DatabaseInSettingsError("No path or name specified for default database.")
|
|
if default.get("ENGINE") != "django.db.backends.sqlite3":
|
|
database_path = None
|
|
return database_path
|
|
|
|
|
|
def is_local_installation() -> bool:
|
|
"""
|
|
Returns True if the command is called for a local installation
|
|
|
|
This is the case if manage.py is used, or when the --local-installation flag is set.
|
|
"""
|
|
return (
|
|
True
|
|
if "--local-installation" in sys.argv or "manage.py" in sys.argv[0]
|
|
else False
|
|
)
|
|
|
|
|
|
def is_windows() -> bool:
|
|
"""
|
|
Returns True if the current system is Windows. Returns False otherwise.
|
|
"""
|
|
return sys.platform == "win32"
|