1use std::fs::{self, File, OpenOptions};
5use std::io::{self, Write};
6use std::path::{Path, PathBuf};
7
8use crate::error::{SettingsErrorKind, SettingsScope};
9use crate::paths::LOCK_SUFFIX;
10
11#[derive(Debug, Clone)]
13pub(crate) enum Storage {
14 Files { global: PathBuf, project: PathBuf },
16 Memory {
18 global: Option<String>,
19 project: Option<String>,
20 },
21}
22
23impl Storage {
24 pub(crate) fn path(&self, scope: SettingsScope) -> Option<&Path> {
25 match (self, scope) {
26 (Self::Files { global, .. }, SettingsScope::Global) => Some(global),
27 (Self::Files { project, .. }, SettingsScope::Project) => Some(project),
28 (Self::Memory { .. }, _) => None,
29 }
30 }
31
32 pub(crate) fn read(&self, scope: SettingsScope) -> Result<Option<String>, SettingsErrorKind> {
34 match self {
35 Self::Files { .. } => {
36 let Some(path) = self.path(scope) else {
37 return Ok(None);
38 };
39 read_optional(path).map_err(|e| SettingsErrorKind::Read(e.to_string()))
40 }
41 Self::Memory { global, project } => Ok(match scope {
42 SettingsScope::Global => global.clone(),
43 SettingsScope::Project => project.clone(),
44 }),
45 }
46 }
47
48 pub(crate) fn update(
52 &mut self,
53 scope: SettingsScope,
54 next: impl FnOnce(Option<&str>) -> Result<String, SettingsErrorKind>,
55 ) -> Result<(), SettingsErrorKind> {
56 match self {
57 Self::Memory { global, project } => {
58 let slot = match scope {
59 SettingsScope::Global => global,
60 SettingsScope::Project => project,
61 };
62 *slot = Some(next(slot.as_deref())?);
63 Ok(())
64 }
65 Self::Files { global, project } => {
66 let path = match scope {
67 SettingsScope::Global => global.as_path(),
68 SettingsScope::Project => project.as_path(),
69 };
70 let _lock =
71 FileLock::acquire(path).map_err(|e| SettingsErrorKind::Lock(e.to_string()))?;
72 let current =
73 read_optional(path).map_err(|e| SettingsErrorKind::Read(e.to_string()))?;
74 let content = next(current.as_deref())?;
75 write_atomic(path, content.as_bytes())
76 .map_err(|e| SettingsErrorKind::Write(e.to_string()))
77 }
78 }
79 }
80}
81
82pub(crate) fn read_optional(path: &Path) -> io::Result<Option<String>> {
84 match fs::read_to_string(path) {
85 Ok(content) => Ok(Some(content)),
86 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
87 Err(error) => Err(error),
88 }
89}
90
91#[derive(Debug)]
94pub struct FileLock {
95 file: File,
96}
97
98impl FileLock {
99 pub fn acquire(path: &Path) -> io::Result<Self> {
105 let file = open_lock(path)?;
106 file.lock()?;
107 Ok(Self { file })
108 }
109}
110
111impl Drop for FileLock {
112 fn drop(&mut self) {
113 let _ = self.file.unlock();
115 }
116}
117
118fn open_lock(path: &Path) -> io::Result<File> {
119 if let Some(parent) = path.parent() {
120 fs::create_dir_all(parent)?;
121 }
122 let mut lock_path = path.as_os_str().to_owned();
123 lock_path.push(LOCK_SUFFIX);
124 OpenOptions::new()
125 .create(true)
126 .truncate(false)
127 .write(true)
128 .open(lock_path)
129}
130
131pub fn write_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
137 let parent = path.parent().unwrap_or_else(|| Path::new("."));
138 fs::create_dir_all(parent)?;
139 let file_name = path
140 .file_name()
141 .map(|name| name.to_string_lossy().into_owned())
142 .unwrap_or_default();
143 let tmp = parent.join(format!(".{file_name}.{}.tmp", std::process::id()));
144 let result = (|| {
145 let mut file = File::create(&tmp)?;
146 file.write_all(bytes)?;
147 file.sync_all()?;
148 fs::rename(&tmp, path)
149 })();
150 if result.is_err() {
151 let _ = fs::remove_file(&tmp);
152 }
153 result
154}