mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Update release checker to GitHub API
This commit is contained in:
@@ -5,16 +5,19 @@ import '../models/update_info.dart';
|
||||
import 'build_info_service.dart';
|
||||
|
||||
/// Service for checking if a new app version is available
|
||||
/// Compares current build's commit hash with latest manifest from server
|
||||
/// Compares current build's commit hash with latest GitHub release
|
||||
class UpdateCheckerService {
|
||||
static final UpdateCheckerService _instance = UpdateCheckerService._internal();
|
||||
static final UpdateCheckerService _instance =
|
||||
UpdateCheckerService._internal();
|
||||
factory UpdateCheckerService() => _instance;
|
||||
UpdateCheckerService._internal();
|
||||
|
||||
final BuildInfoService _buildInfoService = BuildInfoService();
|
||||
|
||||
// Manifest URL for the latest unstable build
|
||||
static const String _manifestUrl = 'https://meshcore-sar.dz0ny.dev/unstable/latest/manifest.json';
|
||||
static const String _repoOwner = 'dz0ny';
|
||||
static const String _repoName = 'meshcore-sar';
|
||||
static const String _latestReleaseUrl =
|
||||
'https://api.github.com/repos/$_repoOwner/$_repoName/releases/latest';
|
||||
|
||||
/// Check if an update is available
|
||||
/// Returns UpdateInfo with availability status and download URL if available
|
||||
@@ -25,61 +28,78 @@ class UpdateCheckerService {
|
||||
|
||||
// Skip check for dev builds (local development)
|
||||
if (currentCommitHash == 'dev' || currentCommitHash == 'unknown') {
|
||||
debugPrint('[UpdateChecker] Skipping update check for dev/unknown build');
|
||||
debugPrint(
|
||||
'[UpdateChecker] Skipping update check for dev/unknown build',
|
||||
);
|
||||
return UpdateInfo.noUpdate(currentCommitHash);
|
||||
}
|
||||
|
||||
debugPrint('[UpdateChecker] Current commit hash: $currentCommitHash');
|
||||
debugPrint('[UpdateChecker] Fetching latest manifest from: $_manifestUrl');
|
||||
|
||||
// Fetch manifest from server
|
||||
final response = await http.get(
|
||||
Uri.parse(_manifestUrl),
|
||||
headers: {'Accept': 'application/json'},
|
||||
).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
debugPrint('[UpdateChecker] Manifest fetch timed out');
|
||||
throw Exception('Manifest fetch timed out');
|
||||
},
|
||||
debugPrint(
|
||||
'[UpdateChecker] Fetching latest release from: $_latestReleaseUrl',
|
||||
);
|
||||
|
||||
// Fetch latest release from GitHub
|
||||
final response = await http
|
||||
.get(
|
||||
Uri.parse(_latestReleaseUrl),
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
},
|
||||
)
|
||||
.timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
debugPrint('[UpdateChecker] Latest release fetch timed out');
|
||||
throw Exception('Latest release fetch timed out');
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
debugPrint('[UpdateChecker] Failed to fetch manifest: ${response.statusCode}');
|
||||
debugPrint(
|
||||
'[UpdateChecker] Failed to fetch release: ${response.statusCode}',
|
||||
);
|
||||
return UpdateInfo.noUpdate(currentCommitHash);
|
||||
}
|
||||
|
||||
// Parse manifest JSON
|
||||
final Map<String, dynamic> manifest = json.decode(response.body);
|
||||
final latestCommitHash = manifest['commit'] as String?;
|
||||
final commitShort = manifest['commit_short'] as String?;
|
||||
final buildId = manifest['build_id'] as String?;
|
||||
final timestamp = manifest['timestamp'] as String?;
|
||||
final artifacts = manifest['artifacts'] as List<dynamic>?;
|
||||
// Parse release JSON
|
||||
final Map<String, dynamic> release = json.decode(response.body);
|
||||
final tagName = release['tag_name'] as String?;
|
||||
final targetCommitish = release['target_commitish'] as String?;
|
||||
final publishedAt = release['published_at'] as String?;
|
||||
final assets = release['assets'] as List<dynamic>?;
|
||||
|
||||
if (latestCommitHash == null || commitShort == null) {
|
||||
debugPrint('[UpdateChecker] Invalid manifest: missing commit information');
|
||||
if (tagName == null) {
|
||||
debugPrint('[UpdateChecker] Invalid release: missing tag_name');
|
||||
return UpdateInfo.noUpdate(currentCommitHash);
|
||||
}
|
||||
|
||||
debugPrint('[UpdateChecker] Latest commit hash: $latestCommitHash');
|
||||
debugPrint('[UpdateChecker] Latest commit short: $commitShort');
|
||||
debugPrint('[UpdateChecker] Latest release tag: $tagName');
|
||||
if (targetCommitish != null && targetCommitish.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[UpdateChecker] Release target commitish: $targetCommitish',
|
||||
);
|
||||
}
|
||||
|
||||
// Compare commit hashes
|
||||
// Current hash might be full SHA or short (7 chars)
|
||||
// Latest from manifest is full SHA
|
||||
final isUpdateAvailable = !_compareCommitHashes(currentCommitHash, latestCommitHash);
|
||||
final isUpdateAvailable = await _isUpdateAvailable(
|
||||
currentCommitHash: currentCommitHash,
|
||||
releaseTag: tagName,
|
||||
targetCommitish: targetCommitish,
|
||||
);
|
||||
|
||||
if (!isUpdateAvailable) {
|
||||
debugPrint('[UpdateChecker] No update available (same commit)');
|
||||
debugPrint('[UpdateChecker] No update available');
|
||||
return UpdateInfo.noUpdate(currentCommitHash);
|
||||
}
|
||||
|
||||
// Find Android APK in artifacts
|
||||
final String? apkUrl = _findAndroidApkUrl(artifacts);
|
||||
// Find Android APK in release assets
|
||||
final String? apkUrl = _findAndroidApkUrl(assets);
|
||||
|
||||
if (apkUrl == null) {
|
||||
debugPrint('[UpdateChecker] Update available but no APK found in artifacts');
|
||||
debugPrint(
|
||||
'[UpdateChecker] Update available but no APK found in artifacts',
|
||||
);
|
||||
return UpdateInfo.noUpdate(currentCommitHash);
|
||||
}
|
||||
|
||||
@@ -87,10 +107,10 @@ class UpdateCheckerService {
|
||||
|
||||
return UpdateInfo.available(
|
||||
currentCommitHash: currentCommitHash,
|
||||
latestCommitHash: commitShort,
|
||||
latestCommitHash: _formatLatestVersion(targetCommitish, tagName),
|
||||
downloadUrl: apkUrl,
|
||||
buildId: buildId,
|
||||
timestamp: timestamp,
|
||||
buildId: tagName,
|
||||
timestamp: publishedAt,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[UpdateChecker] Error checking for update: $e');
|
||||
@@ -118,15 +138,94 @@ class UpdateCheckerService {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Find Android APK URL in artifacts list
|
||||
String? _findAndroidApkUrl(List<dynamic>? artifacts) {
|
||||
if (artifacts == null || artifacts.isEmpty) return null;
|
||||
Future<bool> _isUpdateAvailable({
|
||||
required String currentCommitHash,
|
||||
required String releaseTag,
|
||||
required String? targetCommitish,
|
||||
}) async {
|
||||
// Prefer direct SHA compare when release target is a hash.
|
||||
if (_looksLikeSha(targetCommitish)) {
|
||||
return !_compareCommitHashes(currentCommitHash, targetCommitish!);
|
||||
}
|
||||
|
||||
// Look for .apk file in artifacts
|
||||
for (final artifact in artifacts) {
|
||||
if (artifact is String && artifact.toLowerCase().endsWith('.apk')) {
|
||||
// Construct full URL
|
||||
return 'https://meshcore-sar.dz0ny.dev/unstable/latest/$artifact';
|
||||
// Fallback: ask GitHub how current commit compares to the release tag.
|
||||
final compareResult = await _compareWithReleaseTag(
|
||||
currentCommitHash,
|
||||
releaseTag,
|
||||
);
|
||||
if (compareResult != null) {
|
||||
return compareResult;
|
||||
}
|
||||
|
||||
// If we cannot compare, do not force update prompts.
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool?> _compareWithReleaseTag(
|
||||
String currentCommitHash,
|
||||
String releaseTag,
|
||||
) async {
|
||||
try {
|
||||
final compareUrl =
|
||||
'https://api.github.com/repos/$_repoOwner/$_repoName/compare/$currentCommitHash...$releaseTag';
|
||||
final response = await http
|
||||
.get(
|
||||
Uri.parse(compareUrl),
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
},
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
debugPrint(
|
||||
'[UpdateChecker] Compare API failed: ${response.statusCode}',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final Map<String, dynamic> comparison = json.decode(response.body);
|
||||
final status = comparison['status'] as String?;
|
||||
debugPrint('[UpdateChecker] Compare status: $status');
|
||||
|
||||
if (status == 'behind') return true;
|
||||
if (status == 'identical' || status == 'ahead') return false;
|
||||
|
||||
// "diverged" means the release and current commit differ.
|
||||
if (status == 'diverged') return true;
|
||||
} catch (e) {
|
||||
debugPrint('[UpdateChecker] Compare API error: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _looksLikeSha(String? value) {
|
||||
if (value == null) return false;
|
||||
final v = value.trim();
|
||||
if (v.length < 7 || v.length > 40) return false;
|
||||
return RegExp(r'^[a-fA-F0-9]+$').hasMatch(v);
|
||||
}
|
||||
|
||||
String _formatLatestVersion(String? targetCommitish, String tagName) {
|
||||
if (_looksLikeSha(targetCommitish)) {
|
||||
return targetCommitish!.substring(0, 7).toLowerCase();
|
||||
}
|
||||
return tagName;
|
||||
}
|
||||
|
||||
/// Find Android APK URL in release assets list
|
||||
String? _findAndroidApkUrl(List<dynamic>? assets) {
|
||||
if (assets == null || assets.isEmpty) return null;
|
||||
|
||||
for (final asset in assets) {
|
||||
if (asset is! Map<String, dynamic>) continue;
|
||||
final name = (asset['name'] as String?)?.toLowerCase() ?? '';
|
||||
if (!name.endsWith('.apk')) continue;
|
||||
|
||||
final browserDownloadUrl = asset['browser_download_url'] as String?;
|
||||
if (browserDownloadUrl != null && browserDownloadUrl.isNotEmpty) {
|
||||
return browserDownloadUrl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user