mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-08-11 00:30:12 -04:00
Four of the review's smaller items. E6, the substantive one. tag_uid and tray_uuid are both in the overwrite setattr loop, so a spool matched on one key got the backup's *other* key written onto it. Neither column has a unique constraint (models/spool.py, and no unique index in the migrations), so nothing errors — a duplicate tag simply appears, after which _find_spool's .scalars().first() is non-deterministic and an AMS tag lookup resolves to an arbitrary one of the two spools. The same loop could also clear a tag the user had scanned since the backup was taken, when the backup entry held None. _find_spool now reports which key matched, and _guard_tag_overwrite drops a tag column from the write when the incoming value is empty and the local row has one (the backup predates the scan, so the local tag is the newer fact) or when another local spool already holds it. Announced in the tally the way the archive un-delete case already announces itself, rather than done silently — the spoolTagKept locale key landed with the rest of the i18n block last commit. E5. The Restore button is hidden without github:restore. All three endpoints are gated on it server-side, so the modal 403s on its first preview; offering the button is offering an action that cannot work. Button only — the card stays visible, since configuring backups is a separate permission — and hasPermission returns true with auth off, so a single-user instance is unaffected. E3. models/github_backup.py: the trigger comment said manual/scheduled; this PR added a third value. E4. ha_token_from_env: recommending no change, with the reasoning recorded as a test rather than left in a review thread. It is built only in the settings GET response, is absent from AppSettingsUpdate, and is therefore never a Settings row — it cannot reach a backup, so an allowlist entry would be dead code. Worse, a name-shaped exception to a belt-and-braces denylist is a live hole: an attacker-authored settings/app_settings.json could get a *token*-named row written by choosing that name. 4 unit tests and 1 frontend test that fail against this commit's parent, plus 4 controls: a free tag is still written, an unchanged tag is not reported as kept, an insert is unaffected, and the button still shows with auth disabled.
69 lines
3.3 KiB
Python
69 lines
3.3 KiB
Python
"""GitHub backup configuration and log models."""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, func
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from backend.app.core.database import Base
|
|
|
|
|
|
class GitHubBackupConfig(Base):
|
|
"""Configuration for GitHub profile backup."""
|
|
|
|
__tablename__ = "github_backup_config"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
repository_url: Mapped[str] = mapped_column(String(500)) # Full GitHub URL
|
|
access_token: Mapped[str] = mapped_column(Text) # Personal Access Token
|
|
branch: Mapped[str] = mapped_column(String(100), default="main")
|
|
provider: Mapped[str] = mapped_column(String(30), default="github")
|
|
allow_insecure_http: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
|
|
# Schedule configuration
|
|
schedule_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
schedule_type: Mapped[str] = mapped_column(String(20), default="daily") # hourly/daily/weekly
|
|
schedule_cron: Mapped[str | None] = mapped_column(String(100), nullable=True) # For future cron support
|
|
|
|
# What to backup
|
|
backup_kprofiles: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
backup_cloud_profiles: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
backup_settings: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
backup_spools: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
backup_archives: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
|
|
# Status tracking
|
|
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
last_backup_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
last_backup_status: Mapped[str | None] = mapped_column(String(20), nullable=True) # success/failed/skipped
|
|
last_backup_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
last_backup_commit_sha: Mapped[str | None] = mapped_column(String(40), nullable=True)
|
|
next_scheduled_run: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
|
|
# Timestamps
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
|
|
# Relationships
|
|
logs: Mapped[list["GitHubBackupLog"]] = relationship(back_populates="config", cascade="all, delete-orphan")
|
|
|
|
|
|
class GitHubBackupLog(Base):
|
|
"""Log entry for GitHub backup runs."""
|
|
|
|
__tablename__ = "github_backup_logs"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
config_id: Mapped[int] = mapped_column(ForeignKey("github_backup_config.id", ondelete="CASCADE"))
|
|
|
|
started_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
status: Mapped[str] = mapped_column(String(20)) # running/success/failed/skipped
|
|
trigger: Mapped[str] = mapped_column(String(20)) # manual/scheduled/restore
|
|
|
|
commit_sha: Mapped[str | None] = mapped_column(String(40), nullable=True)
|
|
files_changed: Mapped[int] = mapped_column(Integer, default=0)
|
|
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
# Relationships
|
|
config: Mapped["GitHubBackupConfig"] = relationship(back_populates="logs")
|