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:
|