Update
This commit is contained in:
parent
501b9d3093
commit
964534d0d9
21 changed files with 762 additions and 207 deletions
|
@ -39,3 +39,11 @@ insert_final_newline = true
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
5
Makefile
5
Makefile
|
@ -1,6 +1,9 @@
|
||||||
.PHONY: all build
|
.PHONY: all build ci
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
docker compose build
|
||||||
|
|
||||||
|
ci:
|
||||||
BUILDKIT_PROGRESS=plain docker compose build
|
BUILDKIT_PROGRESS=plain docker compose build
|
||||||
|
|
21
backend/Cargo.lock
generated
21
backend/Cargo.lock
generated
|
@ -355,11 +355,13 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"envconfig",
|
"envconfig",
|
||||||
"fronma",
|
"fronma",
|
||||||
|
"itertools",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"r2d2_redis",
|
"r2d2_redis",
|
||||||
"scan_dir",
|
"scan_dir",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"serde_yaml 0.9.17",
|
"serde_yaml 0.9.17",
|
||||||
"static-files",
|
"static-files",
|
||||||
"tikv-jemallocator",
|
"tikv-jemallocator",
|
||||||
|
@ -777,6 +779,12 @@ version = "0.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
|
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "emojis"
|
name = "emojis"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
@ -1111,6 +1119,15 @@ dependencies = [
|
||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.10.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
|
@ -1784,9 +1801,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.92"
|
version = "1.0.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7434af0dc1cbd59268aa98b4c22c131c0584d2232f6fb166efb993e2832e896a"
|
checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa 1.0.5",
|
"itoa 1.0.5",
|
||||||
"ryu",
|
"ryu",
|
||||||
|
|
|
@ -33,11 +33,13 @@ diesel = { version = "2.0.2", features = ["i-implement-a-third-party-backend-and
|
||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
envconfig = "0.10.0"
|
envconfig = "0.10.0"
|
||||||
fronma = "0.1.1"
|
fronma = "0.1.1"
|
||||||
|
itertools = "0.10.5"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
r2d2_redis = "0.14.0"
|
r2d2_redis = "0.14.0"
|
||||||
scan_dir = "0.3.3"
|
scan_dir = "0.3.3"
|
||||||
serde = { version = "1.0.152", features = ["derive"] }
|
serde = { version = "1.0.152", features = ["derive"] }
|
||||||
|
serde_json = "1.0.93"
|
||||||
serde_yaml = "0.9.17"
|
serde_yaml = "0.9.17"
|
||||||
static-files = "0.2.3"
|
static-files = "0.2.3"
|
||||||
|
|
||||||
|
|
|
@ -29,13 +29,13 @@ RUN cargo chef prepare --recipe-path recipe.json
|
||||||
FROM chef as builder
|
FROM chef as builder
|
||||||
WORKDIR /usr/src/backend
|
WORKDIR /usr/src/backend
|
||||||
COPY --from=planner /usr/src/backend/recipe.json .
|
COPY --from=planner /usr/src/backend/recipe.json .
|
||||||
RUN cargo chef cook --recipe-path recipe.json
|
RUN cargo chef cook --release --recipe-path recipe.json
|
||||||
RUN rm -rf ./src
|
RUN rm -rf ./src
|
||||||
COPY ./build.rs .
|
COPY ./build.rs .
|
||||||
COPY --from=static /usr/src/static ./static
|
COPY --from=static /usr/src/static ./static
|
||||||
COPY --from=templates /usr/src/templates ./templates
|
COPY --from=templates /usr/src/templates ./templates
|
||||||
COPY ./src ./src
|
COPY ./src ./src
|
||||||
RUN cargo build
|
RUN cargo build --release
|
||||||
|
|
||||||
FROM docker.io/debian:bullseye-slim as runner
|
FROM docker.io/debian:bullseye-slim as runner
|
||||||
LABEL maintainer="Dominic Grimm <dominic@dergrimm.net>" \
|
LABEL maintainer="Dominic Grimm <dominic@dergrimm.net>" \
|
||||||
|
@ -45,7 +45,6 @@ LABEL maintainer="Dominic Grimm <dominic@dergrimm.net>" \
|
||||||
org.opencontainers.image.url="https://git.dergrimm.net/dergrimm/blog"
|
org.opencontainers.image.url="https://git.dergrimm.net/dergrimm/blog"
|
||||||
RUN apt update
|
RUN apt update
|
||||||
RUN apt install -y libpq5
|
RUN apt install -y libpq5
|
||||||
RUN apt install -y ca-certificates
|
|
||||||
RUN apt-get clean
|
RUN apt-get clean
|
||||||
RUN apt-get autoremove -y
|
RUN apt-get autoremove -y
|
||||||
RUN rm -rf /var/lib/{apt,dpkg,cache,log}/
|
RUN rm -rf /var/lib/{apt,dpkg,cache,log}/
|
||||||
|
@ -55,5 +54,5 @@ WORKDIR /usr/src/backend
|
||||||
COPY ./run.sh .
|
COPY ./run.sh .
|
||||||
RUN chmod +x ./run.sh
|
RUN chmod +x ./run.sh
|
||||||
COPY ./migrations ./migrations
|
COPY ./migrations ./migrations
|
||||||
COPY --from=builder /usr/src/backend/target/debug/backend /usr/src/backend/target/debug/blogctl ./bin/
|
COPY --from=builder /usr/src/backend/target/release/backend /usr/src/backend/target/release/blogctl ./bin/
|
||||||
ENTRYPOINT [ "./run.sh" ]
|
ENTRYPOINT [ "./run.sh" ]
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
DROP TABLE post_tags;
|
||||||
|
|
||||||
DROP TABLE posts;
|
DROP TABLE posts;
|
||||||
|
|
||||||
DROP TABLE tags;
|
DROP TABLE tags;
|
||||||
|
|
|
@ -12,4 +12,10 @@ CREATE TABLE posts(
|
||||||
published_at DATE NOT NULL,
|
published_at DATE NOT NULL,
|
||||||
edited_at DATE,
|
edited_at DATE,
|
||||||
active BOOLEAN NOT NULL
|
active BOOLEAN NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE post_tags(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
post_id INTEGER NOT NULL REFERENCES posts(id),
|
||||||
|
tag_id INTEGER NOT NULL REFERENCES tags(id)
|
||||||
|
);
|
||||||
|
|
|
@ -45,7 +45,7 @@ hr {
|
||||||
ul.dashed {
|
ul.dashed {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
|
|
||||||
li:before {
|
& > li:before {
|
||||||
content: "-";
|
content: "-";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
// margin-left: -20px;
|
// margin-left: -20px;
|
||||||
|
@ -107,7 +107,7 @@ table.border-rows {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
// overflow: hidden;
|
||||||
float: right;
|
float: right;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
@ -138,9 +138,37 @@ table.border-rows {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
}
|
}
|
||||||
|
|
||||||
#post-index {
|
ul.post-index > li:not(:last-child) {
|
||||||
li:not(:last-child) {
|
margin-bottom: 1rem;
|
||||||
margin-bottom: 1rem;
|
}
|
||||||
|
|
||||||
|
ul.tag-list {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
& > li {
|
||||||
|
float: left;
|
||||||
|
|
||||||
|
&:not(:first-of-type) {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-of-type)::after {
|
||||||
|
content: ",";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#blog-meta {
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding-right: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,6 +232,11 @@ table.border-rows {
|
||||||
border: thin solid black;
|
border: thin solid black;
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
padding: 0 1rem;
|
||||||
|
border-left: medium solid black;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 600px) {
|
@media only screen and (max-width: 600px) {
|
||||||
|
|
|
@ -9,7 +9,7 @@ use anyhow::{bail, Result};
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use r2d2_redis::redis;
|
use itertools::Itertools;
|
||||||
use scan_dir::ScanDir;
|
use scan_dir::ScanDir;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
@ -33,12 +33,13 @@ enum Commands {
|
||||||
Clear,
|
Clear,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
struct PostFrontmatter {
|
struct PostFrontmatter {
|
||||||
id: Option<i32>,
|
id: Option<i32>,
|
||||||
name: String,
|
name: String,
|
||||||
slug: String,
|
slug: String,
|
||||||
description: String,
|
description: String,
|
||||||
|
tags: Vec<String>,
|
||||||
published_at: NaiveDate,
|
published_at: NaiveDate,
|
||||||
edited_at: Option<NaiveDate>,
|
edited_at: Option<NaiveDate>,
|
||||||
active: bool,
|
active: bool,
|
||||||
|
@ -57,9 +58,9 @@ fn main() -> Result<()> {
|
||||||
let db_conn = &mut db::establish_connection()?;
|
let db_conn = &mut db::establish_connection()?;
|
||||||
let redis_conn = &mut cache::establish_connection()?;
|
let redis_conn = &mut cache::establish_connection()?;
|
||||||
|
|
||||||
let posts = ScanDir::dirs()
|
let (tags_raw, posts_raw) = ScanDir::dirs().read("/blog", |iter| -> Result<_> {
|
||||||
.read("/blog", |iter| {
|
let x = iter
|
||||||
iter.map(|(entry, _)| {
|
.map(|(entry, _)| {
|
||||||
let path = entry.path().join("post.md");
|
let path = entry.path().join("post.md");
|
||||||
let src = fs::read_to_string(&path)?;
|
let src = fs::read_to_string(&path)?;
|
||||||
let frontmatter = match fronma::parser::parse::<PostFrontmatter>(&src) {
|
let frontmatter = match fronma::parser::parse::<PostFrontmatter>(&src) {
|
||||||
|
@ -67,54 +68,74 @@ fn main() -> Result<()> {
|
||||||
Err(x) => bail!("Error parsing frontmatter: {:?}", x),
|
Err(x) => bail!("Error parsing frontmatter: {:?}", x),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Post {
|
Ok((
|
||||||
path,
|
frontmatter.headers.tags.to_owned(),
|
||||||
frontmatter: frontmatter.headers,
|
Post {
|
||||||
content: frontmatter.body.to_string(),
|
path,
|
||||||
})
|
frontmatter: frontmatter.headers,
|
||||||
|
content: frontmatter.body.to_string(),
|
||||||
|
},
|
||||||
|
))
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>>>()
|
.collect::<Result<Vec<(Vec<String>, Post)>>>()?;
|
||||||
})??
|
|
||||||
|
let tags: Vec<String> = x.iter().flat_map(|y| y.0.to_owned()).unique().collect();
|
||||||
|
let posts: Vec<Post> = x.into_iter().map(|y| y.1).collect();
|
||||||
|
|
||||||
|
Ok((tags, posts))
|
||||||
|
})??;
|
||||||
|
|
||||||
|
let tags = tags_raw
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|post| -> Result<_> {
|
.map(|tag| {
|
||||||
|
Ok(
|
||||||
|
match db::schema::tags::table
|
||||||
|
.select(db::schema::tags::id)
|
||||||
|
.filter(db::schema::tags::name.eq(&tag))
|
||||||
|
.first::<i32>(db_conn)
|
||||||
|
.optional()?
|
||||||
|
{
|
||||||
|
Some(x) => x,
|
||||||
|
None => diesel::insert_into(db::schema::tags::table)
|
||||||
|
.values(db::models::NewTag { name: &tag })
|
||||||
|
.returning(db::schema::tags::id)
|
||||||
|
.get_result::<i32>(db_conn)?,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
diesel::delete(
|
||||||
|
db::schema::tags::table.filter(diesel::dsl::not(db::schema::tags::id.eq_any(tags))),
|
||||||
|
)
|
||||||
|
.execute(db_conn)?;
|
||||||
|
|
||||||
|
let posts = posts_raw
|
||||||
|
.into_iter()
|
||||||
|
.map(|post| {
|
||||||
let trimmed = PostFrontmatter {
|
let trimmed = PostFrontmatter {
|
||||||
id: post.frontmatter.id,
|
id: post.frontmatter.id,
|
||||||
name: post.frontmatter.name.trim().to_string(),
|
name: post.frontmatter.name.trim().to_string(),
|
||||||
slug: post.frontmatter.slug.trim().to_string(),
|
slug: post.frontmatter.slug.trim().to_string(),
|
||||||
description: post.frontmatter.description.trim().to_string(),
|
description: post.frontmatter.description.trim().to_string(),
|
||||||
|
tags: post
|
||||||
|
.frontmatter
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.trim().to_string())
|
||||||
|
.collect(),
|
||||||
published_at: post.frontmatter.published_at,
|
published_at: post.frontmatter.published_at,
|
||||||
edited_at: post.frontmatter.edited_at,
|
edited_at: post.frontmatter.edited_at,
|
||||||
active: post.frontmatter.active,
|
active: post.frontmatter.active,
|
||||||
};
|
};
|
||||||
let content = post.content.trim();
|
let content = post.content.trim();
|
||||||
|
|
||||||
if let Some(id) = trimmed.id {
|
let id = {
|
||||||
diesel::update(db::schema::posts::table)
|
let res: Result<i32, anyhow::Error> = if let Some(id) = trimmed.id {
|
||||||
.filter(db::schema::posts::id.eq(id))
|
|
||||||
.set(db::models::UpdatePost {
|
|
||||||
name: Some(&trimmed.name),
|
|
||||||
slug: Some(&trimmed.slug),
|
|
||||||
description: Some(&trimmed.description),
|
|
||||||
content: Some(content),
|
|
||||||
published_at: Some(trimmed.published_at),
|
|
||||||
edited_at: Some(trimmed.edited_at),
|
|
||||||
active: Some(trimmed.active),
|
|
||||||
})
|
|
||||||
.execute(db_conn)?;
|
|
||||||
|
|
||||||
Ok(id)
|
|
||||||
} else {
|
|
||||||
let id = if let Some(id) = db::schema::posts::table
|
|
||||||
.select(db::schema::posts::id)
|
|
||||||
.filter(db::schema::posts::slug.eq(&trimmed.slug))
|
|
||||||
.first::<i32>(db_conn)
|
|
||||||
.optional()?
|
|
||||||
{
|
|
||||||
diesel::update(db::schema::posts::table)
|
diesel::update(db::schema::posts::table)
|
||||||
.filter(db::schema::posts::id.eq(id))
|
.filter(db::schema::posts::id.eq(id))
|
||||||
.set(db::models::UpdatePost {
|
.set(db::models::UpdatePost {
|
||||||
name: Some(&trimmed.name),
|
name: Some(&trimmed.name),
|
||||||
slug: None,
|
slug: Some(&trimmed.slug),
|
||||||
description: Some(&trimmed.description),
|
description: Some(&trimmed.description),
|
||||||
content: Some(content),
|
content: Some(content),
|
||||||
published_at: Some(trimmed.published_at),
|
published_at: Some(trimmed.published_at),
|
||||||
|
@ -123,69 +144,111 @@ fn main() -> Result<()> {
|
||||||
})
|
})
|
||||||
.execute(db_conn)?;
|
.execute(db_conn)?;
|
||||||
|
|
||||||
id
|
Ok(id)
|
||||||
} else {
|
} else {
|
||||||
diesel::insert_into(db::schema::posts::table)
|
let id = if let Some(id) = db::schema::posts::table
|
||||||
.values(db::models::NewPost {
|
.select(db::schema::posts::id)
|
||||||
name: &trimmed.name,
|
.filter(db::schema::posts::slug.eq(&trimmed.slug))
|
||||||
slug: &trimmed.slug,
|
.first::<i32>(db_conn)
|
||||||
description: &trimmed.description,
|
.optional()?
|
||||||
content: content,
|
{
|
||||||
published_at: trimmed.published_at,
|
diesel::update(db::schema::posts::table)
|
||||||
edited_at: trimmed.edited_at,
|
.filter(db::schema::posts::id.eq(id))
|
||||||
active: trimmed.active,
|
.set(db::models::UpdatePost {
|
||||||
})
|
name: Some(&trimmed.name),
|
||||||
.returning(db::schema::posts::id)
|
slug: None,
|
||||||
.get_result::<i32>(db_conn)?
|
description: Some(&trimmed.description),
|
||||||
|
content: Some(content),
|
||||||
|
published_at: Some(trimmed.published_at),
|
||||||
|
edited_at: Some(trimmed.edited_at),
|
||||||
|
active: Some(trimmed.active),
|
||||||
|
})
|
||||||
|
.execute(db_conn)?;
|
||||||
|
|
||||||
|
id
|
||||||
|
} else {
|
||||||
|
diesel::insert_into(db::schema::posts::table)
|
||||||
|
.values(db::models::NewPost {
|
||||||
|
name: &trimmed.name,
|
||||||
|
slug: &trimmed.slug,
|
||||||
|
description: &trimmed.description,
|
||||||
|
content: content,
|
||||||
|
published_at: trimmed.published_at,
|
||||||
|
edited_at: trimmed.edited_at,
|
||||||
|
active: trimmed.active,
|
||||||
|
})
|
||||||
|
.returning(db::schema::posts::id)
|
||||||
|
.get_result::<i32>(db_conn)?
|
||||||
|
};
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
post.path,
|
||||||
|
format!(
|
||||||
|
"---\n{}---\n\n{}\n",
|
||||||
|
serde_yaml::to_string(&PostFrontmatter {
|
||||||
|
id: Some(id),
|
||||||
|
..trimmed.to_owned()
|
||||||
|
})?,
|
||||||
|
content
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
};
|
};
|
||||||
|
|
||||||
fs::write(
|
res
|
||||||
post.path,
|
}?;
|
||||||
format!(
|
|
||||||
"---\n{}---\n\n{}\n",
|
|
||||||
serde_yaml::to_string(&PostFrontmatter {
|
|
||||||
id: Some(id),
|
|
||||||
..trimmed
|
|
||||||
})?,
|
|
||||||
content
|
|
||||||
),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(id)
|
let tag_ids = db::schema::tags::table
|
||||||
}
|
.select(db::schema::tags::id)
|
||||||
|
.filter(db::schema::tags::name.eq_any(trimmed.tags))
|
||||||
|
.load::<i32>(db_conn)?;
|
||||||
|
|
||||||
|
let post_tags = db::schema::post_tags::table
|
||||||
|
.filter(db::schema::post_tags::post_id.eq(id))
|
||||||
|
.load::<db::models::PostTag>(db_conn)?;
|
||||||
|
|
||||||
|
diesel::delete(
|
||||||
|
db::schema::post_tags::table
|
||||||
|
.filter(db::schema::post_tags::post_id.eq(id))
|
||||||
|
.filter(diesel::dsl::not(
|
||||||
|
db::schema::post_tags::tag_id.eq_any(&tag_ids),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.execute(db_conn)?;
|
||||||
|
|
||||||
|
let post_tag_tag_ids: Vec<_> = post_tags.iter().map(|x| x.tag_id).collect();
|
||||||
|
diesel::insert_into(db::schema::post_tags::table)
|
||||||
|
.values(
|
||||||
|
tag_ids
|
||||||
|
.into_iter()
|
||||||
|
.filter(|x| !post_tag_tag_ids.contains(x))
|
||||||
|
.map(|x| db::models::NewPostTag {
|
||||||
|
post_id: id,
|
||||||
|
tag_id: x,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.execute(db_conn)?;
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>>>()?;
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
let ids = db::schema::posts::table
|
|
||||||
.select(db::schema::posts::id)
|
|
||||||
.load::<i32>(db_conn)?;
|
|
||||||
|
|
||||||
diesel::delete(
|
diesel::delete(
|
||||||
db::schema::posts::table
|
db::schema::posts::table
|
||||||
.filter(diesel::dsl::not(db::schema::posts::id.eq_any(posts))),
|
.filter(diesel::dsl::not(db::schema::posts::id.eq_any(posts))),
|
||||||
)
|
)
|
||||||
.execute(db_conn)?;
|
.execute(db_conn)?;
|
||||||
|
|
||||||
for id in ids {
|
cache::clear(redis_conn)?;
|
||||||
redis::cmd("DEL")
|
|
||||||
.arg(cache::keys::post_content(id))
|
|
||||||
.query::<()>(redis_conn)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Commands::Clear => {
|
Commands::Clear => {
|
||||||
let db_conn = &mut db::establish_connection()?;
|
|
||||||
let redis_conn = &mut cache::establish_connection()?;
|
let redis_conn = &mut cache::establish_connection()?;
|
||||||
|
|
||||||
for id in db::schema::posts::table
|
cache::clear(redis_conn)?;
|
||||||
.select(db::schema::posts::id)
|
|
||||||
.load::<i32>(db_conn)?
|
|
||||||
{
|
|
||||||
redis::cmd("DEL")
|
|
||||||
.arg(cache::keys::post_content(id))
|
|
||||||
.query::<()>(redis_conn)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use diesel::prelude::*;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use r2d2_redis::{r2d2, redis, RedisConnectionManager};
|
use r2d2_redis::{r2d2, redis, RedisConnectionManager};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::config;
|
use crate::{config, db, markdown, web};
|
||||||
|
|
||||||
pub type RedisPool = r2d2::Pool<RedisConnectionManager>;
|
pub type RedisPool = r2d2::Pool<RedisConnectionManager>;
|
||||||
pub type ConnectionPool = r2d2::PooledConnection<RedisConnectionManager>;
|
pub type ConnectionPool = r2d2::PooledConnection<RedisConnectionManager>;
|
||||||
|
pub type Connection = redis::Connection;
|
||||||
|
|
||||||
pub fn establish_connection() -> Result<redis::Connection> {
|
pub fn establish_connection() -> Result<redis::Connection> {
|
||||||
Ok(redis::Client::open(config::CONFIG.redis_url.as_str())?.get_connection()?)
|
Ok(redis::Client::open(config::CONFIG.redis_url.as_str())?.get_connection()?)
|
||||||
|
@ -22,7 +26,201 @@ lazy_static! {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod keys {
|
pub mod keys {
|
||||||
pub fn post_content(id: i32) -> String {
|
pub const POSTS: &str = "posts";
|
||||||
format!("post_content:{}", id)
|
|
||||||
|
pub const POST: &str = "post:";
|
||||||
|
|
||||||
|
pub fn post(id: i32) -> String {
|
||||||
|
format!("{}{}", POST, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const TAG_POSTS: &str = "tag_posts:";
|
||||||
|
|
||||||
|
pub fn tag_posts(id: i32) -> String {
|
||||||
|
format!("{}{}", TAG_POSTS, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(redis_conn: &mut Connection) -> Result<()> {
|
||||||
|
redis::cmd("UNLINK").arg(keys::POSTS).query(redis_conn)?;
|
||||||
|
|
||||||
|
for key in redis::cmd("KEYS")
|
||||||
|
.arg(format!("{}*", keys::POST))
|
||||||
|
.query::<Vec<String>>(redis_conn)?
|
||||||
|
{
|
||||||
|
redis::cmd("UNLINK").arg(key).query(redis_conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in redis::cmd("KEYS")
|
||||||
|
.arg(format!("{}*", keys::TAG_POSTS))
|
||||||
|
.query::<Vec<String>>(redis_conn)?
|
||||||
|
{
|
||||||
|
redis::cmd("UNLINK").arg(key).query(redis_conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cache_posts(
|
||||||
|
db_conn: &mut db::Connection,
|
||||||
|
redis_conn: &mut Connection,
|
||||||
|
) -> Result<Vec<web::templates::PostIndexPost>> {
|
||||||
|
if let Some(s) = redis::cmd("GET")
|
||||||
|
.arg(keys::POSTS)
|
||||||
|
.query::<Option<String>>(redis_conn)?
|
||||||
|
{
|
||||||
|
Ok(serde_json::from_str(&s)?)
|
||||||
|
} else {
|
||||||
|
let posts_cache: Vec<web::templates::PostIndexPost> = db::schema::posts::table
|
||||||
|
.select((
|
||||||
|
db::schema::posts::id,
|
||||||
|
db::schema::posts::name,
|
||||||
|
db::schema::posts::slug,
|
||||||
|
db::schema::posts::description,
|
||||||
|
db::schema::posts::published_at,
|
||||||
|
db::schema::posts::edited_at,
|
||||||
|
))
|
||||||
|
.filter(db::schema::posts::active)
|
||||||
|
.order(db::schema::posts::published_at.desc())
|
||||||
|
.load::<(i32, String, String, String, NaiveDate, Option<NaiveDate>)>(db_conn)?
|
||||||
|
.into_iter()
|
||||||
|
.map(
|
||||||
|
|p: (i32, String, String, String, NaiveDate, Option<NaiveDate>)| -> Result<_> {
|
||||||
|
let (id, name, slug, description, published_at, edited_at) = p;
|
||||||
|
let tags = web::get_tags_by_post(id, db_conn)?;
|
||||||
|
|
||||||
|
Ok(web::templates::PostIndexPost {
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
|
published_at,
|
||||||
|
edited_at,
|
||||||
|
tags,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect::<Result<_>>()?;
|
||||||
|
|
||||||
|
redis::cmd("SET")
|
||||||
|
.arg(keys::POSTS)
|
||||||
|
.arg(serde_json::to_string(&posts_cache)?)
|
||||||
|
.query(redis_conn)?;
|
||||||
|
redis::cmd("EXPIRE")
|
||||||
|
.arg(keys::POSTS)
|
||||||
|
.arg(config::CONFIG.cache_ttl)
|
||||||
|
.query(redis_conn)?;
|
||||||
|
|
||||||
|
Ok(posts_cache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Post {
|
||||||
|
pub name: String,
|
||||||
|
pub slug: String,
|
||||||
|
pub published_at: NaiveDate,
|
||||||
|
pub edited_at: Option<NaiveDate>,
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cache_post(
|
||||||
|
id: i32,
|
||||||
|
db_conn: &mut db::Connection,
|
||||||
|
redis_conn: &mut Connection,
|
||||||
|
) -> Result<Post> {
|
||||||
|
let key = keys::post(id);
|
||||||
|
|
||||||
|
if let Some(s) = redis::cmd("GET")
|
||||||
|
.arg(&key)
|
||||||
|
.query::<Option<String>>(redis_conn)?
|
||||||
|
{
|
||||||
|
Ok(serde_json::from_str(&s)?)
|
||||||
|
} else {
|
||||||
|
let post = db::schema::posts::table
|
||||||
|
.filter(db::schema::posts::id.eq(id))
|
||||||
|
.first::<db::models::Post>(db_conn)?;
|
||||||
|
|
||||||
|
let post_cache = Post {
|
||||||
|
name: post.name,
|
||||||
|
slug: post.slug,
|
||||||
|
published_at: post.published_at,
|
||||||
|
edited_at: post.edited_at,
|
||||||
|
tags: web::get_tags_by_post(id, db_conn)?,
|
||||||
|
content: markdown::to_html(&post.content),
|
||||||
|
};
|
||||||
|
|
||||||
|
redis::cmd("SET")
|
||||||
|
.arg(&key)
|
||||||
|
.arg(serde_json::to_string(&post_cache)?)
|
||||||
|
.query(redis_conn)?;
|
||||||
|
redis::cmd("EXPIRE")
|
||||||
|
.arg(key)
|
||||||
|
.arg(config::CONFIG.cache_ttl)
|
||||||
|
.query(redis_conn)?;
|
||||||
|
|
||||||
|
Ok(post_cache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cache_tag_posts(
|
||||||
|
id: i32,
|
||||||
|
db_conn: &mut db::Connection,
|
||||||
|
redis_conn: &mut Connection,
|
||||||
|
) -> Result<Vec<web::templates::PostIndexPost>> {
|
||||||
|
let key = keys::tag_posts(id);
|
||||||
|
|
||||||
|
if let Some(s) = redis::cmd("GET")
|
||||||
|
.arg(&key)
|
||||||
|
.query::<Option<String>>(redis_conn)?
|
||||||
|
{
|
||||||
|
Ok(serde_json::from_str(&s)?)
|
||||||
|
} else {
|
||||||
|
let post_tag_post_ids = db::schema::post_tags::table
|
||||||
|
.select(db::schema::post_tags::post_id)
|
||||||
|
.filter(db::schema::post_tags::tag_id.eq(id))
|
||||||
|
.load::<i32>(db_conn)?;
|
||||||
|
|
||||||
|
let posts: Vec<web::templates::PostIndexPost> = db::schema::posts::table
|
||||||
|
.select((
|
||||||
|
db::schema::posts::id,
|
||||||
|
db::schema::posts::name,
|
||||||
|
db::schema::posts::slug,
|
||||||
|
db::schema::posts::description,
|
||||||
|
db::schema::posts::published_at,
|
||||||
|
db::schema::posts::edited_at,
|
||||||
|
))
|
||||||
|
.filter(db::schema::posts::id.eq_any(post_tag_post_ids))
|
||||||
|
.filter(db::schema::posts::active)
|
||||||
|
.order(db::schema::posts::published_at.desc())
|
||||||
|
.load::<(i32, String, String, String, NaiveDate, Option<NaiveDate>)>(db_conn)?
|
||||||
|
.into_iter()
|
||||||
|
.map(
|
||||||
|
|p: (i32, String, String, String, NaiveDate, Option<NaiveDate>)| -> Result<_> {
|
||||||
|
let (id, name, slug, description, published_at, edited_at) = p;
|
||||||
|
let tags = web::get_tags_by_post(id, db_conn)?;
|
||||||
|
|
||||||
|
Ok(web::templates::PostIndexPost {
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
|
published_at,
|
||||||
|
edited_at,
|
||||||
|
tags,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect::<Result<_>>()?;
|
||||||
|
|
||||||
|
redis::cmd("SET")
|
||||||
|
.arg(&key)
|
||||||
|
.arg(serde_json::to_string(&posts)?)
|
||||||
|
.query(redis_conn)?;
|
||||||
|
redis::cmd("EXPIRE")
|
||||||
|
.arg(key)
|
||||||
|
.arg(config::CONFIG.cache_ttl)
|
||||||
|
.query(redis_conn)?;
|
||||||
|
|
||||||
|
Ok(posts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,8 @@ pub struct Config {
|
||||||
#[envconfig(from = "BACKEND_REDIS_URL")]
|
#[envconfig(from = "BACKEND_REDIS_URL")]
|
||||||
pub redis_url: String,
|
pub redis_url: String,
|
||||||
|
|
||||||
#[envconfig(from = "BACKEND_CACHE_POST_CONTENT_TTL")]
|
#[envconfig(from = "BACKEND_CACHE_TTL")]
|
||||||
pub cache_post_content_ttl: usize,
|
pub cache_ttl: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
|
|
@ -10,8 +10,11 @@ pub mod models;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
|
|
||||||
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
|
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
|
||||||
|
pub type Connection = PgConnection;
|
||||||
|
|
||||||
pub fn establish_connection() -> ConnectionResult<PgConnection> {
|
pub fn establish_connection() -> ConnectionResult<PgConnection> {
|
||||||
|
use diesel::Connection;
|
||||||
|
|
||||||
PgConnection::establish(&config::CONFIG.db_url)
|
PgConnection::establish(&config::CONFIG.db_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,3 +52,20 @@ pub struct UpdatePost<'a> {
|
||||||
pub edited_at: Option<Option<NaiveDate>>,
|
pub edited_at: Option<Option<NaiveDate>>,
|
||||||
pub active: Option<bool>,
|
pub active: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Associations, Identifiable, Queryable, Debug)]
|
||||||
|
#[diesel(belongs_to(Post))]
|
||||||
|
#[diesel(belongs_to(Tag))]
|
||||||
|
#[diesel(table_name = schema::post_tags)]
|
||||||
|
pub struct PostTag {
|
||||||
|
pub id: i32,
|
||||||
|
pub post_id: i32,
|
||||||
|
pub tag_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::post_tags)]
|
||||||
|
pub struct NewPostTag {
|
||||||
|
pub post_id: i32,
|
||||||
|
pub tag_id: i32,
|
||||||
|
}
|
||||||
|
|
|
@ -17,3 +17,11 @@ diesel::table! {
|
||||||
active -> Bool,
|
active -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
post_tags {
|
||||||
|
id -> Integer,
|
||||||
|
post_id -> Integer,
|
||||||
|
tag_id -> Integer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
use actix_web::{get, http, web, HttpResponse};
|
use actix_web::{get, http, web, HttpResponse};
|
||||||
use actix_web_static_files::ResourceFiles;
|
use actix_web_static_files::ResourceFiles;
|
||||||
|
use anyhow::Result;
|
||||||
use askama_actix::TemplateToResponse;
|
use askama_actix::TemplateToResponse;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use r2d2_redis::redis;
|
|
||||||
use std::ops::DerefMut;
|
|
||||||
|
|
||||||
use crate::{cache, config, db, markdown};
|
use crate::{cache, db};
|
||||||
|
|
||||||
pub mod templates;
|
pub mod templates;
|
||||||
|
|
||||||
|
@ -24,27 +23,6 @@ async fn not_found() -> HttpResponse {
|
||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/posts")]
|
|
||||||
async fn posts(db_pool: web::Data<db::DbPool>) -> HttpResponse {
|
|
||||||
let db_conn = &mut match db_pool.get() {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let posts = match db::schema::posts::table
|
|
||||||
.filter(db::schema::posts::active)
|
|
||||||
.order(db::schema::posts::published_at.desc())
|
|
||||||
.load::<db::models::Post>(db_conn)
|
|
||||||
{
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(e) => {
|
|
||||||
return HttpResponse::InternalServerError().body(format!("{:?}", e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
templates::Posts { posts }.to_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn index() -> HttpResponse {
|
async fn index() -> HttpResponse {
|
||||||
templates::Index.to_response()
|
templates::Index.to_response()
|
||||||
|
@ -55,6 +33,43 @@ async fn about() -> HttpResponse {
|
||||||
templates::About.to_response()
|
templates::About.to_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_tags_by_post(id: i32, db_conn: &mut diesel::PgConnection) -> Result<Vec<String>> {
|
||||||
|
Ok(db::schema::tags::table
|
||||||
|
.select(db::schema::tags::name)
|
||||||
|
.filter(
|
||||||
|
db::schema::tags::id.eq_any(
|
||||||
|
db::schema::post_tags::table
|
||||||
|
.select(db::schema::post_tags::tag_id)
|
||||||
|
.filter(db::schema::post_tags::post_id.eq(id))
|
||||||
|
.load::<i32>(db_conn)?,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.order(db::schema::tags::name)
|
||||||
|
.load::<String>(db_conn)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/posts")]
|
||||||
|
async fn posts(
|
||||||
|
db_pool: web::Data<db::DbPool>,
|
||||||
|
redis_pool: web::Data<cache::RedisPool>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let db_conn = &mut match db_pool.get() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||||
|
};
|
||||||
|
let redis_conn = &mut match redis_pool.get() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let posts = match cache::cache_posts(db_conn, redis_conn) {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
templates::Posts { posts }.to_response()
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/posts/{slug}")]
|
#[get("/posts/{slug}")]
|
||||||
async fn post_by_slug(
|
async fn post_by_slug(
|
||||||
db_pool: web::Data<db::DbPool>,
|
db_pool: web::Data<db::DbPool>,
|
||||||
|
@ -72,89 +87,181 @@ async fn post_by_slug(
|
||||||
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let post_stripped: Option<(i32, String)> = match db::schema::posts::table
|
if let Some(post_id) = match db::schema::posts::table
|
||||||
.select((db::schema::posts::id, db::schema::posts::name))
|
.select(db::schema::posts::id)
|
||||||
.filter(db::schema::posts::slug.eq(&slug))
|
.filter(db::schema::posts::slug.eq(&slug))
|
||||||
.filter(db::schema::posts::active)
|
.filter(db::schema::posts::active)
|
||||||
.get_result::<(i32, String)>(db_conn)
|
.first::<i32>(db_conn)
|
||||||
.optional()
|
.optional()
|
||||||
{
|
{
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(e) => {
|
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||||
return HttpResponse::InternalServerError().body(format!("{:?}", e));
|
} {
|
||||||
|
let post = match cache::cache_post(post_id, db_conn, redis_conn) {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
templates::PostBySlug { post }.to_response()
|
||||||
|
} else {
|
||||||
|
let mut resp = templates::StatusCode {
|
||||||
|
status_code: http::StatusCode::NOT_FOUND,
|
||||||
|
message: Some("this post does not exists... yet".to_string()),
|
||||||
}
|
}
|
||||||
|
.to_response();
|
||||||
|
*resp.status_mut() = http::StatusCode::NOT_FOUND;
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// let post_stripped: Option<(i32, String, NaiveDate, Option<NaiveDate>)> =
|
||||||
|
// match db::schema::posts::table
|
||||||
|
// .select((
|
||||||
|
// db::schema::posts::id,
|
||||||
|
// db::schema::posts::name,
|
||||||
|
// db::schema::posts::published_at,
|
||||||
|
// db::schema::posts::edited_at,
|
||||||
|
// ))
|
||||||
|
// .filter(db::schema::posts::slug.eq(&slug))
|
||||||
|
// .filter(db::schema::posts::active)
|
||||||
|
// .first::<(i32, String, NaiveDate, Option<NaiveDate>)>(db_conn)
|
||||||
|
// .optional()
|
||||||
|
// {
|
||||||
|
// Ok(x) => x,
|
||||||
|
// Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// match post_stripped {
|
||||||
|
// Some(stripped) => {
|
||||||
|
// let (stripped_id, stripped_name, stripped_published_at, stripped_edited_at) = stripped;
|
||||||
|
|
||||||
|
// let key = cache::keys::post_content(stripped_id);
|
||||||
|
|
||||||
|
// match match redis::cmd("GET")
|
||||||
|
// .arg(&key)
|
||||||
|
// .query::<Option<String>>(redis_conn.deref_mut())
|
||||||
|
// {
|
||||||
|
// Ok(x) => x,
|
||||||
|
// Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||||
|
// } {
|
||||||
|
// Some(s) => {
|
||||||
|
// let tags = match get_tags_by_post(stripped_id, db_conn) {
|
||||||
|
// Ok(x) => x,
|
||||||
|
// Err(e) => {
|
||||||
|
// return HttpResponse::InternalServerError().body(format!("{:?}", e))
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// templates::PostBySlug {
|
||||||
|
// name: stripped_name,
|
||||||
|
// slug,
|
||||||
|
// published_at: stripped_published_at,
|
||||||
|
// edited_at: stripped_edited_at,
|
||||||
|
// tags,
|
||||||
|
// content: s,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .to_response(),
|
||||||
|
// None => {
|
||||||
|
// let post = match db::schema::posts::table
|
||||||
|
// .filter(db::schema::posts::id.eq(stripped_id))
|
||||||
|
// .first::<db::models::Post>(db_conn)
|
||||||
|
// {
|
||||||
|
// Ok(x) => x,
|
||||||
|
// Err(e) => {
|
||||||
|
// return HttpResponse::InternalServerError().body(format!("{:?}", e))
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// let html = markdown::to_html(&post.content);
|
||||||
|
|
||||||
|
// match redis::cmd("SET")
|
||||||
|
// .arg(&key)
|
||||||
|
// .arg(&html)
|
||||||
|
// .query::<Option<String>>(redis_conn.deref_mut())
|
||||||
|
// {
|
||||||
|
// Ok(x) => x,
|
||||||
|
// Err(e) => {
|
||||||
|
// return HttpResponse::InternalServerError().body(format!("{:?}", e))
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// if let Err(e) = redis::cmd("EXPIRE")
|
||||||
|
// .arg(key)
|
||||||
|
// .arg(config::CONFIG.cache_ttl)
|
||||||
|
// .query::<()>(redis_conn.deref_mut())
|
||||||
|
// {
|
||||||
|
// return HttpResponse::InternalServerError().body(format!("{:?}", e));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let tags = match get_tags_by_post(stripped_id, db_conn) {
|
||||||
|
// Ok(x) => x,
|
||||||
|
// Err(e) => {
|
||||||
|
// return HttpResponse::InternalServerError().body(format!("{:?}", e))
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// templates::PostBySlug {
|
||||||
|
// name: post.name,
|
||||||
|
// slug: post.slug,
|
||||||
|
// published_at: stripped_published_at,
|
||||||
|
// edited_at: stripped_edited_at,
|
||||||
|
// tags,
|
||||||
|
// content: html,
|
||||||
|
// }
|
||||||
|
// .to_response()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// None => {
|
||||||
|
// let mut resp = templates::StatusCode {
|
||||||
|
// status_code: http::StatusCode::NOT_FOUND,
|
||||||
|
// message: Some("this post does not exists... yet".to_string()),
|
||||||
|
// }
|
||||||
|
// .to_response();
|
||||||
|
// *resp.status_mut() = http::StatusCode::NOT_FOUND;
|
||||||
|
|
||||||
|
// resp
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/tags/{name}")]
|
||||||
|
async fn tag_by_name(
|
||||||
|
db_pool: web::Data<db::DbPool>,
|
||||||
|
redis_pool: web::Data<cache::RedisPool>,
|
||||||
|
path: web::Path<String>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
const MESSAGE: &str = "this post does not exists... yet";
|
||||||
|
|
||||||
|
let name = path.into_inner();
|
||||||
|
|
||||||
|
let db_conn = &mut match db_pool.get() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||||
|
};
|
||||||
|
let redis_conn = &mut match redis_pool.get() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||||
};
|
};
|
||||||
|
|
||||||
match post_stripped {
|
let tag_id = match db::schema::tags::table
|
||||||
Some(stripped) => {
|
.select(db::schema::tags::id)
|
||||||
let (stripped_id, stripped_name) = stripped;
|
.filter(db::schema::tags::name.eq(&name))
|
||||||
|
.first::<i32>(db_conn)
|
||||||
|
{
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||||
|
};
|
||||||
|
let posts_cache = match cache::cache_tag_posts(tag_id, db_conn, redis_conn) {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
let key = cache::keys::post_content(stripped_id);
|
templates::TagByName {
|
||||||
|
name,
|
||||||
match match redis::cmd("GET")
|
posts: posts_cache,
|
||||||
.arg(&key)
|
|
||||||
.query::<Option<String>>(redis_conn.deref_mut())
|
|
||||||
{
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
|
||||||
} {
|
|
||||||
Some(s) => templates::PostBySlug {
|
|
||||||
name: stripped_name,
|
|
||||||
slug,
|
|
||||||
content: s,
|
|
||||||
}
|
|
||||||
.to_response(),
|
|
||||||
None => {
|
|
||||||
let post = match db::schema::posts::table
|
|
||||||
.filter(db::schema::posts::id.eq(stripped_id))
|
|
||||||
.first::<db::models::Post>(db_conn)
|
|
||||||
{
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(e) => {
|
|
||||||
return HttpResponse::InternalServerError().body(format!("{:?}", e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let html = markdown::to_html(&post.content);
|
|
||||||
|
|
||||||
match redis::cmd("SET")
|
|
||||||
.arg(&key)
|
|
||||||
.arg(&html)
|
|
||||||
.query::<Option<String>>(redis_conn.deref_mut())
|
|
||||||
{
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(e) => {
|
|
||||||
return HttpResponse::InternalServerError().body(format!("{:?}", e))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Err(e) = redis::cmd("EXPIRE")
|
|
||||||
.arg(key)
|
|
||||||
.arg(config::CONFIG.cache_post_content_ttl)
|
|
||||||
.query::<()>(redis_conn.deref_mut())
|
|
||||||
{
|
|
||||||
return HttpResponse::InternalServerError().body(format!("{:?}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
templates::PostBySlug {
|
|
||||||
name: post.name,
|
|
||||||
slug: post.slug,
|
|
||||||
content: html,
|
|
||||||
}
|
|
||||||
.to_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let mut resp = templates::StatusCode {
|
|
||||||
status_code: http::StatusCode::NOT_FOUND,
|
|
||||||
message: Some("this post does not exists... yet".to_string()),
|
|
||||||
}
|
|
||||||
.to_response();
|
|
||||||
*resp.status_mut() = http::StatusCode::NOT_FOUND;
|
|
||||||
|
|
||||||
resp
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.to_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_routes(cfg: &mut web::ServiceConfig) {
|
fn setup_routes(cfg: &mut web::ServiceConfig) {
|
||||||
|
@ -165,6 +272,7 @@ fn setup_routes(cfg: &mut web::ServiceConfig) {
|
||||||
.service(posts)
|
.service(posts)
|
||||||
.service(ResourceFiles::new("/static", generated))
|
.service(ResourceFiles::new("/static", generated))
|
||||||
.service(post_by_slug)
|
.service(post_by_slug)
|
||||||
|
.service(tag_by_name)
|
||||||
.default_service(web::route().to(not_found));
|
.default_service(web::route().to(not_found));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use actix_web::http;
|
use actix_web::http;
|
||||||
use askama_actix::Template;
|
use askama_actix::Template;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::db;
|
use crate::cache;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "status_code.html")]
|
#[template(path = "status_code.html")]
|
||||||
|
@ -18,16 +20,37 @@ pub struct Index;
|
||||||
#[template(path = "web/about.html")]
|
#[template(path = "web/about.html")]
|
||||||
pub struct About;
|
pub struct About;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct PostIndexPost {
|
||||||
|
pub name: String,
|
||||||
|
pub slug: String,
|
||||||
|
pub description: String,
|
||||||
|
pub published_at: NaiveDate,
|
||||||
|
pub edited_at: Option<NaiveDate>,
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "web/posts/index.html")]
|
#[template(path = "web/posts/index.html")]
|
||||||
pub struct Posts {
|
pub struct Posts {
|
||||||
pub posts: Vec<db::models::Post>,
|
pub posts: Vec<PostIndexPost>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "web/posts/{slug}.html")]
|
#[template(path = "web/posts/{slug}.html")]
|
||||||
pub struct PostBySlug {
|
pub struct PostBySlug {
|
||||||
pub name: String,
|
// pub name: String,
|
||||||
pub slug: String,
|
// pub slug: String,
|
||||||
pub content: String,
|
// pub published_at: NaiveDate,
|
||||||
|
// pub edited_at: Option<NaiveDate>,
|
||||||
|
// pub tags: Vec<String>,
|
||||||
|
// pub content: String,
|
||||||
|
pub post: cache::Post,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "web/tags/{name}.html")]
|
||||||
|
pub struct TagByName {
|
||||||
|
pub name: String,
|
||||||
|
pub posts: Vec<PostIndexPost>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,5 +10,5 @@
|
||||||
<h1>This is my site</h1>
|
<h1>This is my site</h1>
|
||||||
<p>Hey, I'm Dominic (aka. dergrimm) and this is my blog!</p>
|
<p>Hey, I'm Dominic (aka. dergrimm) and this is my blog!</p>
|
||||||
<p>I mostly plan to talk about modern tech and server side stuff like Rust, Crystal, C, databases and APIs.</p>
|
<p>I mostly plan to talk about modern tech and server side stuff like Rust, Crystal, C, databases and APIs.</p>
|
||||||
<p>Keep posted by following my feed.</p>
|
<p>Keep posted by following my (not yet existing) feed.</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<ul id="post-index" class="dashed">
|
<ul class="post-index dashed">
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
<li>
|
<li>
|
||||||
<span>
|
<span>
|
||||||
|
@ -17,6 +17,12 @@
|
||||||
(<i>{{ post.published_at }}{% match post.edited_at %}{% when Some with (x) %} -> {{ x }}{% when None %}{% endmatch %}</i>)
|
(<i>{{ post.published_at }}{% match post.edited_at %}{% when Some with (x) %} -> {{ x }}{% when None %}{% endmatch %}</i>)
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
|
<ul class="tag-list">
|
||||||
|
{% for tag in post.tags %}
|
||||||
|
<li><a href="/tags/{{ tag }}">{{ tag }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<br />
|
||||||
<span>{{ post.description }}</span>
|
<span>{{ post.description }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -1,16 +1,40 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}{{ name }}{% endblock %}
|
{% block title %}{{ post.name }}{% endblock %}
|
||||||
|
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
|
|
||||||
{% block breadcrumb %}
|
{% block breadcrumb %}
|
||||||
<li><a href="/posts">posts</a></li>
|
<li><a href="/posts">posts</a></li>
|
||||||
<li><a href="/posts/{{ slug }}">{{ slug }}</a></li>
|
<li><a href="/posts/{{ post.slug }}">{{ post.slug }}</a></li>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<table id="blog-meta">
|
||||||
|
<tr>
|
||||||
|
<th>Published at</th>
|
||||||
|
<td>{{ post.published_at }}</td>
|
||||||
|
</tr>
|
||||||
|
{% match post.edited_at %}
|
||||||
|
{% when Some with (x) %}
|
||||||
|
<tr>
|
||||||
|
<th>Edited at</th>
|
||||||
|
<td>{{ x }}</td>
|
||||||
|
</tr>
|
||||||
|
{% when None %}
|
||||||
|
{% endmatch %}
|
||||||
|
<tr>
|
||||||
|
<th>Tags</th>
|
||||||
|
<td>
|
||||||
|
<ul class="tag-list">
|
||||||
|
{% for tag in post.tags %}
|
||||||
|
<li><a href="/tags/{{ tag }}">{{ tag }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
<article class="wysiwyg">
|
<article class="wysiwyg">
|
||||||
{{content|safe }}
|
{{ post.content|safe }}
|
||||||
</article>
|
</article>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
32
backend/templates/web/tags/{name}.html
Normal file
32
backend/templates/web/tags/{name}.html
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumb %}
|
||||||
|
<li><a href="/tags">tags</a></li>
|
||||||
|
<li><a href="/tags/{{ name }}">{{ name }}</a></li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ name }}</h1>
|
||||||
|
<ul class="post-index dashed">
|
||||||
|
{% for post in posts %}
|
||||||
|
<li>
|
||||||
|
<span>
|
||||||
|
<a href="/posts/{{ post.slug }}">{{ post.name }}</a>
|
||||||
|
(<i>{{ post.published_at }}{% match post.edited_at %}{% when Some with (x) %} -> {{ x }}{% when None %}{% endmatch %}</i>)
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<ul class="tag-list">
|
||||||
|
{% for tag in post.tags %}
|
||||||
|
<li><a href="/tags/{{ tag }}">{{ tag }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<br />
|
||||||
|
<span>{{ post.description }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
|
@ -21,6 +21,8 @@ services:
|
||||||
redis:
|
redis:
|
||||||
image: docker.io/redis:7-alpine
|
image: docker.io/redis:7-alpine
|
||||||
restart: always
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- redis:/data
|
||||||
|
|
||||||
redis-commander:
|
redis-commander:
|
||||||
image: rediscommander/redis-commander:latest
|
image: rediscommander/redis-commander:latest
|
||||||
|
@ -42,7 +44,7 @@ services:
|
||||||
BACKEND_BIND_URL: 0.0.0.0:80
|
BACKEND_BIND_URL: 0.0.0.0:80
|
||||||
BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
|
BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
|
||||||
BACKEND_REDIS_URL: redis://redis
|
BACKEND_REDIS_URL: redis://redis
|
||||||
BACKEND_CACHE_POST_CONTENT_TTL: 3600
|
BACKEND_CACHE_TTL: 3600
|
||||||
volumes:
|
volumes:
|
||||||
- ./blog:/blog
|
- ./blog:/blog
|
||||||
ports:
|
ports:
|
||||||
|
@ -53,3 +55,4 @@ services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db:
|
db:
|
||||||
|
redis:
|
||||||
|
|
Loading…
Reference in a new issue