Skip to main content

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