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:
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user