max / goingson
- Co-Authored-By
- Claude Opus 4.8 (1M context) <noreply@anthropic.com>
187 files changed,
+11805 insertions,
-5752 deletions
| @@ -3,7 +3,7 @@ | |||
| 3 | 3 | //! Runs in the background and creates compressed backups based on user settings. | |
| 4 | 4 | //! Handles backup retention by pruning old backups when max count is exceeded. | |
| 5 | 5 | ||
| 6 | - | use crate::export::backup::{write_backup, FullExport}; | |
| 6 | + | use crate::export::backup::{FullExport, write_backup}; | |
| 7 | 7 | use crate::state::{AppState, DESKTOP_USER_ID}; | |
| 8 | 8 | use chrono::Utc; | |
| 9 | 9 | use std::sync::Arc; | |
| @@ -164,7 +164,9 @@ | |||
| 164 | 164 | let filename = backup_filename(now); | |
| 165 | 165 | let file_path = backup_dir.join(&filename); | |
| 166 | 166 | ||
| 167 | - | let export = collect_full_export(state, DESKTOP_USER_ID).await.map_err(|e| e.to_string())?; | |
| 167 | + | let export = collect_full_export(state, DESKTOP_USER_ID) | |
| 168 | + | .await | |
| 169 | + | .map_err(|e| e.to_string())?; | |
| 168 | 170 | let item_count = export.total_count(); | |
| 169 | 171 | let max_to_keep = settings.max_backups_to_keep as usize; | |
| 170 | 172 | ||
| @@ -259,7 +261,9 @@ | |||
| 259 | 261 | let filename = backup_filename(now); | |
| 260 | 262 | let file_path = backup_dir.join(&filename); | |
| 261 | 263 | ||
| 262 | - | let export = collect_full_export(state, DESKTOP_USER_ID).await.map_err(|e| e.to_string())?; | |
| 264 | + | let export = collect_full_export(state, DESKTOP_USER_ID) | |
| 265 | + | .await | |
| 266 | + | .map_err(|e| e.to_string())?; | |
| 263 | 267 | let item_count = export.total_count(); | |
| 264 | 268 | ||
| 265 | 269 | // Directory creation + gzip serialization are blocking and take seconds on a | |
| @@ -318,7 +322,8 @@ | |||
| 318 | 322 | let path = dir.join(format!("goingson-backup-{i:03}.json.gz")); | |
| 319 | 323 | std::fs::write(&path, [i as u8]).unwrap(); | |
| 320 | 324 | let f = std::fs::File::options().write(true).open(&path).unwrap(); | |
| 321 | - | f.set_modified(base + Duration::from_secs(i as u64)).unwrap(); | |
| 325 | + | f.set_modified(base + Duration::from_secs(i as u64)) | |
| 326 | + | .unwrap(); | |
| 322 | 327 | } | |
| 323 | 328 | } | |
| 324 | 329 | ||
| @@ -357,6 +362,10 @@ | |||
| 357 | 362 | let dir = tempfile::tempdir().unwrap(); | |
| 358 | 363 | seed_backups(dir.path(), 9); | |
| 359 | 364 | prune_old_backups(dir.path(), 5).unwrap(); | |
| 360 | - | assert_eq!(gz_count(dir.path()), 5, "a limit above the floor is honored"); | |
| 365 | + | assert_eq!( | |
| 366 | + | gz_count(dir.path()), | |
| 367 | + | 5, | |
| 368 | + | "a limit above the floor is honored" | |
| 369 | + | ); | |
| 361 | 370 | } | |
| 362 | 371 | } |
| @@ -6,8 +6,8 @@ | |||
| 6 | 6 | use notify::RecursiveMode; | |
| 7 | 7 | use notify_debouncer_mini::new_debouncer; | |
| 8 | 8 | use std::path::Path; | |
| 9 | - | use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; | |
| 10 | 9 | use std::sync::Arc; | |
| 10 | + | use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; | |
| 11 | 11 | use std::time::Duration; | |
| 12 | 12 | use tauri::{Emitter, Manager}; | |
| 13 | 13 | use tracing::{debug, error, info, warn}; | |
| @@ -36,7 +36,10 @@ | |||
| 36 | 36 | let db_path = app_data_dir.join("goingson.db"); | |
| 37 | 37 | ||
| 38 | 38 | if !db_path.exists() { | |
| 39 | - | warn!(?db_path, "Database file does not exist yet, watcher will start when it's created"); | |
| 39 | + | warn!( | |
| 40 | + | ?db_path, | |
| 41 | + | "Database file does not exist yet, watcher will start when it's created" | |
| 42 | + | ); | |
| 40 | 43 | } | |
| 41 | 44 | ||
| 42 | 45 | info!(?db_path, "Starting database file watcher"); | |
| @@ -96,7 +99,10 @@ | |||
| 96 | 99 | }; | |
| 97 | 100 | ||
| 98 | 101 | // Watch the app data directory (contains db, wal, shm files) | |
| 99 | - | if let Err(e) = debouncer.watcher().watch(&app_data_dir, RecursiveMode::NonRecursive) { | |
| 102 | + | if let Err(e) = debouncer | |
| 103 | + | .watcher() | |
| 104 | + | .watch(&app_data_dir, RecursiveMode::NonRecursive) | |
| 105 | + | { | |
| 100 | 106 | error!(?app_data_dir, "Failed to watch directory: {}", e); | |
| 101 | 107 | return; | |
| 102 | 108 | } | |
| @@ -113,9 +119,7 @@ | |||
| 113 | 119 | match rx.recv_timeout(Duration::from_secs(1)) { | |
| 114 | 120 | Ok(Ok(events)) => { | |
| 115 | 121 | // Check if any event is for our database files | |
| 116 | - | let db_changed = events.iter().any(|event| { | |
| 117 | - | is_db_file(&event.path, &db_path) | |
| 118 | - | }); | |
| 122 | + | let db_changed = events.iter().any(|event| is_db_file(&event.path, &db_path)); | |
| 119 | 123 | ||
| 120 | 124 | if db_changed { | |
| 121 | 125 | // Check if enough time has passed since last event | |
| @@ -163,7 +167,10 @@ | |||
| 163 | 167 | ||
| 164 | 168 | /// Check if a path is one of the SQLite database files | |
| 165 | 169 | fn is_db_file(path: &Path, db_path: &Path) -> bool { | |
| 166 | - | let db_name = db_path.file_name().and_then(|n| n.to_str()).unwrap_or("goingson.db"); | |
| 170 | + | let db_name = db_path | |
| 171 | + | .file_name() | |
| 172 | + | .and_then(|n| n.to_str()) | |
| 173 | + | .unwrap_or("goingson.db"); | |
| 167 | 174 | ||
| 168 | 175 | if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { | |
| 169 | 176 | // Match main db file and WAL/SHM/journal files |
| @@ -6,7 +6,7 @@ | |||
| 6 | 6 | use std::collections::HashMap; | |
| 7 | 7 | use std::sync::Arc; | |
| 8 | 8 | use tauri::Manager; | |
| 9 | - | use tokio::time::{interval, Duration}; | |
| 9 | + | use tokio::time::{Duration, interval}; | |
| 10 | 10 | use tokio_util::sync::CancellationToken; | |
| 11 | 11 | use tracing::{debug, error, info, warn}; | |
| 12 | 12 | ||
| @@ -36,7 +36,10 @@ | |||
| 36 | 36 | // Track consecutive failures per account for exponential backoff | |
| 37 | 37 | let mut failure_counts: HashMap<String, u32> = HashMap::new(); | |
| 38 | 38 | ||
| 39 | - | info!("Email sync scheduler started (checking every {} seconds)", CHECK_INTERVAL_SECS); | |
| 39 | + | info!( | |
| 40 | + | "Email sync scheduler started (checking every {} seconds)", | |
| 41 | + | CHECK_INTERVAL_SECS | |
| 42 | + | ); | |
| 40 | 43 | ||
| 41 | 44 | // Infinite tick loop: sleep for CHECK_INTERVAL_SECS, then check all accounts. | |
| 42 | 45 | // The first tick fires immediately (tokio::time::interval behavior). | |
| @@ -85,7 +88,10 @@ | |||
| 85 | 88 | return Ok(()); | |
| 86 | 89 | } | |
| 87 | 90 | ||
| 88 | - | debug!("Email sync scheduler: {} account(s) need sync", accounts.len()); | |
| 91 | + | debug!( | |
| 92 | + | "Email sync scheduler: {} account(s) need sync", | |
| 93 | + | accounts.len() | |
| 94 | + | ); | |
| 89 | 95 | ||
| 90 | 96 | // A failing account never updates last_sync_at, so it stays "due" and remains | |
| 91 | 97 | // in this list every tick; any failure_counts key absent here belongs to an | |
| @@ -102,7 +108,9 @@ | |||
| 102 | 108 | // Exponential backoff: skip this tick if the account has been failing | |
| 103 | 109 | let consecutive_failures = failure_counts.get(&account_key).copied().unwrap_or(0); | |
| 104 | 110 | if consecutive_failures > 0 { | |
| 105 | - | let backoff = 2u32.pow(consecutive_failures.min(4)).min(MAX_BACKOFF_MULTIPLIER); | |
| 111 | + | let backoff = 2u32 | |
| 112 | + | .pow(consecutive_failures.min(4)) | |
| 113 | + | .min(MAX_BACKOFF_MULTIPLIER); | |
| 106 | 114 | // Use a simple modulo check: only attempt every `backoff` ticks | |
| 107 | 115 | // This is approximate but avoids needing per-account timestamps | |
| 108 | 116 | if rand_skip(backoff) { | |
| @@ -138,7 +146,10 @@ | |||
| 138 | 146 | let body = if result.emails_saved == 1 { | |
| 139 | 147 | format!("1 new email in {}", account.account_name) | |
| 140 | 148 | } else { | |
| 141 | - | format!("{} new emails in {}", result.emails_saved, account.account_name) | |
| 149 | + | format!( | |
| 150 | + | "{} new emails in {}", | |
| 151 | + | result.emails_saved, account.account_name | |
| 152 | + | ) | |
| 142 | 153 | }; | |
| 143 | 154 | send_notification(app, "New Mail", &body); | |
| 144 | 155 | } |