This commit is contained in:
Dominic Grimm 2023-02-07 07:02:51 +01:00
commit 94fb270008
No known key found for this signature in database
GPG Key ID: 6F294212DEAAC530
22 changed files with 2424 additions and 0 deletions

33
.editorconfig Normal file
View File

@ -0,0 +1,33 @@
root = true
[*.rs]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.sql]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.sql]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.html]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

2
.env Normal file
View File

@ -0,0 +1,2 @@
POSTGRES_USER="blog"
POSTGRES_PASSWORD="blog"

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
POSTGRES_USER="blog"
POSTGRES_PASSWORD="blog"

6
Makefile Normal file
View File

@ -0,0 +1,6 @@
.PHONY: all build
all: build
build:
BUILDKIT_PROGRESS=plain docker compose build

7
backend/.dockerignore Normal file
View File

@ -0,0 +1,7 @@
/target
Dockerfile
.gitignore
.dockerignore
vendor/
example/

1
backend/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1862
backend/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

37
backend/Cargo.toml Normal file
View File

@ -0,0 +1,37 @@
[package]
name = "backend"
version = "0.1.0"
edition = "2021"
authors = ["Dominic Grimm <dominic@dergrimm.net>"]
[[bin]]
name = "backend"
[[bin]]
name = "blogctl"
[profile.release]
codegen-units = 1
lto = "fat"
strip = true
panic = "abort"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "4.3.0"
anyhow = { version = "1.0.69", features = ["backtrace"] }
chrono = { version = "0.4.23", features = ["serde"] }
clap = { version = "4.1.4", features = ["derive"] }
diesel = { version = "2.0.2", features = ["i-implement-a-third-party-backend-and-opt-into-breaking-changes", "postgres", "chrono", "r2d2"] }
env_logger = "0.10.0"
envconfig = "0.10.0"
fronma = "0.1.1"
lazy_static = "1.4.0"
log = "0.4.17"
scan_dir = "0.3.3"
serde = { version = "1.0.152", features = ["derive"] }
serde_yaml = "0.9.17"
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator = "0.5.0"

36
backend/Dockerfile Normal file
View File

@ -0,0 +1,36 @@
FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1.67.0 as chef
FROM chef as diesel
RUN cargo install diesel_cli --no-default-features --features postgres
FROM chef as planner
WORKDIR /usr/src/backend
RUN mkdir src && touch src/main.rs
COPY ./Cargo.toml ./Cargo.lock ./
RUN cargo chef prepare --recipe-path recipe.json
FROM chef as builder
WORKDIR /usr/src/backend
COPY --from=planner /usr/src/backend/recipe.json .
RUN cargo chef cook --release --recipe-path recipe.json
RUN rm -rf ./src
COPY ./src ./src
RUN cargo build --release
FROM docker.io/debian:bullseye-slim as runner
RUN apt update
RUN apt install -y libpq5
RUN apt install -y ca-certificates
RUN apt-get clean
RUN apt-get autoremove -y
RUN rm -rf /var/lib/{apt,dpkg,cache,log}/
WORKDIR /usr/local/bin
ENV RUST_BACKTRACE=full
COPY --from=diesel /usr/local/cargo/bin/diesel .
WORKDIR /usr/src/backend
COPY ./run.sh .
RUN chmod +x ./run.sh
COPY ./migrations ./migrations
COPY --from=builder /usr/src/backend/target/release/backend /usr/src/backend/target/release/blogctl ./bin/
EXPOSE 80
ENTRYPOINT [ "./run.sh" ]

View File

@ -0,0 +1,7 @@
DROP TABLE posts;
DROP TABLE tags;
DROP INDEX configs_active;
DROP TABLE configs;

View File

@ -0,0 +1,49 @@
CREATE TABLE configs(
id SERIAL PRIMARY KEY,
active BOOLEAN NOT NULL,
name TEXT NOT NULL,
description TEXT NOT NULL,
copyright TEXT NOT NULL,
owner_name TEXT NOT NULL,
owner_email TEXT NOT NULL,
owner_website TEXT
);
CREATE UNIQUE INDEX configs_active ON configs(active)
WHERE
active;
INSERT INTO
configs(
active,
name,
description,
copyright,
owner_name,
owner_email
)
VALUES
(
TRUE,
'generic blog',
'just a generic blog',
'(C) just a generic blog',
'generic blog owner',
'blog@example.com'
);
CREATE TABLE tags(
id SERIAL PRIMARY KEY,
name TEXT UNIQUE NOT NULL
);
CREATE TABLE posts(
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
description TEXT NOT NULL,
content TEXT NOT NULL,
published_at DATE NOT NULL,
edited_at DATE,
active BOOLEAN NOT NULL
);

7
backend/run.sh Normal file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
# -*- coding: utf-8 -*-
DATABASE_URL="$BACKEND_DB_URL" diesel setup \
--migration-dir ./migrations \
--locked-schema &&
./bin/backend run

View File

@ -0,0 +1,46 @@
#[cfg(not(target_env = "msvc"))]
use tikv_jemallocator::Jemalloc;
#[cfg(not(target_env = "msvc"))]
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
use actix_web::{
http::header,
middleware,
web::{self, Data},
App, Error, HttpResponse, HttpServer,
};
use clap::{Parser, Subcommand};
use backend::*;
#[derive(Debug, Parser)]
#[clap(author, version, about, long_about = None)]
struct Cli {
#[clap(subcommand)]
commands: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
#[clap(about = "Starts webserver")]
Run,
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
match Cli::parse().commands {
Commands::Run => {
std::env::set_var("RUST_LOG", "info");
env_logger::init();
let server = HttpServer::new(move || {
App::new()
.wrap(middleware::Compress::default())
.wrap(middleware::Logger::default())
});
server.bind("0.0.0.0:80").unwrap().run().await
}
}
}

127
backend/src/bin/blogctl.rs Normal file
View File

@ -0,0 +1,127 @@
#[cfg(not(target_env = "msvc"))]
use tikv_jemallocator::Jemalloc;
#[cfg(not(target_env = "msvc"))]
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
use anyhow::{bail, Result};
use chrono::prelude::*;
use clap::{Parser, Subcommand};
use diesel::prelude::*;
use scan_dir::ScanDir;
use serde::Deserialize;
use std::fs;
use backend::*;
#[derive(Debug, Parser)]
#[clap(author, version, about, long_about = None)]
struct Cli {
#[clap(subcommand)]
commands: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
#[clap(about = "Imports new posts")]
Import,
}
#[derive(Deserialize, Debug)]
struct Blog {
name: String,
description: String,
copyright: String,
owner: BlogOwner,
}
#[derive(Deserialize, Debug)]
struct BlogOwner {
name: String,
email: String,
website: Option<String>,
}
#[derive(Deserialize, Debug)]
struct PostFrontmatter {
name: String,
slug: String,
description: String,
published_at: NaiveDate,
edited_at: Option<NaiveDate>,
active: bool,
}
#[derive(Debug)]
struct Post {
frontmatter: PostFrontmatter,
content: String,
}
fn main() -> Result<()> {
match Cli::parse().commands {
Commands::Import => {
let conn = &mut db::establish_connection()?;
let blog: Blog = serde_yaml::from_str(&fs::read_to_string("/blog/blog.yml")?)?;
diesel::delete(db::schema::configs::table)
.filter(db::schema::configs::active.eq(true))
.execute(conn)?;
diesel::insert_into(db::schema::configs::table)
.values(db::models::NewConfig {
active: true,
name: &blog.name,
description: &blog.description,
copyright: &blog.copyright,
owner_name: &blog.owner.name,
owner_email: &blog.owner.email,
owner_website: blog.owner.website.as_deref(),
})
.execute(conn)?;
let posts = ScanDir::dirs().read("/blog/posts", |iter| {
iter.map(|(entry, name)| {
dbg!(&entry, &name);
let src = fs::read_to_string(entry.path().join("post.md"))?;
let frontmatter = match fronma::parser::parse::<PostFrontmatter>(&src) {
Ok(x) => x,
Err(x) => bail!("Error parsing frontmatter: {:?}", x),
};
Ok(Post {
frontmatter: PostFrontmatter {
name: frontmatter.headers.name.trim().to_string(),
slug: frontmatter.headers.slug.trim().to_string(),
description: frontmatter.headers.description.trim().to_string(),
..frontmatter.headers
},
content: frontmatter.body.trim().to_string(),
})
})
.collect::<Result<Vec<_>>>()
})??;
dbg!(&posts);
for post in posts {
diesel::delete(db::schema::posts::table)
.filter(db::schema::posts::slug.eq(&post.frontmatter.slug))
.execute(conn)?;
diesel::insert_into(db::schema::posts::table)
.values(db::models::NewPost {
name: &post.frontmatter.name,
slug: &post.frontmatter.slug,
description: &post.frontmatter.description,
content: &post.content,
published_at: post.frontmatter.published_at,
edited_at: post.frontmatter.edited_at,
active: post.frontmatter.active,
})
.execute(conn)?;
}
Ok(())
}
}
}

13
backend/src/config.rs Normal file
View File

@ -0,0 +1,13 @@
use envconfig::Envconfig;
use lazy_static::lazy_static;
#[derive(Envconfig, Debug)]
pub struct Config {
#[envconfig(from = "BACKEND_DB_URL")]
pub db_url: String,
}
lazy_static! {
pub static ref CONFIG: Config = Config::init_from_env().unwrap();
}

28
backend/src/db/mod.rs Normal file
View File

@ -0,0 +1,28 @@
use anyhow::Result;
use diesel::pg::PgConnection;
use diesel::prelude::*;
use diesel::r2d2::{ConnectionManager, Pool};
use lazy_static::lazy_static;
use crate::config;
pub mod models;
pub mod schema;
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
pub fn establish_connection() -> ConnectionResult<PgConnection> {
PgConnection::establish(&config::CONFIG.db_url)
}
pub fn pool() -> Result<DbPool> {
Ok(
Pool::builder().build(ConnectionManager::<PgConnection>::new(
&config::CONFIG.db_url,
))?,
)
}
lazy_static! {
pub static ref POOL: DbPool = pool().unwrap();
}

67
backend/src/db/models.rs Normal file
View File

@ -0,0 +1,67 @@
use chrono::prelude::*;
use diesel::prelude::*;
use crate::db::schema;
#[derive(Identifiable, Queryable, Debug)]
#[diesel(table_name = schema::configs)]
pub struct Config {
pub id: i32,
pub active: bool,
pub name: String,
pub description: String,
pub copyright: String,
pub owner_name: String,
pub owner_email: String,
pub owner_website: Option<String>,
}
#[derive(Insertable, Debug)]
#[diesel(table_name = schema::configs)]
pub struct NewConfig<'a> {
pub active: bool,
pub name: &'a str,
pub description: &'a str,
pub copyright: &'a str,
pub owner_name: &'a str,
pub owner_email: &'a str,
pub owner_website: Option<&'a str>,
}
#[derive(Identifiable, Queryable, Debug)]
#[diesel(table_name = schema::tags)]
pub struct Tag {
pub id: i32,
pub name: String,
}
#[derive(Insertable, Debug)]
#[diesel(table_name = schema::tags)]
pub struct NewTag<'a> {
pub name: &'a str,
}
#[derive(Identifiable, Queryable, Debug)]
#[diesel(table_name = schema::posts)]
pub struct Post {
pub id: i32,
pub name: String,
pub slug: String,
pub description: String,
pub content: String,
pub published_at: NaiveDate,
pub edited_at: Option<NaiveDate>,
pub active: bool,
}
#[derive(Insertable, Debug)]
#[diesel(table_name = schema::posts)]
pub struct NewPost<'a> {
pub name: &'a str,
pub slug: &'a str,
pub description: &'a str,
pub content: &'a str,
pub published_at: NaiveDate,
pub edited_at: Option<NaiveDate>,
pub active: bool,
}

32
backend/src/db/schema.rs Normal file
View File

@ -0,0 +1,32 @@
diesel::table! {
configs {
id -> Integer,
active -> Bool,
name -> Text,
description -> Text,
copyright -> Text,
owner_name -> Text,
owner_email -> Text,
owner_website -> Nullable<Text>,
}
}
diesel::table! {
tags {
id -> Integer,
name -> Text,
}
}
diesel::table! {
posts {
id -> Integer,
name -> Text,
slug -> Text,
description -> Text,
content -> Text,
published_at -> Date,
edited_at -> Nullable<Date>,
active -> Bool,
}
}

2
backend/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod config;
pub mod db;

9
blog/blog.yml Normal file
View File

@ -0,0 +1,9 @@
name: dergrimm's blog
description: |
Just some personal expericences about technology and programming.
Follow my RSS feed for more.
copyright: (C) 2022 - Dominic Grimm
owner:
name: Dominic Grimm
email: dominic@dergrimm.net
website: https://git.dergrimm.net/dergrimm

View File

@ -0,0 +1,14 @@
---
name: Hello world!
slug: hello_world
description: Hello world to the internet. Set up my first blog!
published_at: 2023-02-06
edited_at: null
active: true
---
# Hello world!
I just set up my first blog an am really proud of it!
So anyway, here I am.

37
docker-compose.yml Normal file
View File

@ -0,0 +1,37 @@
version: "3"
services:
db:
image: docker.io/postgres:alpine
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db:/var/lib/postgresql/data
adminer:
image: docker.io/adminer:standalone
restart: always
ports:
- 8080:8080
depends_on:
- db
backend:
image: git.dergrimm.net/dergrimm/blog_backend:latest
build:
context: ./backend
restart: always
command: worker
environment:
BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
volumes:
- ./blog:/blog
ports:
- 80:80
depends_on:
- db
volumes:
db: