diff --git a/spoolman/env.py b/spoolman/env.py index 2330b1d..60042f0 100644 --- a/spoolman/env.py +++ b/spoolman/env.py @@ -2,6 +2,8 @@ import logging import os +import subprocess +import sys from datetime import datetime from enum import Enum from pathlib import Path @@ -11,6 +13,8 @@ from urllib import parse import pkg_resources from platformdirs import user_data_dir +logger = logging.getLogger(__name__) + class DatabaseType(Enum): """The database type.""" @@ -272,3 +276,53 @@ def get_build_date() -> Optional[datetime]: if build_date == "unknown": return None return datetime.fromisoformat(build_date) + + +def can_write_to_data_dir() -> bool: + """Check if the data directory is writable.""" + try: + test_file = get_data_dir().joinpath("test.txt") + test_file.touch() + test_file.unlink() + except: # noqa: E722 + return False + return True + + +def chown_dir(path: str) -> bool: + """Try to chown the data directory to the current user.""" + if os.name == "nt": + return False + + try: + uid = os.getuid() + gid = os.getgid() + subprocess.run(["chown", "-R", f"{uid}:{gid}", path], check=True) # noqa: S603, S607 + except: # noqa: E722 + return False + return True + + +def check_write_permissions() -> None: + """Verify that the data directory is writable, crash with a helpful error message if not.""" + if not can_write_to_data_dir(): + # If windows we can't fix the permissions, so just crash + if os.name == "nt": + logger.error("Data directory is not writable.") + sys.exit(1) + + # Try fixing it by chowning the directory to the current user + logger.warning("Data directory is not writable, trying to fix it...") + if not chown_dir(get_data_dir()) or not can_write_to_data_dir(): + uid = os.getuid() + gid = os.getgid() + + logger.error( + ( + "Data directory is not writable. " + 'Please run "sudo chown -R %s:%s /path/to/spoolman/datadir" on the host OS.' + ), + uid, + gid, + ) + sys.exit(1) diff --git a/spoolman/main.py b/spoolman/main.py index c52ab12..dc7fe12 100644 --- a/spoolman/main.py +++ b/spoolman/main.py @@ -16,11 +16,6 @@ from spoolman.api.v1.router import app as v1_app from spoolman.client import SinglePageApplication from spoolman.database import database -# Define a file logger with log rotation -log_file = env.get_data_dir().joinpath("spoolman.log") -file_handler = TimedRotatingFileHandler(log_file, when="midnight", backupCount=5) -file_handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s", "%Y-%m-%d %H:%M:%S")) - # Define a console logger console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter("%(name)-26s %(levelname)-8s %(message)s")) @@ -28,7 +23,6 @@ console_handler.setFormatter(logging.Formatter("%(name)-26s %(levelname)-8s %(me # Setup the spoolman logger, which all spoolman modules will use root_logger = logging.getLogger() root_logger.setLevel(env.get_logging_level()) -root_logger.addHandler(file_handler) root_logger.addHandler(console_handler) # Get logger instance for this module @@ -60,9 +54,24 @@ if env.is_debug_mode(): ) +def add_file_logging() -> None: + """Add file logging to the root logger.""" + # Define a file logger with log rotation + log_file = env.get_data_dir().joinpath("spoolman.log") + file_handler = TimedRotatingFileHandler(log_file, when="midnight", backupCount=5) + file_handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s", "%Y-%m-%d %H:%M:%S")) + root_logger.addHandler(file_handler) + + @app.on_event("startup") async def startup() -> None: """Run the service's startup sequence.""" + # Check that the data directory is writable + env.check_write_permissions() + + # Don't add file logging until we have verified that the data directory is writable + add_file_logging() + logger.info( "Starting Spoolman v%s (commit: %s) (built: %s)", app.version,