use async_std::fs::{File, OpenOptions}; use async_std::io::{ErrorKind as IOErrorKind, BufReader}; use async_std::io::prelude::*; use async_std::task::spawn_blocking; use async_trait::async_trait; use crate::database::{ErrorKind, Result, Storage, StorageError}; use fd_lock::RwLock; use log::debug; use std::path::{Path, PathBuf}; use std::collections::HashMap; impl From for StorageError { fn from(source: std::io::Error) -> Self { Self::with_source( match source.kind() { IOErrorKind::NotFound => ErrorKind::NotFound, _ => ErrorKind::Backend, }, "file I/O error", Box::new(source), ) } } async fn get_lockable_file(file: File) -> RwLock { debug!("Trying to create a file lock"); spawn_blocking(move || RwLock::new(file)).await } fn url_to_path(root: &Path, url: &str) -> PathBuf { let url = http_types::Url::parse(url).expect("Couldn't parse a URL"); let mut path: PathBuf = root.to_owned(); path.push(url.origin().ascii_serialization() + &url.path().to_string() + ".json"); path } fn modify_post(post: &serde_json::Value, update: &serde_json::Value) -> Result { let mut add_keys: HashMap = HashMap::new(); let mut remove_keys: Vec = vec![]; let mut remove_values: HashMap> = HashMap::new(); let mut post = post.clone(); if let Some(delete) = update["delete"].as_array() { remove_keys.extend(delete.iter().filter_map(|v| v.as_str()).map(|v| v.to_string())); } else if let Some(delete) = update["delete"].as_object() { for (k, v) in delete { if let Some(v) = v.as_array() { remove_values.entry(k.to_string()).or_default().extend(v.clone()); } else { return Err(StorageError::new(ErrorKind::BadRequest, "Malformed update object")); } } } if let Some(add) = update["add"].as_object() { for (k, v) in add { if v.is_array() { add_keys.insert(k.to_string(), v.clone()); } else { return Err(StorageError::new(ErrorKind::BadRequest, "Malformed update object")); } } } if let Some(replace) = update["replace"].as_object() { for (k, v) in replace { remove_keys.push(k.to_string()); add_keys.insert(k.to_string(), v.clone()); } } for k in remove_keys { post["properties"].as_object_mut().unwrap().remove(&k); } for (k, v) in remove_values { let k = &k; let props; if k == "children" { props = &mut post; } else { props = &mut post["properties"]; } v.iter().for_each(|v| { if let Some(vec) = props[k].as_array_mut() { if let Some(index) = vec.iter().position(|w| w == v) { vec.remove(index); } } }); } for (k, v) in add_keys { let props; if k == "children" { props = &mut post; } else { props = &mut post["properties"]; } let k = &k; if let Some(prop) = props[k].as_array_mut() { if k == "children" { v.as_array().unwrap().iter().cloned().rev().for_each(|v| prop.insert(0, v)); } else { prop.extend(v.as_array().unwrap().iter().cloned()); } } else { post["properties"][k] = v } } Ok(post) } #[derive(Clone)] pub struct FileStorage { root_dir: PathBuf, } impl FileStorage { pub async fn new(root_dir: PathBuf) -> Result { // TODO check if the dir is writable Ok(Self { root_dir }) } } #[async_trait] impl Storage for FileStorage { async fn post_exists(&self, url: &str) -> Result { let path = url_to_path(&self.root_dir, url); debug!("Checking if {:?} exists...", path); Ok(spawn_blocking(move || path.is_file()).await) } async fn get_post(&self, url: &str) -> Result> { let path = url_to_path(&self.root_dir, url); debug!("Opening {:?}", path); // We have to special-case in here because the function should return Ok(None) on 404 match File::open(path).await { Ok(f) => { let lock = get_lockable_file(f).await; let guard = lock.read()?; // HOW DOES THIS TYPECHECK?!!!!!!!! // Read::read(&mut self) requires a mutable reference // yet Read is implemented for &File // We can't get a &mut File from &File, can we? // And we need a &mut File to use Read::read_to_string() // Yet if we pass it to a BufReader it works?!! // // I hate magic // // TODO find a way to get rid of BufReader here let mut content = String::new(); let mut reader = BufReader::new(&*guard); reader.read_to_string(&mut content).await?; drop(reader); drop(guard); Ok(Some(serde_json::from_str(&content)?)) } Err(err) => { if err.kind() == IOErrorKind::NotFound { Ok(None) } else { Err(err.into()) } } } } async fn put_post<'a>(&self, post: &'a serde_json::Value, user: &'a str) -> Result<()> { let key = post["properties"]["uid"][0] .as_str() .expect("Tried to save a post without UID"); let path = url_to_path(&self.root_dir, key); debug!("Creating {:?}", path); let parent = path.parent().unwrap().to_owned(); if !spawn_blocking(move || parent.is_dir()).await { async_std::fs::create_dir_all(path.parent().unwrap()).await?; } let f = OpenOptions::new() .write(true) .create_new(true) .open(&path) .await?; let mut lock = get_lockable_file(f).await; let mut guard = lock.write()?; (*guard).write_all(post.to_string().as_bytes()).await?; (*guard).flush().await?; drop(guard); if post["properties"]["url"].is_array() { for url in post["properties"]["url"] .as_array() .unwrap() .iter() .map(|i| i.as_str().unwrap()) { // TODO consider using the symlink crate // to promote cross-platform compat on Windows // do we even need to support Windows?... if url != key && url.starts_with(user) { let link = url_to_path(&self.root_dir, url); debug!("Creating a symlink at {:?}", link); let orig = path.clone(); spawn_blocking(move || { std::os::unix::fs::symlink(orig, link) }).await?; } } } Ok(()) } async fn update_post<'a>(&self, url: &'a str, update: serde_json::Value) -> Result<()> { let path = url_to_path(&self.root_dir, url); let f = OpenOptions::new() .write(true) .read(true) .truncate(false) .open(&path) .await?; let mut lock = get_lockable_file(f).await; let mut guard = lock.write()?; let mut content = String::new(); guard.read_to_string(&mut content).await?; let json: serde_json::Value = serde_json::from_str(&content)?; // Apply the editing algorithms let new_json = modify_post(&json, &update)?; (*guard).set_len(0).await?; (*guard).seek(std::io::SeekFrom::Start(0)).await?; (*guard).write_all(new_json.to_string().as_bytes()).await?; (*guard).flush().await?; drop(guard); // TODO check if URLs changed between old and new JSON Ok(()) } async fn get_channels( &self, user: &crate::indieauth::User, ) -> Result> { todo!() } async fn read_feed_with_limit<'a>( &self, url: &'a str, after: &'a Option, limit: usize, user: &'a Option, ) -> Result> { todo!() } async fn delete_post<'a>(&self, url: &'a str) -> Result<()> { todo!() } async fn get_setting<'a>(&self, setting: &'a str, user: &'a str) -> Result { todo!() } async fn set_setting<'a>(&self, setting: &'a str, user: &'a str, value: &'a str) -> Result<()> { todo!() } }