implement publishing / updating

This commit is contained in:
Peter Cai 2020-04-08 16:51:39 +08:00
parent b910f25cc7
commit 4995de8372
No known key found for this signature in database
GPG Key ID: 71F5FB4E4F3FD54F
3 changed files with 161 additions and 6 deletions

View File

@ -39,7 +39,62 @@ impl PostsList {
// Also consumes self, as this should normally be the last action
// in an API call
pub async fn add_post(mut self, uuid: &str) -> MyResult<()> {
if self.has_post(uuid) {
return Ok(());
}
self.0.insert(0, uuid.into());
store::put_obj_pretty("posts_list", self.0).await
}
}
#[derive(Serialize, Deserialize)]
pub struct Post {
// The UUID of the post (a Standard Notes UUID)
pub uuid: String,
// The UNIX timestamp (in seconds) for the post
pub timestamp: u64,
// URL of the post (relative to the root of the site)
pub url: String,
// Title of the post
pub title: String,
// The Markdown content of the post
// We keep the original content here
// so that we could make changes to the Markdown parser
// in the future; we won't be stuck with a parsed version
pub content: String
}
impl Post {
fn uuid_to_post_key(uuid: &str) -> String {
format!("post_by_uuid_{}", uuid)
}
fn url_to_mapping_key(url: &str) -> String {
format!("url_mapping_{}", url)
}
async fn create_url_mapping(url: &str, uuid: &str) -> MyResult<()> {
store::put_str(&Self::url_to_mapping_key(url), uuid).await
}
// Returns Err(InternalError) if the post is not found
// Note that the existence status of a post here must
// be synchronized with the PostsList; that is, if a
// post is not found in PostsList, it must not be found
// here either; if a post is found in PostsList, then
// this method should not return any error.
pub async fn find_by_uuid(uuid: &str) -> MyResult<Post> {
store::get_obj(&Self::uuid_to_post_key(uuid)).await
}
// Write the Post to KV storage; this can be a new post or
// update to an existing post; either way, the CALLER is
// responsible for making sure PostsList is updated with the
// latest set of posts sorted in order.
// This function will also create a mapping from URL to UUID in the KV
pub async fn write_to_kv(self) -> MyResult<()> {
Self::create_url_mapping(&self.url, &self.uuid).await?;
store::put_obj(&Self::uuid_to_post_key(&self.uuid), self).await
}
}

View File

@ -2,12 +2,15 @@
use crate::{CONFIG, blog};
use crate::router::Router;
use crate::utils::*;
use serde::{Serialize, Serializer};
use js_sys::Date;
use serde::{Deserialize, Serialize, Serializer};
use std::vec::Vec;
use wasm_bindgen_futures::JsFuture;
use web_sys::*;
pub fn build_routes(router: &mut Router) {
router.add_route("/actions", &get_actions);
router.add_route("/post", &create_or_update_post);
}
macro_rules! verify_secret {
@ -29,13 +32,16 @@ async fn get_actions(_req: Request, url: Url) -> MyResult<Response> {
let mut actions = vec![];
// Show different options depending on whether the post already exists
let post_exists = match params.get("item_uuid") {
Some(uuid) => {
let posts = blog::PostsList::load().await;
posts.has_post(&uuid)
// Use Post here because PostsList is larger to read into memory
// also slower to check one-by-one
let post = match params.get("item_uuid") {
Some(uuid) => match blog::Post::find_by_uuid(&uuid).await {
Ok(post) => Some(post),
Err(_) => None
},
None => false
None => None
};
let post_exists = post.is_some();
actions.push(Action {
label: if post_exists { "Update".into() } else { "Publish".into() },
@ -66,6 +72,62 @@ async fn get_actions(_req: Request, url: Url) -> MyResult<Response> {
).internal_err()
}
async fn create_or_update_post(req: Request, url: Url) -> MyResult<Response> {
verify_secret!(url, params);
if req.method() != "POST" {
return Err(Error::BadRequest("Unsupported method".into()));
}
// Load the information sent as POST body
let data: ActionsPostData = serde_json::from_str(
&JsFuture::from(req.text().internal_err()?)
.await.internal_err()?
.as_string().ok_or(Error::BadRequest("Unable to parse POST body".into()))?
).internal_err()?;
if data.items.len() == 0 {
return Err(Error::BadRequest("At least one item must be supplied".into()));
}
// TODO: we should support customizing timestamp and URL from text
// and if there are option detected in text, it always overrides
// whatever was stored before
// If the URL is changed via this way, we should make sure the old
// URL actually 301's to the new URL
let uuid = data.items[0].uuid.clone();
let text = data.items[0].content.text.clone();
let title = data.items[0].content.title.clone();
let post = match blog::Post::find_by_uuid(&uuid).await {
Ok(mut post) => {
post.content = text;
post.title = title;
post
},
Err(_) => {
blog::Post {
url: title_to_url(&uuid, &title),
uuid: uuid,
title: title,
content: text,
timestamp: Date::now() as u64 / 1000 // Seconds
}
}
};
// Write the new post to storage
// As you may have seen by now, the process is far from atomic
// This is fine because we don't expect users to update posts from
// multiple endpoints simultaneously all the time
blog::PostsList::load().await.add_post(&post.uuid).await?;
post.write_to_kv().await?;
Response::new_with_opt_str_and_init(
None,
ResponseInit::new()
.status(200)
.headers(headers!().add_cors().as_ref())
).internal_err()
}
pub enum Verb {
Show,
Post,
@ -149,4 +211,22 @@ pub struct ActionsExtension {
content_type: ContentType,
supported_types: Vec<ContentType>,
actions: Vec<Action>
}
// Many fields are omitted here since we don't use them for now
#[derive(Deserialize)]
pub struct ActionsPostItem {
uuid: String,
content: ActionsPostContent
}
#[derive(Deserialize)]
pub struct ActionsPostContent {
title: String,
text: String
}
#[derive(Deserialize)]
pub struct ActionsPostData {
items: Vec<ActionsPostItem>
}

View File

@ -41,6 +41,26 @@ macro_rules! headers(
() => { ::web_sys::Headers::new().unwrap() };
);
// Remove all non-ascii characters from string
pub fn filter_non_ascii(s: &str) -> String {
s.chars().into_iter()
.filter(|c| c.is_ascii())
.collect()
}
// A URL is "<uuid_first_four_chars>/<title_without_non_ascii>"
// The UUID involvement is to reduce the chance that two
// articles have the same URL by having the same title when
// all non-ASCII characters are removed
pub fn title_to_url(uuid: &str, title: &str) -> String {
let title_part = filter_non_ascii(title)
.split_whitespace()
.collect::<Vec<&str>>()
.join("-")
.to_lowercase();
format!("{}/{}", &uuid[0..4], title_part)
}
pub trait HeadersExt {
fn add_cors(self) -> Self;
}