sos_core/
paths.rs

1//! File system paths for application level folders
2//! and user-specific account folders.
3use crate::{
4    constants::{
5        ACCOUNT_EVENTS, APP_AUTHOR, APP_NAME, AUDIT_FILE_NAME, BLOBS_DIR,
6        DATABASE_FILE, DEVICE_EVENTS, DEVICE_FILE, EVENT_LOG_EXT, FILES_DIR,
7        FILE_EVENTS, IDENTITY_DIR, JSON_EXT, LOCAL_DIR, LOGS_DIR,
8        PREFERENCES_FILE, REMOTES_FILE, REMOTE_DIR, SYSTEM_MESSAGES_FILE,
9        VAULTS_DIR, VAULT_EXT,
10    },
11    AccountId, ExternalFile, ExternalFileName, Result, SecretId, VaultId,
12};
13#[cfg(not(target_arch = "wasm32"))]
14use etcetera::{
15    app_strategy::choose_native_strategy, AppStrategy, AppStrategyArgs,
16};
17use once_cell::sync::Lazy;
18use serde::{Deserialize, Serialize};
19use sos_vfs as vfs;
20use std::{
21    path::{Path, PathBuf},
22    sync::{Arc, RwLock},
23};
24
25static DATA_DIR: Lazy<RwLock<Option<PathBuf>>> =
26    Lazy::new(|| RwLock::new(None));
27
28/// File system paths.
29///
30/// Clients and servers may be configured to run on the same machine
31/// and point to the same data directory so different prefixes are
32/// used to distinguish.
33///
34/// Clients write to a `local` directory whilst servers write to a
35/// `remote` directory.
36///
37/// Several functions require a user identifier and will panic if
38/// a user identifier has not been set, see the function documentation
39/// for details.
40#[derive(Default, Debug, Clone, Serialize, Deserialize)]
41pub struct Paths {
42    /// Whether these paths are for server storage.
43    server: bool,
44    /// User identifier.
45    account_id: Option<AccountId>,
46    /// Top-level documents folder.
47    documents_dir: PathBuf,
48    /// Directory for application logs.
49    logs_dir: PathBuf,
50
51    /*
52     * v1 paths (filesystem)
53     */
54    /// Directory for identity vaults.
55    identity_dir: PathBuf,
56    /// Directory for local storage.
57    local_dir: PathBuf,
58    /// File for local audit logs.
59    audit_file: PathBuf,
60    /// User segregated storage.
61    user_dir: PathBuf,
62    /// User file storage.
63    files_dir: PathBuf,
64    /// User vault storage.
65    vaults_dir: PathBuf,
66    /// User devices storage.
67    device_file: PathBuf,
68
69    /*
70     * v2 paths (database)
71     */
72    /// Database file.
73    database_file: PathBuf,
74    /// External file blob storage.
75    blobs_dir: PathBuf,
76}
77
78impl Paths {
79    /// Create paths for a client without an account identifier.
80    pub fn new_client(documents_dir: impl AsRef<Path>) -> Arc<Self> {
81        Arc::new(Self::new_with_prefix(false, documents_dir, None, LOCAL_DIR))
82    }
83
84    /// Create paths for a server without an account identifier.
85    pub fn new_server(documents_dir: impl AsRef<Path>) -> Arc<Self> {
86        Arc::new(Self::new_with_prefix(true, documents_dir, None, REMOTE_DIR))
87    }
88
89    fn new_with_prefix(
90        server: bool,
91        documents_dir: impl AsRef<Path>,
92        account_id: Option<&AccountId>,
93        prefix: impl AsRef<Path>,
94    ) -> Self {
95        let documents_dir = documents_dir.as_ref().to_path_buf();
96        let local_dir = documents_dir.join(prefix);
97        let logs_dir = documents_dir.join(LOGS_DIR);
98        let identity_dir = documents_dir.join(IDENTITY_DIR);
99        let audit_file = local_dir.join(AUDIT_FILE_NAME);
100        let user_dir = local_dir
101            .join(account_id.map(|id| id.to_string()).unwrap_or_default());
102
103        let files_dir = user_dir.join(FILES_DIR);
104        let vaults_dir = user_dir.join(VAULTS_DIR);
105        let device_file =
106            user_dir.join(format!("{}.{}", DEVICE_FILE, VAULT_EXT));
107
108        // Version 2 of the account storage backend (SQLite)
109        let blobs_dir = documents_dir.join(BLOBS_DIR);
110        let database_file = documents_dir.join(DATABASE_FILE);
111
112        Self {
113            server,
114            account_id: account_id.cloned(),
115            documents_dir,
116            database_file,
117            blobs_dir,
118            logs_dir,
119
120            identity_dir,
121            local_dir,
122            audit_file,
123            user_dir,
124            files_dir,
125            vaults_dir,
126            device_file,
127        }
128    }
129
130    /// Whether these paths are for server-side storage.
131    pub fn is_server(&self) -> bool {
132        self.server
133    }
134
135    /// Clone of paths with an account identifier.
136    pub fn with_account_id(&self, account_id: &AccountId) -> Arc<Self> {
137        Arc::new(Self::new_with_prefix(
138            self.server,
139            &self.documents_dir,
140            Some(account_id),
141            if self.server { REMOTE_DIR } else { LOCAL_DIR },
142        ))
143    }
144
145    /// Ensure the local storage directory exists.
146    ///
147    /// If a user identifier is available this will
148    /// also create some user-specific directories.
149    pub async fn ensure(&self) -> Result<()> {
150        // Version 1 needs to local/remote directory
151        vfs::create_dir_all(&self.local_dir).await?;
152
153        if !self.is_global() {
154            // Version 1 file system - needs to be removed eventually
155            vfs::create_dir_all(&self.user_dir).await?;
156            vfs::create_dir_all(&self.files_dir).await?;
157            vfs::create_dir_all(&self.vaults_dir).await?;
158        }
159        Ok(())
160    }
161
162    /// Ensure the local storage directories exist
163    /// for version 2 database storage.
164    pub async fn ensure_db(&self) -> Result<()> {
165        // Version 2 just needs the blobs directory
166        vfs::create_dir_all(&self.blobs_dir).await?;
167
168        if !self.is_global() {
169            // Version 2 database backend needs a blobs folder
170            // for the account
171            vfs::create_dir_all(self.blobs_account_dir()).await?;
172        }
173        Ok(())
174    }
175
176    /// Determine if a database file exists.
177    pub fn is_using_db(&self) -> bool {
178        self.database_file().exists()
179    }
180
181    /// Try to determine if the account is ready to be used
182    /// by checking for the presence of required files on disc.
183    pub async fn is_usable(&self) -> Result<bool> {
184        assert!(!self.is_global(), "not accessible for global paths");
185        if self.is_using_db() {
186            Ok(vfs::try_exists(self.database_file()).await?)
187        } else {
188            Ok(vfs::try_exists(self.identity_vault()).await?
189                && vfs::try_exists(self.identity_events()).await?
190                && vfs::try_exists(self.account_events()).await?
191                && vfs::try_exists(self.device_events()).await?)
192        }
193    }
194
195    /// Expected directory for external file blobs
196    /// for an account.
197    ///
198    /// # Panics
199    ///
200    /// If this set of paths are global (no user identifier).
201    pub fn into_files_dir(&self) -> PathBuf {
202        assert!(!self.is_global(), "not accessible for global paths");
203        if self.is_using_db() {
204            self.blobs_account_dir()
205        } else {
206            self.files_dir().to_owned()
207        }
208    }
209
210    /// Expected location for the directory containing
211    /// all the external files for a folder.
212    ///
213    /// # Panics
214    ///
215    /// If this set of paths are global (no user identifier).
216    pub fn into_file_folder_path(&self, folder_id: &VaultId) -> PathBuf {
217        self.into_files_dir().join(folder_id.to_string())
218    }
219
220    /// Expected location for the directory containing
221    /// all the external files for a secret.
222    ///
223    /// # Panics
224    ///
225    /// If this set of paths are global (no user identifier).
226    pub fn into_file_secret_path(
227        &self,
228        folder_id: &VaultId,
229        secret_id: &SecretId,
230    ) -> PathBuf {
231        self.into_file_folder_path(folder_id)
232            .join(secret_id.to_string())
233    }
234
235    /// External file path.
236    ///
237    /// # Panics
238    ///
239    /// If this set of paths are global (no user identifier).
240    pub fn into_file_path(&self, file: &ExternalFile) -> PathBuf {
241        self.into_file_path_parts(
242            file.vault_id(),
243            file.secret_id(),
244            file.file_name(),
245        )
246    }
247
248    /// External file path from parts.
249    ///
250    /// # Panics
251    ///
252    /// If this set of paths are global (no user identifier).
253    pub fn into_file_path_parts(
254        &self,
255        folder_id: &VaultId,
256        secret_id: &SecretId,
257        file_name: &ExternalFileName,
258    ) -> PathBuf {
259        self.into_file_secret_path(folder_id, secret_id)
260            .join(file_name.to_string())
261    }
262
263    /// Path to the database file for an account.
264    pub fn database_file(&self) -> &PathBuf {
265        &self.database_file
266    }
267
268    /// External file blobs directory.
269    #[doc(hidden)]
270    #[cfg(debug_assertions)]
271    pub fn blobs_dir(&self) -> &PathBuf {
272        &self.blobs_dir
273    }
274
275    /// Expected location for the directory containing
276    /// all the external file blobs for an account.
277    ///
278    /// # Panics
279    ///
280    /// If this set of paths are global (no user identifier).
281    fn blobs_account_dir(&self) -> PathBuf {
282        if self.is_global() {
283            panic!(
284                "blobs account directory is not accessible for global paths"
285            );
286        }
287        self.blobs_dir
288            .join(self.account_id.as_ref().map(|id| id.to_string()).unwrap())
289    }
290
291    /// Expected location for an external file blob.
292    ///
293    /// # Panics
294    ///
295    /// If this set of paths are global (no user identifier).
296    pub fn into_blob_file_path(
297        &self,
298        vault_id: &VaultId,
299        secret_id: &SecretId,
300        file_name: impl AsRef<str>,
301    ) -> PathBuf {
302        self.blobs_account_dir()
303            .join(vault_id.to_string())
304            .join(secret_id.to_string())
305            .join(file_name.as_ref())
306    }
307
308    /// User identifier.
309    pub fn account_id(&self) -> Option<&AccountId> {
310        self.account_id.as_ref()
311    }
312
313    /// Top-level storage directory.
314    pub fn documents_dir(&self) -> &PathBuf {
315        &self.documents_dir
316    }
317
318    /// Determine if the paths are global.
319    ///
320    /// Paths are global when a user identifier
321    /// is not available.
322    pub fn is_global(&self) -> bool {
323        self.account_id.is_none()
324    }
325
326    /// Path to the identity vault directory.
327    pub fn identity_dir(&self) -> &PathBuf {
328        &self.identity_dir
329    }
330
331    /// Path to the local storage.
332    pub fn local_dir(&self) -> &PathBuf {
333        &self.local_dir
334    }
335
336    /// Path to the logs directory.
337    pub fn logs_dir(&self) -> &PathBuf {
338        &self.logs_dir
339    }
340
341    /// Path to the audit file.
342    pub fn audit_file(&self) -> &PathBuf {
343        &self.audit_file
344    }
345
346    /// Path to the file used to store global or
347    /// account-level preferences.
348    pub fn preferences_file(&self) -> PathBuf {
349        let mut path = if self.is_global() {
350            self.documents_dir().join(PREFERENCES_FILE)
351        } else {
352            self.user_dir().join(PREFERENCES_FILE)
353        };
354        path.set_extension(JSON_EXT);
355        path
356    }
357
358    /// Path to the file used to store account-level system messages.
359    ///
360    /// # Panics
361    ///
362    /// If this set of paths are global (no user identifier).
363    pub fn system_messages_file(&self) -> PathBuf {
364        assert!(!self.is_global(), "not accessible for global paths");
365        let mut path = self.user_dir().join(SYSTEM_MESSAGES_FILE);
366        path.set_extension(JSON_EXT);
367        path
368    }
369
370    /// User specific storage directory.
371    ///
372    /// # Panics
373    ///
374    /// If this set of paths are global (no user identifier).
375    pub fn user_dir(&self) -> &PathBuf {
376        assert!(!self.is_global(), "not accessible for global paths");
377        &self.user_dir
378    }
379
380    /// User's files directory.
381    ///
382    /// # Panics
383    ///
384    /// If this set of paths are global (no user identifier).
385    pub fn files_dir(&self) -> &PathBuf {
386        assert!(!self.is_global(), "not accessible for global paths");
387        &self.files_dir
388    }
389
390    /// Expected location for a file.
391    ///
392    /// # Panics
393    ///
394    /// If this set of paths are global (no user identifier).
395    pub fn into_legacy_file_path(
396        &self,
397        vault_id: &VaultId,
398        secret_id: &SecretId,
399        file_name: impl AsRef<str>,
400    ) -> PathBuf {
401        self.files_dir()
402            .join(vault_id.to_string())
403            .join(secret_id.to_string())
404            .join(file_name.as_ref())
405    }
406
407    /// User's vaults storage directory.
408    ///
409    /// # Panics
410    ///
411    /// If this set of paths are global (no user identifier).
412    pub fn vaults_dir(&self) -> &PathBuf {
413        assert!(!self.is_global(), "not accessible for global paths");
414        &self.vaults_dir
415    }
416
417    /// User's device signing key vault file.
418    ///
419    /// # Panics
420    ///
421    /// If this set of paths are global (no user identifier).
422    pub fn device_file(&self) -> &PathBuf {
423        assert!(!self.is_global(), "not accessible for global paths");
424        &self.device_file
425    }
426
427    /// Path to the identity vault file for this user.
428    ///
429    /// # Panics
430    ///
431    /// If this set of paths are global (no user identifier).
432    pub fn identity_vault(&self) -> PathBuf {
433        assert!(!self.is_global(), "not accessible for global paths");
434        let mut identity_vault_file = self
435            .identity_dir
436            .join(self.account_id.as_ref().map(|id| id.to_string()).unwrap());
437        identity_vault_file.set_extension(VAULT_EXT);
438        identity_vault_file
439    }
440
441    /// Path to the identity events log for this user.
442    ///
443    /// # Panics
444    ///
445    /// If this set of paths are global (no user identifier).
446    pub fn identity_events(&self) -> PathBuf {
447        let mut events_path = self.identity_vault();
448        events_path.set_extension(EVENT_LOG_EXT);
449        events_path
450    }
451
452    /// Path to a vault file from it's identifier.
453    ///
454    /// # Panics
455    ///
456    /// If this set of paths are global (no user identifier).
457    pub fn vault_path(&self, id: &VaultId) -> PathBuf {
458        assert!(!self.is_global(), "not accessible for global paths");
459        let mut vault_path = self.vaults_dir.join(id.to_string());
460        vault_path.set_extension(VAULT_EXT);
461        vault_path
462    }
463
464    /// Path to an event log file from it's identifier.
465    ///
466    /// # Panics
467    ///
468    /// If this set of paths are global (no user identifier).
469    pub fn event_log_path(&self, id: &VaultId) -> PathBuf {
470        assert!(!self.is_global(), "not accessible for global paths");
471        let mut vault_path = self.vaults_dir.join(id.to_string());
472        vault_path.set_extension(EVENT_LOG_EXT);
473        vault_path
474    }
475
476    /// Path to the user's account event log file.
477    ///
478    /// # Panics
479    ///
480    /// If this set of paths are global (no user identifier).
481    pub fn account_events(&self) -> PathBuf {
482        assert!(!self.is_global(), "not accessible for global paths");
483        let mut vault_path = self.user_dir.join(ACCOUNT_EVENTS);
484        vault_path.set_extension(EVENT_LOG_EXT);
485        vault_path
486    }
487
488    /// Path to the user's event log of device changes.
489    ///
490    /// # Panics
491    ///
492    /// If this set of paths are global (no user identifier).
493    pub fn device_events(&self) -> PathBuf {
494        assert!(!self.is_global(), "not accessible for global paths");
495        let mut vault_path = self.user_dir.join(DEVICE_EVENTS);
496        vault_path.set_extension(EVENT_LOG_EXT);
497        vault_path
498    }
499
500    /// Path to the user's event log of external file changes.
501    ///
502    /// # Panics
503    ///
504    /// If this set of paths are global (no user identifier).
505    pub fn file_events(&self) -> PathBuf {
506        assert!(!self.is_global(), "not accessible for global paths");
507        let mut vault_path = self.user_dir.join(FILE_EVENTS);
508        vault_path.set_extension(EVENT_LOG_EXT);
509        vault_path
510    }
511
512    /// Path to the file used to store remote origins.
513    ///
514    /// # Panics
515    ///
516    /// If this set of paths are global (no user identifier).
517    pub fn remote_origins(&self) -> PathBuf {
518        assert!(!self.is_global(), "not accessible for global paths");
519        let mut vault_path = self.user_dir.join(REMOTES_FILE);
520        vault_path.set_extension(JSON_EXT);
521        vault_path
522    }
523
524    /// Ensure the root directories exist for storage.
525    pub async fn scaffold(
526        data_dir: impl Into<Option<&PathBuf>>,
527    ) -> Result<()> {
528        let data_dir = if let Some(data_dir) = data_dir.into() {
529            data_dir.to_owned()
530        } else {
531            Paths::data_dir()?
532        };
533
534        let paths = Self::new_client(data_dir);
535        vfs::create_dir_all(paths.documents_dir()).await?;
536        if !paths.is_using_db() {
537            vfs::create_dir_all(paths.identity_dir()).await?;
538        }
539        vfs::create_dir_all(paths.logs_dir()).await?;
540        Ok(())
541    }
542
543    /// Set an explicit data directory used to store all
544    /// application files.
545    pub fn set_data_dir(path: PathBuf) {
546        let mut writer = DATA_DIR.write().unwrap();
547        *writer = Some(path);
548    }
549
550    /// Clear an explicitly set data directory.
551    pub fn clear_data_dir() {
552        let mut writer = DATA_DIR.write().unwrap();
553        *writer = None;
554    }
555
556    /// Get the default root directory used for caching application data.
557    ///
558    /// If the `SOS_DATA_DIR` environment variable is set it is used.
559    ///
560    /// Otherwise if an explicit directory has been set
561    /// using `set_data_dir()` then that will be used instead.
562    ///
563    /// Finally if no environment variable or explicit directory has been
564    /// set then a path will be computed by platform convention.
565    ///
566    /// When running with `debug_assertions` a `debug` path is appended
567    /// (except when executing tests) so that we can use different
568    /// storage locations for debug and release builds.
569    ///
570    /// If the `SOS_TEST` environment variable is set then we use
571    /// `test` rather than `debug` as the nested directory so that
572    /// test data does not collide with debug data.
573    pub fn data_dir() -> Result<PathBuf> {
574        let dir = if let Ok(env_data_dir) = std::env::var("SOS_DATA_DIR") {
575            Ok(PathBuf::from(env_data_dir))
576        } else {
577            let reader = DATA_DIR.read().unwrap();
578            if let Some(explicit) = reader.as_ref() {
579                Ok(explicit.to_path_buf())
580            } else {
581                default_storage_dir()
582            }
583        };
584
585        let has_explicit_env = std::env::var("SOS_DATA_DIR").ok().is_some();
586        if cfg!(debug_assertions) && !has_explicit_env {
587            // Don't follow the convention for separating debug and
588            // release data when running the integration tests as it
589            // makes paths very hard to reason about when they are
590            // being explicitly set in test specs.
591            if !cfg!(test) {
592                let sub_dir = if std::env::var("SOS_TEST").is_ok() {
593                    "test"
594                } else {
595                    "debug"
596                };
597                dir.map(|dir| dir.join(sub_dir))
598            } else {
599                dir
600            }
601        } else {
602            dir
603        }
604    }
605}
606
607#[cfg(target_os = "android")]
608fn default_storage_dir() -> Result<PathBuf> {
609    Ok(PathBuf::from(""))
610}
611
612#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
613fn default_storage_dir() -> Result<PathBuf> {
614    let strategy = choose_native_strategy(AppStrategyArgs {
615        top_level_domain: "com".to_string(),
616        author: APP_AUTHOR.to_string(),
617        app_name: APP_NAME.to_string(),
618    })
619    .map_err(Box::from)?;
620
621    #[cfg(not(windows))]
622    {
623        let mut path = strategy.data_dir();
624        path.set_file_name(APP_AUTHOR);
625        Ok(path)
626    }
627    #[cfg(windows)]
628    {
629        let mut path = strategy.cache_dir();
630        // Backwards compatible when we moved from app_dirs2
631        path.pop();
632        Ok(path)
633    }
634}
635
636#[cfg(target_arch = "wasm32")]
637fn default_storage_dir() -> Result<PathBuf> {
638    Ok(PathBuf::from("/"))
639}