1pub mod bookmarks;
5pub mod cache;
6pub mod commands;
7pub mod diagnostics;
8pub mod diff;
9pub mod error;
10pub mod events;
11pub mod ids;
12pub mod locks;
13pub mod media_server;
14pub mod menus;
15pub mod notifications;
16pub mod path;
17pub mod profiles;
18pub mod s3;
19pub mod search;
20pub mod settings;
21pub mod transfers;
22pub mod updater;
23
24use std::path::PathBuf;
25use std::sync::Arc;
26
27use bookmarks::{
28 BookmarkStore, BookmarkStoreHandle, BookmarkStoreState, RecentsHandle, RecentsStore,
29};
30use cache::{store::CacheHandle, CacheConfig, CapabilityHandle};
31use diagnostics::DiagnosticsRedactorHandle;
32use diff::DiffStoreHandle;
33use locks::{lifecycle, LockRegistryHandle, ReleaseReason};
34use media_server::{start_on_localhost, TokenRegistry, TokenRegistryHandle};
35use notifications::NotificationLogHandle;
36use profiles::{KeychainHandle, ProfileStore, ProfileStoreHandle};
37use s3::cross_account::ConfirmationCacheHandle;
38use s3::multipart::{MultipartTable, MultipartTableHandle};
39use s3::{ClientPool, ProxyConfig, S3ClientPoolHandle};
40use search::SearchRegistryHandle;
41use settings::{Settings, SettingsHandle};
42use tauri::{Emitter, Manager};
43use tokio::sync::RwLock;
44use transfers::{TransferQueue, TransferQueueHandle};
45
46#[tauri::command]
48fn greet(name: &str) -> String {
49 format!("Hello, {}! You've been greeted from Rust!", name)
50}
51
52fn settings_path(app: &tauri::App) -> PathBuf {
57 app.path()
58 .app_config_dir()
59 .unwrap_or_else(|_| std::env::temp_dir())
60 .join("settings.json")
61}
62
63fn profiles_path(app: &tauri::App) -> PathBuf {
65 app.path()
66 .app_config_dir()
67 .unwrap_or_else(|_| std::env::temp_dir())
68 .join("profiles.json")
69}
70
71fn cache_db_path(app: &tauri::App) -> PathBuf {
73 app.path()
74 .app_config_dir()
75 .unwrap_or_else(|_| std::env::temp_dir())
76 .join("cache.redb")
77}
78
79fn bookmarks_path(app: &tauri::App) -> PathBuf {
81 app.path()
82 .app_config_dir()
83 .unwrap_or_else(|_| std::env::temp_dir())
84 .join("bookmarks.json")
85}
86
87fn recents_path(app: &tauri::App) -> PathBuf {
89 app.path()
90 .app_config_dir()
91 .unwrap_or_else(|_| std::env::temp_dir())
92 .join("recents.json")
93}
94
95fn init_multipart_table(cache_handle: &CacheHandle) -> MultipartTableHandle {
111 if let Some(db) = cache_handle.db() {
112 if let Ok(table) = MultipartTable::new(db) {
113 return MultipartTableHandle::new(table);
114 }
115 }
116
117 let fallback_path = std::env::temp_dir().join("brows3r_multipart_fallback.redb");
118 let (fallback_db, _was_in_memory) = cache::store::open_redb_or_in_memory(&fallback_path);
119
120 if let Ok(table) = MultipartTable::new(Arc::new(fallback_db)) {
121 return MultipartTableHandle::new(table);
122 }
123
124 let pristine_in_memory = redb::Database::builder()
128 .create_with_backend(redb::backends::InMemoryBackend::new())
129 .expect("pristine in-memory redb cannot fail");
130 let table = MultipartTable::new(Arc::new(pristine_in_memory))
131 .expect("MultipartTable::new must succeed on a pristine in-memory redb");
132 MultipartTableHandle::new(table)
133}
134
135#[cfg_attr(mobile, tauri::mobile_entry_point)]
136pub fn run() {
137 tauri::Builder::default()
138 .plugin(tauri_plugin_opener::init())
139 .plugin(tauri_plugin_fs::init())
140 .plugin(tauri_plugin_dialog::init())
141 .plugin(tauri_plugin_shell::init())
142 .plugin(tauri_plugin_notification::init())
143 .plugin(tauri_plugin_updater::Builder::new().build())
144 .setup(|app| {
145 let path = settings_path(app);
146 let settings = Settings::load_sync(&path);
149
150 let cache_config = CacheConfig {
154 default_ttl_secs: settings.cache_ttl_secs,
155 ..CacheConfig::default()
156 };
157 let transfer_concurrency = settings.transfer_concurrency;
158
159 app.manage(SettingsHandle::new(settings, path));
160 app.manage(NotificationLogHandle::default());
161
162 let redactor = Arc::new(diagnostics::redact::Redactor::new());
165 app.manage::<DiagnosticsRedactorHandle>(redactor);
166
167 let profiles_path = profiles_path(app);
170 let keychain_dir = profiles_path
172 .parent()
173 .map(|p| p.to_path_buf())
174 .unwrap_or_else(std::env::temp_dir);
175 let temp_profiles_path = std::env::temp_dir().join("profiles.json");
183 let store = ProfileStore::load(&profiles_path)
184 .or_else(|_| ProfileStore::load(&temp_profiles_path))
185 .unwrap_or_else(|_| {
186 eprintln!(
191 "profile store failed to load from {} and {} — starting empty",
192 profiles_path.display(),
193 temp_profiles_path.display(),
194 );
195 ProfileStore::load(&temp_profiles_path)
196 .unwrap_or_else(|_| ProfileStore::empty(temp_profiles_path.clone()))
197 });
198 let initial_profiles: Vec<(ids::ProfileId, profiles::CompatFlags)> = store
204 .list()
205 .into_iter()
206 .map(|p| (p.id.clone(), p.compat_flags.clone()))
207 .collect();
208 app.manage(ProfileStoreHandle::new(store));
209
210 let (backend, used_fallback) = profiles::keychain::select_backend(keychain_dir, "");
216 app.manage(KeychainHandle::from_box(backend));
217
218 if used_fallback {
219 let app_for_emit = app.app_handle().clone();
224 tauri::async_runtime::spawn(async move {
225 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
226 let _ =
227 app_for_emit.emit(events::EventKind::KeychainFallbackRequired.as_str(), ());
228 });
229 }
230
231 let pool = ClientPool::new(ProxyConfig::System);
233 tauri::async_runtime::block_on(async {
237 for (profile_id, compat) in initial_profiles {
238 pool.register_profile(profile_id, compat).await;
239 }
240 });
241 let pool_handle = S3ClientPoolHandle::new(pool);
242 let pool_handle_for_media = pool_handle.clone();
244 app.manage(pool_handle);
245
246 let cache_path = cache_db_path(app);
250 let cache_handle: CacheHandle =
251 cache::store::CacheStore::open(&cache_path, cache_config).unwrap_or_else(|_| {
252 cache::store::CacheStore::in_memory(CacheConfig::default())
253 });
254
255 let multipart_table_handle = init_multipart_table(&cache_handle);
264 app.manage(multipart_table_handle);
265
266 app.manage(cache_handle);
267
268 app.manage(CapabilityHandle::default());
270
271 let lock_registry = LockRegistryHandle::default();
273 let leftover = lock_registry.inner().startup_cleanup();
275 for lock in &leftover {
276 let _ = locks::emit_released(app.app_handle(), lock, ReleaseReason::StartupCleanup);
278 }
279 let heartbeat_interval = std::time::Duration::from_secs(150);
281 lifecycle::start_heartbeat_loop_handle(
282 &lock_registry,
283 heartbeat_interval,
284 std::sync::Arc::new(app.app_handle().clone()),
285 );
286 app.manage(lock_registry);
287
288 let queue = TransferQueue::new(transfer_concurrency);
290 app.manage(TransferQueueHandle::new(queue));
291
292 app.manage(ConfirmationCacheHandle::default());
294
295 app.manage(DiffStoreHandle::default());
297
298 app.manage(SearchRegistryHandle::new());
300
301 let bm_path = bookmarks_path(app);
303 let bm_store = BookmarkStore::load(&bm_path).unwrap_or_default();
304 let bm_handle: BookmarkStoreHandle =
305 Arc::new(RwLock::new(BookmarkStoreState::new(bm_store, bm_path)));
306 app.manage(bm_handle);
307
308 let rec_path = recents_path(app);
311 let rec_store = RecentsStore::load(rec_path);
312 let rec_handle: RecentsHandle = Arc::new(RwLock::new(rec_store));
313 app.manage(rec_handle);
314
315 let media_session_id = uuid::Uuid::new_v4().to_string();
325 let media_registry: TokenRegistryHandle = std::sync::Arc::new(TokenRegistry::new());
326 match tauri::async_runtime::block_on(start_on_localhost(
327 pool_handle_for_media,
328 media_registry,
329 media_session_id,
330 )) {
331 Ok(media_handle) => {
332 app.manage(media_handle);
333 }
334 Err(e) => {
335 eprintln!(
336 "media server failed to start: {e:?} — media previews disabled this session"
337 );
338 }
339 }
340
341 if let Ok(menu) = menus::build_menu(app.app_handle()) {
345 let _ = app.set_menu(menu);
346 }
347
348 Ok(())
349 })
350 .on_menu_event(|app, event| {
351 let _ = app.emit(event.id().0.as_str(), ());
355 })
356 .invoke_handler(tauri::generate_handler![
357 greet,
358 commands::settings_cmd::settings_get,
359 commands::settings_cmd::settings_update,
360 commands::notifications_cmd::notifications_list,
361 commands::notifications_cmd::notification_dismiss,
362 commands::profiles_cmd::profiles_list,
363 commands::profiles_cmd::profile_get,
364 commands::profiles_cmd::profile_create_manual,
365 commands::profiles_cmd::profile_update,
366 commands::profiles_cmd::profile_delete,
367 commands::profiles_cmd::profile_validate,
368 commands::profiles_cmd::keychain_fallback_unlock,
369 commands::inspector_cmd::bucket_inspect,
370 commands::inspector_cmd::object_inspect,
371 commands::inspector_cmd::object_head,
372 commands::inspector_cmd::capability_get,
373 commands::inspector_cmd::capability_clear,
374 commands::locks_cmd::locks_list,
375 commands::locks_cmd::lock_release_stale,
376 commands::buckets_cmd::buckets_list,
377 commands::buckets_cmd::bucket_region_get,
378 commands::objects_cmd::objects_list,
379 commands::objects_cmd::objects_list_flat,
380 commands::objects_cmd::object_copy,
381 commands::objects_cmd::object_move,
382 commands::objects_cmd::object_create_folder,
383 commands::objects_cmd::object_delete_batch,
384 commands::objects_cmd::object_set_metadata,
385 commands::objects_cmd::object_set_tags,
386 commands::objects_cmd::object_presign,
387 commands::objects_cmd::cross_account_confirm,
388 commands::transfers_cmd::transfer_download,
389 commands::transfers_cmd::transfer_upload,
390 commands::transfers_cmd::transfer_list,
391 commands::transfers_cmd::transfer_cancel,
392 commands::transfers_cmd::transfer_retry,
393 commands::transfers_cmd::transfer_upload_many,
394 commands::transfers_cmd::transfer_download_many,
395 commands::transfers_cmd::multipart_scan,
396 commands::transfers_cmd::multipart_abort,
397 commands::diff_cmd::diff_preview_create,
398 commands::diff_cmd::diff_preview_cancel,
399 commands::objects_cmd::object_set_storage_class,
400 commands::media_cmd::media_register,
401 commands::media_cmd::media_revoke,
402 commands::objects_cmd::object_get_text,
403 commands::objects_cmd::object_get_bytes,
404 commands::objects_cmd::object_put_text,
405 commands::search_cmd::search_local_filter,
406 commands::search_cmd::search_prefix,
407 commands::search_cmd::search_cancel,
408 commands::bookmarks_cmd::bookmarks_list,
409 commands::bookmarks_cmd::bookmark_add,
410 commands::bookmarks_cmd::bookmark_remove,
411 commands::bookmarks_cmd::bookmark_update,
412 commands::bookmarks_cmd::recents_list,
413 commands::bookmarks_cmd::recent_track,
414 commands::bookmarks_cmd::recents_clear,
415 commands::updater_cmd::updater_check,
416 commands::updater_cmd::updater_install,
417 commands::diagnostics_cmd::diagnostics_collect,
418 commands::diagnostics_cmd::diagnostics_export,
419 ])
420 .run(tauri::generate_context!())
421 .expect("error while running tauri application");
422}
423
424#[cfg(test)]
425mod tests {
426 use tauri_plugin_dialog::init as dialog_init;
429 use tauri_plugin_fs::init as fs_init;
430 use tauri_plugin_notification::init as notification_init;
431 use tauri_plugin_shell::init as shell_init;
432
433 #[test]
434 fn plugin_init_symbols_are_reachable() {
435 type R = tauri::Wry;
439 let _ = fs_init::<R>;
440 let _ = dialog_init::<R>;
441 let _ = shell_init::<R>;
442 let _ = notification_init::<R>;
443 }
444
445 #[test]
446 fn updater_plugin_builder_is_constructible() {
447 let _builder = tauri_plugin_updater::Builder::new();
451 }
452}