Add git repository selection for panel updates

- Add repository URL setting to panel_settings table
- Support for multiple git hosting providers (GitHub, Gitea, GitLab, etc.)
- UI selector for predefined repositories (git.tzim.uz and github.com) or custom URL
- Update service now uses repository from database settings
- API endpoints: GET/PUT /api/updates/repository
- Migration for default repository setting
- Translations for repository selection UI
This commit is contained in:
2025-12-16 20:04:57 +05:00
parent 2541360f51
commit 8dced5479d
5 changed files with 301 additions and 30 deletions

View File

@@ -697,6 +697,47 @@ async def get_version():
return {"version": get_current_version()}
@router.get("/api/updates/repository")
async def get_repository_url(
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("settings.manage"))
):
"""Get configured git repository URL"""
setting = db.query(PanelSettings).filter(PanelSettings.key == "git_repository_url").first()
if setting:
return {"repository_url": setting.value}
return {"repository_url": "https://git.tzim.uz/dilshod/pve_manager"}
@router.put("/api/updates/repository")
async def set_repository_url(
repository_url: str,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("settings.manage"))
):
"""Set git repository URL for updates"""
# Validate URL format
if not repository_url.startswith(('http://', 'https://')):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Repository URL must start with http:// or https://"
)
set_setting(db, "git_repository_url", repository_url, "Git repository URL for updates")
LoggingService.log(
db=db,
level=LoggingService.WARNING,
category=LoggingService.SYSTEM,
action="update_repository_changed",
message=f"Administrator {current_user.username} changed update repository to: {repository_url}",
username=current_user.username,
user_id=current_user.id
)
return {"message": "Repository URL updated", "repository_url": repository_url}
@router.get("/api/updates/check")
async def check_updates(
db: Session = Depends(get_db),

View File

@@ -690,6 +690,26 @@ class I18nService:
"ru": "коммитов позади",
"en": "commits behind"
},
"git_repository": {
"ru": "Git-репозиторий",
"en": "Git Repository"
},
"repository_url": {
"ru": "URL репозитория",
"en": "Repository URL"
},
"select_repository": {
"ru": "Выберите репозиторий",
"en": "Select Repository"
},
"custom_repository": {
"ru": "Другой репозиторий",
"en": "Custom Repository"
},
"repository_saved": {
"ru": "Репозиторий сохранён",
"en": "Repository saved"
},
"panel_info": {
"ru": "Информация о панели",
"en": "Panel Information"

View File

@@ -131,6 +131,53 @@ def configure_git_for_public_access():
return False
def get_repository_url_from_settings():
"""Get configured repository URL from database settings"""
try:
from ..db import SessionLocal
from ..models import PanelSettings
db = SessionLocal()
try:
setting = db.query(PanelSettings).filter(PanelSettings.key == "git_repository_url").first()
if setting and setting.value:
return setting.value
finally:
db.close()
except Exception as e:
logger.warning(f"Could not get repository URL from settings: {e}")
# Default fallback
return "https://git.tzim.uz/dilshod/pve_manager"
def parse_repo_url(url: str) -> tuple:
"""Parse repository URL to extract owner and repo name"""
# Remove .git suffix
url = url.replace(".git", "")
# GitHub: https://github.com/owner/repo
if "github.com" in url:
parts = url.split("github.com/")
if len(parts) == 2:
owner_repo = parts[1].split("/")
if len(owner_repo) >= 2:
return owner_repo[0], owner_repo[1], "github"
# GitLab or other Git hosting (Gitea, etc.)
# https://git.tzim.uz/dilshod/pve_manager
parts = url.split("://")
if len(parts) == 2:
path_parts = parts[1].split("/")
if len(path_parts) >= 3:
domain = path_parts[0]
owner = path_parts[1]
repo = path_parts[2]
return owner, repo, domain
return None, None, None
async def check_for_updates() -> Dict[str, Any]:
"""
Проверить наличие обновлений через GitHub API
@@ -155,37 +202,17 @@ async def check_for_updates() -> Dict[str, Any]:
result["latest_version"] = current_version
return result
# Получаем URL репозитория
repo_url = None
if result["project_mounted"]:
try:
ensure_safe_directory()
remote_result = subprocess.run(
["git", "remote", "get-url", "origin"],
cwd=PROJECT_ROOT,
capture_output=True,
text=True,
timeout=5
)
if remote_result.returncode == 0:
repo_url = remote_result.stdout.strip()
except Exception as e:
logger.warning(f"Could not get git remote URL: {e}")
# Получаем URL репозитория из настроек БД
repo_url = get_repository_url_from_settings()
result["repository_url"] = repo_url
# Парсим owner/repo из URL
owner, repo = None, None
if repo_url:
# https://github.com/owner/repo.git -> owner/repo
if "github.com" in repo_url:
parts = repo_url.replace(".git", "").split("github.com/")
if len(parts) == 2:
owner_repo = parts[1].split("/")
if len(owner_repo) >= 2:
owner, repo = owner_repo[0], owner_repo[1]
owner, repo, host = parse_repo_url(repo_url)
if not owner or not repo:
# Fallback на хардкод для этого проекта
owner, repo = "markmorado", "pvemanager"
result["error"] = "Could not parse repository URL"
result["latest_version"] = current_version
return result
try:
# Подготавливаем заголовки для запроса
@@ -193,14 +220,20 @@ async def check_for_updates() -> Dict[str, Any]:
if GITHUB_TOKEN:
headers["Authorization"] = f"token {GITHUB_TOKEN}"
# Используем GitHub API для получения файлов
# Используем raw URL для получения файлов
async with httpx.AsyncClient(timeout=30.0) as client:
# Пробуем оба варианта: main и master
version_url = None
version_response = None
for branch in ["main", "master"]:
test_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/VERSION"
# Формируем URL в зависимости от хостинга
if host == "github":
test_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/VERSION"
else:
# Для Gitea, GitLab и других (формат: https://domain/owner/repo/raw/branch/file)
test_url = f"https://{host}/{owner}/{repo}/raw/branch/{branch}/VERSION"
test_response = await client.get(test_url, headers=headers)
if test_response.status_code == 200:
@@ -225,7 +258,12 @@ async def check_for_updates() -> Dict[str, Any]:
# Получить CHANGELOG.md (используем ту же ветку, что и для VERSION)
branch = version_url.split("/")[-2] if version_url else "main"
changelog_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/CHANGELOG.md"
if host == "github":
changelog_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/CHANGELOG.md"
else:
changelog_url = f"https://{host}/{owner}/{repo}/raw/branch/{branch}/CHANGELOG.md"
changelog_response = await client.get(changelog_url, headers=headers)
if changelog_response.status_code == 200:
@@ -318,6 +356,19 @@ async def perform_update() -> Dict[str, Any]:
# Настраиваем git для публичного доступа
configure_git_for_public_access()
# Получаем URL репозитория из настроек и устанавливаем remote
try:
repo_url = get_repository_url_from_settings()
subprocess.run(
["git", "remote", "set-url", "origin", repo_url],
cwd=PROJECT_ROOT,
capture_output=True,
timeout=10
)
logger.info(f"Set git remote to: {repo_url}")
except Exception as e:
logger.warning(f"Could not set git remote: {e}")
# Сбросить статус
update_status = {
"is_updating": True,

View File

@@ -323,6 +323,27 @@
<div class="settings-section admin-only-section" id="system-updates-section">
<h2><i class="fa-solid fa-cloud-arrow-down"></i> {{ t('system_updates') }}</h2>
<!-- Repository Selection -->
<div class="form-group">
<label for="git-repository-select">
<i class="fab fa-git-alt"></i> {{ t('git_repository') }}
</label>
<select id="git-repository-select" class="form-control" onchange="handleRepositoryChange()">
<option value="https://git.tzim.uz/dilshod/pve_manager">git.tzim.uz/dilshod/pve_manager (Default)</option>
<option value="https://github.com/markmorado/pvemanager">github.com/markmorado/pvemanager</option>
<option value="custom">{{ t('custom_repository') }}...</option>
</select>
<small class="form-text">{{ t('repository_url') }}</small>
</div>
<div id="custom-repo-input" class="form-group" style="display: none; margin-top: 10px;">
<input type="text" id="custom-repository-url" class="form-control"
placeholder="https://git.example.com/owner/repo">
<button class="btn btn-primary mt-2" onclick="saveCustomRepository()">
<i class="fas fa-save"></i> {{ t('save') }}
</button>
</div>
<div class="update-info-card">
<div class="update-version-row">
<div class="version-item">
@@ -1086,6 +1107,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Load panel settings
loadPanelSettings();
// Load repository setting
loadRepositorySetting();
// Profile form
document.getElementById('profile-form').addEventListener('submit', async function(e) {
@@ -1825,6 +1849,102 @@ async function loadCurrentVersion() {
}
}
// Load current repository setting
async function loadRepositorySetting() {
try {
const response = await fetchWithAuth('/settings/api/updates/repository');
if (response && response.ok) {
const data = await response.json();
const select = document.getElementById('git-repository-select');
const repoUrl = data.repository_url;
// Check if it's one of the predefined options
let found = false;
for (let option of select.options) {
if (option.value === repoUrl) {
select.value = repoUrl;
found = true;
break;
}
}
// If not found, it's a custom repository
if (!found && repoUrl !== 'custom') {
select.value = 'custom';
document.getElementById('custom-repo-input').style.display = 'block';
document.getElementById('custom-repository-url').value = repoUrl;
}
}
} catch (error) {
console.error('Error loading repository setting:', error);
}
}
// Handle repository selection change
async function handleRepositoryChange() {
const select = document.getElementById('git-repository-select');
const customInput = document.getElementById('custom-repo-input');
const value = select.value;
if (value === 'custom') {
customInput.style.display = 'block';
return;
}
customInput.style.display = 'none';
// Save the selected repository
try {
const response = await fetchWithAuth('/settings/api/updates/repository', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(value)
});
if (response && response.ok) {
showNotification(t('repository_saved') || 'Репозиторий сохранён', 'success');
}
} catch (error) {
console.error('Error saving repository:', error);
showNotification(t('error_saving') || 'Ошибка сохранения', 'error');
}
}
// Save custom repository URL
async function saveCustomRepository() {
const input = document.getElementById('custom-repository-url');
const url = input.value.trim();
if (!url) {
showNotification(t('enter_repository_url') || 'Введите URL репозитория', 'error');
return;
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
showNotification('URL должен начинаться с http:// или https://', 'error');
return;
}
try {
const response = await fetchWithAuth('/settings/api/updates/repository', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(url)
});
if (response && response.ok) {
showNotification(t('repository_saved') || 'Репозиторий сохранён', 'success');
}
} catch (error) {
console.error('Error saving custom repository:', error);
showNotification(t('error_saving') || 'Ошибка сохранения', 'error');
}
}
async function checkForUpdates() {
const btn = document.getElementById('check-updates-btn');
const updateBtn = document.getElementById('update-now-btn');

View File

@@ -891,6 +891,37 @@ def migrate_user_ssh_key(conn):
logger.info("✓ User SSH public key column added")
# ==================== Migration 13: Git Repository URL Setting ====================
def migrate_git_repository_setting(conn):
"""Add git repository URL setting to panel_settings"""
logger.info("Adding git repository URL setting...")
# Check if setting already exists
result = conn.execute(text(
"SELECT COUNT(*) FROM panel_settings WHERE key = 'git_repository_url'"
)).scalar()
if result > 0:
logger.info("✓ Git repository URL setting already exists")
return
# Insert default repository setting
conn.execute(text("""
INSERT INTO panel_settings (key, value, description, created_at, updated_at)
VALUES (
'git_repository_url',
'https://git.tzim.uz/dilshod/pve_manager',
'Git repository URL for panel updates',
NOW(),
NOW()
)
"""))
logger.info("✓ Git repository URL setting added")
# ==================== Main Migration Function ====================
def run_all_migrations(engine, db_session=None):
@@ -1002,6 +1033,14 @@ def run_all_migrations(engine, db_session=None):
except Exception as e:
logger.warning(f"User SSH key migration: {e}")
conn.rollback()
# Migration 13: Git Repository URL Setting
try:
migrate_git_repository_setting(conn)
conn.commit()
except Exception as e:
logger.warning(f"Git repository setting migration: {e}")
conn.rollback()
logger.info("=" * 50)
logger.info("✓ All migrations completed successfully")