Files
2026-06-03 17:33:19 +03:30

202 lines
6.8 KiB
Python

from __future__ import annotations
import json
from datetime import datetime, timezone
from app.extensions import db
def utcnow() -> datetime:
return datetime.now(timezone.utc)
class Mirror(db.Model):
__tablename__ = "mirrors"
id = db.Column(db.Integer, primary_key=True)
category = db.Column(db.String(32), nullable=False, index=True)
name = db.Column(db.String(128), nullable=False)
url = db.Column(db.String(512), nullable=True)
ips = db.Column(db.Text, nullable=True)
priority = db.Column(db.Integer, default=100, nullable=False)
enabled = db.Column(db.Boolean, default=True, nullable=False)
notes = db.Column(db.Text, nullable=True)
meta_json = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=utcnow, onupdate=utcnow, nullable=False)
test_results = db.relationship("TestResult", back_populates="mirror", cascade="all, delete-orphan")
CATEGORIES = ("dns", "apt", "docker", "github", "pip", "npm")
CATEGORY_LABELS = {
"dns": "DNS",
"apt": "مخزن APT",
"docker": "رجیstry داکر",
"github": "گیت‌هاب",
"pip": "pip",
"npm": "npm",
}
def get_ips(self) -> list[str]:
if not self.ips:
return []
return [ip.strip() for ip in self.ips.split(",") if ip.strip()]
def get_meta(self) -> dict:
if not self.meta_json:
return {}
try:
return json.loads(self.meta_json)
except json.JSONDecodeError:
return {}
def set_meta(self, data: dict) -> None:
self.meta_json = json.dumps(data, ensure_ascii=False)
def __repr__(self) -> str:
return f"<Mirror {self.category}:{self.name}>"
class Profile(db.Model):
__tablename__ = "profiles"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), nullable=False, unique=True)
description = db.Column(db.Text, nullable=True)
is_default = db.Column(db.Boolean, default=False, nullable=False)
created_at = db.Column(db.DateTime, default=utcnow, nullable=False)
items = db.relationship("ProfileItem", back_populates="profile", cascade="all, delete-orphan")
apply_logs = db.relationship("ApplyLog", back_populates="profile")
def mirrors_by_category(self) -> dict[str, Mirror]:
result: dict[str, Mirror] = {}
for item in sorted(self.items, key=lambda x: x.order):
if item.mirror and item.mirror.enabled:
result[item.category] = item.mirror
return result
class ProfileItem(db.Model):
__tablename__ = "profile_items"
id = db.Column(db.Integer, primary_key=True)
profile_id = db.Column(db.Integer, db.ForeignKey("profiles.id"), nullable=False)
mirror_id = db.Column(db.Integer, db.ForeignKey("mirrors.id"), nullable=False)
category = db.Column(db.String(32), nullable=False)
order = db.Column(db.Integer, default=0, nullable=False)
profile = db.relationship("Profile", back_populates="items")
mirror = db.relationship("Mirror")
class Setting(db.Model):
__tablename__ = "settings"
key = db.Column(db.String(64), primary_key=True)
value = db.Column(db.Text, nullable=False)
@staticmethod
def get(key: str, default: str | None = None) -> str | None:
row = Setting.query.get(key)
return row.value if row else default
@staticmethod
def set(key: str, value: str) -> None:
row = Setting.query.get(key)
if row:
row.value = value
else:
db.session.add(Setting(key=key, value=value))
db.session.commit()
class ApplyLog(db.Model):
__tablename__ = "apply_logs"
id = db.Column(db.Integer, primary_key=True)
profile_id = db.Column(db.Integer, db.ForeignKey("profiles.id"), nullable=True)
status = db.Column(db.String(32), nullable=False, default="pending")
backup_path = db.Column(db.String(512), nullable=True)
previous_backup_path = db.Column(db.String(512), nullable=True)
details_json = db.Column(db.Text, nullable=True)
rollback_at = db.Column(db.DateTime, nullable=True)
confirmed = db.Column(db.Boolean, default=False, nullable=False)
started_at = db.Column(db.DateTime, default=utcnow, nullable=False)
finished_at = db.Column(db.DateTime, nullable=True)
profile = db.relationship("Profile", back_populates="apply_logs")
def get_details(self) -> dict:
if not self.details_json:
return {}
try:
return json.loads(self.details_json)
except json.JSONDecodeError:
return {}
def set_details(self, data: dict) -> None:
self.details_json = json.dumps(data, ensure_ascii=False)
class TestResult(db.Model):
__tablename__ = "test_results"
id = db.Column(db.Integer, primary_key=True)
mirror_id = db.Column(db.Integer, db.ForeignKey("mirrors.id"), nullable=False)
test_type = db.Column(db.String(64), nullable=False)
success = db.Column(db.Boolean, nullable=False)
latency_ms = db.Column(db.Float, nullable=True)
error = db.Column(db.Text, nullable=True)
tested_at = db.Column(db.DateTime, default=utcnow, nullable=False)
mirror = db.relationship("Mirror", back_populates="test_results")
class SystemState(db.Model):
__tablename__ = "system_state"
id = db.Column(db.Integer, primary_key=True)
initial_backup_path = db.Column(db.String(512), nullable=True)
initial_backup_done = db.Column(db.Boolean, default=False, nullable=False)
current_profile_id = db.Column(db.Integer, db.ForeignKey("profiles.id"), nullable=True)
pending_apply_log_id = db.Column(db.Integer, db.ForeignKey("apply_logs.id"), nullable=True)
updated_at = db.Column(db.DateTime, default=utcnow, onupdate=utcnow, nullable=False)
current_profile = db.relationship("Profile", foreign_keys=[current_profile_id])
pending_apply = db.relationship("ApplyLog", foreign_keys=[pending_apply_log_id])
class AdminUser(db.Model):
__tablename__ = "admin_users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=False)
password_hash = db.Column(db.String(256), nullable=False)
def check_password(self, password: str) -> bool:
from werkzeug.security import check_password_hash
return check_password_hash(self.password_hash, password)
def set_password(self, password: str) -> None:
from werkzeug.security import generate_password_hash
self.password_hash = generate_password_hash(password)
@property
def is_authenticated(self) -> bool:
return True
@property
def is_active(self) -> bool:
return True
@property
def is_anonymous(self) -> bool:
return False
def get_id(self) -> str:
return str(self.id)