diff --git a/src/secret/mod.rs b/src/secret/mod.rs index 9a5287d7..3c908ab6 100644 --- a/src/secret/mod.rs +++ b/src/secret/mod.rs @@ -1,6 +1,6 @@ //! API to store the data of a session in a secret store on the system. -use std::{borrow::Cow, fmt, path::PathBuf}; +use std::{fmt, path::PathBuf}; use gtk::glib; use matrix_sdk::{ @@ -28,7 +28,11 @@ pub use self::linux::{restore_sessions, store_session}; use self::unimplemented::delete_session; #[cfg(not(target_os = "linux"))] pub use self::unimplemented::{restore_sessions, store_session}; -use crate::{application::AppProfile, prelude::*, spawn_tokio, GETTEXT_PACKAGE, PROFILE}; +use crate::{ + prelude::*, + spawn_tokio, + utils::{data_dir_path, DataType}, +}; /// The length of a session ID, in chars or bytes as the string is ASCII. pub const SESSION_ID_LENGTH: usize = 8; @@ -93,7 +97,7 @@ impl StoredSession { // Generate a unique random session ID. let mut id = None; - let data_path = db_dir_path(DbContentType::Data); + let data_path = data_dir_path(DataType::Persistent); // Try 10 times, so we do not have an infinite loop. for _ in 0..10 { @@ -137,14 +141,14 @@ impl StoredSession { /// The path where the persistent data of this session lives. pub fn data_path(&self) -> PathBuf { - let mut path = db_dir_path(DbContentType::Data); + let mut path = data_dir_path(DataType::Persistent); path.push(&self.id); path } /// The path where the cached data of this session lives. pub fn cache_path(&self) -> PathBuf { - let mut path = db_dir_path(DbContentType::Cache); + let mut path = data_dir_path(DataType::Cache); path.push(&self.id); path } @@ -179,28 +183,3 @@ pub struct Secret { /// The passphrase used to encrypt the local databases. pub passphrase: String, } - -/// The path of the directory where a database should be stored, depending on -/// the type of content. -fn db_dir_path(content_type: DbContentType) -> PathBuf { - let dir_name = match PROFILE { - AppProfile::Stable => Cow::Borrowed(GETTEXT_PACKAGE), - _ => Cow::Owned(format!("{GETTEXT_PACKAGE}-{PROFILE}")), - }; - - let mut path = match content_type { - DbContentType::Data => glib::user_data_dir(), - DbContentType::Cache => glib::user_cache_dir(), - }; - path.push(dir_name.as_ref()); - - path -} - -/// The type of content of a database. -enum DbContentType { - /// Data that should not be deleted. - Data, - /// Cache that can be deleted freely. - Cache, -} diff --git a/src/session_list/mod.rs b/src/session_list/mod.rs index 5c3f1050..a7798244 100644 --- a/src/session_list/mod.rs +++ b/src/session_list/mod.rs @@ -1,4 +1,4 @@ -use std::cmp::Ordering; +use std::{cmp::Ordering, ffi::OsString}; use gettextrs::gettext; use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*}; @@ -16,7 +16,7 @@ use crate::{ secret::{self, StoredSession}, session::model::{Session, SessionState}, spawn, spawn_tokio, - utils::LoadingState, + utils::{data_dir_path, DataType, LoadingState}, }; mod imp { @@ -222,58 +222,121 @@ impl SessionList { self.set_state(LoadingState::Loading); let handle = spawn_tokio!(secret::restore_sessions()); - match handle.await.unwrap() { - Ok(mut sessions) => { - let settings = self.settings(); - settings.load(); - let session_ids = settings.session_ids(); - - // Keep the order from the settings. - sessions.sort_by(|a, b| { - let pos_a = session_ids.get_index_of(&a.id); - let pos_b = session_ids.get_index_of(&b.id); - - match (pos_a, pos_b) { - (Some(pos_a), Some(pos_b)) => pos_a.cmp(&pos_b), - // Keep unknown sessions at the end. - (Some(_), None) => Ordering::Greater, - (None, Some(_)) => Ordering::Less, - _ => Ordering::Equal, - } - }); - - for stored_session in sessions { - info!( - "Restoring previous session {} for user {}", - stored_session.id, stored_session.user_id, - ); - self.insert(NewSession::new(stored_session.clone())); - - spawn!( - glib::Priority::DEFAULT_IDLE, - clone!( - #[weak(rename_to = obj)] - self, - async move { - obj.restore_stored_session(stored_session).await; - } - ) - ); - } + let mut sessions = match handle.await.unwrap() { + Ok(sessions) => sessions, + Err(error) => { + let message = format!( + "{}\n\n{}", + gettext("Could not restore previous sessions"), + error.to_user_facing(), + ); - self.set_state(LoadingState::Ready) + self.set_error(message); + self.set_state(LoadingState::Error); + return; + } + }; + + let settings = self.settings(); + settings.load(); + let session_ids = settings.session_ids(); + + // Keep the order from the settings. + sessions.sort_by(|a, b| { + let pos_a = session_ids.get_index_of(&a.id); + let pos_b = session_ids.get_index_of(&b.id); + + match (pos_a, pos_b) { + (Some(pos_a), Some(pos_b)) => pos_a.cmp(&pos_b), + // Keep unknown sessions at the end. + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + _ => Ordering::Equal, } + }); + + // Get the directories present in the data path to only restore sessions with + // data on the system. This is necessary for users sharing their secrets between + // devices. + let mut directories = match self.data_directories(sessions.len()).await { + Ok(directories) => directories, Err(error) => { + error!("Could not access data directory: {error}"); let message = format!( "{}\n\n{}", gettext("Could not restore previous sessions"), - error.to_user_facing(), + gettext("An unexpected error happened while accessing the data directory"), ); self.set_error(message); self.set_state(LoadingState::Error); + return; } + }; + + for stored_session in sessions { + if let Some(pos) = directories + .iter() + .position(|dir_name| dir_name == stored_session.id.as_str()) + { + directories.swap_remove(pos); + info!( + "Restoring previous session {} for user {}", + stored_session.id, stored_session.user_id, + ); + self.insert(NewSession::new(stored_session.clone())); + + spawn!( + glib::Priority::DEFAULT_IDLE, + clone!( + #[weak(rename_to = obj)] + self, + async move { + obj.restore_stored_session(stored_session).await; + } + ) + ); + } else { + info!( + "Ignoring session {} for user {}: no data directory", + stored_session.id, stored_session.user_id, + ); + } + } + + self.set_state(LoadingState::Ready) + } + + /// The list of directories in the data directory. + async fn data_directories(&self, capacity: usize) -> std::io::Result> { + let data_path = data_dir_path(DataType::Persistent); + + if !data_path.try_exists()? { + return Ok(Vec::new()); } + + spawn_tokio!(async move { + let mut read_dir = tokio::fs::read_dir(data_path).await?; + let mut directories = Vec::with_capacity(capacity); + + loop { + let Some(entry) = read_dir.next_entry().await? else { + // We are at the end of the list. + break; + }; + + if !entry.file_type().await?.is_dir() { + // We are only interested in directories. + continue; + } + + directories.push(entry.file_name()); + } + + std::io::Result::Ok(directories) + }) + .await + .expect("task was not aborted") } /// Restore a stored session. diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 9a1ebd0f..06f8a7e5 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -14,8 +14,10 @@ pub mod string; pub mod template_callbacks; use std::{ + borrow::Cow, cell::{Cell, OnceCell, RefCell}, fmt, + path::PathBuf, rc::{Rc, Weak}, }; @@ -33,7 +35,32 @@ pub use self::{ location::{Location, LocationError, LocationExt}, single_item_list_model::SingleItemListModel, }; -use crate::RUNTIME; +use crate::{AppProfile, GETTEXT_PACKAGE, PROFILE, RUNTIME}; + +/// The path of the directory where data should be stored, depending on its +/// type. +pub fn data_dir_path(data_type: DataType) -> PathBuf { + let dir_name = match PROFILE { + AppProfile::Stable => Cow::Borrowed(GETTEXT_PACKAGE), + _ => Cow::Owned(format!("{GETTEXT_PACKAGE}-{PROFILE}")), + }; + + let mut path = match data_type { + DataType::Persistent => glib::user_data_dir(), + DataType::Cache => glib::user_cache_dir(), + }; + path.push(dir_name.as_ref()); + + path +} + +/// The type of data. +pub enum DataType { + /// Data that should not be deleted. + Persistent, + /// Cache that can be deleted freely. + Cache, +} pub enum TimeoutFuture { Timeout,