From efba5e489a73996a473b85940a6bdd14bf0a7f2a Mon Sep 17 00:00:00 2001 From: Automated Action Date: Fri, 16 May 2025 12:47:17 +0000 Subject: [PATCH] Fix database permissions issues by using more accessible paths - Updated database configuration to use /app/storage/db directory - Added robust directory and file permission handling - Implemented fallback paths for database location - Enhanced Alembic migration script with retry logic and clear error diagnostics - Updated the README with the new database path information --- README.md | 7 +-- alembic.ini | 2 +- alembic/env.py | 123 ++++++++++++++++++++++++++++++++++++++------- app/core/config.py | 36 +++++++++++-- app/db/init_db.py | 1 + app/db/session.py | 57 +++++++++++++++++++-- 6 files changed, 196 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index c234998..a928e21 100644 --- a/README.md +++ b/README.md @@ -194,9 +194,10 @@ taskmanagerapi/ The application is configured to use SQLite with the following database locations (in order of priority): 1. Path defined in the `DB_PATH` environment variable (if set) -2. `/app/db/db.sqlite` (production environment) -3. `./db/db.sqlite` (local development) -4. `/tmp/taskmanager/db.sqlite` (fallback) +2. `/app/storage/db/db.sqlite` (primary production path) +3. `/app/storage/db.sqlite` (alternate storage path) +4. `./storage/db/db.sqlite` (local development) +5. `/tmp/taskmanager/db.sqlite` (fallback) The application automatically selects the first available and writable location from this list. diff --git a/alembic.ini b/alembic.ini index a4d5407..612fb12 100644 --- a/alembic.ini +++ b/alembic.ini @@ -35,7 +35,7 @@ script_location = alembic # are written from script.py.mako # output_encoding = utf-8 -sqlalchemy.url = sqlite:////app/db/db.sqlite +sqlalchemy.url = sqlite:////app/storage/db/db.sqlite [post_write_hooks] # post_write_hooks defines scripts or Python functions that are run diff --git a/alembic/env.py b/alembic/env.py index 65724c4..0a08c69 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -2,7 +2,6 @@ from logging.config import fileConfig import os import sys import logging -from pathlib import Path import sqlite3 from sqlalchemy import engine_from_config, event @@ -24,7 +23,7 @@ fileConfig(config.config_file_name) # Setup logger logger = logging.getLogger("alembic.env") -# Ensure database directory exists +# Ensure database directory exists and is writable db_url = config.get_main_option("sqlalchemy.url") if db_url.startswith("sqlite:///"): # Extract the path part after sqlite:/// @@ -39,29 +38,115 @@ if db_url.startswith("sqlite:///"): logger.info(f"Database path: {db_path}") logger.info(f"Database directory: {db_dir}") - # Create directory if it doesn't exist - try: - os.makedirs(db_dir, exist_ok=True) - logger.info(f"Ensured database directory exists: {db_dir}") + # Define alternate paths to try if the primary path isn't writable + alternate_paths = [ + "/app/storage/db/db.sqlite", # Primary alternate + os.path.join(os.path.expanduser("~"), "db.sqlite"), # Home directory + "/tmp/taskmanager/db.sqlite", # Fallback to tmp + ] + + # Try to create the main directory first + db_created = False + + for attempt_path in [db_path] + alternate_paths: + if db_created: + break - # Test if we can create the database file try: - # Try to touch the database file - Path(db_path).touch(exist_ok=True) - logger.info(f"Database file is accessible: {db_path}") + # Get directory part + current_dir = os.path.dirname(attempt_path) - # Test direct SQLite connection + # Create directory if it doesn't exist + os.makedirs(current_dir, exist_ok=True) + logger.info(f"Ensured directory exists: {current_dir}") + + # Set directory permissions if possible (755 = rwxr-xr-x) try: - conn = sqlite3.connect(db_path) - conn.execute("SELECT 1") - conn.close() - logger.info("Successfully connected to SQLite database") + import stat + + dir_mode = ( + stat.S_IRWXU + | stat.S_IRGRP + | stat.S_IXGRP + | stat.S_IROTH + | stat.S_IXOTH + ) + os.chmod(current_dir, dir_mode) + logger.info(f"Set directory permissions on {current_dir}") + except Exception as chmod_err: + logger.warning(f"Could not set directory permissions: {chmod_err}") + + # Check if the directory is writable + if not os.access(current_dir, os.W_OK): + logger.warning( + f"Directory {current_dir} exists but is not writable, trying next path" + ) + continue + + # Try to create/access the database file + try: + # Create an empty file if it doesn't exist + if not os.path.exists(attempt_path): + with open(attempt_path, "wb") as f: + f.write(b"") # Write empty content + logger.info(f"Created database file: {attempt_path}") + + # Try to set file permissions (666 = rw-rw-rw-) + try: + file_mode = ( + stat.S_IRUSR + | stat.S_IWUSR + | stat.S_IRGRP + | stat.S_IWGRP + | stat.S_IROTH + | stat.S_IWOTH + ) + os.chmod(attempt_path, file_mode) + logger.info(f"Set file permissions on {attempt_path}") + except Exception as chmod_err: + logger.warning(f"Could not set file permissions: {chmod_err}") + + # Verify file is writable + if not os.access(attempt_path, os.W_OK): + logger.warning( + f"File {attempt_path} exists but is not writable, trying next path" + ) + continue + + # Test direct SQLite connection + try: + conn = sqlite3.connect(attempt_path) + conn.execute("SELECT 1") + conn.close() + logger.info( + f"Successfully connected to SQLite database at {attempt_path}" + ) + + # If this isn't the original path, update the config + if attempt_path != db_path: + new_url = f"sqlite:///{attempt_path}" + logger.info(f"Updating database URL to: {new_url}") + config.set_main_option("sqlalchemy.url", new_url) + db_path = attempt_path # Update the db_path for diagnostics + + db_created = True + break + + except Exception as e: + logger.error(f"Direct SQLite connection failed: {e}") + raise + except Exception as e: - logger.error(f"Direct SQLite connection failed: {e}") + logger.error(f"Could not access database file: {e}") + raise + except Exception as e: - logger.error(f"Could not access database file: {e}") - except Exception as e: - logger.error(f"Could not create database directory: {e}") + logger.error(f"Could not create/access database at {attempt_path}: {e}") + # Continue to next path + + if not db_created: + logger.error("Failed to create database in any of the attempted locations") + # We'll continue anyway to show more diagnostic info in errors # add your model's MetaData object here # for 'autogenerate' support diff --git a/app/core/config.py b/app/core/config.py index ec1c1f2..6149543 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -14,10 +14,11 @@ if DB_PATH: DB_DIR = Path(DB_PATH) print(f"Using database path from environment: {DB_DIR}") else: - # Try production path first, then local directory + # Try production path first, then local directories paths_to_try = [ - Path("/app/db"), # Production path - Path.cwd() / "db", # Local development path + Path("/app/storage/db"), # Primary production path with proper permissions + Path("/app/storage"), # Alternate storage path + Path.cwd() / "storage/db", # Local development path Path("/tmp/taskmanager"), # Fallback path ] @@ -25,11 +26,40 @@ else: DB_DIR = None for path in paths_to_try: try: + # Create directory with parents if it doesn't exist path.mkdir(parents=True, exist_ok=True) + + # Explicitly set permissions if possible (755 = rwxr-xr-x) + try: + import stat + + path_mode = ( + stat.S_IRWXU + | stat.S_IRGRP + | stat.S_IXGRP + | stat.S_IROTH + | stat.S_IXOTH + ) + os.chmod(path, path_mode) + except Exception as chmod_err: + print(f"Warning: Could not set permissions on {path}: {chmod_err}") + # Test if it's writable test_file = path / ".write_test" test_file.touch() + + # Try to write to it + with open(test_file, "w") as f: + f.write("test") + + # Read back to verify + with open(test_file, "r") as f: + content = f.read() + if content != "test": + raise Exception("Write verification failed") + test_file.unlink() # Remove the test file + DB_DIR = path print(f"Using database path: {DB_DIR}") break diff --git a/app/db/init_db.py b/app/db/init_db.py index f1db14a..a091860 100644 --- a/app/db/init_db.py +++ b/app/db/init_db.py @@ -81,6 +81,7 @@ def init_db() -> None: now = datetime.utcnow().isoformat() # Import get_password_hash function here to avoid F823 error from app.core.security import get_password_hash + admin_password_hash = get_password_hash("adminpassword") conn.execute( """ diff --git a/app/db/session.py b/app/db/session.py index 1bcb23c..90fe9a5 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -1,5 +1,6 @@ import time import sqlite3 +import os from typing import Generator from sqlalchemy import create_engine, event @@ -11,14 +12,62 @@ from app.core.config import settings, DB_DIR db_file = DB_DIR / "db.sqlite" print(f"Database file path: {db_file}") -# Try to touch the database file to ensure it exists +# Ensure database directory exists with proper permissions try: + # Make sure directory exists + os.makedirs(DB_DIR, exist_ok=True) + + # Try to set directory permissions (755 = rwxr-xr-x) + try: + import stat + + dir_mode = ( + stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH + ) + os.chmod(DB_DIR, dir_mode) + print(f"Set directory permissions on {DB_DIR}") + except Exception as chmod_err: + print(f"Warning: Could not set directory permissions on {DB_DIR}: {chmod_err}") + + # Check if we can write to the directory + dir_writable = os.access(DB_DIR, os.W_OK) + print(f"Directory {DB_DIR} is writable: {dir_writable}") + + if not dir_writable: + print( + f"WARNING: Directory {DB_DIR} is not writable! Database operations may fail." + ) + + # Try to create or access the database file if not db_file.exists(): # Try to create an empty database file - db_file.touch() - print(f"Database file created or verified: {db_file}") + with open(db_file, "wb") as f: + f.write(b"") # Write empty content + print(f"Database file created: {db_file}") + + # Try to set file permissions (644 = rw-r--r--) + try: + file_mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + os.chmod(db_file, file_mode) + print(f"Set file permissions on {db_file}") + except Exception as chmod_err: + print(f"Warning: Could not set file permissions on {db_file}: {chmod_err}") + + # Check if file is writable + file_writable = os.access(db_file, os.W_OK) + print(f"Database file {db_file} is writable: {file_writable}") + + if not file_writable: + print( + f"WARNING: Database file {db_file} is not writable! Database operations may fail." + ) + + print(f"Database file prepared: {db_file}") except Exception as e: - print(f"Warning: Could not create database file: {e}") + print(f"Warning: Could not prepare database file: {e}") + import traceback + + print(f"Traceback: {traceback.format_exc()}") # Configure SQLite connection with simplified, robust settings engine = create_engine(