kasl/libs/update.rs
1//! Self-updating functionality for the kasl application.
2//!
3//! Provides comprehensive auto-update capabilities that enable the application
4//! to automatically check for, download, and install newer versions from GitHub releases.
5//!
6//! ## Features
7//!
8//! - **Safety Mechanisms**: Automatic backup, rollback capability, atomic operations
9//! - **Platform Detection**: Architecture awareness, OS detection, ABI compatibility
10//! - **Network Resilience**: Throttled checks, graceful degradation, retry logic
11//! - **Version Management**: Semantic versioning, GitHub API integration
12//! - **Platform Support**: Windows, macOS Intel/Apple Silicon, Linux
13//!
14//! ## Usage
15//!
16//! ```rust,no_run
17//! use kasl::libs::update::Updater;
18//!
19//! #[tokio::main]
20//! async fn main() -> anyhow::Result<()> {
21//! let mut updater = Updater::new()?;
22//!
23//! if updater.check_for_latest_release().await? {
24//! updater.perform_update().await?;
25//! }
26//!
27//! Ok(())
28//! }
29//! ```
30
31use crate::libs::data_storage::DataStorage;
32use crate::libs::messages::Message;
33use crate::{msg_bail_anyhow, msg_error_anyhow, msg_info};
34use anyhow::Result;
35use chrono::{DateTime, Duration, Utc};
36use flate2::read::GzDecoder;
37use reqwest::Client;
38use serde::Deserialize;
39use std::env;
40use std::fs::{self, File};
41use std::path::PathBuf;
42use tar::Archive;
43
44// Include application metadata (name, version, owner) generated at build time.
45include!(concat!(env!("OUT_DIR"), "/app_metadata.rs"));
46
47/// Filename for storing the timestamp of the last update check.
48///
49/// This file enables throttling of update checks to avoid excessive API calls
50/// and respect GitHub's rate limiting policies.
51const LAST_CHECK_FILE: &str = ".last_update_check";
52
53/// Minimum interval between update checks in days.
54///
55/// This prevents excessive API calls while ensuring users receive timely
56/// notifications about new releases. The interval balances user experience
57/// with API rate limiting considerations.
58const DAILY_CHECK_INTERVAL: i64 = 1;
59
60/// File extension used for backing up the current executable.
61///
62/// Before replacing the current executable, it's backed up with this extension
63/// to enable rollback in case of update failures.
64const BACKUP_EXTENSION: &str = "bak";
65
66/// Represents a GitHub release response from the API.
67///
68/// This structure deserializes the JSON response from GitHub's releases API,
69/// containing version information and download assets for the latest release.
70#[derive(Deserialize, Debug)]
71struct GitHubRelease {
72 /// The version tag name (e.g., "v1.2.3" or "1.2.3")
73 tag_name: String,
74 /// Array of downloadable assets for this release
75 assets: Vec<GitHubAsset>,
76}
77
78/// Represents a single downloadable asset within a GitHub release.
79///
80/// Each release typically contains multiple assets for different platforms
81/// and architectures. This structure provides the information needed to
82/// identify and download the appropriate asset.
83#[derive(Deserialize, Debug)]
84struct GitHubAsset {
85 /// Direct download URL for this asset
86 browser_download_url: String,
87 /// Filename of the asset (used for platform identification)
88 name: String,
89}
90
91/// Manages the complete application update process from version checking to binary replacement.
92///
93/// The Updater encapsulates all state and behavior needed for safe, reliable application
94/// updates. It handles GitHub API communication, platform detection, download management,
95/// and atomic binary replacement with backup and rollback capabilities.
96///
97/// ## State Management
98///
99/// The Updater maintains several pieces of state throughout the update process:
100/// - **Version Information**: Current and latest version tracking
101/// - **Download State**: URLs and file paths for update assets
102/// - **Configuration**: API endpoints and platform identification
103/// - **Check Throttling**: Timestamps for rate-limited update checks
104///
105/// ## Thread Safety
106///
107/// The Updater is designed for single-threaded use during update operations.
108/// While individual methods are safe to call, the update process itself should
109/// not be parallelized to avoid file system conflicts during binary replacement.
110#[derive(Debug)]
111pub struct Updater {
112 /// HTTP client for making API requests to GitHub.
113 ///
114 /// Configured with appropriate headers and timeouts for reliable
115 /// communication with GitHub's API endpoints.
116 pub client: Client,
117
118 /// GitHub repository owner (organization or user account).
119 ///
120 /// Extracted from build-time metadata to identify the source repository
121 /// for release information and asset downloads.
122 pub owner: String,
123
124 /// Application/repository name for GitHub API requests.
125 ///
126 /// Combined with owner to form complete repository identification for
127 /// API endpoint construction and asset discovery.
128 pub name: String,
129
130 /// Current version of the running application.
131 ///
132 /// Embedded at compile time to enable comparison with latest available
133 /// versions from GitHub releases. Used for determining update necessity.
134 pub version: String,
135
136 /// Latest version available from GitHub (if newer than current).
137 ///
138 /// Populated after successful version check if a newer version is found.
139 /// Used for user notifications and update confirmation messages.
140 pub latest_version: Option<String>,
141
142 /// Download URL for the latest release asset matching current platform.
143 ///
144 /// Determined through platform detection and asset filtering. Used for
145 /// downloading the appropriate binary for the current system configuration.
146 pub download_url: Option<String>,
147
148 /// Complete URL for fetching latest release information from GitHub API.
149 ///
150 /// Constructed from repository information and GitHub's API format.
151 /// Used for all version checking and asset discovery operations.
152 releases_url: String,
153
154 /// Path to file storing the timestamp of the last update check.
155 ///
156 /// Enables throttling of update checks to respect API rate limits and
157 /// avoid excessive network requests while providing timely notifications.
158 last_check_file: PathBuf,
159}
160
161impl Updater {
162 /// Creates a new Updater instance with configuration from build-time metadata.
163 ///
164 /// This constructor initializes the updater with all necessary configuration
165 /// for communicating with GitHub's API and managing the update process. It
166 /// uses compile-time metadata to automatically configure repository information.
167 ///
168 /// ## Configuration Sources
169 ///
170 /// The constructor uses several sources for configuration:
171 /// - **Build Metadata**: Repository owner, name, and current version
172 /// - **GitHub API**: Standard endpoints for releases and asset discovery
173 /// - **Data Storage**: Platform-appropriate paths for cache and state files
174 /// - **Network Configuration**: HTTP client with reasonable defaults
175 ///
176 /// ## File System Setup
177 ///
178 /// The constructor creates necessary file system entries:
179 /// - **Check Cache File**: For storing last update check timestamp
180 /// - **Data Directory**: Platform-specific application data location
181 /// - **Permissions**: Appropriate read/write permissions for update operations
182 ///
183 /// # Returns
184 ///
185 /// Returns a configured Updater instance ready for version checking and
186 /// update operations, or an error if initialization fails.
187 ///
188 /// # Errors
189 ///
190 /// - **Data Storage**: Cannot determine or create application data directory
191 /// - **File System**: Permission issues with cache file creation
192 /// - **Configuration**: Invalid repository information in build metadata
193 ///
194 /// # Examples
195 ///
196 /// ```rust,no_run
197 /// use kasl::libs::update::Updater;
198 ///
199 /// let updater = Updater::new()?;
200 /// println!("Updater configured for {} v{}", updater.name, updater.version);
201 /// ```
202 pub fn new() -> Result<Self> {
203 // Extract repository information from compile-time metadata
204 let owner = APP_METADATA_OWNER.to_owned();
205 let name = APP_METADATA_NAME.to_owned();
206
207 // Set up cache file for update check throttling
208 let last_check_file = DataStorage::new().get_path(LAST_CHECK_FILE)?;
209
210 // Construct GitHub API endpoint for latest release information
211 let releases_url = format!("https://api.github.com/repos/{}/{}/releases/latest", owner, name);
212
213 Ok(Self {
214 client: Client::new(),
215 owner,
216 name,
217 version: APP_METADATA_VERSION.to_owned(),
218 latest_version: None,
219 download_url: None,
220 last_check_file,
221 releases_url,
222 })
223 }
224
225 /// Displays a notification if a new version is available, with throttled checking.
226 ///
227 /// This method provides a user-friendly way to check for updates without being
228 /// intrusive. It implements intelligent throttling to avoid excessive API calls
229 /// while ensuring users are notified of important updates in a timely manner.
230 ///
231 /// ## Throttling Logic
232 ///
233 /// The method implements several levels of throttling:
234 /// 1. **Time-Based**: Respects the daily check interval configuration
235 /// 2. **Graceful Failure**: Silently handles initialization or network errors
236 /// 3. **Non-Blocking**: Returns immediately if checks aren't due
237 /// 4. **Background Operation**: Doesn't interrupt normal application flow
238 ///
239 /// ## User Experience
240 ///
241 /// - **Non-Intrusive**: Only shows notifications when updates are available
242 /// - **Informative**: Provides clear version information in notifications
243 /// - **Actionable**: Suggests how users can install available updates
244 /// - **Reliable**: Handles network errors gracefully without user impact
245 ///
246 /// ## Implementation Strategy
247 ///
248 /// The method uses a fail-fast approach:
249 /// - Returns immediately if updater initialization fails
250 /// - Skips check if not enough time has passed since last check
251 /// - Only displays notification if newer version is confirmed available
252 /// - Handles all errors silently to avoid disrupting user workflow
253 ///
254 /// # Examples
255 ///
256 /// ```rust,no_run
257 /// use kasl::libs::update::Updater;
258 ///
259 /// // Call during application startup
260 /// Updater::show_update_notification().await;
261 /// // User sees notification only if update is available and check is due
262 /// ```
263 ///
264 /// # Background Behavior
265 ///
266 /// This method is designed to be called during application startup:
267 /// - **Startup Integration**: Called automatically during main application init
268 /// - **Non-Blocking**: Doesn't delay application startup or user operations
269 /// - **Error Resilience**: Network or API failures don't affect application functionality
270 /// - **Rate Limiting**: Respects GitHub API limits through intelligent throttling
271 pub async fn show_update_notification() {
272 // Attempt to create updater instance - fail silently if not possible
273 let mut updater = match Self::new() {
274 Ok(up) => up,
275 Err(_) => return, // Graceful degradation if updater can't be initialized
276 };
277
278 // Check if enough time has passed since last update check
279 if !updater.is_check_due() {
280 return;
281 }
282
283 // Perform version check and display notification if update available
284 if let Ok(true) = updater.check_for_latest_release().await {
285 if let Some(latest_version) = &updater.latest_version {
286 // Display user-friendly update notification
287 msg_info!(
288 Message::UpdateAvailable {
289 app_name: updater.name,
290 latest: latest_version.to_string()
291 },
292 true // Show with extra spacing for visibility
293 )
294 }
295 }
296 }
297
298 /// Performs the complete update process: download, verification, and installation.
299 ///
300 /// This method orchestrates the entire update workflow, from downloading the
301 /// latest release to safely replacing the current executable. It implements
302 /// multiple safety mechanisms to ensure the update process is reliable and
303 /// recoverable in case of failures.
304 ///
305 /// ## Update Process Flow
306 ///
307 /// The method follows a carefully designed sequence:
308 ///
309 /// 1. **Pre-flight Validation**: Verifies that download URL is available
310 /// 2. **Asset Download**: Retrieves the release archive from GitHub
311 /// 3. **Local Storage**: Saves archive to temporary location for processing
312 /// 4. **Binary Extraction**: Extracts and validates the new executable
313 /// 5. **Backup Creation**: Creates backup of current executable
314 /// 6. **Atomic Replacement**: Replaces current executable with new version
315 /// 7. **Cleanup**: Removes temporary files and completes the process
316 ///
317 /// ## Safety Mechanisms
318 ///
319 /// ### Backup and Recovery
320 /// - **Current Executable Backup**: Automatically created before replacement
321 /// - **Rollback Capability**: Failed updates can be reverted using backup
322 /// - **Atomic Operations**: Binary replacement is performed atomically
323 /// - **Error Recovery**: Partial failures are cleaned up automatically
324 ///
325 /// ### Validation and Verification
326 /// - **Download Validation**: Ensures complete archive download
327 /// - **Archive Integrity**: Validates archive format and structure
328 /// - **Binary Verification**: Confirms executable is present in archive
329 /// - **Platform Compatibility**: Verifies binary matches current platform
330 ///
331 /// ## Error Handling
332 ///
333 /// The method implements comprehensive error handling:
334 /// - **Network Errors**: Download failures are reported with clear messages
335 /// - **File System Errors**: Permission and disk space issues are handled
336 /// - **Archive Errors**: Corrupted or invalid archives are detected
337 /// - **Backup Failures**: Issues with backup creation abort the process
338 ///
339 /// # Preconditions
340 ///
341 /// This method requires that `check_for_latest_release()` has been called
342 /// successfully and that `self.download_url` contains a valid URL.
343 ///
344 /// # Returns
345 ///
346 /// Returns `Ok(())` on successful update completion, or an error describing
347 /// the specific failure that occurred during the update process.
348 ///
349 /// # Examples
350 ///
351 /// ```rust,no_run
352 /// use kasl::libs::update::Updater;
353 ///
354 /// let mut updater = Updater::new()?;
355 /// if updater.check_for_latest_release().await? {
356 /// updater.perform_update().await?;
357 /// println!("Update completed successfully");
358 /// }
359 /// ```
360 ///
361 /// # Error Scenarios
362 ///
363 /// - **No Download URL**: `check_for_latest_release()` hasn't been called successfully
364 /// - **Network Failure**: Unable to download release archive from GitHub
365 /// - **Disk Space**: Insufficient space for temporary files or backup
366 /// - **Permissions**: Cannot write to application directory or create backup
367 /// - **Archive Corruption**: Downloaded archive is corrupted or invalid format
368 /// - **Missing Binary**: Archive doesn't contain expected executable file
369 pub async fn perform_update(&self) -> Result<()> {
370 // Validate that download URL is available from previous version check
371 let download_url = self.download_url.as_ref().ok_or(msg_error_anyhow!(Message::UpdateDownloadUrlNotSet))?;
372
373 // Download the release archive from GitHub
374 let response = self.client.get(download_url).send().await?;
375 let content = response.bytes().await?;
376
377 // Save the downloaded archive to a temporary file for processing
378 let tar_gz_path = env::temp_dir().join(format!("{}.tar.gz", &self.name));
379 fs::write(&tar_gz_path, &content)?;
380
381 // Extract the new binary and replace the current executable
382 // This includes backup creation and atomic replacement
383 self.extract_and_replace_binary(&tar_gz_path)?;
384
385 // Clean up the downloaded archive after successful installation
386 fs::remove_file(&tar_gz_path)?;
387
388 Ok(())
389 }
390
391 /// Checks GitHub for the latest release and determines if an update is available.
392 ///
393 /// This method communicates with GitHub's releases API to fetch information about
394 /// the latest available version. It compares version strings to determine if the
395 /// current application version is outdated and populates the updater's state with
396 /// download information if an update is needed.
397 ///
398 /// ## Version Comparison Logic
399 ///
400 /// The method uses string comparison for version precedence:
401 /// 1. **Version Normalization**: Strips 'v' prefix from GitHub tags if present
402 /// 2. **String Comparison**: Uses lexicographic comparison for version ordering
403 /// 3. **Update Detection**: Identifies when remote version is greater than current
404 /// 4. **State Population**: Stores version and download information for later use
405 ///
406 /// ## API Communication
407 ///
408 /// ### Request Configuration
409 /// - **User Agent**: Identifies requests with application name
410 /// - **Rate Limiting**: Respects GitHub's API rate limits
411 /// - **Error Handling**: Gracefully handles API errors and timeouts
412 /// - **JSON Parsing**: Deserializes GitHub's release response format
413 ///
414 /// ### Response Processing
415 /// - **Version Extraction**: Parses tag_name from release information
416 /// - **Asset Discovery**: Finds platform-appropriate download assets
417 /// - **URL Resolution**: Determines correct download URL for current platform
418 /// - **Cache Update**: Records check timestamp for throttling
419 ///
420 /// ## State Updates
421 ///
422 /// When a newer version is found, the method updates:
423 /// - **latest_version**: Stores the newer version string for display
424 /// - **download_url**: Sets the URL for downloading the platform-specific binary
425 /// - **Check Timestamp**: Records when this check was performed for throttling
426 ///
427 /// # Returns
428 ///
429 /// Returns `true` if a newer version is available and download URL is found,
430 /// `false` if the current version is up-to-date or no compatible asset exists.
431 ///
432 /// # Errors
433 ///
434 /// - **Network Errors**: API request failures, timeouts, or connectivity issues
435 /// - **API Errors**: GitHub API rate limiting or service unavailability
436 /// - **Parsing Errors**: Invalid JSON response format from GitHub
437 /// - **File System Errors**: Cannot update check timestamp cache file
438 ///
439 /// # Examples
440 ///
441 /// ```rust,no_run
442 /// let mut updater = Updater::new()?;
443 /// if updater.check_for_latest_release().await? {
444 /// println!("Update available: {} -> {}",
445 /// updater.version,
446 /// updater.latest_version.unwrap());
447 /// }
448 /// ```
449 pub async fn check_for_latest_release(&mut self) -> Result<bool> {
450 // Fetch latest release information from GitHub API
451 let release = self.fetch_latest_github_release().await?;
452
453 // Update check timestamp for throttling future checks
454 self.update_last_check_time();
455
456 // Normalize version string by removing 'v' prefix if present
457 let latest_version = release.tag_name.trim_start_matches('v').to_string();
458
459 // Compare versions using string comparison (works for semantic versioning)
460 if latest_version > self.version {
461 // Store the newer version information
462 self.latest_version = Some(latest_version);
463
464 // Find and store the download URL for the current platform
465 self.download_url = self.find_platform_asset_url(&release.assets).map(|url| url.to_string());
466
467 Ok(true) // Update is available
468 } else {
469 Ok(false) // Current version is up-to-date
470 }
471 }
472
473 /// Fetches release data from the GitHub releases API.
474 ///
475 /// This method handles the low-level communication with GitHub's API,
476 /// including proper request headers and JSON deserialization. It's designed
477 /// to be reliable and follow GitHub's API best practices.
478 ///
479 /// ## Request Configuration
480 ///
481 /// - **User-Agent Header**: Required by GitHub API, set to application name
482 /// - **Accept Header**: Implicitly requests JSON response format
483 /// - **Timeout Handling**: Uses client default timeouts for reliability
484 /// - **Error Propagation**: Network errors are propagated to caller
485 ///
486 /// # Returns
487 ///
488 /// Returns the parsed GitHub release information or an error if the
489 /// request fails or the response cannot be parsed.
490 async fn fetch_latest_github_release(&self) -> Result<GitHubRelease, reqwest::Error> {
491 self.client
492 .get(&self.releases_url)
493 .header("User-Agent", &self.name) // Required by GitHub API
494 .send()
495 .await?
496 .json::<GitHubRelease>()
497 .await
498 }
499
500 /// Finds the download URL for an asset matching the current platform.
501 ///
502 /// This method searches through release assets to find the binary that
503 /// matches the current platform's architecture and operating system.
504 /// It uses platform identification to select the appropriate asset.
505 ///
506 /// ## Asset Selection Logic
507 ///
508 /// 1. **Platform Identification**: Generate current platform identifier
509 /// 2. **Asset Filtering**: Search assets for matching platform identifier
510 /// 3. **URL Extraction**: Return download URL for matching asset
511 /// 4. **Fallback Handling**: Return None if no matching asset found
512 ///
513 /// ## Platform Matching
514 ///
515 /// The method looks for assets containing platform identifiers like:
516 /// - `x86_64-pc-windows-msvc` for Windows
517 /// - `x86_64-apple-darwin` for macOS Intel
518 /// - `aarch64-apple-darwin` for macOS Apple Silicon
519 /// - `x86_64-unknown-linux-musl` for Linux
520 ///
521 /// # Arguments
522 ///
523 /// * `assets` - Array of release assets from GitHub API response
524 ///
525 /// # Returns
526 ///
527 /// Returns the download URL for the matching asset, or None if no
528 /// compatible asset is found for the current platform.
529 fn find_platform_asset_url<'a>(&self, assets: &'a [GitHubAsset]) -> Option<&'a str> {
530 let platform_name = self.get_platform_identifier();
531 assets
532 .iter()
533 .find(|asset| asset.name.contains(&platform_name))
534 .map(|asset| asset.browser_download_url.as_str())
535 }
536
537 /// Extracts the new binary from the downloaded archive and replaces the current executable.
538 ///
539 /// This method performs the most critical part of the update process: safely
540 /// replacing the current executable with the new version. It implements multiple
541 /// safety mechanisms to ensure the operation is atomic and recoverable.
542 ///
543 /// ## Extraction Process
544 ///
545 /// 1. **Archive Opening**: Opens and validates the tar.gz archive
546 /// 2. **Entry Iteration**: Processes each file in the archive
547 /// 3. **Binary Identification**: Finds the main executable file
548 /// 4. **Backup Creation**: Creates backup of current executable
549 /// 5. **Atomic Replacement**: Replaces executable with new version
550 /// 6. **Auxiliary Files**: Extracts other files to appropriate locations
551 ///
552 /// ## Safety Mechanisms
553 ///
554 /// ### Backup Strategy
555 /// - **Automatic Backup**: Current executable is backed up before replacement
556 /// - **Backup Naming**: Uses consistent `.bak` extension for identification
557 /// - **Rollback Support**: Backup enables recovery from failed updates
558 /// - **Cleanup**: Old backups are replaced with new ones
559 ///
560 /// ### Atomic Operations
561 /// - **Rename Operation**: Uses filesystem rename for atomic replacement
562 /// - **Error Recovery**: Partially completed operations are cleaned up
563 /// - **Validation**: Confirms successful extraction before cleanup
564 /// - **Rollback**: Failed operations can be reverted using backup
565 ///
566 /// ## File Handling
567 ///
568 /// ### Main Executable
569 /// - **Identification**: Matches filename with current executable
570 /// - **Backup Creation**: Renames current executable to backup
571 /// - **Replacement**: Extracts new executable to current location
572 /// - **Permissions**: Preserves executable permissions
573 ///
574 /// ### Auxiliary Files
575 /// - **Location**: Extracted to same directory as executable
576 /// - **Overwrite**: Existing files are replaced with new versions
577 /// - **Permissions**: Standard file permissions are applied
578 /// - **Cleanup**: Temporary files are removed after extraction
579 ///
580 /// # Arguments
581 ///
582 /// * `tar_gz_path` - Path to the downloaded release archive
583 ///
584 /// # Returns
585 ///
586 /// Returns `Ok(())` on successful extraction and replacement, or an error
587 /// if any step of the process fails.
588 ///
589 /// # Error Scenarios
590 ///
591 /// - **Archive Errors**: Corrupted or invalid tar.gz format
592 /// - **Missing Binary**: Archive doesn't contain expected executable
593 /// - **File System Errors**: Permission issues or disk space problems
594 /// - **Backup Failures**: Cannot create backup of current executable
595 /// - **Extraction Errors**: Cannot extract files from archive
596 fn extract_and_replace_binary(&self, tar_gz_path: &PathBuf) -> Result<()> {
597 // Open and prepare the archive for extraction
598 let tar_gz = File::open(tar_gz_path)?;
599 let tar = GzDecoder::new(tar_gz);
600 let mut archive = Archive::new(tar);
601 let mut is_updated = false;
602
603 // Determine current executable path and backup location
604 let current_exe = env::current_exe()?;
605 let current_exe_backup = current_exe.with_extension(BACKUP_EXTENSION);
606
607 // Process each entry in the archive
608 for entry_result in archive.entries()? {
609 let mut entry = entry_result?;
610 let entry_path = entry.path()?;
611
612 // Check if this entry is the main executable
613 if entry_path.ends_with(current_exe.file_name().unwrap()) {
614 // Create backup of current executable before replacement
615 fs::rename(¤t_exe, ¤t_exe_backup)?;
616
617 // Extract new executable to current location
618 entry.unpack(¤t_exe)?;
619 is_updated = true;
620 } else {
621 // Extract auxiliary files to the executable directory
622 let dest_path = current_exe.parent().unwrap().join(&entry_path);
623 entry.unpack(dest_path)?;
624 }
625 }
626
627 // Verify that the main executable was found and updated
628 if is_updated {
629 Ok(())
630 } else {
631 msg_bail_anyhow!(Message::UpdateBinaryNotFoundInArchive);
632 }
633 }
634
635 /// Constructs the platform-specific identifier used in release asset names.
636 ///
637 /// This method generates a string that identifies the current platform's
638 /// architecture and operating system in the format used by GitHub release
639 /// assets. The identifier follows Rust's target triple format for consistency.
640 ///
641 /// ## Platform Detection
642 ///
643 /// The method uses Rust's built-in constants to detect:
644 /// - **Architecture**: From `env::consts::ARCH` (x86_64, aarch64, etc.)
645 /// - **Operating System**: From `env::consts::OS` (windows, macos, linux)
646 /// - **ABI/Toolchain**: Mapped to appropriate toolchain identifier
647 ///
648 /// ## Identifier Format
649 ///
650 /// The generated identifiers follow this pattern:
651 /// `{architecture}-{vendor}-{os}-{abi}`
652 ///
653 /// ### Architecture Values
654 /// - `x86_64`: 64-bit Intel/AMD processors
655 /// - `aarch64`: 64-bit ARM processors (Apple Silicon, ARM64)
656 ///
657 /// ### Operating System Mapping
658 /// - `windows` → `pc-windows-msvc`: Windows with MSVC toolchain
659 /// - `macos` → `apple-darwin`: macOS with Darwin ABI
660 /// - Other → `unknown-linux-musl`: Linux with statically linked musl
661 ///
662 /// # Returns
663 ///
664 /// Returns a platform identifier string suitable for matching against
665 /// GitHub release asset names.
666 ///
667 /// # Examples
668 ///
669 /// Generated identifiers:
670 /// - Windows: `"x86_64-pc-windows-msvc"`
671 /// - macOS Intel: `"x86_64-apple-darwin"`
672 /// - macOS Apple Silicon: `"aarch64-apple-darwin"`
673 /// - Linux: `"x86_64-unknown-linux-musl"`
674 fn get_platform_identifier(&self) -> String {
675 let arch = env::consts::ARCH;
676 let os = match env::consts::OS {
677 "windows" => "pc-windows-msvc",
678 "macos" => "apple-darwin",
679 _ => "unknown-linux-musl", // Default to Linux with musl for compatibility
680 };
681
682 // Construct target triple format: architecture-vendor-os-abi
683 format!("{}-{}", arch, os)
684 }
685
686 /// Updates the timestamp file to record when the last update check was performed.
687 ///
688 /// This method implements the persistence layer for update check throttling,
689 /// ensuring that checks are performed at appropriate intervals without being
690 /// too frequent or too infrequent for user needs.
691 ///
692 /// ## Throttling Implementation
693 ///
694 /// - **Timestamp Format**: Uses RFC 3339 format for precise time recording
695 /// - **File Persistence**: Stores timestamp in application data directory
696 /// - **Error Tolerance**: File write failures are silently ignored
697 /// - **Atomic Update**: Timestamp is updated immediately after API call
698 ///
699 /// ## File Management
700 ///
701 /// - **Location**: Stored in platform-specific application data directory
702 /// - **Format**: Plain text file containing ISO 8601 timestamp
703 /// - **Permissions**: Standard file permissions for user data
704 /// - **Cleanup**: File is automatically managed, no manual cleanup needed
705 ///
706 /// ## Error Handling
707 ///
708 /// File write failures are intentionally ignored because:
709 /// - Update check throttling is a convenience feature, not critical functionality
710 /// - Missing timestamp files default to allowing immediate checks
711 /// - File system errors shouldn't prevent application operation
712 /// - Next successful write will restore normal throttling behavior
713 fn update_last_check_time(&self) {
714 let now = Utc::now().to_rfc3339();
715
716 // Intentionally ignore write errors - throttling is not critical functionality
717 let _ = fs::write(&self.last_check_file, now);
718 }
719
720 /// Determines if sufficient time has passed since the last update check.
721 ///
722 /// This method implements the core logic for update check throttling, ensuring
723 /// that checks are performed at reasonable intervals while respecting both
724 /// user experience and API rate limiting considerations.
725 ///
726 /// ## Throttling Logic
727 ///
728 /// The method implements several decision points:
729 ///
730 /// ### File Existence Check
731 /// - **Missing File**: Indicates first run or file system issue → allow check
732 /// - **Read Errors**: File corruption or permission issues → allow check
733 /// - **Successful Read**: Parse timestamp and evaluate recency
734 ///
735 /// ### Timestamp Parsing
736 /// - **Valid Timestamp**: Parse and compare with current time
737 /// - **Invalid Format**: Corrupted timestamp data → allow check
738 /// - **Parse Errors**: File corruption or format changes → allow check
739 ///
740 /// ### Interval Evaluation
741 /// - **Recent Check**: Within daily interval → deny check
742 /// - **Overdue Check**: Exceeds daily interval → allow check
743 /// - **Future Timestamp**: System clock issues → allow check
744 ///
745 /// ## Error Handling Strategy
746 ///
747 /// The method uses a fail-open approach where any error condition results
748 /// in allowing the check to proceed. This ensures that:
749 /// - File system issues don't prevent updates
750 /// - Timestamp corruption doesn't block checks permanently
751 /// - Clock synchronization problems are handled gracefully
752 /// - Users receive update notifications despite technical issues
753 ///
754 /// # Returns
755 ///
756 /// Returns `true` if a check should be performed (enough time has passed
757 /// or error conditions favor allowing the check), `false` if the check
758 /// should be skipped to respect throttling intervals.
759 ///
760 /// # Examples
761 ///
762 /// ```rust,no_run
763 /// let updater = Updater::new()?;
764 /// if updater.is_check_due() {
765 /// // Perform update check
766 /// } else {
767 /// // Skip check, too recent
768 /// }
769 /// ```
770 fn is_check_due(&self) -> bool {
771 match fs::read_to_string(&self.last_check_file) {
772 Ok(content) => {
773 // Attempt to parse the stored timestamp
774 let last_check = content.parse::<DateTime<Utc>>().unwrap_or_else(|_| {
775 // If parsing fails, default to a time that will trigger a check
776 Utc::now() - Duration::days(DAILY_CHECK_INTERVAL + 1)
777 });
778
779 // Check if enough time has passed since the last check
780 Utc::now().signed_duration_since(last_check) > Duration::days(DAILY_CHECK_INTERVAL)
781 }
782 Err(_) => true, // If file doesn't exist or can't be read, always allow check
783 }
784 }
785}