Spaces:
Running
π 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', '[email protected]'],
|
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
|