Backend (Phase 1-6): - Pydantic schemas for request/response validation - Service layer (SSH Key, Server, Repo, Sync) - API routes with authentication - FastAPI main application with lifespan management - ORM models (SshKey, Server, Repo, SyncLog) Frontend (Phase 7): - Vue 3 + Element Plus + Pinia + Vue Router - API client with Axios and interceptors - State management stores - All page components (Dashboard, Servers, Repos, SyncLogs, SshKeys, Settings) Deployment (Phase 8): - README with quick start guide - Startup script (start.sh) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
228 lines
5.8 KiB
Python
228 lines
5.8 KiB
Python
"""
|
|
Repo Service.
|
|
|
|
Business logic for repository management including:
|
|
- Creating repository records
|
|
- Listing repositories by server
|
|
- Retrieving repository details
|
|
- Updating repository status
|
|
- Deleting repository records
|
|
"""
|
|
import time
|
|
from typing import List, Optional
|
|
from pathlib import Path
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.repo import Repo
|
|
from app.models.server import Server
|
|
from app.config import get_settings
|
|
|
|
|
|
class RepoService:
|
|
"""
|
|
Service for managing repository records.
|
|
|
|
Handles CRUD operations for repositories that are being
|
|
mirrored from Gitea servers.
|
|
"""
|
|
|
|
def __init__(self, db: Session):
|
|
"""
|
|
Initialize the service with a database session.
|
|
|
|
Args:
|
|
db: SQLAlchemy database session
|
|
"""
|
|
self.db = db
|
|
self.settings = get_settings()
|
|
|
|
def create_repo(
|
|
self,
|
|
server_id: int,
|
|
name: str,
|
|
full_name: str,
|
|
clone_url: str,
|
|
local_path: Optional[str] = None
|
|
) -> Repo:
|
|
"""
|
|
Create a new repository record.
|
|
|
|
Args:
|
|
server_id: ID of the server this repo belongs to
|
|
name: Repository name (e.g., "my-repo")
|
|
full_name: Full repository name (e.g., "owner/my-repo")
|
|
clone_url: Git clone URL (typically SSH format)
|
|
local_path: Optional local path for the mirrored repo.
|
|
If not provided, will be generated based on server and repo name.
|
|
|
|
Returns:
|
|
Created Repo model instance
|
|
|
|
Raises:
|
|
ValueError: If server_id is invalid
|
|
"""
|
|
# Verify server exists
|
|
server = self.db.query(Server).filter_by(id=server_id).first()
|
|
if not server:
|
|
raise ValueError(f"Server not found with ID {server_id}")
|
|
|
|
# Generate local_path if not provided
|
|
if local_path is None:
|
|
local_path = str(Path(server.local_path) / name)
|
|
|
|
# Create the repo record
|
|
current_time = int(time.time())
|
|
repo = Repo(
|
|
server_id=server_id,
|
|
name=name,
|
|
full_name=full_name,
|
|
clone_url=clone_url,
|
|
local_path=local_path,
|
|
status="pending",
|
|
last_sync_at=None,
|
|
created_at=current_time
|
|
)
|
|
|
|
self.db.add(repo)
|
|
self.db.commit()
|
|
self.db.refresh(repo)
|
|
|
|
return repo
|
|
|
|
def list_repos(self, server_id: int) -> List[Repo]:
|
|
"""
|
|
List all repositories for a specific server.
|
|
|
|
Args:
|
|
server_id: ID of the server
|
|
|
|
Returns:
|
|
List of Repo model instances for the server, ordered by creation time
|
|
"""
|
|
return self.db.query(Repo).filter_by(
|
|
server_id=server_id
|
|
).order_by(Repo.created_at).all()
|
|
|
|
def get_repo(self, repo_id: int) -> Optional[Repo]:
|
|
"""
|
|
Get a repository by ID.
|
|
|
|
Args:
|
|
repo_id: ID of the repository
|
|
|
|
Returns:
|
|
Repo model instance or None if not found
|
|
"""
|
|
return self.db.query(Repo).filter_by(id=repo_id).first()
|
|
|
|
def update_repo_status(self, repo_id: int, status: str) -> Repo:
|
|
"""
|
|
Update the status of a repository.
|
|
|
|
Common status values:
|
|
- "pending": Initial state, not yet synced
|
|
- "syncing": Currently being synced
|
|
- "success": Last sync was successful
|
|
- "failed": Last sync failed
|
|
|
|
Args:
|
|
repo_id: ID of the repository
|
|
status: New status value
|
|
|
|
Returns:
|
|
Updated Repo model instance
|
|
|
|
Raises:
|
|
ValueError: If repo not found
|
|
"""
|
|
repo = self.get_repo(repo_id)
|
|
if not repo:
|
|
raise ValueError(f"Repo not found with ID {repo_id}")
|
|
|
|
repo.status = status
|
|
|
|
# If status is success, update last_sync_at
|
|
if status == "success":
|
|
repo.last_sync_at = int(time.time())
|
|
|
|
self.db.commit()
|
|
self.db.refresh(repo)
|
|
|
|
return repo
|
|
|
|
def delete_repo(self, repo_id: int) -> bool:
|
|
"""
|
|
Delete a repository.
|
|
|
|
Args:
|
|
repo_id: ID of the repository to delete
|
|
|
|
Returns:
|
|
True if deleted, False if not found
|
|
"""
|
|
repo = self.get_repo(repo_id)
|
|
if not repo:
|
|
return False
|
|
|
|
self.db.delete(repo)
|
|
self.db.commit()
|
|
|
|
return True
|
|
|
|
def get_repo_by_name(self, server_id: int, name: str) -> Optional[Repo]:
|
|
"""
|
|
Get a repository by server and name.
|
|
|
|
Args:
|
|
server_id: ID of the server
|
|
name: Repository name
|
|
|
|
Returns:
|
|
Repo model instance or None if not found
|
|
"""
|
|
return self.db.query(Repo).filter_by(
|
|
server_id=server_id,
|
|
name=name
|
|
).first()
|
|
|
|
def list_all_repos(self) -> List[Repo]:
|
|
"""
|
|
List all repositories across all servers.
|
|
|
|
Returns:
|
|
List of all Repo model instances, ordered by creation time
|
|
"""
|
|
return self.db.query(Repo).order_by(Repo.created_at).all()
|
|
|
|
def update_repo(
|
|
self,
|
|
repo_id: int,
|
|
**kwargs
|
|
) -> Repo:
|
|
"""
|
|
Update a repository's configuration.
|
|
|
|
Args:
|
|
repo_id: ID of the repository to update
|
|
**kwargs: Fields to update (name, full_name, clone_url, local_path, status)
|
|
|
|
Returns:
|
|
Updated Repo model instance
|
|
|
|
Raises:
|
|
ValueError: If repo not found
|
|
"""
|
|
repo = self.get_repo(repo_id)
|
|
if not repo:
|
|
raise ValueError(f"Repo not found with ID {repo_id}")
|
|
|
|
# Update fields
|
|
for key, value in kwargs.items():
|
|
if hasattr(repo, key):
|
|
setattr(repo, key, value)
|
|
|
|
self.db.commit()
|
|
self.db.refresh(repo)
|
|
|
|
return repo
|