detsys_ids_client/storage/
json_file.rs1use std::io::Write;
2use std::path::PathBuf;
3
4use crate::storage::{Storage, StoredProperties};
5use tokio::fs::OpenOptions;
6use tokio::io::AsyncReadExt;
7
8const XDG_PREFIX: &str = "systems.determinate.detsys-ids-client";
9const XDG_STORAGE_FILENAME: &str = "storage.json";
10const NOTES: &[&str] = &[
11 "The IDs in this file are randomly generated UUIDs.",
12 "Determinate Systems uses these IDs to know how many people use our software and how to focus our limited resources for research and development.",
13 "The data here contains no personally identifiable information.",
14 "You can delete this file at any time to create new IDs.",
15 "",
16 "See our privacy policy: https://determinate.systems/policies/privacy",
17 "See our docs on telemetry: https://dtr.mn/telemetry",
18];
19
20#[derive(thiserror::Error, Debug)]
21pub enum Error {
22 #[error("No HOME is available")]
23 NoHome,
24
25 #[error("The storage location has no parent directory")]
26 LocationHasNoParent,
27
28 #[error("Serializing / deserializing failure: {0}")]
29 Json(#[from] serde_json::Error),
30
31 #[error("Loading from storage failed when opening the file `{0}`: {1}")]
32 Open(PathBuf, std::io::Error),
33
34 #[error("Creating the storage file `{0}` failed: {1}")]
35 Create(PathBuf, std::io::Error),
36
37 #[error("Reading from storage at `{0}` failed: {1}")]
38 Read(PathBuf, std::io::Error),
39
40 #[error("Writing storage to `{0}` failed: {1}")]
41 Write(PathBuf, std::io::Error),
42
43 #[error(transparent)]
44 Persist(#[from] tempfile::PersistError),
45
46 #[error(transparent)]
47 Join(#[from] tokio::task::JoinError),
48}
49
50#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
51struct WrappedStorage {
52 notes: Vec<String>,
53 body: StoredProperties,
54}
55
56pub struct JsonFile {
57 location: PathBuf,
58 directory: PathBuf,
59}
60
61impl JsonFile {
62 #[allow(dead_code)]
63 #[tracing::instrument]
64 pub fn new(location: PathBuf) -> Option<Self> {
65 Some(Self {
66 directory: location.parent()?.to_owned(),
67 location,
68 })
69 }
70
71 pub async fn try_default() -> Result<Self, Error> {
72 let xdg_dirs = xdg::BaseDirectories::with_prefix(XDG_PREFIX);
73
74 let file = xdg_dirs
75 .place_state_file(XDG_STORAGE_FILENAME)
76 .map_err(|e| {
77 match xdg_dirs
78 .get_state_file(XDG_STORAGE_FILENAME)
79 .ok_or(Error::NoHome)
80 {
81 Ok(loc) => Error::Create(loc, e),
82 Err(e) => e,
83 }
84 })?;
85
86 Self::new(file).ok_or(Error::LocationHasNoParent)
87 }
88}
89
90impl Storage for JsonFile {
91 type Error = Error;
92
93 #[tracing::instrument(skip(self))]
94 async fn load(&self) -> Result<Option<StoredProperties>, Error> {
95 let mut file = OpenOptions::new()
96 .read(true)
97 .open(&self.location)
98 .await
99 .map_err(|e| Error::Open(self.location.clone(), e))?;
100
101 let mut contents = vec![];
102 file.read_to_end(&mut contents)
103 .await
104 .map_err(|e| Error::Read(self.location.clone(), e))?;
105
106 let wrapped: WrappedStorage = serde_json::from_slice(&contents)?;
107
108 Ok(Some(wrapped.body))
109 }
110
111 #[tracing::instrument(skip(self, props))]
112 async fn store(&mut self, props: StoredProperties) -> Result<(), Error> {
113 let wrapped = WrappedStorage {
114 notes: NOTES.iter().map(|v| String::from(*v)).collect(),
115 body: props,
116 };
117 let json = serde_json::to_string_pretty(&wrapped)?;
118
119 let directory = self.directory.clone();
120 let location = self.location.clone();
121
122 tracing::trace!("Storing properties");
123 tokio::task::spawn_blocking(move || -> Result<(), Error> {
124 let mut tempfile = tempfile::NamedTempFile::new_in(&directory)
125 .map_err(|e| Error::Create(directory.clone(), e))?;
126
127 tempfile
128 .write_all(json.as_bytes())
129 .map_err(|e| Error::Write(tempfile.path().into(), e))?;
130
131 tempfile.persist(&location)?;
132
133 Ok(())
134 })
135 .await??;
136
137 tracing::trace!(location = ?self.location, "Storage persisted");
138
139 Ok(())
140 }
141}
142
143#[cfg(test)]
144mod test {
145 use crate::{
146 AnonymousDistinctId,
147 storage::{Storage, StoredProperties},
148 };
149
150 #[tokio::test]
151 async fn round_trips() {
152 let tempfile = tempfile::NamedTempFile::new().unwrap();
153
154 let mut store = super::JsonFile::new(tempfile.path().into()).unwrap();
155 let identity = StoredProperties {
156 anonymous_distinct_id: AnonymousDistinctId::default(),
157 device_id: "hi".to_string().into(),
158 ..Default::default()
159 };
160
161 store.store(identity.clone()).await.unwrap();
162
163 assert_eq!(identity, store.load().await.unwrap().unwrap());
164 }
165}