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 fmt::Debug for BackendTarget {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        write!(f, "{}", {
104            match self {
105                Self::FileSystem(paths) => format!(
106                    "file:{}",
107                    paths.documents_dir().to_string_lossy()
108                ),
109                Self::Database(paths, _) => format!(
110                    "sqlite:{}",
111                    paths.database_file().to_string_lossy()
112                ),
113            }
114        })
115    }
116}
117
118impl BackendTarget {
119    /// Infer and initialize a new backend target.
120    ///
121    /// A database backend will be used if a database file already
122    /// exists or if there are no accounts. If a database backend
123    /// is selected migrations are run otherwise paths are scaffolded
124    /// for the file system backend.
125    ///
126    /// If the `audit` feature is enabled the corresponding audit
127    /// provider for the backend is initialized.
128    pub async fn infer<T: AsRef<Paths>>(
129        paths: T,
130        options: InferOptions,
131    ) -> Result<Self> {
132        let target = BackendTarget::from_paths(paths).await?;
133
134        let mut target = if options.use_database_when_accounts_empty {
135            // If there are zero accounts select the database backend
136            let accounts = target.list_accounts().await?;
137            if accounts.is_empty() {
138                let paths = target.paths().clone();
139                let client =
140                    open_file(paths.as_ref().database_file()).await?;
141                BackendTarget::Database(paths, client)
142            } else {
143                target
144            }
145        } else {
146            target
147        };
148
149        match &mut target {
150            BackendTarget::FileSystem(paths) => {
151                // File system accounts must have
152                // the directories scaffolded
153                Paths::scaffold(paths.documents_dir()).await?;
154            }
155            BackendTarget::Database(_, client) => {
156                if options.apply_migrations {
157                    // Database backend must run migrations
158                    crate::database::migrations::migrate_client(client)
159                        .await?;
160                }
161            }
162        };
163
164        #[cfg(feature = "audit")]
165        if options.select_audit_provider {
166            let provider = match &target {
167                BackendTarget::FileSystem(paths) => {
168                    crate::audit::new_fs_provider(
169                        paths.audit_file().to_owned(),
170                    )
171                }
172                BackendTarget::Database(_, client) => {
173                    crate::audit::new_db_provider(client.clone())
174                }
175            };
176            crate::audit::init_providers(vec![provider]);
177        }
178
179        Ok(target)
180    }
181
182    /// Trace information about the backend.
183    ///
184    /// Typically used when an application starts.
185    pub async fn dump_info(&self) -> Result<()> {
186        tracing::debug!(
187            backend_target = %self,
188            "backend::dump_info",
189        );
190
191        match self {
192            Self::Database(_, client) => {
193                let (sqlite_version, compile_options) = client
194                    .conn_and_then(|conn| {
195                        conn.execute("PRAGMA foreign_keys = ON", [])?;
196                        let version: String = conn.query_row(
197                            "SELECT sqlite_version()",
198                            [],
199                            |row| row.get(0),
200                        )?;
201
202                        let mut stmt =
203                            conn.prepare("PRAGMA compile_options")?;
204                        let mut compile_options = Vec::new();
205                        let rows = stmt
206                            .query_map([], |row| row.get::<_, String>(0))?;
207                        for option in rows {
208                            compile_options.push(option?);
209                        }
210
211                        Ok::<_, sos_database::Error>((
212                            version,
213                            compile_options,
214                        ))
215                    })
216                    .await?;
217                tracing::debug!(
218                    version = %sqlite_version,
219                    compile_options = ?compile_options,
220                    "backend::dump_info::sqlite",
221                );
222            }
223            _ => {}
224        }
225        Ok(())
226    }
227
228    /// Create a backend target from paths.
229    pub async fn from_paths<T: AsRef<Paths>>(
230        paths: T,
231    ) -> Result<BackendTarget> {
232        Ok(if paths.as_ref().is_using_db() {
233            let client = open_file(paths.as_ref().database_file()).await?;
234            BackendTarget::Database(Arc::new(paths.as_ref().clone()), client)
235        } else {
236            BackendTarget::FileSystem(Arc::new(paths.as_ref().clone()))
237        })
238    }
239
240    /// Paths for the backend target.
241    pub fn paths(&self) -> Arc<Paths> {
242        match self {
243            Self::FileSystem(paths) => paths.clone(),
244            Self::Database(paths, _) => paths.clone(),
245        }
246    }
247
248    /// Read the device vault.
249    pub async fn read_device_vault(
250        &self,
251        account_id: &AccountId,
252    ) -> Result<Option<Vault>> {
253        match self {
254            BackendTarget::FileSystem(paths) => {
255                if vfs::try_exists(paths.device_file()).await? {
256                    let buffer = vfs::read(paths.device_file()).await?;
257                    let vault: Vault = decode(&buffer).await?;
258                    Ok(Some(vault))
259                } else {
260                    Ok(None)
261                }
262            }
263            BackendTarget::Database(_, client) => {
264                let account_id = *account_id;
265                let device_folder = client
266                    .conn_and_then(move |conn| {
267                        let account = AccountEntity::new(&conn);
268                        let folder = FolderEntity::new(&conn);
269                        let account_row = account.find_one(&account_id)?;
270                        let device_folder =
271                            folder.find_device_folder(account_row.row_id)?;
272                        let secrets = if let Some(device_folder) =
273                            &device_folder
274                        {
275                            Some(folder.load_secrets(device_folder.row_id)?)
276                        } else {
277                            None
278                        };
279                        Ok::<_, sos_database::Error>(
280                            device_folder.zip(secrets),
281                        )
282                    })
283                    .await?;
284
285                if let Some((folder, secret_rows)) = device_folder {
286                    let record = FolderRecord::from_row(folder).await?;
287                    let mut vault = record.into_vault()?;
288                    for row in secret_rows {
289                        let record = SecretRecord::from_row(row).await?;
290                        let SecretRecord {
291                            secret_id, commit, ..
292                        } = record;
293                        vault.insert_entry(secret_id, commit);
294                    }
295                    Ok(Some(vault))
296                } else {
297                    Ok(None)
298                }
299            }
300        }
301    }
302
303    /// Set paths to be for an account identifier.
304    pub fn with_account_id(self, account_id: &AccountId) -> Self {
305        match self {
306            Self::FileSystem(paths) => {
307                Self::FileSystem(paths.with_account_id(account_id))
308            }
309            Self::Database(paths, client) => {
310                Self::Database(paths.with_account_id(account_id), client)
311            }
312        }
313    }
314
315    /// List accounts.
316    pub async fn list_accounts(&self) -> Result<Vec<PublicIdentity>> {
317        match self {
318            BackendTarget::FileSystem(paths) => {
319                Ok(sos_vault::list_accounts(Some(paths)).await?)
320            }
321            BackendTarget::Database(_, client) => {
322                let account_rows = client
323                    .conn_and_then(move |conn| {
324                        let account = AccountEntity::new(&conn);
325                        account.list_accounts()
326                    })
327                    .await?;
328                let mut accounts = Vec::new();
329                for row in account_rows {
330                    let record: AccountRecord = row.try_into()?;
331                    accounts.push(record.identity);
332                }
333                Ok(accounts)
334            }
335        }
336    }
337
338    /// List user folders for an account.
339    pub async fn list_folders(
340        &self,
341        account_id: &AccountId,
342    ) -> Result<Vec<Summary>> {
343        match self {
344            BackendTarget::FileSystem(paths) => {
345                let paths = paths.with_account_id(account_id);
346                Ok(sos_vault::list_local_folders(&paths)
347                    .await?
348                    .into_iter()
349                    .map(|(s, _)| s)
350                    .collect())
351            }
352            BackendTarget::Database(_, client) => {
353                let account_id = *account_id;
354                let folder_rows = client
355                    .conn_and_then(move |conn| {
356                        let account = AccountEntity::new(&conn);
357                        let folders = FolderEntity::new(&conn);
358                        let account_row = account.find_one(&account_id)?;
359                        folders.list_user_folders(account_row.row_id)
360                    })
361                    .await?;
362                let mut folders = Vec::new();
363                for row in folder_rows {
364                    let record = FolderRecord::from_row(row).await?;
365                    folders.push(record.summary);
366                }
367                Ok(folders)
368            }
369        }
370    }
371
372    /// List external files for this backend target.
373    #[cfg(feature = "files")]
374    pub async fn list_files(&self) -> Result<IndexSet<ExternalFile>> {
375        Ok(match self {
376            BackendTarget::FileSystem(paths) => {
377                sos_external_files::list_external_files(paths).await?
378            }
379            BackendTarget::Database(paths, _) => {
380                sos_external_files::list_external_files(paths).await?
381            }
382        })
383    }
384}