about summary refs log tree commit diff
path: root/kittybox-rs/src/database/file
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2023-07-09 01:22:46 +0300
committerVika <vika@fireburn.ru>2023-07-09 01:22:46 +0300
commit82346b89c7d166817f3450759cc9f6df1ec7c74d (patch)
tree41cebe9ff2fefc88b20dd080d42a2730b5c6c1b8 /kittybox-rs/src/database/file
parentb56bbffbd421f7f047970d19ad86f8f10c217e9b (diff)
downloadkittybox-82346b89c7d166817f3450759cc9f6df1ec7c74d.tar.zst
database{,/file}: clean up code, add documentation and logging
`filter_post` is now out of here and moved into the frontend. This
kind of non-intrusive filtering can be done on the frontend, and the
database need not concern itself with this.

It can still be done as an optimisation... probably? but the frontend
is going to sanitize things like location in the post by itself now,
so it is not required anymore (and might be harmful, if frontend
starts indicating that there are some hidden fields by replacing them
with placeholders that ask one to log in to view information).
Diffstat (limited to 'kittybox-rs/src/database/file')
-rw-r--r--kittybox-rs/src/database/file/mod.rs188
1 files changed, 108 insertions, 80 deletions
diff --git a/kittybox-rs/src/database/file/mod.rs b/kittybox-rs/src/database/file/mod.rs
index 0f63c9d..842a834 100644
--- a/kittybox-rs/src/database/file/mod.rs
+++ b/kittybox-rs/src/database/file/mod.rs
@@ -1,5 +1,5 @@
 //#![warn(clippy::unwrap_used)]
-use crate::database::{filter_post, ErrorKind, Result, settings, Storage, StorageError};
+use crate::database::{ErrorKind, Result, settings, Storage, StorageError};
 use crate::micropub::{MicropubUpdate, MicropubPropertyDeletion};
 use async_trait::async_trait;
 use futures::{stream, StreamExt, TryStreamExt};
@@ -173,20 +173,20 @@ fn modify_post(post: &serde_json::Value, update: MicropubUpdate) -> Result<serde
         });
     }
     for (k, v) in add_keys {
+        tracing::debug!("Adding k/v to post: {} => {:?}", k, v);
         let props = if k == "children" {
             &mut post
         } else {
             &mut post["properties"]
         };
-        let k = &k;
-        if let Some(prop) = props[k].as_array_mut() {
+        if let Some(prop) = props[&k].as_array_mut() {
             if k == "children" {
                 v.into_iter().rev().for_each(|v| prop.insert(0, v));
             } else {
                 prop.extend(v.into_iter());
             }
         } else {
-            post["properties"][k] = serde_json::Value::Array(v)
+            props[&k] = serde_json::Value::Array(v)
         }
     }
     Ok(post)
@@ -227,10 +227,7 @@ async fn hydrate_author<S: Storage>(
                     if let Some(i) = i.as_str() {
                         match storage.get_post(i).await {
                             Ok(post) => match post {
-                                Some(post) => match filter_post(post, user) {
-                                    Some(author) => author,
-                                    None => json!(i),
-                                },
+                                Some(post) => post,
                                 None => json!(i),
                             },
                             Err(e) => {
@@ -383,27 +380,41 @@ impl Storage for FileStorage {
                 .map(|s| s.to_string())
                 .unwrap_or_else(String::default);
             let key = key.to_string();
-
+            tracing::debug!("Opening temporary file to modify chnanels...");
             let mut tempfile = OpenOptions::new()
                 .write(true)
                 .create_new(true)
                 .open(&tempfilename)
                 .await?;
-            let mut file = OpenOptions::new()
-                .read(true)
-                .write(true)
-                .truncate(false)
-                .create(true)
-                .open(&path)
-                .await?;
-
-            let mut content = String::new();
-            file.read_to_string(&mut content).await?;
-            drop(file);
-            let mut channels: Vec<super::MicropubChannel> = if !content.is_empty() {
-                serde_json::from_str(&content)?
-            } else {
-                Vec::default()
+            tracing::debug!("Opening real channel file...");
+            let mut channels: Vec<super::MicropubChannel> = {
+                match OpenOptions::new()
+                    .read(true)
+                    .write(false)
+                    .truncate(false)
+                    .create(false)
+                    .open(&path)
+                    .await
+                 {
+                     Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
+                         Vec::default()
+                     }
+                     Err(err) => {
+                         // Propagate the error upwards
+                         return Err(err.into());
+                     }
+                     Ok(mut file) => {
+                         let mut content = String::new();
+                         file.read_to_string(&mut content).await?;
+                         drop(file);
+
+                         if !content.is_empty() {
+                             serde_json::from_str(&content)?
+                         } else {
+                             Vec::default()
+                         }
+                     }
+                 }
             };
 
             channels.push(super::MicropubChannel {
@@ -489,7 +500,32 @@ impl Storage for FileStorage {
         limit: usize,
         user: Option<&'_ str>
     ) -> Result<Option<(serde_json::Value, Option<String>)>> {
-        todo!()
+        Ok(self.read_feed_with_limit(
+            url,
+            &cursor.map(|v| v.to_owned()),
+            limit,
+            &user.map(|v| v.to_owned())
+        ).await?
+            .map(|feed| {
+                tracing::debug!("Feed: {:#}", serde_json::Value::Array(
+                    feed["children"]
+                    .as_array()
+                    .map(|v| v.as_slice())
+                    .unwrap_or_default()
+                    .iter()
+                    .map(|mf2| mf2["properties"]["uid"][0].clone())
+                    .collect::<Vec<_>>()
+                ));
+                let cursor: Option<String> = feed["children"]
+                    .as_array()
+                    .map(|v| v.as_slice())
+                    .unwrap_or_default()
+                    .last()
+                    .map(|v| v["properties"]["uid"][0].as_str().unwrap().to_owned());
+                tracing::debug!("Extracted the cursor: {:?}", cursor);
+                (feed, cursor)
+            })
+        )
     }
 
     #[tracing::instrument(skip(self))]
@@ -500,65 +536,57 @@ impl Storage for FileStorage {
         limit: usize,
         user: &'_ Option<String>,
     ) -> Result<Option<serde_json::Value>> {
-        if let Some(feed) = self.get_post(url).await? {
-            if let Some(mut feed) = filter_post(feed, user) {
-                if feed["children"].is_array() {
-                    // Take this out of the MF2-JSON document to save memory
-                    //
-                    // This uses a clever match with enum destructuring
-                    // to extract the underlying Vec without cloning it
-                    let children: Vec<serde_json::Value> = match feed["children"].take() {
-                        serde_json::Value::Array(children) => children,
-                        // We've already checked it's an array
-                        _ => unreachable!()
-                    };
-
-                    let mut posts_iter = children
-                        .into_iter()
-                        .map(|s: serde_json::Value| s.as_str().unwrap().to_string());
-                    // Note: we can't actually use `skip_while` here because we end up emitting `after`.
-                    // This imperative snippet consumes after instead of emitting it, allowing the
-                    // stream of posts to return only those items that truly come *after* that one.
-                    // If I would implement an Iter combinator like this, I would call it `skip_until`
-                    if let Some(after) = after {
-                        for s in posts_iter.by_ref() {
-                            if &s == after {
-                                break;
-                            }
-                        }
-                    };
-                    let posts = stream::iter(posts_iter)
-                        .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.
-                        .try_filter_map(|post: Option<serde_json::Value>| async move { Ok(post) })
-                        .try_filter_map(|post| async move { Ok(filter_post(post, user)) })
-                        .and_then(|mut post| async move {
-                            hydrate_author(&mut post, user, self).await;
-                            Ok(post)
-                        })
-                        .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,
-                                Cow::Owned(format!("Feed assembly error: {}", &err)),
-                                Box::new(err),
-                            ));
+        if let Some(mut feed) = self.get_post(url).await? {
+            if feed["children"].is_array() {
+                // Take this out of the MF2-JSON document to save memory
+                //
+                // This uses a clever match with enum destructuring
+                // to extract the underlying Vec without cloning it
+                let children: Vec<serde_json::Value> = match feed["children"].take() {
+                    serde_json::Value::Array(children) => children,
+                    // We've already checked it's an array
+                    _ => unreachable!()
+                };
+                tracing::debug!("Full children array: {:#}", serde_json::Value::Array(children.clone()));
+                let mut posts_iter = children
+                    .into_iter()
+                    .map(|s: serde_json::Value| s.as_str().unwrap().to_string());
+                // Note: we can't actually use `skip_while` here because we end up emitting `after`.
+                // This imperative snippet consumes after instead of emitting it, allowing the
+                // stream of posts to return only those items that truly come *after* that one.
+                // If I would implement an Iter combinator like this, I would call it `skip_until`
+                if let Some(after) = after {
+                    for s in posts_iter.by_ref() {
+                        if &s == after {
+                            break;
                         }
                     }
+                };
+                let posts = stream::iter(posts_iter)
+                    .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.
+                    .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)
+                    })
+                    .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,
+                            Cow::Owned(format!("Feed assembly error: {}", &err)),
+                            Box::new(err),
+                        ));
+                    }
                 }
-                hydrate_author(&mut feed, user, self).await;
-                Ok(Some(feed))
-            } else {
-                Err(StorageError::from_static(
-                    ErrorKind::PermissionDenied,
-                    "specified user cannot access this post",
-                ))
             }
+            hydrate_author(&mut feed, user, self).await;
+            Ok(Some(feed))
         } else {
             Ok(None)
         }