amaye15 commited on
Commit
292f6be
·
1 Parent(s): 4809b28

Debug - Database

Browse files
Files changed (2) hide show
  1. Dockerfile +1 -1
  2. 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 # For URL manipulation
8
 
9
  load_dotenv()
10
  logger = logging.getLogger(__name__)
11
 
12
  # --- Database URL Configuration ---
13
- DEFAULT_DB_PATH = "/data/app.db"
14
- # Start with the base URL from env or default
 
 
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'] # Needs to be a list for urlencode
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 (using SQLAlchemy Core) ---
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
- # Create the engine using the URL which now includes ?check_same_thread=False
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
- # Ensure it's an absolute path if it starts with /
69
- if path_part.startswith('/'):
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
- db_dir = os.path.dirname(db_file_path)
 
79
  logger.info(f"Ensuring database directory exists: {db_dir}")
80
  try:
81
- if db_dir and not os.path.exists(db_dir):
82
- os.makedirs(db_dir, exist_ok=True)
83
- logger.info(f"Created database directory: {db_dir}")
84
- # Add a check for writability after ensuring directory exists
85
- if db_dir and not os.access(db_dir, os.W_OK):
86
- logger.error(f"Database directory {db_dir} is not writable!")
87
- # Also check if the file itself can be created/opened (might fail here if dir is writable but file isn't)
88
- # This check is implicitly done by engine.connect() below
 
 
 
 
89
 
90
  except OSError as e:
91
- logger.error(f"Error creating or accessing database directory {db_dir}: {e}")
92
  except Exception as e:
93
- logger.error(f"Unexpected error checking/creating DB directory {db_dir}: {e}")
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: # Catch specific DB errors if possible
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
- # This OperationalError "unable to open database file" might still indicate
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
- # If the sync engine failed earlier due to permissions, this might fail too.
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: