Files

139 строки
4.8 KiB
Python

import json
from datetime import datetime
from pathlib import Path
from urllib.parse import urljoin
import paramiko
from backend.android_tools import find_apks
from backend.artifacts_registry import record_apk_artifact
from backend.projects_registry import find_project_by_workspace
from backend.pubspec_tools import read_pubspec_version
from backend.runtime_logs import add_log
def _safe_name(value: str) -> str:
return value.strip().replace('/', '-').replace(' ', '-').lower()
def _apply_pubspec_version(settings: dict, workspace: str) -> dict:
synced = dict(settings)
pubspec = read_pubspec_version(workspace)
if pubspec:
synced['version'] = pubspec.get('version') or synced.get('version')
synced['build'] = pubspec.get('build') or synced.get('build')
return synced
def _latest_json(settings: dict) -> dict:
notes_raw = settings.get('notes') or ''
notes = [line.strip() for line in notes_raw.splitlines() if line.strip()]
if not notes:
notes = ['Немного нового']
version = settings.get('version') or '0.0.1'
build = int(settings.get('build') or 1)
public_base_url = settings.get('public_base_url') or ''
latest_apk_name = settings.get('latest_apk_name') or 'pulse-latest.apk'
versioned_apk_name = settings.get('versioned_apk_name') or f'pulse-{version}+{build}.apk'
return {
'version': version,
'build': build,
'notes': notes,
'apk_url': urljoin(public_base_url.rstrip('/') + '/', latest_apk_name),
'versioned_apk_url': urljoin(public_base_url.rstrip('/') + '/', versioned_apk_name),
}
def _mkdir_p_sftp(sftp, remote_path: str) -> None:
path = remote_path.strip('/')
if not path:
return
current = ''
for part in path.split('/'):
current += '/' + part
try:
sftp.stat(current)
except IOError:
sftp.mkdir(current)
def publish_project_ota(workspace: str) -> dict:
project = find_project_by_workspace(workspace)
if not project:
return {'success': False, 'message': 'Project not found in registry'}
settings = _apply_pubspec_version(project.get('ota') or {}, workspace)
if not settings.get('enabled'):
return {'success': False, 'message': 'OTA publish disabled'}
apks = find_apks(workspace)
if not apks:
return {'success': False, 'message': 'APK not found'}
host = settings.get('sftp_host') or ''
username = settings.get('sftp_username') or ''
password = settings.get('sftp_password') or ''
remote_path = settings.get('remote_path') or ''
if not host or not username or not password or not remote_path:
return {'success': False, 'message': 'SFTP settings are incomplete'}
latest_apk_name = settings.get('latest_apk_name') or 'pulse-latest.apk'
version = settings.get('version') or '0.0.1'
build = int(settings.get('build') or 1)
versioned_apk_name = settings.get('versioned_apk_name') or f'pulse-{version}+{build}.apk'
latest_json_name = settings.get('latest_json_name') or 'latest.json'
latest_json = _latest_json(settings)
tmp_json = Path('/tmp') / f'devconsole-{_safe_name(project.get("name", "project"))}-latest.json'
tmp_json.write_text(json.dumps(latest_json, ensure_ascii=False, indent=4), encoding='utf-8')
apk_path = apks[0]
add_log(f'OTA publish started: {project.get("name")} -> {host}:{remote_path}')
add_log(f'OTA version synced from pubspec: {version}+{build}')
transport = paramiko.Transport((host, int(settings.get('sftp_port') or 22)))
try:
transport.connect(username=username, password=password)
sftp = paramiko.SFTPClient.from_transport(transport)
remote_dir = '/' + remote_path.strip('/')
_mkdir_p_sftp(sftp, remote_dir)
sftp.put(apk_path, f'{remote_dir}/{latest_apk_name}')
sftp.put(apk_path, f'{remote_dir}/{versioned_apk_name}')
sftp.put(tmp_json.as_posix(), f'{remote_dir}/{latest_json_name}')
sftp.close()
finally:
transport.close()
artifact = record_apk_artifact(
workspace=workspace,
apk_path=apk_path,
project_name=project.get('name'),
version=version,
build=build,
ota_result={
'success': True,
'latest_json': latest_json,
'uploaded': [latest_apk_name, versioned_apk_name, latest_json_name],
},
source='ota_publish',
)
add_log(f'APK artifact saved: {artifact.get("apk_name")}')
add_log(f'OTA publish completed: {latest_json.get("apk_url")}')
return {
'success': True,
'apk': apk_path,
'latest_json': latest_json,
'uploaded': [latest_apk_name, versioned_apk_name, latest_json_name],
'artifact': artifact,
'published_at': datetime.utcnow().isoformat() + 'Z',
}