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 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 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 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 Paths::scaffold(paths.documents_dir()).await?;
154 }
155 BackendTarget::Database(_, client) => {
156 if options.apply_migrations {
157 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 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 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 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 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 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 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 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 #[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}