192 lines
6.8 KiB
Python
192 lines
6.8 KiB
Python
# db.py - Database Configuration and Operations (v4.3)
|
|
|
|
import os
|
|
import sqlite3
|
|
from flask import g
|
|
from contextlib import contextmanager
|
|
|
|
from config import DATABASE
|
|
|
|
|
|
def get_db():
|
|
if 'db' not in g:
|
|
g.db = sqlite3.connect(DATABASE)
|
|
g.db.row_factory = sqlite3.Row
|
|
return g.db
|
|
|
|
|
|
def close_db(error=None):
|
|
db = g.pop('db', None)
|
|
if db is not None:
|
|
db.close()
|
|
|
|
|
|
@contextmanager
|
|
def get_db_connection():
|
|
conn = sqlite3.connect(DATABASE)
|
|
conn.row_factory = sqlite3.Row
|
|
try:
|
|
yield conn
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def init_db():
|
|
"""Initialize database tables. Auto-creates parent directory + media storage."""
|
|
# ফোল্ডার না থাকলে নিজে থেকেই তৈরি করবে (Coolify volume mount-এর জন্য জরুরি)
|
|
db_dir = os.path.dirname(os.path.abspath(DATABASE))
|
|
if db_dir and not os.path.exists(db_dir):
|
|
os.makedirs(db_dir, exist_ok=True)
|
|
print(f"📂 Created data directory: {db_dir}")
|
|
|
|
# v4.3: media-storage ফোল্ডার তৈরি
|
|
from config import MEDIA_STORAGE_DIR
|
|
if not os.path.exists(MEDIA_STORAGE_DIR):
|
|
os.makedirs(MEDIA_STORAGE_DIR, exist_ok=True)
|
|
print(f"📂 Created media storage directory: {MEDIA_STORAGE_DIR}")
|
|
|
|
with get_db_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Projects table
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS projects (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
''')
|
|
|
|
# Chapters table
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS chapters (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
project_id INTEGER NOT NULL,
|
|
chapter_number INTEGER NOT NULL,
|
|
title TEXT DEFAULT 'Section',
|
|
voice TEXT DEFAULT 'af_heart',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
|
UNIQUE(project_id, chapter_number)
|
|
)
|
|
''')
|
|
|
|
try:
|
|
cursor.execute('ALTER TABLE chapters ADD COLUMN title TEXT DEFAULT "Section"')
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
|
|
# Markdown blocks table
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS markdown_blocks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
chapter_id INTEGER NOT NULL,
|
|
block_order INTEGER NOT NULL,
|
|
block_type TEXT NOT NULL DEFAULT 'paragraph',
|
|
content TEXT NOT NULL,
|
|
tts_text TEXT,
|
|
audio_data TEXT,
|
|
audio_format TEXT DEFAULT 'mp3',
|
|
transcription TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (chapter_id) REFERENCES chapters(id) ON DELETE CASCADE
|
|
)
|
|
''')
|
|
|
|
# Images table
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS block_images (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
block_id INTEGER NOT NULL,
|
|
image_data TEXT NOT NULL,
|
|
image_format TEXT DEFAULT 'png',
|
|
alt_text TEXT,
|
|
position TEXT DEFAULT 'before',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (block_id) REFERENCES markdown_blocks(id) ON DELETE CASCADE
|
|
)
|
|
''')
|
|
|
|
# PDF Documents table
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS pdf_documents (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
project_id INTEGER,
|
|
filename TEXT NOT NULL,
|
|
page_count INTEGER DEFAULT 0,
|
|
metadata TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL
|
|
)
|
|
''')
|
|
|
|
# v4.2 publishing migrations + v4.3 file-based media path columns
|
|
migrations = [
|
|
('projects', 'is_published', 'INTEGER DEFAULT 0'),
|
|
('projects', 'published_at', 'TIMESTAMP'),
|
|
('projects', 'thumbnail_data', 'TEXT'),
|
|
('projects', 'thumbnail_format', 'TEXT DEFAULT "png"'),
|
|
('projects', 'description', 'TEXT DEFAULT ""'),
|
|
('projects', 'author', 'TEXT DEFAULT ""'),
|
|
('projects', 'category', 'TEXT DEFAULT ""'),
|
|
('projects', 'view_count', 'INTEGER DEFAULT 0'),
|
|
# --- v4.3: file-based media path columns ---
|
|
('projects', 'thumbnail_path', 'TEXT'),
|
|
('markdown_blocks', 'audio_path', 'TEXT'),
|
|
('block_images', 'image_path', 'TEXT'),
|
|
# --- v4.4: thumbnail auto-generated flag (1=auto, 0=user-uploaded) ---
|
|
('projects', 'thumbnail_auto', 'INTEGER DEFAULT 0'),
|
|
]
|
|
|
|
for table, column, definition in migrations:
|
|
try:
|
|
cursor.execute(f'ALTER TABLE {table} ADD COLUMN {column} {definition}')
|
|
print(f" ✅ Added {table}.{column}")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
|
|
conn.commit()
|
|
print(f"✅ Database initialized at {DATABASE} (v4.3)")
|
|
|
|
|
|
def vacuum_db():
|
|
"""ম্যানুয়াল VACUUM (এখন আর অটোমেটিক চলে না)।"""
|
|
with get_db_connection() as conn:
|
|
conn.execute('VACUUM')
|
|
|
|
|
|
def get_db_stats():
|
|
"""
|
|
ডেটাবেসের সাইজ এবং কত% ফাঁকা (reusable free) স্পেস আছে তা রিটার্ন করে।
|
|
SQLite-এর freelist_count আর page_count দিয়ে হিসাব করা হয়।
|
|
"""
|
|
file_size = os.path.getsize(DATABASE) if os.path.exists(DATABASE) else 0
|
|
with get_db_connection() as conn:
|
|
cur = conn.cursor()
|
|
page_count = cur.execute('PRAGMA page_count').fetchone()[0]
|
|
freelist_count = cur.execute('PRAGMA freelist_count').fetchone()[0]
|
|
page_size = cur.execute('PRAGMA page_size').fetchone()[0]
|
|
|
|
free_bytes = freelist_count * page_size
|
|
free_percent = round((freelist_count / page_count) * 100, 1) if page_count > 0 else 0.0
|
|
|
|
return {
|
|
'file_size_bytes': file_size,
|
|
'file_size_mb': round(file_size / (1024 * 1024), 2),
|
|
'page_count': page_count,
|
|
'freelist_count': freelist_count,
|
|
'page_size': page_size,
|
|
'free_bytes': free_bytes,
|
|
'free_mb': round(free_bytes / (1024 * 1024), 2),
|
|
'free_percent': free_percent,
|
|
}
|
|
|
|
|
|
def init_app(app):
|
|
app.teardown_appcontext(close_db)
|
|
init_db()
|
|
|
|
from auth import init_users_table
|
|
init_users_table()
|