about summary refs log tree commit diff
path: root/src/database
diff options
context:
space:
mode:
Diffstat (limited to 'src/database')
-rw-r--r--src/database/file/mod.rs151
-rw-r--r--src/database/mod.rs58
2 files changed, 139 insertions, 70 deletions
diff --git a/src/database/file/mod.rs b/src/database/file/mod.rs
index d67c920..4fb7f47 100644
--- a/src/database/file/mod.rs
+++ b/src/database/file/mod.rs
@@ -1,17 +1,17 @@
+use crate::database::{filter_post, ErrorKind, Result, Storage, StorageError};
 use async_std::fs::{File, OpenOptions};
-use async_std::io::{ErrorKind as IOErrorKind};
 use async_std::io::prelude::*;
+use async_std::io::ErrorKind as IOErrorKind;
 use async_std::task::spawn_blocking;
 use async_trait::async_trait;
+use fd_lock::RwLock;
 use futures::stream;
 use futures_util::StreamExt;
 use futures_util::TryStreamExt;
-use serde_json::json;
-use crate::database::{ErrorKind, Result, Storage, StorageError, filter_post};
-use fd_lock::RwLock;
 use log::debug;
-use std::path::{Path, PathBuf};
+use serde_json::json;
 use std::collections::HashMap;
+use std::path::{Path, PathBuf};
 
 impl From<std::io::Error> for StorageError {
     fn from(source: std::io::Error) -> Self {
@@ -106,13 +106,24 @@ fn modify_post(post: &serde_json::Value, update: &serde_json::Value) -> Result<s
     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()));
+        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());
+                remove_values
+                    .entry(k.to_string())
+                    .or_default()
+                    .extend(v.clone());
             } else {
-                return Err(StorageError::new(ErrorKind::BadRequest, "Malformed update object"));
+                return Err(StorageError::new(
+                    ErrorKind::BadRequest,
+                    "Malformed update object",
+                ));
             }
         }
     }
@@ -121,7 +132,10 @@ fn modify_post(post: &serde_json::Value, update: &serde_json::Value) -> Result<s
             if v.is_array() {
                 add_keys.insert(k.to_string(), v.clone());
             } else {
-                return Err(StorageError::new(ErrorKind::BadRequest, "Malformed update object"));
+                return Err(StorageError::new(
+                    ErrorKind::BadRequest,
+                    "Malformed update object",
+                ));
             }
         }
     }
@@ -161,7 +175,12 @@ fn modify_post(post: &serde_json::Value, update: &serde_json::Value) -> Result<s
         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));
+                v.as_array()
+                    .unwrap()
+                    .iter()
+                    .cloned()
+                    .rev()
+                    .for_each(|v| prop.insert(0, v));
             } else {
                 prop.extend(v.as_array().unwrap().iter().cloned());
             }
@@ -169,10 +188,12 @@ fn modify_post(post: &serde_json::Value, update: &serde_json::Value) -> Result<s
             post["properties"][k] = v
         }
     }
-   Ok(post)
+    Ok(post)
 }
 
 #[derive(Clone)]
+/// A backend using a folder with JSON files as a backing store.
+/// Uses symbolic links to represent a many-to-one mapping of URLs to a post.
 pub struct FileStorage {
     root_dir: PathBuf,
 }
@@ -185,10 +206,19 @@ impl FileStorage {
     }
 }
 
-async fn hydrate_author<S: Storage>(feed: &mut serde_json::Value, user: &'_ Option<String>, storage: &S) {
+async fn hydrate_author<S: Storage>(
+    feed: &mut serde_json::Value,
+    user: &'_ Option<String>,
+    storage: &S,
+) {
     let url = feed["properties"]["uid"][0].as_str().unwrap();
     if let Some(author) = feed["properties"]["author"].clone().as_array() {
-        if !feed["type"].as_array().unwrap().iter().any(|i| i == "h-card") {
+        if !feed["type"]
+            .as_array()
+            .unwrap()
+            .iter()
+            .any(|i| i == "h-card")
+        {
             let author_list: Vec<serde_json::Value> = stream::iter(author.iter())
                 .then(|i| async move {
                     if let Some(i) = i.as_str() {
@@ -196,18 +226,21 @@ async fn hydrate_author<S: Storage>(feed: &mut serde_json::Value, user: &'_ Opti
                             Ok(post) => match post {
                                 Some(post) => match filter_post(post, user) {
                                     Some(author) => author,
-                                        None => json!(i)
+                                    None => json!(i),
                                 },
-                                None => json!(i)
+                                None => json!(i),
                             },
                             Err(e) => {
                                 log::error!("Error while hydrating post {}: {}", url, e);
                                 json!(i)
                             }
                         }
-                    } else { i.clone() }
+                    } else {
+                        i.clone()
+                    }
                 })
-                .collect::<Vec<_>>().await;
+                .collect::<Vec<_>>()
+                .await;
             feed["properties"].as_object_mut().unwrap()["author"] = json!(author_list);
         }
     }
@@ -251,8 +284,8 @@ impl Storage for FileStorage {
 
     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");
+            .as_str()
+            .expect("Tried to save a post without UID");
         let path = url_to_path(&self.root_dir, key);
 
         debug!("Creating {:?}", path);
@@ -288,26 +321,39 @@ impl Storage for FileStorage {
                     let orig = path.clone();
                     spawn_blocking::<_, Result<()>>(move || {
                         // We're supposed to have a parent here.
-                        let basedir = link.parent().ok_or(StorageError::new(ErrorKind::Backend, "Failed to calculate parent directory when creating a symlink"))?;
+                        let basedir = link.parent().ok_or(StorageError::new(
+                            ErrorKind::Backend,
+                            "Failed to calculate parent directory when creating a symlink",
+                        ))?;
                         let relative = path_relative_from(&orig, &basedir).unwrap();
                         println!("{:?} - {:?} = {:?}", &orig, &basedir, &relative);
                         println!("Created a symlink at {:?}", &link);
                         let symlink_result;
                         #[cfg(unix)]
-                        { symlink_result = std::os::unix::fs::symlink(relative, link); }
+                        {
+                            symlink_result = std::os::unix::fs::symlink(relative, link);
+                        }
                         // Wow it even supports windows. Not sure if I need it to run on Windows but oh well
                         #[cfg(windows)]
-                        { symlink_result = std::os::windows::fs::symlink_file(relative, link); }
+                        {
+                            symlink_result = std::os::windows::fs::symlink_file(relative, link);
+                        }
                         match symlink_result {
                             Ok(()) => Ok(()),
-                            Err(e) => Err(e.into())
+                            Err(e) => Err(e.into()),
                         }
-                    }).await?;
+                    })
+                    .await?;
                 }
             }
         }
 
-        if post["type"].as_array().unwrap().iter().any(|s| s.as_str() == Some("h-feed")) {
+        if post["type"]
+            .as_array()
+            .unwrap()
+            .iter()
+            .any(|s| s.as_str() == Some("h-feed"))
+        {
             println!("Adding to channel list...");
             // Add the h-feed to the channel list
             let mut path = relative_path::RelativePathBuf::new();
@@ -320,7 +366,8 @@ impl Storage for FileStorage {
                 .write(true)
                 .truncate(false)
                 .create(true)
-                .open(&path).await?;
+                .open(&path)
+                .await?;
             let mut lock = get_lockable_file(file).await;
             let mut guard = lock.write()?;
 
@@ -338,11 +385,13 @@ impl Storage for FileStorage {
                 name: post["properties"]["name"][0]
                     .as_str()
                     .map(|s| s.to_string())
-                    .unwrap_or_else(|| String::default())
+                    .unwrap_or_else(|| String::default()),
             });
             guard.seek(std::io::SeekFrom::Start(0)).await?;
             guard.set_len(0).await?;
-            guard.write_all(serde_json::to_string(&channels)?.as_bytes()).await?;
+            guard
+                .write_all(serde_json::to_string(&channels)?.as_bytes())
+                .await?;
         }
 
         Ok(())
@@ -375,10 +424,7 @@ impl Storage for FileStorage {
         Ok(())
     }
 
-    async fn get_channels<'a>(
-        &self,
-        user: &'a str,
-    ) -> Result<Vec<super::MicropubChannel>> {
+    async fn get_channels<'a>(&self, user: &'a str) -> Result<Vec<super::MicropubChannel>> {
         let mut path = relative_path::RelativePathBuf::new();
         path.push(user.to_string());
         path.push("channels");
@@ -393,11 +439,11 @@ impl Storage for FileStorage {
                 (&mut &*guard).read_to_string(&mut content).await?;
                 // This should not happen, but if it does, let's handle it gracefully instead of failing.
                 if content.len() == 0 {
-                    return Ok(vec![])
+                    return Ok(vec![]);
                 }
                 let channels: Vec<super::MicropubChannel> = serde_json::from_str(&content)?;
                 Ok(channels)
-            },
+            }
             Err(e) => {
                 if e.kind() == IOErrorKind::NotFound {
                     Ok(vec![])
@@ -419,7 +465,8 @@ impl Storage for FileStorage {
             if let Some(mut feed) = filter_post(feed, user) {
                 if feed["children"].is_array() {
                     let children = feed["children"].as_array().unwrap().clone();
-                    let mut posts_iter = children.into_iter()
+                    let mut posts_iter = children
+                        .into_iter()
                         .map(|s: serde_json::Value| s.as_str().unwrap().to_string());
                     if after.is_some() {
                         loop {
@@ -430,24 +477,25 @@ impl Storage for FileStorage {
                         }
                     }
                     let posts = stream::iter(posts_iter)
-                        .map(|url: String| async move {
-                            self.get_post(&url).await
-                        })
+                        .map(|url: String| async move { self.get_post(&url).await })
                         .buffered(std::cmp::min(3, limit))
-                    // Hack to unwrap the Option and sieve out broken links
-                    // Broken links return None, and Stream::filter_map skips Nones.
+                        // Hack to unwrap the Option and sieve out broken links
+                        // Broken links return None, and Stream::filter_map skips Nones.
                         .try_filter_map(|post: Option<serde_json::Value>| async move { Ok(post) })
-                        .and_then(|mut post| async move { hydrate_author(&mut post, user, self).await; Ok(post) })
+                        .and_then(|mut post| async move {
+                            hydrate_author(&mut post, user, self).await;
+                            Ok(post)
+                        })
                         .try_filter_map(|post| async move { Ok(filter_post(post, user)) })
-
                         .take(limit);
 
                     match posts.try_collect::<Vec<serde_json::Value>>().await {
                         Ok(posts) => feed["children"] = serde_json::json!(posts),
                         Err(err) => {
                             return Err(StorageError::with_source(
-                                ErrorKind::Other, "Feed assembly error",
-                                Box::new(err)
+                                ErrorKind::Other,
+                                "Feed assembly error",
+                                Box::new(err),
                             ));
                         }
                     }
@@ -469,7 +517,9 @@ impl Storage for FileStorage {
         let path = url_to_path(&self.root_dir, url);
         if let Err(e) = async_std::fs::remove_file(path).await {
             Err(e.into())
-        } else { Ok(()) }
+        } else {
+            Ok(())
+        }
     }
 
     async fn get_setting<'a>(&self, setting: &'a str, user: &'a str) -> Result<String> {
@@ -489,10 +539,10 @@ impl Storage for FileStorage {
         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)
+        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<()> {
@@ -513,7 +563,8 @@ impl Storage for FileStorage {
             .read(true)
             .truncate(false)
             .create(true)
-            .open(&path).await?;
+            .open(&path)
+            .await?;
         let mut lock = get_lockable_file(file).await;
         log::debug!("Created a lock. Locking for writing...");
         let mut guard = lock.write()?;
@@ -530,7 +581,9 @@ impl Storage for FileStorage {
         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?;
+        guard
+            .write_all(serde_json::to_string(&settings)?.as_bytes())
+            .await?;
         Ok(())
     }
 }
diff --git a/src/database/mod.rs b/src/database/mod.rs
index a57e243..6a874ed 100644
--- a/src/database/mod.rs
+++ b/src/database/mod.rs
@@ -1,5 +1,4 @@
 #![warn(missing_docs)]
-use crate::indieauth::User;
 use async_trait::async_trait;
 use serde::{Deserialize, Serialize};
 
@@ -70,7 +69,9 @@ impl From<StorageError> for tide::Response {
 }
 impl std::error::Error for StorageError {
     fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
-        self.source.as_ref().map(|e| e.as_ref() as &dyn std::error::Error)
+        self.source
+            .as_ref()
+            .map(|e| e.as_ref() as &dyn std::error::Error)
     }
 }
 impl From<serde_json::Error> for StorageError {
@@ -115,11 +116,15 @@ impl StorageError {
         }
     }
     /// Create a StorageError using another arbitrary Error as a source.
-    fn with_source(kind: ErrorKind, msg: &str, source: Box<dyn std::error::Error + Send + Sync>) -> Self {
+    fn with_source(
+        kind: ErrorKind,
+        msg: &str,
+        source: Box<dyn std::error::Error + Send + Sync>,
+    ) -> Self {
         Self {
             msg: msg.to_string(),
             source: Some(source),
-            kind
+            kind,
         }
     }
     /// Get the kind of an error.
@@ -135,7 +140,15 @@ impl StorageError {
 /// A special Result type for the Micropub backing storage.
 pub type Result<T> = std::result::Result<T, StorageError>;
 
-pub fn filter_post(mut post: serde_json::Value, user: &'_ Option<String>) -> Option<serde_json::Value> {
+/// Filter the post according to the value of `user`.
+///
+/// Anonymous users cannot view private posts and protected locations;
+/// Logged-in users can only view private posts targeted at them;
+/// Logged-in users can't view private location data
+pub fn filter_post(
+    mut post: serde_json::Value,
+    user: &'_ Option<String>,
+) -> Option<serde_json::Value> {
     if post["properties"]["deleted"][0].is_string() {
         return Some(serde_json::json!({
             "type": post["type"],
@@ -253,8 +266,8 @@ mod tests {
     //#[cfg(feature="redis")]
     //use super::redis::tests::get_redis_instance;
     use super::{MicropubChannel, Storage};
-    use serde_json::json;
     use paste::paste;
+    use serde_json::json;
 
     async fn test_backend_basic_operations<Backend: Storage>(backend: Backend) {
         let post: serde_json::Value = json!({
@@ -338,15 +351,21 @@ mod tests {
             .await
             .unwrap();
 
-        backend.update_post(&key, json!({
-            "url": &key,
-            "add": {
-                "category": ["testing"],
-            },
-            "replace": {
-                "content": ["Different test content"]
-            }
-        })).await.unwrap();
+        backend
+            .update_post(
+                &key,
+                json!({
+                    "url": &key,
+                    "add": {
+                        "category": ["testing"],
+                    },
+                    "replace": {
+                        "content": ["Different test content"]
+                    }
+                }),
+            )
+            .await
+            .unwrap();
 
         if let Some(returned_post) = backend.get_post(&key).await.unwrap() {
             assert!(returned_post.is_object());
@@ -385,10 +404,7 @@ mod tests {
             .put_post(&feed, "https://fireburn.ru/")
             .await
             .unwrap();
-        let chans = backend
-            .get_channels("https://fireburn.ru/")
-            .await
-            .unwrap();
+        let chans = backend.get_channels("https://fireburn.ru/").await.unwrap();
         assert_eq!(chans.len(), 1);
         assert_eq!(
             chans[0],
@@ -412,7 +428,7 @@ mod tests {
             "Vika's Hideout"
         );
     }
-    
+
     /*macro_rules! redis_test {
         ($func_name:expr) => {
             paste! {
@@ -441,7 +457,7 @@ mod tests {
                     $func_name(backend).await
                 }
             }
-        }
+        };
     }
 
     /*redis_test!(test_backend_basic_operations);