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()}
|
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")
|
@router.get("/api/updates/check")
|
||||||
async def check_updates(
|
async def check_updates(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
|||||||
@@ -690,6 +690,26 @@ class I18nService:
|
|||||||
"ru": "коммитов позади",
|
"ru": "коммитов позади",
|
||||||
"en": "commits behind"
|
"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": {
|
"panel_info": {
|
||||||
"ru": "Информация о панели",
|
"ru": "Информация о панели",
|
||||||
"en": "Panel Information"
|
"en": "Panel Information"
|
||||||
|
|||||||
@@ -131,6 +131,53 @@ def configure_git_for_public_access():
|
|||||||
return False
|
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]:
|
async def check_for_updates() -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Проверить наличие обновлений через GitHub API
|
Проверить наличие обновлений через GitHub API
|
||||||
@@ -155,37 +202,17 @@ async def check_for_updates() -> Dict[str, Any]:
|
|||||||
result["latest_version"] = current_version
|
result["latest_version"] = current_version
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Получаем URL репозитория
|
# Получаем URL репозитория из настроек БД
|
||||||
repo_url = None
|
repo_url = get_repository_url_from_settings()
|
||||||
if result["project_mounted"]:
|
result["repository_url"] = repo_url
|
||||||
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}")
|
|
||||||
|
|
||||||
# Парсим owner/repo из URL
|
# Парсим owner/repo из URL
|
||||||
owner, repo = None, None
|
owner, repo, host = parse_repo_url(repo_url)
|
||||||
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]
|
|
||||||
|
|
||||||
if not owner or not repo:
|
if not owner or not repo:
|
||||||
# Fallback на хардкод для этого проекта
|
result["error"] = "Could not parse repository URL"
|
||||||
owner, repo = "markmorado", "pvemanager"
|
result["latest_version"] = current_version
|
||||||
|
return result
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Подготавливаем заголовки для запроса
|
# Подготавливаем заголовки для запроса
|
||||||
@@ -193,14 +220,20 @@ async def check_for_updates() -> Dict[str, Any]:
|
|||||||
if GITHUB_TOKEN:
|
if GITHUB_TOKEN:
|
||||||
headers["Authorization"] = f"token {GITHUB_TOKEN}"
|
headers["Authorization"] = f"token {GITHUB_TOKEN}"
|
||||||
|
|
||||||
# Используем GitHub API для получения файлов
|
# Используем raw URL для получения файлов
|
||||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
# Пробуем оба варианта: main и master
|
# Пробуем оба варианта: main и master
|
||||||
version_url = None
|
version_url = None
|
||||||
version_response = None
|
version_response = None
|
||||||
|
|
||||||
for branch in ["main", "master"]:
|
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)
|
test_response = await client.get(test_url, headers=headers)
|
||||||
|
|
||||||
if test_response.status_code == 200:
|
if test_response.status_code == 200:
|
||||||
@@ -225,7 +258,12 @@ async def check_for_updates() -> Dict[str, Any]:
|
|||||||
|
|
||||||
# Получить CHANGELOG.md (используем ту же ветку, что и для VERSION)
|
# Получить CHANGELOG.md (используем ту же ветку, что и для VERSION)
|
||||||
branch = version_url.split("/")[-2] if version_url else "main"
|
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)
|
changelog_response = await client.get(changelog_url, headers=headers)
|
||||||
|
|
||||||
if changelog_response.status_code == 200:
|
if changelog_response.status_code == 200:
|
||||||
@@ -318,6 +356,19 @@ async def perform_update() -> Dict[str, Any]:
|
|||||||
# Настраиваем git для публичного доступа
|
# Настраиваем git для публичного доступа
|
||||||
configure_git_for_public_access()
|
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 = {
|
update_status = {
|
||||||
"is_updating": True,
|
"is_updating": True,
|
||||||
|
|||||||
@@ -323,6 +323,27 @@
|
|||||||
<div class="settings-section admin-only-section" id="system-updates-section">
|
<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>
|
<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-info-card">
|
||||||
<div class="update-version-row">
|
<div class="update-version-row">
|
||||||
<div class="version-item">
|
<div class="version-item">
|
||||||
@@ -1086,6 +1107,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Load panel settings
|
// Load panel settings
|
||||||
loadPanelSettings();
|
loadPanelSettings();
|
||||||
|
|
||||||
|
// Load repository setting
|
||||||
|
loadRepositorySetting();
|
||||||
|
|
||||||
// Profile form
|
// Profile form
|
||||||
document.getElementById('profile-form').addEventListener('submit', async function(e) {
|
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() {
|
async function checkForUpdates() {
|
||||||
const btn = document.getElementById('check-updates-btn');
|
const btn = document.getElementById('check-updates-btn');
|
||||||
const updateBtn = document.getElementById('update-now-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")
|
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 ====================
|
# ==================== Main Migration Function ====================
|
||||||
|
|
||||||
def run_all_migrations(engine, db_session=None):
|
def run_all_migrations(engine, db_session=None):
|
||||||
@@ -1002,6 +1033,14 @@ def run_all_migrations(engine, db_session=None):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"User SSH key migration: {e}")
|
logger.warning(f"User SSH key migration: {e}")
|
||||||
conn.rollback()
|
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("=" * 50)
|
||||||
logger.info("✓ All migrations completed successfully")
|
logger.info("✓ All migrations completed successfully")
|
||||||
|
|||||||
Reference in New Issue
Block a user