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 global preferences file.
342    pub fn global_preferences_file(&self) -> PathBuf {
343        let mut path = self.documents_dir().join(PREFERENCES_FILE);
344        path.set_extension(JSON_EXT);
345        path
346    }
347
348    /// Path to the file used to store account-level system messages.
349    ///
350    /// # Panics
351    ///
352    /// If this set of paths are global (no user identifier).
353    pub fn system_messages_file(&self) -> PathBuf {
354        assert!(!self.is_global(), "not accessible for global paths");
355        let mut path = self.user_dir().join(SYSTEM_MESSAGES_FILE);
356        path.set_extension(JSON_EXT);
357        path
358    }
359
360    /// User specific storage directory.
361    ///
362    /// # Panics
363    ///
364    /// If this set of paths are global (no user identifier).
365    pub fn user_dir(&self) -> &PathBuf {
366        assert!(!self.is_global(), "not accessible for global paths");
367        &self.user_dir
368    }
369
370    /// User's files directory.
371    ///
372    /// # Panics
373    ///
374    /// If this set of paths are global (no user identifier).
375    pub fn files_dir(&self) -> &PathBuf {
376        assert!(!self.is_global(), "not accessible for global paths");
377        &self.files_dir
378    }
379
380    /// Expected location for a file.
381    ///
382    /// # Panics
383    ///
384    /// If this set of paths are global (no user identifier).
385    pub fn into_legacy_file_path(
386        &self,
387        vault_id: &VaultId,
388        secret_id: &SecretId,
389        file_name: impl AsRef<str>,
390    ) -> PathBuf {
391        self.files_dir()
392            .join(vault_id.to_string())
393            .join(secret_id.to_string())
394            .join(file_name.as_ref())
395    }
396
397    /// User's vaults storage directory.
398    ///
399    /// # Panics
400    ///
401    /// If this set of paths are global (no user identifier).
402    pub fn vaults_dir(&self) -> &PathBuf {
403        assert!(!self.is_global(), "not accessible for global paths");
404        &self.vaults_dir
405    }
406
407    /// User's device signing key vault file.
408    ///
409    /// # Panics
410    ///
411    /// If this set of paths are global (no user identifier).
412    pub fn device_file(&self) -> &PathBuf {
413        assert!(!self.is_global(), "not accessible for global paths");
414        &self.device_file
415    }
416
417    /// Path to the identity vault file for this user.
418    ///
419    /// # Panics
420    ///
421    /// If this set of paths are global (no user identifier).
422    pub fn identity_vault(&self) -> PathBuf {
423        assert!(!self.is_global(), "not accessible for global paths");
424        let mut identity_vault_file = self
425            .identity_dir
426            .join(self.account_id.as_ref().map(|id| id.to_string()).unwrap());
427        identity_vault_file.set_extension(VAULT_EXT);
428        identity_vault_file
429    }
430
431    /// Path to the identity events log for this user.
432    ///
433    /// # Panics
434    ///
435    /// If this set of paths are global (no user identifier).
436    pub fn identity_events(&self) -> PathBuf {
437        let mut events_path = self.identity_vault();
438        events_path.set_extension(EVENT_LOG_EXT);
439        events_path
440    }
441
442    /// Path to a vault file from it's identifier.
443    ///
444    /// # Panics
445    ///
446    /// If this set of paths are global (no user identifier).
447    pub fn vault_path(&self, id: &VaultId) -> PathBuf {
448        assert!(!self.is_global(), "not accessible for global paths");
449        let mut vault_path = self.vaults_dir.join(id.to_string());
450        vault_path.set_extension(VAULT_EXT);
451        vault_path
452    }
453
454    /// Path to an event log file from it's identifier.
455    ///
456    /// # Panics
457    ///
458    /// If this set of paths are global (no user identifier).
459    pub fn event_log_path(&self, id: &VaultId) -> PathBuf {
460        assert!(!self.is_global(), "not accessible for global paths");
461        let mut vault_path = self.vaults_dir.join(id.to_string());
462        vault_path.set_extension(EVENT_LOG_EXT);
463        vault_path
464    }
465
466    /// Path to the user's account event log file.
467    ///
468    /// # Panics
469    ///
470    /// If this set of paths are global (no user identifier).
471    pub fn account_events(&self) -> PathBuf {
472        assert!(!self.is_global(), "not accessible for global paths");
473        let mut vault_path = self.user_dir.join(ACCOUNT_EVENTS);
474        vault_path.set_extension(EVENT_LOG_EXT);
475        vault_path
476    }
477
478    /// Path to the user's event log of device changes.
479    ///
480    /// # Panics
481    ///
482    /// If this set of paths are global (no user identifier).
483    pub fn device_events(&self) -> PathBuf {
484        assert!(!self.is_global(), "not accessible for global paths");
485        let mut vault_path = self.user_dir.join(DEVICE_EVENTS);
486        vault_path.set_extension(EVENT_LOG_EXT);
487        vault_path
488    }
489
490    /// Path to the user's event log of external file changes.
491    ///
492    /// # Panics
493    ///
494    /// If this set of paths are global (no user identifier).
495    pub fn file_events(&self) -> PathBuf {
496        assert!(!self.is_global(), "not accessible for global paths");
497        let mut vault_path = self.user_dir.join(FILE_EVENTS);
498        vault_path.set_extension(EVENT_LOG_EXT);
499        vault_path
500    }
501
502    /// Path to the file used to store remote origins.
503    ///
504    /// # Panics
505    ///
506    /// If this set of paths are global (no user identifier).
507    pub fn remote_origins(&self) -> PathBuf {
508        assert!(!self.is_global(), "not accessible for global paths");
509        let mut vault_path = self.user_dir.join(REMOTES_FILE);
510        vault_path.set_extension(JSON_EXT);
511        vault_path
512    }
513
514    /// Ensure the root directories exist for storage.
515    pub async fn scaffold(
516        data_dir: impl Into<Option<&PathBuf>>,
517    ) -> Result<()> {
518        let data_dir = if let Some(data_dir) = data_dir.into() {
519            data_dir.to_owned()
520        } else {
521            Paths::data_dir()?
522        };
523
524        let paths = Self::new_client(data_dir);
525        vfs::create_dir_all(paths.documents_dir()).await?;
526        if !paths.is_using_db() {
527            vfs::create_dir_all(paths.identity_dir()).await?;
528        }
529        vfs::create_dir_all(paths.logs_dir()).await?;
530        Ok(())
531    }
532
533    /// Set an explicit data directory used to store all
534    /// application files.
535    pub fn set_data_dir(path: PathBuf) {
536        DATA_DIR.get_or_init(|| path);
537    }
538
539    /// Get the default root directory used for caching application data.
540    ///
541    /// If the `SOS_DATA_DIR` environment variable is set it is used.
542    ///
543    /// Otherwise if an explicit directory has been set
544    /// using `set_data_dir()` then that will be used instead.
545    ///
546    /// Finally if no environment variable or explicit directory has been
547    /// set then a path will be computed by platform convention.
548    ///
549    /// When running with `debug_assertions` a `debug` path is appended
550    /// (except when executing tests) so that we can use different
551    /// storage locations for debug and release builds.
552    ///
553    /// If the `SOS_TEST` environment variable is set then we use
554    /// `test` rather than `debug` as the nested directory so that
555    /// test data does not collide with debug data.
556    pub fn data_dir() -> Result<PathBuf> {
557        let dir = if let Ok(env_data_dir) = std::env::var("SOS_DATA_DIR") {
558            Ok(PathBuf::from(env_data_dir))
559        } else {
560            if let Some(data_dir) = DATA_DIR.get() {
561                Ok(data_dir.to_owned())
562            } else {
563                default_storage_dir()
564            }
565        };
566
567        let has_explicit_env = std::env::var("SOS_DATA_DIR").ok().is_some();
568        if cfg!(debug_assertions) && !has_explicit_env {
569            // Don't follow the convention for separating debug and
570            // release data when running the integration tests as it
571            // makes paths very hard to reason about when they are
572            // being explicitly set in test specs.
573            if !cfg!(test) {
574                let sub_dir = if std::env::var("SOS_TEST").is_ok() {
575                    "test"
576                } else {
577                    "debug"
578                };
579                dir.map(|dir| dir.join(sub_dir))
580            } else {
581                dir
582            }
583        } else {
584            dir
585        }
586    }
587}
588
589#[cfg(target_os = "android")]
590fn default_storage_dir() -> Result<PathBuf> {
591    Ok(PathBuf::from(""))
592}
593
594#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
595fn default_storage_dir() -> Result<PathBuf> {
596    let strategy = choose_native_strategy(AppStrategyArgs {
597        top_level_domain: "com".to_string(),
598        author: APP_AUTHOR.to_string(),
599        app_name: APP_NAME.to_string(),
600    })
601    .map_err(Box::from)?;
602
603    #[cfg(not(windows))]
604    {
605        let mut path = strategy.data_dir();
606        path.set_file_name(APP_AUTHOR);
607        Ok(path)
608    }
609    #[cfg(windows)]
610    {
611        let mut path = strategy.cache_dir();
612        // Backwards compatible when we moved from app_dirs2
613        path.pop();
614        Ok(path)
615    }
616}
617
618#[cfg(target_arch = "wasm32")]
619fn default_storage_dir() -> Result<PathBuf> {
620    Ok(PathBuf::from("/"))
621}