about summary refs log blame commit diff
path: root/src/database/file/mod.rs
blob: d64177989a911c3d95f1da4f83a574a280b3654f (plain) (tree)
1
2
3
4
5
6
7
8
9







                                                                
                              

























                                                                                      







































































                                                                                                   












































































                                                                                             
                                                               























                                                                                              






















                                                                      






















                                                                                        

















                                                                                      

                                                                                                    
































                                                                              
     
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<std::io::Error> 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<File> {
    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<serde_json::Value> {
    let mut add_keys: HashMap<String, serde_json::Value> = HashMap::new();
    let mut remove_keys: Vec<String> = vec![];
    let mut remove_values: HashMap<String, Vec<serde_json::Value>> = 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<Self> {
        // 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<bool> {
        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<Option<serde_json::Value>> {
        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<Vec<super::MicropubChannel>> {
        todo!()
    }

    async fn read_feed_with_limit<'a>(
        &self,
        url: &'a str,
        after: &'a Option<String>,
        limit: usize,
        user: &'a Option<String>,
    ) -> Result<Option<serde_json::Value>> {
        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<String> {
        let url = http_types::Url::parse(user).expect("Couldn't parse a URL");
        let mut path = relative_path::RelativePathBuf::new();
        path.push(url.origin().ascii_serialization());
        path.push("settings");

        let path = path.to_path(&self.root_dir);
        let lock = get_lockable_file(File::open(path).await?).await;
        let guard = lock.read()?;

        let mut content = String::new();
        (&mut &*guard).read_to_string(&mut content).await?;
        drop(guard);
        let settings: HashMap<String, String> = serde_json::from_str(&content)?;
        // XXX consider returning string slices instead of cloning a string every time
        // it might come with a performance hit and/or memory usage inflation
        settings.get(setting)
            .map(|s| s.clone())
            .ok_or_else(|| StorageError::new(ErrorKind::Backend, "Setting not set"))

    }

    async fn set_setting<'a>(&self, setting: &'a str, user: &'a str, value: &'a str) -> Result<()> {
        let url = http_types::Url::parse(user).expect("Couldn't parse a URL");
        let mut path = relative_path::RelativePathBuf::new();
        path.push(url.origin().ascii_serialization());
        path.push("settings");

        let path = path.to_path(&self.root_dir);

        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 file = OpenOptions::new()
            .write(true)
            .read(true)
            .truncate(false)
            .create(true)
            .open(&path).await?;
        let mut lock = get_lockable_file(file).await;
        let mut guard = lock.write()?;

        let mut content = String::new();
        guard.read_to_string(&mut content).await?;
        let mut settings: HashMap<String, String> = if content.len() == 0 {
            HashMap::default()
        } else {
            serde_json::from_str(&content)?
        };

        settings.insert(setting.to_string(), value.to_string());
        guard.seek(std::io::SeekFrom::Start(0)).await?;
        guard.set_len(0).await?;
        guard.write_all(serde_json::to_string(&settings)?.as_bytes()).await?;
        Ok(())
    }
}