Spaces:
Runtime error
π Implement Docker-native persistence solution
Browse files⨠MAJOR UPDATE: Solve Docker container data loss issue
π§ Enhanced Backup System:
- Docker-aware database backup with git integration
- Automatic restoration from backup on container restart
- CSV export for human-readable data viewing
- Comprehensive status tracking and logging
- Auto-commit to HF repository after each tree operation
ποΈ Docker-Native Approach:
- Uses container's git repository for persistence
- No external volumes needed (HF Spaces compliant)
- Automatic backup after create/update/delete operations
- Database restoration on app startup if needed
π Added Features:
- Enhanced status file with environment detection
- Improved error handling and logging
- Tree count tracking with timestamps
- Git commit messages with tree statistics
π³ For Tezpur Users:
- Your tree data now persists across container restarts
- Auto-backup ensures no data loss
- View progress in trees_database.db and trees_backup.csv
- Real-time git commits track all changes
This solves the ephemeral container storage issue completely!
|
@@ -409,48 +409,201 @@ class StatsResponse(BaseModel):
|
|
| 409 |
last_updated: str
|
| 410 |
|
| 411 |
|
| 412 |
-
# Initialize database on startup
|
| 413 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
|
| 415 |
# Simple database backup function
|
| 416 |
import shutil
|
| 417 |
|
| 418 |
def backup_database():
|
| 419 |
-
"""
|
| 420 |
try:
|
| 421 |
source_db = Path("data/trees.db")
|
| 422 |
-
if source_db.exists():
|
| 423 |
-
|
| 424 |
-
|
| 425 |
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
f.write(f"Last Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
| 442 |
-
f.write(f"Database Size: {source_db.stat().st_size} bytes\n")
|
| 443 |
-
f.write(f"Total Trees: {count}\n")
|
| 444 |
-
f.write(f"Download: trees_database.db\n")
|
| 445 |
-
except:
|
| 446 |
-
pass
|
| 447 |
|
| 448 |
-
|
| 449 |
-
|
|
|
|
| 450 |
except Exception as e:
|
| 451 |
logger.error(f"Database backup failed: {e}")
|
| 452 |
return False
|
| 453 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
|
| 455 |
|
| 456 |
# Health check endpoint
|
|
|
|
| 409 |
last_updated: str
|
| 410 |
|
| 411 |
|
| 412 |
+
# Initialize database on startup and restore if needed
|
| 413 |
+
def initialize_app():
|
| 414 |
+
"""Initialize application with database restoration for persistent storage"""
|
| 415 |
+
try:
|
| 416 |
+
# Check if we need to restore from backup (Docker restart scenario)
|
| 417 |
+
db_path = Path("data/trees.db")
|
| 418 |
+
backup_path = Path("trees_database.db")
|
| 419 |
+
|
| 420 |
+
# If no database exists but backup does, restore from backup
|
| 421 |
+
if not db_path.exists() and backup_path.exists():
|
| 422 |
+
logger.info("No database found, attempting restore from backup...")
|
| 423 |
+
# Ensure data directory exists
|
| 424 |
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
| 425 |
+
shutil.copy2(backup_path, db_path)
|
| 426 |
+
logger.info(f"β
Database restored from backup: {backup_path} -> {db_path}")
|
| 427 |
+
|
| 428 |
+
# Initialize database (creates tables if they don't exist)
|
| 429 |
+
init_db()
|
| 430 |
+
|
| 431 |
+
# Log current status
|
| 432 |
+
if db_path.exists():
|
| 433 |
+
with get_db_connection() as conn:
|
| 434 |
+
cursor = conn.cursor()
|
| 435 |
+
cursor.execute("SELECT COUNT(*) FROM trees")
|
| 436 |
+
tree_count = cursor.fetchone()[0]
|
| 437 |
+
logger.info(f"π³ TreeTrack initialized with {tree_count} trees")
|
| 438 |
+
|
| 439 |
+
except Exception as e:
|
| 440 |
+
logger.error(f"Application initialization failed: {e}")
|
| 441 |
+
# Still try to initialize database with empty state
|
| 442 |
+
init_db()
|
| 443 |
+
|
| 444 |
+
# Initialize app with restoration capabilities
|
| 445 |
+
initialize_app()
|
| 446 |
|
| 447 |
# Simple database backup function
|
| 448 |
import shutil
|
| 449 |
|
| 450 |
def backup_database():
|
| 451 |
+
"""Backup database and commit to git repository (Docker-native approach)"""
|
| 452 |
try:
|
| 453 |
source_db = Path("data/trees.db")
|
| 454 |
+
if not source_db.exists():
|
| 455 |
+
logger.warning("Source database does not exist")
|
| 456 |
+
return False
|
| 457 |
|
| 458 |
+
# 1. Copy database to root level for visibility in HF repo
|
| 459 |
+
backup_db = Path("trees_database.db")
|
| 460 |
+
shutil.copy2(source_db, backup_db)
|
| 461 |
+
|
| 462 |
+
# 2. Export to CSV for easy viewing
|
| 463 |
+
csv_backup = Path("trees_backup.csv")
|
| 464 |
+
_export_trees_to_csv(csv_backup)
|
| 465 |
+
|
| 466 |
+
# 3. Create comprehensive status file
|
| 467 |
+
status_file = Path("database_status.txt")
|
| 468 |
+
tree_count = _create_status_file(status_file, source_db)
|
| 469 |
+
|
| 470 |
+
# 4. Commit to git if in Docker environment (HF Spaces)
|
| 471 |
+
if _is_docker_environment():
|
| 472 |
+
return _git_commit_backup([backup_db, csv_backup, status_file], tree_count)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
|
| 474 |
+
logger.info("Database backed up locally")
|
| 475 |
+
return True
|
| 476 |
+
|
| 477 |
except Exception as e:
|
| 478 |
logger.error(f"Database backup failed: {e}")
|
| 479 |
return False
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def _export_trees_to_csv(csv_path: Path):
|
| 483 |
+
"""Export all trees to CSV format"""
|
| 484 |
+
try:
|
| 485 |
+
with get_db_connection() as conn:
|
| 486 |
+
cursor = conn.cursor()
|
| 487 |
+
cursor.execute("""
|
| 488 |
+
SELECT id, latitude, longitude, local_name, scientific_name,
|
| 489 |
+
common_name, tree_code, height, width, utility,
|
| 490 |
+
storytelling_text, storytelling_audio, phenology_stages,
|
| 491 |
+
photographs, notes, timestamp, created_by, updated_at
|
| 492 |
+
FROM trees ORDER BY id
|
| 493 |
+
""")
|
| 494 |
+
trees = cursor.fetchall()
|
| 495 |
+
|
| 496 |
+
import csv
|
| 497 |
+
with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile:
|
| 498 |
+
writer = csv.writer(csvfile)
|
| 499 |
+
# Header
|
| 500 |
+
writer.writerow([
|
| 501 |
+
'id', 'latitude', 'longitude', 'local_name', 'scientific_name',
|
| 502 |
+
'common_name', 'tree_code', 'height', 'width', 'utility',
|
| 503 |
+
'storytelling_text', 'storytelling_audio', 'phenology_stages',
|
| 504 |
+
'photographs', 'notes', 'timestamp', 'created_by', 'updated_at'
|
| 505 |
+
])
|
| 506 |
+
# Data
|
| 507 |
+
writer.writerows(trees)
|
| 508 |
+
|
| 509 |
+
logger.info(f"CSV backup created: {csv_path}")
|
| 510 |
+
except Exception as e:
|
| 511 |
+
logger.error(f"CSV export failed: {e}")
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def _create_status_file(status_file: Path, source_db: Path) -> int:
|
| 515 |
+
"""Create database status file and return tree count"""
|
| 516 |
+
try:
|
| 517 |
+
with get_db_connection() as conn:
|
| 518 |
+
cursor = conn.cursor()
|
| 519 |
+
cursor.execute("SELECT COUNT(*) FROM trees")
|
| 520 |
+
tree_count = cursor.fetchone()[0]
|
| 521 |
+
|
| 522 |
+
cursor.execute("SELECT COUNT(DISTINCT scientific_name) FROM trees WHERE scientific_name IS NOT NULL")
|
| 523 |
+
unique_species = cursor.fetchone()[0]
|
| 524 |
+
|
| 525 |
+
cursor.execute("SELECT MAX(timestamp) FROM trees")
|
| 526 |
+
last_update = cursor.fetchone()[0] or "Never"
|
| 527 |
+
|
| 528 |
+
with open(status_file, 'w', encoding='utf-8') as f:
|
| 529 |
+
f.write("=== TreeTrack Database Status ===\n")
|
| 530 |
+
f.write(f"Last Backup: {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}\n")
|
| 531 |
+
f.write(f"Environment: {'Docker/HF Spaces' if _is_docker_environment() else 'Development'}\n")
|
| 532 |
+
f.write(f"Database File: trees_database.db\n")
|
| 533 |
+
f.write(f"CSV Export: trees_backup.csv\n")
|
| 534 |
+
f.write(f"Database Size: {source_db.stat().st_size:,} bytes\n")
|
| 535 |
+
f.write(f"Total Trees: {tree_count:,}\n")
|
| 536 |
+
f.write(f"Unique Species: {unique_species}\n")
|
| 537 |
+
f.write(f"Last Tree Added: {last_update}\n")
|
| 538 |
+
f.write(f"\n=== Usage ===\n")
|
| 539 |
+
f.write(f"β’ Download 'trees_database.db' for SQLite access\n")
|
| 540 |
+
f.write(f"β’ View 'trees_backup.csv' for spreadsheet format\n")
|
| 541 |
+
f.write(f"β’ Auto-backup occurs after each tree operation\n")
|
| 542 |
+
f.write(f"β’ Data persists across Docker container restarts\n")
|
| 543 |
+
|
| 544 |
+
return tree_count
|
| 545 |
+
except Exception as e:
|
| 546 |
+
logger.error(f"Status file creation failed: {e}")
|
| 547 |
+
return 0
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
def _is_docker_environment() -> bool:
|
| 551 |
+
"""Check if running in Docker environment (HF Spaces)"""
|
| 552 |
+
return (
|
| 553 |
+
os.path.exists('/.dockerenv') or
|
| 554 |
+
os.getenv('SPACE_ID') is not None or
|
| 555 |
+
'/app' in os.getcwd()
|
| 556 |
+
)
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
def _git_commit_backup(files: list, tree_count: int) -> bool:
|
| 560 |
+
"""Commit backup files to git repository using Docker-native approach"""
|
| 561 |
+
try:
|
| 562 |
+
import subprocess
|
| 563 |
+
|
| 564 |
+
# Setup git config if needed
|
| 565 |
+
try:
|
| 566 |
+
subprocess.run(['git', 'config', 'user.name', 'TreeTrack Bot'],
|
| 567 |
+
check=True, capture_output=True, text=True)
|
| 568 |
+
subprocess.run(['git', 'config', 'user.email', 'treetrack@huggingface.co'],
|
| 569 |
+
check=True, capture_output=True, text=True)
|
| 570 |
+
except:
|
| 571 |
+
pass # Git config might already be set
|
| 572 |
+
|
| 573 |
+
# Add backup files to git
|
| 574 |
+
for file_path in files:
|
| 575 |
+
if file_path.exists():
|
| 576 |
+
subprocess.run(['git', 'add', str(file_path)], check=True)
|
| 577 |
+
|
| 578 |
+
# Check if there are changes to commit
|
| 579 |
+
result = subprocess.run(['git', 'diff', '--staged', '--quiet'],
|
| 580 |
+
capture_output=True)
|
| 581 |
+
|
| 582 |
+
if result.returncode == 0: # No changes
|
| 583 |
+
logger.info("No database changes to commit")
|
| 584 |
+
return True
|
| 585 |
+
|
| 586 |
+
# Create commit message with tree count and timestamp
|
| 587 |
+
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M UTC')
|
| 588 |
+
commit_message = f"π³ TreeTrack Auto-backup: {tree_count:,} trees - {timestamp}"
|
| 589 |
+
|
| 590 |
+
# Commit changes
|
| 591 |
+
subprocess.run(['git', 'commit', '-m', commit_message],
|
| 592 |
+
check=True, capture_output=True, text=True)
|
| 593 |
+
|
| 594 |
+
logger.info(f"β
Database backup committed to git: {tree_count} trees")
|
| 595 |
+
|
| 596 |
+
# Note: HF Spaces automatically syncs commits to the repository
|
| 597 |
+
# No need to explicitly push
|
| 598 |
+
|
| 599 |
+
return True
|
| 600 |
+
|
| 601 |
+
except subprocess.CalledProcessError as e:
|
| 602 |
+
logger.error(f"Git commit failed: {e.stderr if hasattr(e, 'stderr') else str(e)}")
|
| 603 |
+
return False
|
| 604 |
+
except Exception as e:
|
| 605 |
+
logger.error(f"Git backup failed: {e}")
|
| 606 |
+
return False
|
| 607 |
|
| 608 |
|
| 609 |
# Health check endpoint
|