sos_backend/
lib.rs

1#![deny(missing_docs)]
2#![forbid(unsafe_code)]
3#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))]
4//! Backend database and file system storage.
5mod access_point;
6#[cfg(feature = "archive")]
7pub mod archive;
8#[cfg(feature = "audit")]
9pub mod audit;
10pub mod compact;
11mod error;
12mod event_log;
13mod folder;
14mod helpers;
15#[cfg(feature = "preferences")]
16mod preferences;
17mod server_origins;
18#[cfg(feature = "system-messages")]
19mod system_messages;
20mod vault_writer;
21
22pub use access_point::BackendAccessPoint as AccessPoint;
23pub use error::{Error, StorageError};
24pub use event_log::{
25    AccountEventLog, BackendEventLog, DeviceEventLog, FolderEventLog,
26};
27pub use folder::Folder;
28pub use helpers::extract_vault;
29#[cfg(feature = "preferences")]
30pub use preferences::BackendPreferences as Preferences;
31pub use server_origins::ServerOrigins;
32pub use sos_database as database;
33#[cfg(feature = "system-messages")]
34pub use system_messages::SystemMessages;
35pub use vault_writer::VaultWriter;
36
37#[cfg(feature = "files")]
38pub use event_log::FileEventLog;
39
40/// Result type for the library.
41pub(crate) type Result<T> = std::result::Result<T, Error>;
42
43use sos_core::{decode, AccountId, Paths, PublicIdentity};
44use sos_database::{
45    async_sqlite::Client,
46    entity::{
47        AccountEntity, AccountRecord, FolderEntity, FolderRecord,
48        SecretRecord,
49    },
50    open_file,
51};
52use sos_vault::{Summary, Vault};
53use sos_vfs as vfs;
54use std::{fmt, sync::Arc};
55
56#[cfg(feature = "files")]
57use {indexmap::IndexSet, sos_core::ExternalFile};
58
59/// Options for backend target inference.
60pub struct InferOptions {
61    /// Select the database backend when no accounts.
62    pub use_database_when_accounts_empty: bool,
63    /// Apply database migrations.
64    pub apply_migrations: bool,
65    /// Select the backend target audit provider.
66    #[cfg(feature = "audit")]
67    pub select_audit_provider: bool,
68}
69
70impl Default for InferOptions {
71    fn default() -> Self {
72        Self {
73            use_database_when_accounts_empty: true,
74            apply_migrations: true,
75            #[cfg(feature = "audit")]
76            select_audit_provider: true,
77        }
78    }
79}
80
81/// Target backend.
82#[derive(Clone)]
83pub enum BackendTarget {
84    /// File system backend
85    FileSystem(Arc<Paths>),
86    /// Database backend.
87    Database(Arc<Paths>, Client),
88}
89
90impl fmt::Display for BackendTarget {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        write!(f, "{}", {
93            match self {
94                Self::FileSystem(_) => "filesystem",
95                Self::Database(_, _) => "database",
96            }
97        })
98    }
99}
100
101impl BackendTarget {
102    /// Infer and initialize a new backend target.
103    ///
104    /// A database backend will be used if a database file already
105    /// exists or if there are no accounts. If a database backend
106    /// is selected migrations are run otherwise paths are scaffolded
107    /// for the file system backend.
108    ///
109    /// If the `audit` feature is enabled the corresponding audit
110    /// provider for the backend is initialized.
111    pub async fn infer<T: AsRef<Paths>>(
112        paths: T,
113        options: InferOptions,
114    ) -> Result<Self> {
115        let target = BackendTarget::from_paths(paths).await?;
116
117        let mut target = if options.use_database_when_accounts_empty {
118            // If there are zero accounts select the database backend
119            let accounts = target.list_accounts().await?;
120            if accounts.is_empty() {
121                let paths = target.paths().clone();
122                let client =
123                    open_file(paths.as_ref().database_file()).await?;
124                BackendTarget::Database(paths, client)
125            } else {
126                target
127            }
128        } else {
129            target
130        };
131
132        match &mut target {
133            BackendTarget::FileSystem(paths) => {
134                // File system accounts must have
135                // the directories scaffolded
136                Paths::scaffold(paths.documents_dir()).await?;
137            }
138            BackendTarget::Database(_, client) => {
139                if options.apply_migrations {
140                    // Database backend must run migrations
141                    crate::database::migrations::migrate_client(client)
142                        .await?;
143                }
144            }
145        };
146
147        #[cfg(feature = "audit")]
148        if options.select_audit_provider {
149            let provider = match &target {
150                BackendTarget::FileSystem(paths) => {
151                    crate::audit::new_fs_provider(
152                        paths.audit_file().to_owned(),
153                    )
154                }
155                BackendTarget::Database(_, client) => {
156                    crate::audit::new_db_provider(client.clone())
157                }
158            };
159            crate::audit::init_providers(vec![provider]);
160        }
161
162        Ok(target)
163    }
164
165    /// Trace information about the backend.
166    ///
167    /// Typically used when an application starts.
168    pub async fn dump_info(&self) -> Result<()> {
169        tracing::debug!(
170            backend_target = %self,
171            data_dir = %self.paths().documents_dir().display(),
172            "backend::dump_info",
173        );
174
175        match self {
176            Self::Database(_, client) => {
177                let (sqlite_version, compile_options) = client
178                    .conn_and_then(|conn| {
179                        conn.execute("PRAGMA foreign_keys = ON", [])?;
180                        let version: String = conn.query_row(
181                            "SELECT sqlite_version()",
182                            [],
183                            |row| row.get(0),
184                        )?;
185
186                        let mut stmt =
187                            conn.prepare("PRAGMA compile_options")?;
188                        let mut compile_options = Vec::new();
189                        let rows = stmt
190                            .query_map([], |row| row.get::<_, String>(0))?;
191                        for option in rows {
192                            compile_options.push(option?);
193                        }
194
195                        Ok::<_, sos_database::Error>((
196                            version,
197                            compile_options,
198                        ))
199                    })
200                    .await?;
201                tracing::debug!(
202                    version = %sqlite_version,
203                    compile_options = ?compile_options,
204                    "backend::dump_info::sqlite",
205                );
206            }
207            _ => {}
208        }
209        Ok(())
210    }
211
212    /// Create a backend target from paths.
213    pub async fn from_paths<T: AsRef<Paths>>(
214        paths: T,
215    ) -> Result<BackendTarget> {
216        Ok(if paths.as_ref().is_using_db() {
217            let client = open_file(paths.as_ref().database_file()).await?;
218            BackendTarget::Database(Arc::new(paths.as_ref().clone()), client)
219        } else {
220            BackendTarget::FileSystem(Arc::new(paths.as_ref().clone()))
221        })
222    }
223
224    /// Paths for the backend target.
225    pub fn paths(&self) -> Arc<Paths> {
226        match self {
227            Self::FileSystem(paths) => paths.clone(),
228            Self::Database(paths, _) => paths.clone(),
229        }
230    }
231
232    /// Read the device vault.
233    pub async fn read_device_vault(
234        &self,
235        account_id: &AccountId,
236    ) -> Result<Option<Vault>> {
237        match self {
238            BackendTarget::FileSystem(paths) => {
239                if vfs::try_exists(paths.device_file()).await? {
240                    let buffer = vfs::read(paths.device_file()).await?;
241                    let vault: Vault = decode(&buffer).await?;
242                    Ok(Some(vault))
243                } else {
244                    Ok(None)
245                }
246            }
247            BackendTarget::Database(_, client) => {
248                let account_id = *account_id;
249                let device_folder = client
250                    .conn_and_then(move |conn| {
251                        let account = AccountEntity::new(&conn);
252                        let folder = FolderEntity::new(&conn);
253                        let account_row = account.find_one(&account_id)?;
254                        let device_folder =
255                            folder.find_device_folder(account_row.row_id)?;
256                        let secrets = if let Some(device_folder) =
257                            &device_folder
258                        {
259                            Some(folder.load_secrets(device_folder.row_id)?)
260                        } else {
261                            None
262                        };
263                        Ok::<_, sos_database::Error>(
264                            device_folder.zip(secrets),
265                        )
266                    })
267                    .await?;
268
269                if let Some((folder, secret_rows)) = device_folder {
270                    let record = FolderRecord::from_row(folder).await?;
271                    let mut vault = record.into_vault()?;
272                    for row in secret_rows {
273                        let record = SecretRecord::from_row(row).await?;
274                        let SecretRecord {
275                            secret_id, commit, ..
276                        } = record;
277                        vault.insert_entry(secret_id, commit);
278                    }
279                    Ok(Some(vault))
280                } else {
281                    Ok(None)
282                }
283            }
284        }
285    }
286
287    /// Set paths to be for an account identifier.
288    pub fn with_account_id(self, account_id: &AccountId) -> Self {
289        match self {
290            Self::FileSystem(paths) => {
291                Self::FileSystem(paths.with_account_id(account_id))
292            }
293            Self::Database(paths, client) => {
294                Self::Database(paths.with_account_id(account_id), client)
295            }
296        }
297    }
298
299    /// List accounts.
300    pub async fn list_accounts(&self) -> Result<Vec<PublicIdentity>> {
301        match self {
302            BackendTarget::FileSystem(paths) => {
303                Ok(sos_vault::list_accounts(Some(paths)).await?)
304            }
305            BackendTarget::Database(_, client) => {
306                let account_rows = client
307                    .conn_and_then(move |conn| {
308                        let account = AccountEntity::new(&conn);
309                        account.list_accounts()
310                    })
311                    .await?;
312                let mut accounts = Vec::new();
313                for row in account_rows {
314                    let record: AccountRecord = row.try_into()?;
315                    accounts.push(record.identity);
316                }
317                Ok(accounts)
318            }
319        }
320    }
321
322    /// List user folders for an account.
323    pub async fn list_folders(
324        &self,
325        account_id: &AccountId,
326    ) -> Result<Vec<Summary>> {
327        match self {
328            BackendTarget::FileSystem(paths) => {
329                let paths = paths.with_account_id(account_id);
330                Ok(sos_vault::list_local_folders(&paths)
331                    .await?
332                    .into_iter()
333                    .map(|(s, _)| s)
334                    .collect())
335            }
336            BackendTarget::Database(_, client) => {
337                let account_id = *account_id;
338                let folder_rows = client
339                    .conn_and_then(move |conn| {
340                        let account = AccountEntity::new(&conn);
341                        let folders = FolderEntity::new(&conn);
342                        let account_row = account.find_one(&account_id)?;
343                        folders.list_user_folders(account_row.row_id)
344                    })
345                    .await?;
346                let mut folders = Vec::new();
347                for row in folder_rows {
348                    let record = FolderRecord::from_row(row).await?;
349                    folders.push(record.summary);
350                }
351                Ok(folders)
352            }
353        }
354    }
355
356    /// List external files for this backend target.
357    #[cfg(feature = "files")]
358    pub async fn list_files(&self) -> Result<IndexSet<ExternalFile>> {
359        Ok(match self {
360            BackendTarget::FileSystem(paths) => {
361                sos_external_files::list_external_files(paths).await?
362            }
363            BackendTarget::Database(paths, _) => {
364                sos_external_files::list_external_files(paths).await?
365            }
366        })
367    }
368}