use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use crate::error::{SettingsErrorKind, SettingsScope};
use crate::paths::LOCK_SUFFIX;
#[derive(Debug, Clone)]
pub(crate) enum Storage {
Files { global: PathBuf, project: PathBuf },
Memory {
global: Option<String>,
project: Option<String>,
},
}
impl Storage {
pub(crate) fn path(&self, scope: SettingsScope) -> Option<&Path> {
match (self, scope) {
(Self::Files { global, .. }, SettingsScope::Global) => Some(global),
(Self::Files { project, .. }, SettingsScope::Project) => Some(project),
(Self::Memory { .. }, _) => None,
}
}
pub(crate) fn read(&self, scope: SettingsScope) -> Result<Option<String>, SettingsErrorKind> {
match self {
Self::Files { .. } => {
let Some(path) = self.path(scope) else {
return Ok(None);
};
read_optional(path).map_err(|e| SettingsErrorKind::Read(e.to_string()))
}
Self::Memory { global, project } => Ok(match scope {
SettingsScope::Global => global.clone(),
SettingsScope::Project => project.clone(),
}),
}
}
pub(crate) fn update(
&mut self,
scope: SettingsScope,
next: impl FnOnce(Option<&str>) -> Result<String, SettingsErrorKind>,
) -> Result<(), SettingsErrorKind> {
match self {
Self::Memory { global, project } => {
let slot = match scope {
SettingsScope::Global => global,
SettingsScope::Project => project,
};
*slot = Some(next(slot.as_deref())?);
Ok(())
}
Self::Files { global, project } => {
let path = match scope {
SettingsScope::Global => global.as_path(),
SettingsScope::Project => project.as_path(),
};
let _lock =
FileLock::acquire(path).map_err(|e| SettingsErrorKind::Lock(e.to_string()))?;
let current =
read_optional(path).map_err(|e| SettingsErrorKind::Read(e.to_string()))?;
let content = next(current.as_deref())?;
write_atomic(path, content.as_bytes())
.map_err(|e| SettingsErrorKind::Write(e.to_string()))
}
}
}
}
pub(crate) fn read_optional(path: &Path) -> io::Result<Option<String>> {
match fs::read_to_string(path) {
Ok(content) => Ok(Some(content)),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error),
}
}
#[derive(Debug)]
pub struct FileLock {
file: File,
}
impl FileLock {
pub fn acquire(path: &Path) -> io::Result<Self> {
let file = open_lock(path)?;
file.lock()?;
Ok(Self { file })
}
}
impl Drop for FileLock {
fn drop(&mut self) {
let _ = self.file.unlock();
}
}
fn open_lock(path: &Path) -> io::Result<File> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut lock_path = path.as_os_str().to_owned();
lock_path.push(LOCK_SUFFIX);
OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(lock_path)
}
pub fn write_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
fs::create_dir_all(parent)?;
let file_name = path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default();
let tmp = parent.join(format!(".{file_name}.{}.tmp", std::process::id()));
let result = (|| {
let mut file = File::create(&tmp)?;
file.write_all(bytes)?;
file.sync_all()?;
fs::rename(&tmp, path)
})();
if result.is_err() {
let _ = fs::remove_file(&tmp);
}
result
}