Add spool inventory and print archive backup to GitHub backup (#870)

GitHub backup can now optionally include spool inventory (with usage
  history) and print archive metadata as JSON. Both toggles are off by
  default. No binary files (gcode/3MF) are included.
This commit is contained in:
maziggy
2026-04-02 09:59:00 +02:00
parent ad96337587
commit f4df4393be
19 changed files with 393 additions and 67 deletions
+137
View File
@@ -16,9 +16,12 @@ from sqlalchemy import desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.core.database import async_session
from backend.app.models.archive import PrintArchive
from backend.app.models.github_backup import GitHubBackupConfig, GitHubBackupLog
from backend.app.models.printer import Printer
from backend.app.models.settings import Settings
from backend.app.models.spool import Spool
from backend.app.models.spool_usage_history import SpoolUsageHistory
from backend.app.services.bambu_cloud import get_cloud_service
from backend.app.services.printer_manager import printer_manager
@@ -331,6 +334,8 @@ class GitHubBackupService:
"kprofiles": config.backup_kprofiles,
"cloud_profiles": config.backup_cloud_profiles,
"settings": config.backup_settings,
"spools": config.backup_spools,
"archives": config.backup_archives,
},
}
files["backup_metadata.json"] = metadata
@@ -350,6 +355,16 @@ class GitHubBackupService:
self._backup_progress = "Collecting app settings..."
await self._collect_settings(db, files)
# Collect spool inventory
if config.backup_spools:
self._backup_progress = "Collecting spool inventory..."
await self._collect_spools(db, files)
# Collect print archives
if config.backup_archives:
self._backup_progress = "Collecting print archives..."
await self._collect_archives(db, files)
return files
async def _collect_kprofiles(self, db: AsyncSession, files: dict):
@@ -472,6 +487,128 @@ class GitHubBackupService:
"settings": settings_data,
}
async def _collect_spools(self, db: AsyncSession, files: dict):
"""Collect spool inventory data."""
result = await db.execute(select(Spool))
spools = result.scalars().all()
if not spools:
return
spool_list = []
for s in spools:
spool_data = {
"id": s.id,
"material": s.material,
"subtype": s.subtype,
"color_name": s.color_name,
"rgba": s.rgba,
"brand": s.brand,
"label_weight": s.label_weight,
"core_weight": s.core_weight,
"weight_used": s.weight_used,
"weight_locked": s.weight_locked,
"slicer_filament": s.slicer_filament,
"slicer_filament_name": s.slicer_filament_name,
"nozzle_temp_min": s.nozzle_temp_min,
"nozzle_temp_max": s.nozzle_temp_max,
"note": s.note,
"cost_per_kg": s.cost_per_kg,
"tag_uid": s.tag_uid,
"tray_uuid": s.tray_uuid,
"data_origin": s.data_origin,
"tag_type": s.tag_type,
"archived_at": str(s.archived_at) if s.archived_at else None,
"created_at": str(s.created_at) if s.created_at else None,
}
spool_list.append(spool_data)
files["spools/inventory.json"] = {
"version": "1.0",
"spools": spool_list,
}
# Collect usage history
usage_result = await db.execute(select(SpoolUsageHistory))
usages = usage_result.scalars().all()
if usages:
usage_list = []
for u in usages:
usage_list.append(
{
"id": u.id,
"spool_id": u.spool_id,
"printer_id": u.printer_id,
"print_name": u.print_name,
"archive_id": u.archive_id,
"weight_used": u.weight_used,
"percent_used": u.percent_used,
"status": u.status,
"cost": u.cost,
"created_at": str(u.created_at) if u.created_at else None,
}
)
files["spools/usage_history.json"] = {
"version": "1.0",
"usage_history": usage_list,
}
logger.info("Collected %d spools and %d usage records", len(spool_list), len(usages))
async def _collect_archives(self, db: AsyncSession, files: dict):
"""Collect print archive metadata (no binary files)."""
result = await db.execute(select(PrintArchive))
archives = result.scalars().all()
if not archives:
return
archive_list = []
for a in archives:
archive_data = {
"id": a.id,
"printer_id": a.printer_id,
"project_id": a.project_id,
"filename": a.filename,
"file_size": a.file_size,
"content_hash": a.content_hash,
"print_name": a.print_name,
"print_time_seconds": a.print_time_seconds,
"filament_used_grams": a.filament_used_grams,
"filament_type": a.filament_type,
"filament_color": a.filament_color,
"layer_height": a.layer_height,
"total_layers": a.total_layers,
"nozzle_diameter": a.nozzle_diameter,
"bed_temperature": a.bed_temperature,
"nozzle_temperature": a.nozzle_temperature,
"sliced_for_model": a.sliced_for_model,
"status": a.status,
"started_at": str(a.started_at) if a.started_at else None,
"completed_at": str(a.completed_at) if a.completed_at else None,
"makerworld_url": a.makerworld_url,
"designer": a.designer,
"external_url": a.external_url,
"is_favorite": a.is_favorite,
"tags": a.tags,
"notes": a.notes,
"cost": a.cost,
"failure_reason": a.failure_reason,
"quantity": a.quantity,
"energy_kwh": a.energy_kwh,
"energy_cost": a.energy_cost,
"created_at": str(a.created_at) if a.created_at else None,
}
archive_list.append(archive_data)
files["archives/print_history.json"] = {
"version": "1.0",
"archives": archive_list,
}
logger.info("Collected %d print archives", len(archive_list))
async def _push_to_github(self, config: GitHubBackupConfig, files: dict) -> dict:
"""Push files to GitHub using the GitHub API.