1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
use super::{
home_path, load_all_from_path, load_from_path, load_home_env, save_to_path, system_paths,
};
use std::{
env,
path::{Path, PathBuf},
str::Utf8Error,
string::String,
};
use serde::{de, ser, Deserialize, Serialize};
/// Stores data unrelated to the config or state files: logs, history and so on.
///
/// Example of things to store:
/// * logs
/// * history
pub trait DataFile<E, T = Self>: for<'d> Deserialize<'d> + Serialize
where
for<'d> T: Serialize + Deserialize<'d>,
E: std::error::Error + std::convert::From<std::io::Error>,
{
/// Name of the directory to-be-used for the application's storage. If no directory is desired, set this to "" (empty string).
///
/// Defaults to "`$CARGO_CRATE_NAME`" which is the name of the crate at compile-time.
const APP_DIR_NAME: &'static str = env!("CARGO_CRATE_NAME");
/// Name of the directory to-be-used for more complicated setups, like organizing multiple
/// configuration files into less of a mess, or allowing overrides to be separated from the
/// actual config file.
///
/// Defaults to "`$CARGO_CRATE_NAME`.d" which is the name of the crate at compile-time.
const EXTRA_DIR: &'static str = concat!(env!("CARGO_CRATE_NAME"), ".d");
/// Name of the environment variable which specifies the home path directory,
///
/// Defaults to `$XDG_DATA_HOME`, which usually is `~/.local/share`.
const HOME_PATH_ENV: &'static str = "XDG_DATA_HOME";
/// Fallback path for `HOME_PATH_ENV`, assumes getting `$HOME` is valid.
const HOME_PATH_FALLBACK: &'static str = ".local/share";
/// Name of the environment variable which specifies a preference-ordered set of directories
/// for files to be searched for.
///
/// Defaults to `XDG_DATA_DIRS`, which usually is `/usr/local/share/:/usr/share/`.
///
/// # Note
/// You could override this at runtime if you wish to for example use `/etc` instead.
const SYSTEM_PATHS_ENV: &'static str = "XDG_DATA_DIRS";
/// Fallback paths for `SYSTEM_PATHS_ENV`.
const SYSTEM_PATHS_FALLBACK: &'static str = "/usr/local/share/:/usr/share/";
/// Name of the data file.
const FILENAME: &'static str = "data";
/// Fully constructed home-path.
fn home() -> PathBuf {
home_path(
Self::HOME_PATH_ENV,
Self::HOME_PATH_FALLBACK,
Self::APP_DIR_NAME,
Self::FILENAME,
)
}
/// Fully constructed list of system paths.
fn system() -> Vec<PathBuf> {
system_paths(
Self::SYSTEM_PATHS_ENV,
Self::SYSTEM_PATHS_FALLBACK,
Self::APP_DIR_NAME,
Self::FILENAME,
)
}
/// Save [DataFile] to home.
fn save(&self) -> Result<(), E> {
Self::save_to_path(self, &Self::home(), true)
}
/// Save [DataFile] to first available system path.
fn save_to_system(&self) -> Result<(), E> {
Self::save_to_path(
self,
&Self::system()
.first()
.expect("no paths specified in trait-constants"),
true,
)
}
/// Save [DataFile] to a custom-specified path, optionally creating all dirs needed on the way.
fn save_to_path(&self, path: impl AsRef<Path>, create_dirs: bool) -> Result<(), E> {
save_to_path(self, path.as_ref(), create_dirs)
}
/// Load [DataFile] from first available source in the following order:
/// 1. `XDG_DATA_HOME/{Self::APP_DIR_NAME}/{Self::FILENAME}`
/// 2. `$HOME/.config/{Self::APP_DIR_NAME}/{Self::FILENAME}`
/// 3. `XDG_CONFIG_DIRS{{/Self::APP_DIR_NAME}/{Self::FILENAME}}`
/// 4. `/etx/xdg/{Self::APP_DIR_NAME}/`
fn load() -> Result<T, E> {
match Self::load_from_home() {
Ok(s) => Ok(s),
Err(_) => Self::load_first_from_system(),
}
}
/// Load [DataFile] from home.
fn load_from_home() -> Result<T, E> {
Self::load_from_path(&Self::home())
}
/// Load first available [DataFile] from system paths.
fn load_first_from_system() -> Result<T, E> {
Self::load_from_path(
&Self::system()
.first()
.expect("no paths specified in trait-constants"),
)
}
/// Load all available [DataFile]s from system paths.
fn load_from_system() -> Result<Vec<T>, E> {
let paths = Self::system();
let mut loaded = Vec::with_capacity(paths.len());
for path in paths {
loaded.push(Self::load_from_path(path)?);
}
Ok(loaded)
}
/// Load all available [DataFile]s.
fn load_all() -> Result<Vec<T>, E> {
let paths = Self::system();
let mut loaded = Vec::with_capacity(paths.len() + 1);
loaded.push(Self::load_from_home()?);
loaded.append(&mut Self::load_from_system()?);
Ok(loaded)
}
/// Load all available [DataFile]s from home's [Self::EXTRA_DIR].
fn load_from_home_extra_dirs() -> Result<Vec<T>, E> {
load_all_from_path(&Self::home())
}
/// Load all available [DataFile]s from all [Self::EXTRA_DIR]s in [Self::SYSTEM_PATHS_ENV].
fn load_from_system_extra_dirs() -> Result<Vec<T>, E> {
let paths = Self::system();
let mut loaded = Vec::with_capacity(paths.len());
for path in paths {
loaded.append(&mut load_all_from_path(path)?);
}
Ok(loaded)
}
/// Load [DataFile] from a given path.
fn load_from_path(path: impl AsRef<Path>) -> Result<T, E> {
load_from_path(path.as_ref())
}
}
mod tests {
use super::*;
use std::env::set_var;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Data;
impl DataFile<std::io::Error> for Data {}
#[test]
fn home_path() {
// regular test
set_var("XDG_DATA_HOME", "/tmp/.local/share/");
assert_eq!(
Data::home(),
PathBuf::from("/tmp/.local/share/docopticon/data")
);
// relative test
set_var("XDG_DATA_HOME", "relative/dir");
set_var("HOME", "/tmp/");
assert_eq!(
Data::home(),
PathBuf::from("/tmp/.local/share/docopticon/data")
);
}
#[test]
fn system_paths() {
assert!(Data::system().contains(&PathBuf::from("/usr/share/docopticon/data")));
assert!(Data::system().contains(&PathBuf::from("/usr/local/share/docopticon/data")));
}
}