1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
use anyhow::{Context, Result};
use std::fs::read_dir;
use std::io::prelude::Write;

/// Provides basic filesystem operations, working within a config directory
#[derive(Clone, Debug)]
pub struct FileService {
  cfg_dir: String,
  log: slog::Logger
}

impl FileService {
  pub(crate) fn new(cfg_dir: &str, logger: &slog::Logger) -> FileService {
    let ret = FileService {
      cfg_dir: cfg_dir.into(),
      log: logger.new(slog::o!("service" => "file"))
    };
    match std::fs::create_dir_all(&ret.path()) {
      Ok(_) => (),
      Err(e) => slog::warn!(logger, "Can't create config directory at [{:?}]: {}", &ret.path(), e)
    };
    ret
  }

  pub fn cfg_dir(&self) -> &String {
    &self.cfg_dir
  }

  pub(crate) fn path(&self) -> &std::path::Path {
    std::path::Path::new(&self.cfg_dir)
  }

  pub(crate) fn list_json(&self, path: &str) -> Result<Vec<String>> {
    let p = self.path().join(path);
    if p.is_dir() {
      let rd = read_dir(&p).with_context(|| format!("Can't read directory [{}]", p.to_string_lossy()))?;

      let mut entries = rd
        .filter_map(|entry| {
          entry.ok().and_then(|e| {
            e.path().file_name().and_then(|n| {
              n.to_str()
                .filter(|x| x.ends_with(".json"))
                .map(|x| x.trim_end_matches(".json").into())
            })
          })
        })
        .collect::<Vec<String>>();
      entries.sort();
      Ok(entries)
    } else {
      Ok(Vec::new())
    }
  }

  fn read(&self, path: &str) -> Result<String> {
    let p = self.path().join(clean_path(path));
    std::fs::read_to_string(&p).with_context(|| format!("Can't read from file [{}]", p.to_string_lossy()))
  }

  pub(crate) fn read_json<T>(&self, path: &str) -> Result<T>
  where T: serde::de::DeserializeOwned {
    let json = self.read(path)?;
    serde_json::from_str(&json).with_context(|| "Can't decode json")
  }

  pub(crate) fn write(&self, path: &str, content: &str) -> Result<()> {
    let p = self.path().join(clean_path(path));
    let mut file = std::fs::File::create(p).with_context(|| {
      format!(
        "Can't create file [{}] in directory [{}]",
        clean_path(path),
        self.path().to_string_lossy()
      )
    })?;
    file
      .write_all(&content.as_bytes())
      .with_context(|| format!("Can't write to file [{}]", clean_path(path)))?;
    Ok(())
  }

  pub fn write_json<T>(&self, o: T, path: &str) -> Result<()>
  where T: serde::Serialize {
    let content = serde_json::to_string_pretty(&o).with_context(|| "Can't encode json")?;
    self.write(path, &content)
  }

  pub(crate) fn create_dir_if_needed(&self, path: &str) -> Result<()> {
    let p = self.path().join(path);
    std::fs::create_dir_all(p).with_context(|| format!("Can't create directory at path [{}]", path))
  }

  pub(crate) fn remove(&self, path: &str) -> Result<()> {
    let p = self.path().join(clean_path(path));
    std::fs::remove_file(p).with_context(|| format!("Can't remove file at [{}]", path))
  }
}

fn clean_path(path: &str) -> String {
  if path.ends_with(".json") {
    path.into()
  } else {
    format!("{}.json", path)
  }
}