Spaces:
Sleeping
Sleeping
Debug - Database
Browse files- Dockerfile +1 -1
- app/database.py +30 -54
Dockerfile
CHANGED
|
@@ -20,7 +20,7 @@ EXPOSE 7860
|
|
| 20 |
|
| 21 |
# Explicitly create the /data directory where the SQLite DB will live
|
| 22 |
# Running as root by default, so permissions should be okay initially
|
| 23 |
-
RUN mkdir -p /data
|
| 24 |
|
| 25 |
# Command to run the application using uvicorn
|
| 26 |
# It will run the FastAPI app instance created in app/main.py
|
|
|
|
| 20 |
|
| 21 |
# Explicitly create the /data directory where the SQLite DB will live
|
| 22 |
# Running as root by default, so permissions should be okay initially
|
| 23 |
+
# RUN mkdir -p /data
|
| 24 |
|
| 25 |
# Command to run the application using uvicorn
|
| 26 |
# It will run the FastAPI app instance created in app/main.py
|
app/database.py
CHANGED
|
@@ -4,40 +4,32 @@ from databases import Database
|
|
| 4 |
from dotenv import load_dotenv
|
| 5 |
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, text
|
| 6 |
import logging
|
| 7 |
-
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
| 8 |
|
| 9 |
load_dotenv()
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
|
| 12 |
# --- Database URL Configuration ---
|
| 13 |
-
|
| 14 |
-
#
|
|
|
|
|
|
|
| 15 |
raw_db_url = os.getenv("DATABASE_URL", f"sqlite+aiosqlite:///{DEFAULT_DB_PATH}")
|
| 16 |
|
| 17 |
-
# Ensure 'check_same_thread=False' is in the URL for SQLite async connection
|
| 18 |
final_database_url = raw_db_url
|
| 19 |
if raw_db_url.startswith("sqlite+aiosqlite"):
|
| 20 |
-
# Parse the URL
|
| 21 |
parsed_url = urlparse(raw_db_url)
|
| 22 |
-
# Parse existing query parameters into a dictionary
|
| 23 |
query_params = parse_qs(parsed_url.query)
|
| 24 |
-
# Add check_same_thread=False ONLY if it's not already there
|
| 25 |
-
# (in case it's set via DATABASE_URL env var)
|
| 26 |
if 'check_same_thread' not in query_params:
|
| 27 |
-
query_params['check_same_thread'] = ['False']
|
| 28 |
-
# Rebuild the query string
|
| 29 |
new_query = urlencode(query_params, doseq=True)
|
| 30 |
-
# Rebuild the URL using _replace method of the named tuple
|
| 31 |
final_database_url = urlunparse(parsed_url._replace(query=new_query))
|
| 32 |
logger.info(f"Using final async DB URL: {final_database_url}")
|
| 33 |
else:
|
| 34 |
logger.info(f"Using non-SQLite async DB URL: {final_database_url}")
|
| 35 |
|
| 36 |
-
|
| 37 |
-
# --- Async Database Instance (using 'databases' library) ---
|
| 38 |
-
# Pass the *modified* URL. DO NOT pass connect_args separately here.
|
| 39 |
database = Database(final_database_url)
|
| 40 |
-
|
| 41 |
metadata = MetaData()
|
| 42 |
users = Table(
|
| 43 |
"users",
|
|
@@ -47,82 +39,66 @@ users = Table(
|
|
| 47 |
Column("hashed_password", String, nullable=False),
|
| 48 |
)
|
| 49 |
|
| 50 |
-
# --- Synchronous Engine for Initial Table Creation
|
| 51 |
-
# Derive the sync URL (remove +aiosqlite). The query param should remain.
|
| 52 |
sync_db_url = final_database_url.replace("+aiosqlite", "")
|
| 53 |
-
|
| 54 |
-
# SQLAlchemy's create_engine *can* take connect_args, but for check_same_thread,
|
| 55 |
-
# it also understands it from the URL query string. Let's rely on the URL for simplicity.
|
| 56 |
-
# sync_connect_args = {"check_same_thread": False} if sync_db_url.startswith("sqlite") else {} # Keep for reference if other args are needed
|
| 57 |
-
|
| 58 |
logger.info(f"Using synchronous DB URL for initial check/create: {sync_db_url}")
|
| 59 |
-
|
| 60 |
-
engine = create_engine(sync_db_url) # No connect_args needed here if only using check_same_thread
|
| 61 |
|
| 62 |
# --- Directory and Table Creation Logic ---
|
| 63 |
-
# Extract path correctly, ignoring query parameters for os.path operations
|
| 64 |
db_file_path = ""
|
| 65 |
if sync_db_url.startswith("sqlite"):
|
| 66 |
-
# Get the path part after 'sqlite:///' and before '?'
|
| 67 |
path_part = sync_db_url.split("sqlite:///")[-1].split("?")[0]
|
| 68 |
-
#
|
| 69 |
-
|
| 70 |
-
db_file_path = path_part
|
| 71 |
-
else:
|
| 72 |
-
# Handle relative paths if they were somehow configured (though /data should be absolute)
|
| 73 |
-
# This case is less likely with our default /data/app.db
|
| 74 |
-
db_file_path = os.path.abspath(path_part)
|
| 75 |
-
|
| 76 |
|
| 77 |
if db_file_path:
|
| 78 |
-
|
|
|
|
| 79 |
logger.info(f"Ensuring database directory exists: {db_dir}")
|
| 80 |
try:
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
except OSError as e:
|
| 91 |
-
logger.error(f"Error
|
| 92 |
except Exception as e:
|
| 93 |
-
logger.error(f"Unexpected error checking
|
| 94 |
-
|
| 95 |
|
| 96 |
# Now try connecting and creating the table with the sync engine
|
| 97 |
try:
|
| 98 |
logger.info("Attempting to connect with sync engine to check/create table...")
|
| 99 |
with engine.connect() as connection:
|
| 100 |
try:
|
| 101 |
-
# Use text() for literal SQL
|
| 102 |
connection.execute(text("SELECT 1 FROM users LIMIT 1"))
|
| 103 |
logger.info("Users table already exists.")
|
| 104 |
-
except Exception as table_check_exc:
|
| 105 |
logger.warning(f"Users table check failed ({type(table_check_exc).__name__}), attempting creation...")
|
| 106 |
-
# Pass the engine explicitly to create_all
|
| 107 |
metadata.create_all(bind=engine)
|
| 108 |
logger.info("Users table created (or creation attempted).")
|
| 109 |
|
| 110 |
except Exception as e:
|
| 111 |
-
#
|
| 112 |
-
# a fundamental permission issue with /data/app.db in the HF environment.
|
| 113 |
logger.exception(f"CRITICAL: Failed to connect/create database tables using sync engine: {e}")
|
| 114 |
|
| 115 |
|
| 116 |
# --- Async connect/disconnect functions ---
|
| 117 |
async def connect_db():
|
| 118 |
try:
|
| 119 |
-
# The 'database' instance now uses the URL with the query param
|
| 120 |
await database.connect()
|
| 121 |
logger.info(f"Database connection established (async): {final_database_url}")
|
| 122 |
except Exception as e:
|
| 123 |
logger.exception(f"Failed to establish async database connection: {e}")
|
| 124 |
-
|
| 125 |
-
raise # Reraise critical error during startup lifespan
|
| 126 |
|
| 127 |
async def disconnect_db():
|
| 128 |
try:
|
|
|
|
| 4 |
from dotenv import load_dotenv
|
| 5 |
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, text
|
| 6 |
import logging
|
| 7 |
+
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
| 8 |
|
| 9 |
load_dotenv()
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
|
| 12 |
# --- Database URL Configuration ---
|
| 13 |
+
# --- CHANGE THIS LINE: Use path relative to WORKDIR (/code) ---
|
| 14 |
+
# Using an absolute path inside /code is also fine: '/code/app.db'
|
| 15 |
+
DEFAULT_DB_PATH = "/code/app.db" # Store DB in the main workdir
|
| 16 |
+
|
| 17 |
raw_db_url = os.getenv("DATABASE_URL", f"sqlite+aiosqlite:///{DEFAULT_DB_PATH}")
|
| 18 |
|
|
|
|
| 19 |
final_database_url = raw_db_url
|
| 20 |
if raw_db_url.startswith("sqlite+aiosqlite"):
|
|
|
|
| 21 |
parsed_url = urlparse(raw_db_url)
|
|
|
|
| 22 |
query_params = parse_qs(parsed_url.query)
|
|
|
|
|
|
|
| 23 |
if 'check_same_thread' not in query_params:
|
| 24 |
+
query_params['check_same_thread'] = ['False']
|
|
|
|
| 25 |
new_query = urlencode(query_params, doseq=True)
|
|
|
|
| 26 |
final_database_url = urlunparse(parsed_url._replace(query=new_query))
|
| 27 |
logger.info(f"Using final async DB URL: {final_database_url}")
|
| 28 |
else:
|
| 29 |
logger.info(f"Using non-SQLite async DB URL: {final_database_url}")
|
| 30 |
|
| 31 |
+
# --- Async Database Instance ---
|
|
|
|
|
|
|
| 32 |
database = Database(final_database_url)
|
|
|
|
| 33 |
metadata = MetaData()
|
| 34 |
users = Table(
|
| 35 |
"users",
|
|
|
|
| 39 |
Column("hashed_password", String, nullable=False),
|
| 40 |
)
|
| 41 |
|
| 42 |
+
# --- Synchronous Engine for Initial Table Creation ---
|
|
|
|
| 43 |
sync_db_url = final_database_url.replace("+aiosqlite", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
logger.info(f"Using synchronous DB URL for initial check/create: {sync_db_url}")
|
| 45 |
+
engine = create_engine(sync_db_url)
|
|
|
|
| 46 |
|
| 47 |
# --- Directory and Table Creation Logic ---
|
|
|
|
| 48 |
db_file_path = ""
|
| 49 |
if sync_db_url.startswith("sqlite"):
|
|
|
|
| 50 |
path_part = sync_db_url.split("sqlite:///")[-1].split("?")[0]
|
| 51 |
+
# Use os.path.abspath to resolve relative paths based on WORKDIR
|
| 52 |
+
db_file_path = os.path.abspath(path_part) # Should resolve to /code/app.db
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
if db_file_path:
|
| 55 |
+
# --- CHANGE THIS LINE: Check writability of the target directory ---
|
| 56 |
+
db_dir = os.path.dirname(db_file_path) # Should be /code
|
| 57 |
logger.info(f"Ensuring database directory exists: {db_dir}")
|
| 58 |
try:
|
| 59 |
+
# Directory /code should exist because it's the WORKDIR
|
| 60 |
+
# We mainly need to check if it's writable
|
| 61 |
+
if not os.path.exists(db_dir):
|
| 62 |
+
logger.warning(f"Database directory {db_dir} does not exist! Attempting creation (may fail).")
|
| 63 |
+
# This shouldn't really happen for /code unless WORKDIR is wrong
|
| 64 |
+
os.makedirs(db_dir, exist_ok=True)
|
| 65 |
+
|
| 66 |
+
if not os.access(db_dir, os.W_OK):
|
| 67 |
+
# If /code isn't writable, we have a bigger problem
|
| 68 |
+
logger.error(f"Database directory {db_dir} is not writable! Database creation will likely fail.")
|
| 69 |
+
else:
|
| 70 |
+
logger.info(f"Database directory {db_dir} appears writable.")
|
| 71 |
|
| 72 |
except OSError as e:
|
| 73 |
+
logger.error(f"Error accessing database directory {db_dir}: {e}")
|
| 74 |
except Exception as e:
|
| 75 |
+
logger.error(f"Unexpected error checking directory {db_dir}: {e}")
|
|
|
|
| 76 |
|
| 77 |
# Now try connecting and creating the table with the sync engine
|
| 78 |
try:
|
| 79 |
logger.info("Attempting to connect with sync engine to check/create table...")
|
| 80 |
with engine.connect() as connection:
|
| 81 |
try:
|
|
|
|
| 82 |
connection.execute(text("SELECT 1 FROM users LIMIT 1"))
|
| 83 |
logger.info("Users table already exists.")
|
| 84 |
+
except Exception as table_check_exc:
|
| 85 |
logger.warning(f"Users table check failed ({type(table_check_exc).__name__}), attempting creation...")
|
|
|
|
| 86 |
metadata.create_all(bind=engine)
|
| 87 |
logger.info("Users table created (or creation attempted).")
|
| 88 |
|
| 89 |
except Exception as e:
|
| 90 |
+
# Hopefully, this won't happen now if /code is writable
|
|
|
|
| 91 |
logger.exception(f"CRITICAL: Failed to connect/create database tables using sync engine: {e}")
|
| 92 |
|
| 93 |
|
| 94 |
# --- Async connect/disconnect functions ---
|
| 95 |
async def connect_db():
|
| 96 |
try:
|
|
|
|
| 97 |
await database.connect()
|
| 98 |
logger.info(f"Database connection established (async): {final_database_url}")
|
| 99 |
except Exception as e:
|
| 100 |
logger.exception(f"Failed to establish async database connection: {e}")
|
| 101 |
+
raise
|
|
|
|
| 102 |
|
| 103 |
async def disconnect_db():
|
| 104 |
try:
|