Initial commit of proof-of-concept
This is /really/ rough, but is a really rough sketch of how I plan to build my overengineered personal website with Rust and eventually Fly.io + LiteFS.
This commit is contained in:
parent
a83729bd08
commit
3b849e6e6e
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -14,3 +14,8 @@ Cargo.lock
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
|
.direnv/
|
||||||
|
gs.*
|
||||||
|
result
|
||||||
|
posts/
|
||||||
|
!posts/.keep
|
||||||
|
|
22
Cargo.toml
Normal file
22
Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "quick-start"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = { version = "0.6.18", features = ["json"] }
|
||||||
|
sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-rustls", "sqlite" ] }
|
||||||
|
serde = { version = "1.0.167", features = ["derive"] }
|
||||||
|
tokio = { version = "1.29.1", features = ["full"] }
|
||||||
|
maud = { version = "*", features = ["axum"] }
|
||||||
|
tower = "0.4.13"
|
||||||
|
hyper = { version = "0.14", features = ["full"] }
|
||||||
|
tower-http = { version = "0.4.1", features = ["add-extension", "auth", "compression-full", "trace"] }
|
||||||
|
sha2 = "0.10.7"
|
||||||
|
hex = "0.4"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
futures = "0.3.28"
|
||||||
|
comrak = "0.1"
|
||||||
|
orgize = "0.9"
|
198
flake.lock
Normal file
198
flake.lock
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"advisory-db": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1689698236,
|
||||||
|
"narHash": "sha256-Qz9JxGKeA3jwuj1CdK9ejMJ7VsJRdiZniF8lx4mft9s=",
|
||||||
|
"owner": "rustsec",
|
||||||
|
"repo": "advisory-db",
|
||||||
|
"rev": "4aa517564d1d06f0e79784c8ad973a59d68aa9c8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "rustsec",
|
||||||
|
"repo": "advisory-db",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"crane": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1688772518,
|
||||||
|
"narHash": "sha256-ol7gZxwvgLnxNSZwFTDJJ49xVY5teaSvF7lzlo3YQfM=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "8b08e96c9af8c6e3a2b69af5a7fa168750fcf88e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fenix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-analyzer-src": []
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1689747703,
|
||||||
|
"narHash": "sha256-abwTXTz2u2P32fN9XRQKV+TUkcRZDfNIQ73mq9fyTxg=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "5e70fbab6c431bd8454d336ef06ef609f4d6e6f3",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1673956053,
|
||||||
|
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1687709756,
|
||||||
|
"narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils_2": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1689068808,
|
||||||
|
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1689631193,
|
||||||
|
"narHash": "sha256-AGSkBZaiTODQc8eT1rZDrQIjtb8JtFwJ0wVPzArlrnM=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "57695599bdc4f7bfe5d28cfa23f14b3d8bdf8a5f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"advisory-db": "advisory-db",
|
||||||
|
"crane": "crane",
|
||||||
|
"fenix": "fenix",
|
||||||
|
"flake-utils": "flake-utils_2",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": [
|
||||||
|
"crane",
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"crane",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1688351637,
|
||||||
|
"narHash": "sha256-CLTufJ29VxNOIZ8UTg0lepsn3X03AmopmaLTTeHDCL4=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "f9b92316727af9e6c7fee4a761242f7f46880329",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
139
flake.nix
Normal file
139
flake.nix
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
{
|
||||||
|
description = "Build a cargo project";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
|
||||||
|
crane = {
|
||||||
|
url = "github:ipetkov/crane";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
fenix = {
|
||||||
|
url = "github:nix-community/fenix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
inputs.rust-analyzer-src.follows = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
|
||||||
|
advisory-db = {
|
||||||
|
url = "github:rustsec/advisory-db";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, crane, fenix, flake-utils, advisory-db, ... }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
};
|
||||||
|
|
||||||
|
inherit (pkgs) lib;
|
||||||
|
|
||||||
|
craneLib = crane.lib.${system};
|
||||||
|
src = craneLib.cleanCargoSource (craneLib.path ./.);
|
||||||
|
|
||||||
|
# Common arguments can be set here to avoid repeating them later
|
||||||
|
commonArgs = {
|
||||||
|
inherit src;
|
||||||
|
buildInputs = [
|
||||||
|
pkgs.sqlite
|
||||||
|
] ++ lib.optionals pkgs.stdenv.isDarwin [
|
||||||
|
# Additional darwin specific inputs can be set here
|
||||||
|
pkgs.libiconv
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
craneLibLLvmTools = craneLib.overrideToolchain
|
||||||
|
(fenix.packages.${system}.complete.withComponents [
|
||||||
|
"cargo"
|
||||||
|
"llvm-tools"
|
||||||
|
"rustc"
|
||||||
|
]);
|
||||||
|
|
||||||
|
# Build *just* the cargo dependencies, so we can reuse
|
||||||
|
# all of that work (e.g. via cachix) when running in CI
|
||||||
|
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||||
|
|
||||||
|
# Build the actual crate itself, reusing the dependency
|
||||||
|
# artifacts from above.
|
||||||
|
my-crate = craneLib.buildPackage (commonArgs // {
|
||||||
|
inherit cargoArtifacts;
|
||||||
|
});
|
||||||
|
in
|
||||||
|
{
|
||||||
|
checks = {
|
||||||
|
# Build the crate as part of `nix flake check` for convenience
|
||||||
|
inherit my-crate;
|
||||||
|
|
||||||
|
# Run clippy (and deny all warnings) on the crate source,
|
||||||
|
# again, resuing the dependency artifacts from above.
|
||||||
|
#
|
||||||
|
# Note that this is done as a separate derivation so that
|
||||||
|
# we can block the CI if there are issues here, but not
|
||||||
|
# prevent downstream consumers from building our crate by itself.
|
||||||
|
my-crate-clippy = craneLib.cargoClippy (commonArgs // {
|
||||||
|
inherit cargoArtifacts;
|
||||||
|
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
|
||||||
|
});
|
||||||
|
|
||||||
|
my-crate-doc = craneLib.cargoDoc (commonArgs // {
|
||||||
|
inherit cargoArtifacts;
|
||||||
|
});
|
||||||
|
|
||||||
|
# Check formatting
|
||||||
|
my-crate-fmt = craneLib.cargoFmt {
|
||||||
|
inherit src;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Audit dependencies
|
||||||
|
my-crate-audit = craneLib.cargoAudit {
|
||||||
|
inherit src advisory-db;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Run tests with cargo-nextest
|
||||||
|
# Consider setting `doCheck = false` on `my-crate` if you do not want
|
||||||
|
# the tests to run twice
|
||||||
|
my-crate-nextest = craneLib.cargoNextest (commonArgs // {
|
||||||
|
inherit cargoArtifacts;
|
||||||
|
partitions = 1;
|
||||||
|
partitionType = "count";
|
||||||
|
});
|
||||||
|
} // lib.optionalAttrs (system == "x86_64-linux") {
|
||||||
|
# NB: cargo-tarpaulin only supports x86_64 systems
|
||||||
|
# Check code coverage (note: this will not upload coverage anywhere)
|
||||||
|
my-crate-coverage = craneLib.cargoTarpaulin (commonArgs // {
|
||||||
|
inherit cargoArtifacts;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
packages = {
|
||||||
|
default = my-crate;
|
||||||
|
my-crate-llvm-coverage = craneLibLLvmTools.cargoLlvmCov (commonArgs // {
|
||||||
|
inherit cargoArtifacts;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
apps.default = flake-utils.lib.mkApp {
|
||||||
|
drv = my-crate;
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
inputsFrom = builtins.attrValues self.checks.${system};
|
||||||
|
|
||||||
|
# Additional dev-shell environment variables can be set directly
|
||||||
|
# MY_CUSTOM_DEVELOPMENT_VAR = "something else";
|
||||||
|
|
||||||
|
# Extra inputs can be added here
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
cargo
|
||||||
|
rustc
|
||||||
|
rust-analyzer
|
||||||
|
sqlite
|
||||||
|
sqlx-cli
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
9
migrations/20230719160618_initial.sql
Normal file
9
migrations/20230719160618_initial.sql
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
-- Add migration script here
|
||||||
|
create table cached (
|
||||||
|
route text not null unique,
|
||||||
|
cached int not null,
|
||||||
|
content_type text not null,
|
||||||
|
content text not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create unique index path_unique_idx on cached (lower(route));
|
0
posts/.keep
Normal file
0
posts/.keep
Normal file
267
src/main.rs
Normal file
267
src/main.rs
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
use std::{collections::HashMap, time};
|
||||||
|
|
||||||
|
use axum::extract::Path;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::{
|
||||||
|
body::Full,
|
||||||
|
extract::State,
|
||||||
|
http::StatusCode,
|
||||||
|
http::{Request, Response},
|
||||||
|
middleware::{self, Next},
|
||||||
|
routing::get,
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use hyper::body::Bytes;
|
||||||
|
use maud::{html, Markup};
|
||||||
|
use orgize::Org;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions};
|
||||||
|
use sqlx::{FromRow, Pool, Sqlite};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref CACHE: Mutex<HashMap<String, CachedPage>> = { Mutex::new(HashMap::new()) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct AppState {
|
||||||
|
database: Pool<Sqlite>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, FromRow)]
|
||||||
|
struct CachedPage {
|
||||||
|
content_type: String,
|
||||||
|
content: String,
|
||||||
|
cached: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), sqlx::Error> {
|
||||||
|
let opts = SqliteConnectOptions::from_str("sqlite:gs.db")?
|
||||||
|
.journal_mode(SqliteJournalMode::Wal)
|
||||||
|
.create_if_missing(true);
|
||||||
|
|
||||||
|
let pool = SqlitePoolOptions::new().connect_with(opts).await?;
|
||||||
|
|
||||||
|
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||||
|
|
||||||
|
let state = AppState { database: pool };
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/", get(homepage))
|
||||||
|
.route("/blog", get(list_blog_posts))
|
||||||
|
.route("/blog/:post", get(blog_post))
|
||||||
|
.layer(middleware::from_fn_with_state(state.clone(), cached_page))
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
println!("Running webserver on port :3000");
|
||||||
|
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn homepage() -> Markup {
|
||||||
|
html! {
|
||||||
|
h1 { "Gabriel Simmer" }
|
||||||
|
h2 { "Infrastructure and DevOps" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_blog_posts() -> Markup {
|
||||||
|
let mut posts = Vec::new();
|
||||||
|
for entry in fs::read_dir("./posts").unwrap() {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
let path = entry.path();
|
||||||
|
let filename = path.file_name().unwrap().to_str().unwrap();
|
||||||
|
let ext = path.extension().unwrap().to_str().unwrap();
|
||||||
|
// strip extension
|
||||||
|
let fname = filename.replace(&format!(".{}", ext), "");
|
||||||
|
if ext == "md" || ext == "org" {
|
||||||
|
posts.push(fname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html! {
|
||||||
|
h1 { "Blog Posts" }
|
||||||
|
ul {
|
||||||
|
@for post in posts {
|
||||||
|
li { a href=(format!("/blog/{}", post)) { (post) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn blog_post(Path(post): Path<String>) -> Result<impl IntoResponse, StatusCode> {
|
||||||
|
// Search through /posts directory and find the post with either .md or .org extension
|
||||||
|
// If the post is not found, return 404
|
||||||
|
for entry in fs::read_dir("./posts").unwrap() {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
let path = entry.path();
|
||||||
|
let filename = path.file_name().unwrap().to_str().unwrap();
|
||||||
|
let ext = path.extension().unwrap().to_str().unwrap();
|
||||||
|
// strip extension
|
||||||
|
let fname = filename.replace(&format!(".{}", ext), "");
|
||||||
|
if fname == post && (ext == "md" || ext == "org") {
|
||||||
|
let content = fs::read_to_string(&path).unwrap();
|
||||||
|
// Parse markdown with comark
|
||||||
|
let mut html = "".to_owned();
|
||||||
|
if ext == "md" {
|
||||||
|
html = comrak::markdown_to_html(&content, &comrak::ComrakOptions::default());
|
||||||
|
} else if ext == "org" {
|
||||||
|
let mut writer = Vec::new();
|
||||||
|
Org::parse(&content).write_html(&mut writer).unwrap();
|
||||||
|
html = String::from_utf8(writer).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(Response::builder()
|
||||||
|
.header("content-type", "text/html")
|
||||||
|
.header("cache", "hit")
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Full::from(html))
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cached_page<T>(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
request: Request<T>,
|
||||||
|
next: Next<T>,
|
||||||
|
) -> Response<Full<Bytes>> {
|
||||||
|
let default = CachedPage {
|
||||||
|
content_type: "text/plain".to_owned(),
|
||||||
|
content: "".to_owned(),
|
||||||
|
cached: 0,
|
||||||
|
};
|
||||||
|
let path = request.uri().path().to_string();
|
||||||
|
let mut data = CACHE.lock().await;
|
||||||
|
let content = data.get(&path).unwrap_or(&default);
|
||||||
|
|
||||||
|
let current_time = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("SystemTime before UNIX EPOCH!");
|
||||||
|
|
||||||
|
if current_time.as_secs() <= (content.cached as u64 + 30 as u64) && content.content != "" {
|
||||||
|
// Return the cached page content
|
||||||
|
let c = content.clone();
|
||||||
|
|
||||||
|
return Response::builder()
|
||||||
|
.header("content-type", c.content_type)
|
||||||
|
.header("cache", "hit-memory")
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Full::from(c.content))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let res = sqlx::query_as::<_, CachedPage>("SELECT * FROM cached WHERE route = $1")
|
||||||
|
.bind(&path)
|
||||||
|
.fetch_one(&state.database)
|
||||||
|
.await;
|
||||||
|
if let Ok(res) = res {
|
||||||
|
let current_time = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("SystemTime before UNIX EPOCH!");
|
||||||
|
|
||||||
|
// SQLite cache is valid for 10 minutes.
|
||||||
|
if current_time.as_secs() <= (content.cached as u64 + (10 * 60) as u64) {
|
||||||
|
let c = CachedPage {
|
||||||
|
content_type: res.content_type,
|
||||||
|
content: res.content,
|
||||||
|
cached: SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("Failed to get current time")
|
||||||
|
.as_secs()
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Refresh our memory cache.
|
||||||
|
data.insert(path, c.clone());
|
||||||
|
return Response::builder()
|
||||||
|
.header("content-type", c.content_type)
|
||||||
|
.header("cache", "hit-sqlite")
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Full::from(c.content))
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
let cache_sqlite = sqlx::query("DELETE FROM cached WHERE route = $1")
|
||||||
|
.bind(&path)
|
||||||
|
.execute(&state.database)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = next.run(request).await;
|
||||||
|
let (res_parts, res_body) = res.into_parts();
|
||||||
|
let bytes = match hyper::body::to_bytes(res_body).await {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(_err) => {
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Full::from("error"))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = bytes.to_vec();
|
||||||
|
let contenttype = match res_parts.headers.get("content-type") {
|
||||||
|
Some(c) => c.to_str().unwrap(),
|
||||||
|
None => "text/plain",
|
||||||
|
};
|
||||||
|
|
||||||
|
if !res_parts.status.is_success() {
|
||||||
|
return Response::builder()
|
||||||
|
.header("content-type", contenttype)
|
||||||
|
.status(res_parts.status)
|
||||||
|
.body(Full::from(bytes))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = String::from_utf8(res).unwrap();
|
||||||
|
|
||||||
|
let cache = CachedPage {
|
||||||
|
content_type: String::from_str(contenttype).unwrap(),
|
||||||
|
content,
|
||||||
|
cached: SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("Failed to get current time")
|
||||||
|
.as_secs()
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
data.insert(
|
||||||
|
path.clone(),
|
||||||
|
cache.clone()
|
||||||
|
);
|
||||||
|
|
||||||
|
let cache_sqlite = sqlx::query("INSERT INTO cached (route, cached, content_type, content) VALUES ( $1, $2, $3, $4 )")
|
||||||
|
.bind(path)
|
||||||
|
.bind(cache.cached)
|
||||||
|
.bind(cache.content_type)
|
||||||
|
.bind(cache.content)
|
||||||
|
.execute(&state.database)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match cache_sqlite {
|
||||||
|
Ok(_) => println!("cached"),
|
||||||
|
Err(e) => println!("{}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::builder()
|
||||||
|
.header("content-type", contenttype)
|
||||||
|
.header("cache", "miss")
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Full::from(bytes))
|
||||||
|
.unwrap()
|
||||||
|
}
|
Loading…
Reference in a new issue