use std::{
error::Error,
fs::{self, File},
io::{Read, Write},
marker::PhantomData,
path::PathBuf,
};
use chrono::{DateTime, Local};
use glob::glob;
use serde::{Deserialize, Serialize};
use crate::{
errors::SaveError,
schema::Schema,
utils::{format_date, now, read_date},
vault_encrypted::{RecordEncrypted, VaultEncrypted},
};
pub trait ProjectFile<'de, Data>
where
Data: Serialize + Deserialize<'de>,
{
fn base_path() -> PathBuf {
if let Some(project_dirs) = directories_next::ProjectDirs::from("com", "bski", "pants") {
project_dirs.data_dir().into()
} else {
std::env::current_dir().unwrap_or_default()
}
}
fn path(&self) -> PathBuf;
fn create(&self) -> Result<File, Box<dyn Error>> {
let path = self.path();
if let Some(dir) = path.parent() {
fs::create_dir_all(dir)?;
}
let file = File::create(path)?;
Ok(file)
}
fn delete(&self) -> Result<(), Box<dyn Error>> {
let path = self.path();
Ok(fs::remove_file(path)?)
}
fn open(&self) -> Result<File, Box<dyn Error>> {
let path = self.path();
let file = File::open(path)?;
Ok(file)
}
fn write(&mut self, data: &Data) -> Result<(), Box<dyn Error>> {
let mut file = self.create()?;
let output = serde_json::to_string(data)?;
file.write_all(output.as_ref())
.map_err(|_| SaveError::Write)?;
Ok(())
}
fn read(&self) -> Result<ReadIn<Data>, Box<dyn Error>> {
let mut file = self.open()?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(ReadIn {
data: content,
data_type: PhantomData,
})
}
}
pub struct ReadIn<Data> {
data: String,
data_type: PhantomData<Data>,
}
impl<'de, Data: Deserialize<'de>> ReadIn<Data> {
pub fn deserialize(&'de self) -> Data {
serde_json::from_str(&self.data).unwrap()
}
}
#[derive(Debug, Clone)]
pub struct TimestampedFile<Data> {
name: String,
timestamp: DateTime<Local>,
data_type: PhantomData<Data>,
}
#[derive(Debug, Clone)]
pub struct NonTimestampedFile<Data> {
name: String,
data_type: PhantomData<Data>,
}
impl<'de, Data> ProjectFile<'de, Data> for TimestampedFile<Data>
where
Data: Serialize + Deserialize<'de>,
{
fn path(&self) -> PathBuf {
let mut path = Self::base_path();
path.push(self.name.clone());
path.push(format!("{}-{}", self.name, format_date(self.timestamp)));
path.set_extension("json");
path
}
}
impl<'de, Data> ProjectFile<'de, Data> for NonTimestampedFile<Data>
where
Data: Serialize + Deserialize<'de>,
{
fn path(&self) -> PathBuf {
let mut path = Self::base_path();
path.push(self.name.clone());
path.push(self.name.clone());
path.set_extension("json");
path
}
}
impl<'a, Data> TimestampedFile<Data>
where
Self: Name,
Data: Serialize + Deserialize<'a>,
{
fn new(timestamp: DateTime<Local>) -> Self {
Self {
name: Self::name(),
timestamp,
data_type: PhantomData,
}
}
fn now() -> Self {
Self::new(now())
}
pub fn last() -> Option<Self> {
let mut path = Self::base_path();
path.push(&Self::name());
path.push(format!("{}-*.json", Self::name()));
glob(path.to_str().unwrap())
.expect("Failed to read glob pattern")
.fold(None, |acc, entry| match entry {
Ok(p) => {
let file_name = p.file_stem().unwrap().to_str().unwrap();
let split = file_name.split_once('-').unwrap();
let time = read_date(split.1).unwrap();
match acc {
None => Some(Self::new(time)),
Some(ref f) => {
if f.timestamp < time {
Some(Self::new(time))
} else {
acc
}
}
}
}
_ => acc,
})
}
pub fn all() -> Vec<Self> {
let mut path = Self::base_path();
path.push(&Self::name());
path.push(format!("{}-*.json", Self::name()));
let mut paths = vec![];
for entry in glob(path.to_str().unwrap())
.expect("Failed to read glob pattern")
.flatten()
{
let file_name = entry.file_stem().unwrap().to_str().unwrap();
let split = file_name.split_once('-').unwrap();
let _name = split.0.to_owned();
let timestamp = read_date(split.1);
match timestamp {
Err(err) => println!("Malformed timestamp in filename: {:?}. {:?}", entry, err),
Ok(t) => paths.push(Self::new(t)),
}
}
paths
}
}
impl<'a, Data> NonTimestampedFile<Data>
where
Self: Name,
Data: Serialize + Deserialize<'a>,
{
fn new() -> Self {
Self {
name: Self::name(),
data_type: PhantomData,
}
}
pub fn check(&self) -> bool {
self.path().exists()
}
}
pub type VaultFile = NonTimestampedFile<VaultEncrypted>;
pub type RecordFile = TimestampedFile<RecordEncrypted>;
pub type BackupFile = TimestampedFile<VaultEncrypted>;
pub type SchemaFile = NonTimestampedFile<Schema>;
pub trait Name {
fn name() -> String;
}
impl Name for VaultFile {
fn name() -> String {
"vault".to_string()
}
}
impl Name for RecordFile {
fn name() -> String {
"record".to_string()
}
}
impl Name for BackupFile {
fn name() -> String {
"backup".to_string()
}
}
impl Name for SchemaFile {
fn name() -> String {
"schema".to_string()
}
}
impl<'a, Data> Default for TimestampedFile<Data>
where
TimestampedFile<Data>: Name,
Data: Serialize + Deserialize<'a>,
{
fn default() -> Self {
Self::now()
}
}
impl<'a, Data> Default for NonTimestampedFile<Data>
where
NonTimestampedFile<Data>: Name,
Data: Serialize + Deserialize<'a>,
{
fn default() -> Self {
Self::new()
}
}