sos_core/
paths.rs

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