From 13bcfb013c4a5ac5ea15c7ebe04f243431165c03 Mon Sep 17 00:00:00 2001 From: Vika Date: Sun, 15 Aug 2021 15:13:34 +0300 Subject: Added a WIP file backend Currently unavailable for use and only has basic GET and POST operations implemented. A lot more work is needed to make it truly usable. Locking is implemented using flock() system call on Linux. --- src/database/file/mod.rs | 173 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 src/database/file/mod.rs (limited to 'src/database/file') diff --git a/src/database/file/mod.rs b/src/database/file/mod.rs new file mode 100644 index 0000000..36300fc --- /dev/null +++ b/src/database/file/mod.rs @@ -0,0 +1,173 @@ +//pub mod async_file_ext; +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}; + +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 +} + +#[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(post.to_string().as_bytes()).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<()> { + todo!() + } + + 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!() + } +} -- cgit 1.4.1