1use std::{
2 fs, io,
3 path::{Path, PathBuf},
4};
5
6use directories::ProjectDirs;
7use serde::{de::DeserializeOwned, Serialize};
8use tempfile::NamedTempFile;
9
10use crate::{
11 cli::{CliCommand, SeaplaneInit},
12 context::Ctx,
13 error::{CliError, CliErrorKind, Context, Result},
14 printer::Color,
15};
16
17#[inline]
19fn project_dirs() -> Option<ProjectDirs> {
20 directories::ProjectDirs::from("io", "Seaplane", "seaplane")
21}
22
23pub fn conf_dirs() -> Vec<PathBuf> {
25 let mut dirs = Vec::new();
26 if let Some(proj_dirs) = project_dirs() {
27 dirs.push(proj_dirs.config_dir().to_owned());
28 }
29 if let Some(base_dirs) = directories::BaseDirs::new() {
30 if !cfg!(target_os = "linux") {
32 dirs.push(base_dirs.home_dir().join(".config/seaplane"));
33 }
34 dirs.push(base_dirs.home_dir().join(".seaplane"));
35 }
36 dirs
37}
38
39#[cfg(not(feature = "ui_tests"))]
41#[inline]
42pub fn data_dir() -> PathBuf {
43 project_dirs()
44 .expect("Failed to determine usable directories")
45 .data_dir()
46 .to_owned()
47}
48
49#[cfg(feature = "ui_tests")]
50#[cfg_attr(feature = "ui_tests", inline)]
51pub fn data_dir() -> PathBuf { std::env::current_dir().unwrap() }
52
53#[derive(Debug)]
55pub struct AtomicFile<'p> {
56 path: &'p Path,
57 temp_file: Option<NamedTempFile>,
58}
59
60impl<'p> AtomicFile<'p> {
61 pub fn new(p: &'p Path) -> Result<Self> {
63 Ok(Self { path: p, temp_file: Some(NamedTempFile::new()?) })
64 }
65
66 #[allow(dead_code)]
68 pub fn persist(mut self) -> Result<()> {
69 let tf = self.temp_file.take().unwrap();
70 tf.persist(self.path).map(|_| ()).map_err(CliError::from)
71 }
72
73 pub fn temp_path(&self) -> &Path { self.temp_file.as_ref().unwrap().path() }
75}
76
77impl<'p> io::Write for AtomicFile<'p> {
78 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
79 if let Some(ref mut tf) = &mut self.temp_file {
80 return tf.write(buf);
81 }
82
83 Ok(0)
84 }
85
86 fn flush(&mut self) -> io::Result<()> {
87 if let Some(ref mut tf) = &mut self.temp_file {
88 return tf.flush();
89 }
90
91 Ok(())
92 }
93}
94
95impl<'p> Drop for AtomicFile<'p> {
96 fn drop(&mut self) {
97 let tf = self.temp_file.take().unwrap();
99 let _ = tf.persist(self.path);
100 }
101}
102
103pub trait FromDisk {
105 fn set_loaded_from<P: AsRef<Path>>(&mut self, _p: P) {}
107
108 fn loaded_from(&self) -> Option<&Path> { None }
110
111 fn load_if<P: AsRef<Path>>(p: P, yes: bool) -> Option<Result<Self>>
113 where
114 Self: Sized + DeserializeOwned,
115 {
116 if yes {
117 return Some(Self::load(p));
118 }
119 None
120 }
121
122 fn load<P: AsRef<Path>>(p: P) -> Result<Self>
124 where
125 Self: Sized + DeserializeOwned,
126 {
127 let path = p.as_ref();
128
129 let json_str = match fs::read_to_string(path) {
130 Ok(s) => s,
131 Err(e) => {
132 if e.kind() == io::ErrorKind::NotFound {
135 let mut ctx = Ctx::default();
136 ctx.internal_run = true;
137 SeaplaneInit.run(&mut ctx)?;
138
139 fs::read_to_string(path)
140 .map_err(CliError::from)
141 .context("\n\tpath: ")
142 .with_color_context(|| (Color::Yellow, format!("{path:?}")))?
143 } else {
144 return Err(CliError::from(e));
145 }
146 }
147 };
148 let mut item: Self = serde_json::from_str(&json_str)
149 .map_err(CliError::from)
150 .context("\n\tpath: ")
151 .with_color_context(|| (Color::Yellow, format!("{path:?}")))?;
152
153 item.set_loaded_from(p);
154
155 Ok(item)
156 }
157}
158
159pub trait ToDisk: FromDisk {
161 fn persist_if(&self, yes: bool) -> Result<()>
163 where
164 Self: Sized + Serialize,
165 {
166 if yes {
167 return self.persist();
168 }
169 Ok(())
170 }
171
172 fn persist(&self) -> Result<()>
174 where
175 Self: Sized + Serialize,
176 {
177 if let Some(path) = self.loaded_from() {
178 let file = AtomicFile::new(path)?;
179 Ok(serde_json::to_writer(file, self)
181 .map_err(CliError::from)
182 .context("\n\tpath: ")
183 .with_color_context(|| (Color::Yellow, format!("{path:?}")))?)
184 } else {
185 Err(CliErrorKind::MissingPath.into_err())
186 }
187 }
188}