#!/usr/bin/env python3 """ Migration 002: Add storage quota fields to User and Organization models. Adds the following fields: - User: private_quota_bytes, public_quota_bytes, private_used_bytes, public_used_bytes - Organization: private_quota_bytes, public_quota_bytes, private_used_bytes, public_used_bytes """ import sys import os # Add src to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src")) # Add db_migrations to path (for _migration_utils) sys.path.insert(0, os.path.dirname(__file__)) from kohakuhub.db import db from kohakuhub.config import cfg from _migration_utils import should_skip_due_to_future_migrations, check_column_exists MIGRATION_NUMBER = 2 def is_applied(db, cfg): """Check if THIS migration has been applied. Returns True if User.private_quota_bytes column exists. """ return check_column_exists(db, cfg, "user", "private_quota_bytes") def check_migration_needed(): """Check if this migration needs to run by checking if columns exist.""" cursor = db.cursor() if cfg.app.db_backend == "postgres": # Check if User.private_quota_bytes exists cursor.execute( """ SELECT column_name FROM information_schema.columns WHERE table_name='user' AND column_name='private_quota_bytes' """ ) return cursor.fetchone() is None else: # SQLite: Check via PRAGMA cursor.execute("PRAGMA table_info(user)") columns = [row[1] for row in cursor.fetchall()] return "private_quota_bytes" not in columns def migrate_sqlite(): """Migrate SQLite database. Note: This function runs inside a transaction (db.atomic()). Do NOT call db.commit() or db.rollback() inside this function. """ cursor = db.cursor() # User table for column, sql in [ ( "private_quota_bytes", "ALTER TABLE user ADD COLUMN private_quota_bytes INTEGER DEFAULT NULL", ), ( "public_quota_bytes", "ALTER TABLE user ADD COLUMN public_quota_bytes INTEGER DEFAULT NULL", ), ( "private_used_bytes", "ALTER TABLE user ADD COLUMN private_used_bytes INTEGER DEFAULT 0", ), ( "public_used_bytes", "ALTER TABLE user ADD COLUMN public_used_bytes INTEGER DEFAULT 0", ), ]: try: cursor.execute(sql) print(f" ✓ Added User.{column}") except Exception as e: if "duplicate column" in str(e).lower(): print(f" - User.{column} already exists") else: raise # Organization table for column, sql in [ ( "private_quota_bytes", "ALTER TABLE organization ADD COLUMN private_quota_bytes INTEGER DEFAULT NULL", ), ( "public_quota_bytes", "ALTER TABLE organization ADD COLUMN public_quota_bytes INTEGER DEFAULT NULL", ), ( "private_used_bytes", "ALTER TABLE organization ADD COLUMN private_used_bytes INTEGER DEFAULT 0", ), ( "public_used_bytes", "ALTER TABLE organization ADD COLUMN public_used_bytes INTEGER DEFAULT 0", ), ]: try: cursor.execute(sql) print(f" ✓ Added Organization.{column}") except Exception as e: if "duplicate column" in str(e).lower(): print(f" - Organization.{column} already exists") else: raise def migrate_postgres(): """Migrate PostgreSQL database. Note: This function runs inside a transaction (db.atomic()). Do NOT call db.commit() or db.rollback() inside this function. """ cursor = db.cursor() # User table for column, sql in [ ( "private_quota_bytes", 'ALTER TABLE "user" ADD COLUMN private_quota_bytes BIGINT DEFAULT NULL', ), ( "public_quota_bytes", 'ALTER TABLE "user" ADD COLUMN public_quota_bytes BIGINT DEFAULT NULL', ), ( "private_used_bytes", 'ALTER TABLE "user" ADD COLUMN private_used_bytes BIGINT DEFAULT 0', ), ( "public_used_bytes", 'ALTER TABLE "user" ADD COLUMN public_used_bytes BIGINT DEFAULT 0', ), ]: try: cursor.execute(sql) print(f" ✓ Added User.{column}") except Exception as e: if "already exists" in str(e).lower(): print(f" - User.{column} already exists") else: raise # Organization table for column, sql in [ ( "private_quota_bytes", "ALTER TABLE organization ADD COLUMN private_quota_bytes BIGINT DEFAULT NULL", ), ( "public_quota_bytes", "ALTER TABLE organization ADD COLUMN public_quota_bytes BIGINT DEFAULT NULL", ), ( "private_used_bytes", "ALTER TABLE organization ADD COLUMN private_used_bytes BIGINT DEFAULT 0", ), ( "public_used_bytes", "ALTER TABLE organization ADD COLUMN public_used_bytes BIGINT DEFAULT 0", ), ]: try: cursor.execute(sql) print(f" ✓ Added Organization.{column}") except Exception as e: if "already exists" in str(e).lower(): print(f" - Organization.{column} already exists") else: raise def run(): """Run this migration. IMPORTANT: Do NOT call db.close() in finally block! The db connection is managed by run_migrations.py and should stay open across all migrations to avoid stdout/stderr closure issues on Windows. """ db.connect(reuse_if_open=True) try: # Pre-flight checks (outside transaction for performance) if should_skip_due_to_future_migrations(MIGRATION_NUMBER, db, cfg): print("Migration 002: Skipped (superseded by future migration)") return True if not check_migration_needed(): print("Migration 002: Already applied (columns exist)") return True print("Migration 002: Adding User/Organization quota fields...") # Run migration in a transaction - will auto-rollback on exception with db.atomic(): if cfg.app.db_backend == "postgres": migrate_postgres() else: migrate_sqlite() print("Migration 002: ✓ Completed") return True except Exception as e: # Transaction automatically rolled back if we reach here print(f"Migration 002: ✗ Failed - {e}") print(" All changes have been rolled back") import traceback traceback.print_exc() return False # NOTE: No finally block - db connection stays open if __name__ == "__main__": success = run() sys.exit(0 if success else 1)