use std::fmt;
use std::fs;
use std::future::Future;
use std::io;
use std::io::{BufWriter, Read, Write};
use std::path::Path;
use std::str::FromStr;
use anyhow::{anyhow, Context, Error, Result};
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::service::paths;
pub(crate) enum Format {
Yaml,
Json,
}
impl Format {
fn from_path<P>(path: &P) -> Option<Format>
where
P: ?Sized + AsRef<Path>,
{
match path.as_ref().extension().and_then(|e| e.to_str()) {
Some("json") => Some(Self::Json),
Some("yaml") => Some(Self::Yaml),
_ => None,
}
}
pub(crate) fn deserialize_array<T, R>(&self, f: R) -> Result<Vec<T>, Error>
where
T: DeserializeOwned,
R: Read,
{
fn from_json<T, R>(input: R) -> Result<Vec<T>>
where
T: DeserializeOwned,
R: Read,
{
use std::io::{BufRead, BufReader};
let mut output = Vec::new();
for line in BufReader::new(input).lines() {
let line = line?;
let line = line.trim();
if line.starts_with('#') || line.is_empty() {
continue;
}
output.push(serde_json::from_str(line)?);
}
Ok(output)
}
match self {
Format::Yaml => {
let mut array = Vec::new();
for doc in serde_yaml::Deserializer::from_reader(f) {
array.push(T::deserialize(doc)?);
}
Ok(array)
}
Format::Json => from_json(f),
}
}
fn start_array<O>(self, output: &mut O) -> SerializeArray<'_, O> {
SerializeArray {
count: 0,
mode: self,
output,
}
}
fn deserialize<T>(&self, bytes: &[u8]) -> Result<T>
where
T: DeserializeOwned,
{
match self {
Format::Yaml => Ok(serde_yaml::from_slice(bytes)?),
Format::Json => Ok(serde_json::from_slice(bytes)?),
}
}
fn serialize_pretty<O, T>(self, f: &mut O, data: &T) -> Result<()>
where
O: Write,
T: Serialize,
{
match self {
Format::Yaml => {
serde_yaml::to_writer(&mut *f, data)?;
f.write_all(&[b'\n'])?;
}
Format::Json => {
serde_json::to_writer_pretty(&mut *f, data)?;
f.write_all(&[b'\n'])?;
}
}
Ok(())
}
}
struct SerializeArray<'a, O> {
count: usize,
mode: Format,
output: &'a mut O,
}
impl<O> SerializeArray<'_, O>
where
O: Write,
{
fn serialize_item<T>(&mut self, item: &T) -> Result<()>
where
T: Serialize,
{
match self.mode {
Format::Yaml => {
if self.count > 0 {
self.output.write_all(b"---\n")?;
}
serde_yaml::to_writer(&mut *self.output, item)?;
}
Format::Json => {
serde_json::to_writer(&mut *self.output, item)?;
self.output.write_all(b"\n")?;
}
}
self.count += 1;
Ok(())
}
fn finish(self) -> Result<()> {
Ok(())
}
}
pub(crate) fn load<T>(path: &paths::Candidate) -> Result<Option<(Format, T)>>
where
T: DeserializeOwned,
{
for path in path.read() {
let Some(format) = Format::from_path(path) else {
continue;
};
let bytes = match fs::read(path) {
Ok(bytes) => bytes,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => return Err(e.into()),
};
let output = format.deserialize(&bytes)?;
return Ok(Some((format, output)));
}
Ok(None)
}
pub(crate) async fn save_pretty<T>(
what: &'static str,
path: &paths::Candidate,
data: T,
) -> Result<()>
where
T: 'static + Send + Serialize,
{
save_pretty_inner(what, path.as_ref(), data).await?;
for path in path.remainder() {
match tokio::fs::remove_file(path).await {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e.into()),
}
}
Ok(())
}
async fn save_pretty_inner<P, T>(what: &'static str, path: &P, data: T) -> Result<()>
where
P: ?Sized + AsRef<Path>,
T: 'static + Send + Serialize,
{
let path = Box::<Path>::from(path.as_ref());
tracing::debug!("saving {what}: {}", path.display());
let task = tokio::task::spawn_blocking(move || {
let Some(dir) = path.parent() else {
anyhow::bail!("{what}: missing parent directory: {}", path.display());
};
if !matches!(fs::metadata(dir), Ok(m) if m.is_dir()) {
fs::create_dir_all(dir)?;
}
let mode = Format::from_path(&path)
.with_context(|| anyhow!("{}: unsupported mode", path.display()))?;
let mut f = tempfile::NamedTempFile::new_in(dir)?;
tracing::trace!("writing {what}: {}", f.path().display());
mode.serialize_pretty(&mut f, &data)?;
let (mut f, temp_path) = f.keep()?;
f.flush()?;
drop(f);
tracing::trace!(
"rename {what}: {} -> {}",
temp_path.display(),
path.display()
);
fs::rename(temp_path, path)?;
Ok(())
});
task.await?
}
pub(crate) async fn save_array<I>(
what: &'static str,
path: &paths::Candidate,
data: I,
) -> Result<()>
where
I: 'static + Send + IntoIterator,
I::Item: Serialize,
{
save_array_inner(what, path.as_ref(), data).await?;
for path in path.remainder() {
match tokio::fs::remove_file(path).await {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e.into()),
}
}
Ok(())
}
fn save_array_inner<P, I>(what: &'static str, path: P, data: I) -> impl Future<Output = Result<()>>
where
P: AsRef<Path>,
I: 'static + Send + IntoIterator,
I::Item: Serialize,
{
let path = path.as_ref();
tracing::trace!("saving {what}: {}", path.display());
let path = Box::<Path>::from(path);
let task = tokio::task::spawn_blocking(move || {
let Some(dir) = path.parent() else {
anyhow::bail!("{what}: missing parent directory: {}", path.display());
};
if !matches!(fs::metadata(dir), Ok(m) if m.is_dir()) {
fs::create_dir_all(dir)?;
}
let mode = Format::from_path(&path)
.with_context(|| anyhow!("{}: unsupported mode", path.display()))?;
let f = tempfile::NamedTempFile::new_in(dir)?;
tracing::trace!("writing {what}: {}", f.path().display());
let mut f = BufWriter::new(f);
let mut writer = mode.start_array(&mut f);
for line in data {
writer.serialize_item(&line)?;
}
writer.finish()?;
let (mut f, temp_path) = f.into_inner()?.keep()?;
f.flush()?;
drop(f);
tracing::trace!(
"rename {what}: {} -> {}",
temp_path.display(),
path.display()
);
fs::rename(temp_path, path)?;
Ok(())
});
async move { task.await? }
}
pub(crate) fn load_directory<P, I, T>(path: &P) -> Result<Option<Vec<(I, Format, Vec<T>)>>>
where
P: ?Sized + AsRef<Path>,
I: FromStr,
I::Err: fmt::Display,
T: DeserializeOwned,
{
let d = match fs::read_dir(path) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e.into()),
};
let mut output = Vec::new();
for e in d {
let e = e?;
let m = e.metadata()?;
if !m.is_file() {
continue;
}
let path = e.path();
let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) else {
continue;
};
let mode = match Format::from_path(&path) {
Some(value) => value,
None => continue,
};
let Ok(id) = stem.parse() else {
continue;
};
let f = std::fs::File::open(&path)?;
let value = mode.deserialize_array(f)?;
output.push((id, mode, value));
}
Ok(Some(output))
}
pub(crate) fn load_array<T>(path: &paths::Candidate) -> Result<Option<(Format, Vec<T>)>>
where
T: DeserializeOwned,
{
for path in path.read() {
if let Some(output) = load_array_inner(path)? {
return Ok(Some(output));
}
}
Ok(None)
}
fn load_array_inner<P, T>(path: P) -> Result<Option<(Format, Vec<T>)>>
where
T: DeserializeOwned,
P: AsRef<Path>,
{
let path = path.as_ref();
let f = match std::fs::File::open(path) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(Error::from(e)).with_context(|| anyhow!("{}", path.display())),
};
let format = Format::from_path(&path)
.with_context(|| anyhow!("{}: unsupported file extension", path.display()))?;
let array = format
.deserialize_array(f)
.with_context(|| anyhow!("{}", path.display()))?;
Ok(Some((format, array)))
}