1#![deny(missing_docs)]
2#![forbid(unsafe_code)]
3#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))]
4mod 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
40pub(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
59pub struct InferOptions {
61 pub use_database_when_accounts_empty: bool,
63 pub apply_migrations: bool,
65 #[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#[derive(Clone)]
83pub enum BackendTarget {
84 FileSystem(Arc<Paths>),
86 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 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 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 Paths::scaffold(paths.documents_dir()).await?;
137 }
138 BackendTarget::Database(_, client) => {
139 if options.apply_migrations {
140 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 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 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 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 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 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 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 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 #[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}