sos_core/
paths.rs

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