Implement SPA paths and redirects
All checks were successful
Build / build (push) Successful in 1m49s
All checks were successful
Build / build (push) Successful in 1m49s
This commit is contained in:
parent
000598d95b
commit
175519f0cb
2 changed files with 149 additions and 15 deletions
90
src/caddy.rs
90
src/caddy.rs
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::sites::{SITE_VERSION_CONFIG_FILE_NAME, SiteConfig, SiteConfigPathsMode, SiteConfigRedirectKind};
|
||||||
use async_std::io::WriteExt;
|
use async_std::io::WriteExt;
|
||||||
use async_std::os::unix::net::UnixStream;
|
use async_std::os::unix::net::UnixStream;
|
||||||
use async_std::process::Command;
|
use async_std::process::Command;
|
||||||
|
@ -18,7 +19,85 @@ pub struct CaddyController {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CaddyController {
|
impl CaddyController {
|
||||||
pub async fn upsert_site_configuration(&mut self, domain: &String, content_path: &Utf8Path) -> Result<()> {
|
pub async fn upsert_site_configuration(&mut self, domain: &String, content_path: &Utf8Path, config: &SiteConfig) -> Result<()> {
|
||||||
|
let mut routes_array = vec![];
|
||||||
|
|
||||||
|
routes_array.push(json!({
|
||||||
|
"handle": [{ "handler": "vars", "root": content_path.to_string() }]
|
||||||
|
}));
|
||||||
|
|
||||||
|
for redirect in &config.redirects {
|
||||||
|
let handler = match redirect.kind {
|
||||||
|
SiteConfigRedirectKind::Temporary => json!({
|
||||||
|
"handler": "static_response",
|
||||||
|
"headers": {
|
||||||
|
"Location": [redirect.to]
|
||||||
|
},
|
||||||
|
"status_code": 307
|
||||||
|
}),
|
||||||
|
SiteConfigRedirectKind::Permanent => json!({
|
||||||
|
"handler": "static_response",
|
||||||
|
"headers": {
|
||||||
|
"Location": [redirect.to]
|
||||||
|
},
|
||||||
|
"status_code": 308
|
||||||
|
}),
|
||||||
|
SiteConfigRedirectKind::Rewrite => json!({
|
||||||
|
"handler": "rewrite",
|
||||||
|
"uri": redirect.to
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
routes_array.push(json!({
|
||||||
|
"match": [{ "path": [redirect.from] }],
|
||||||
|
"handle": [handler]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.paths_mode == SiteConfigPathsMode::Spa {
|
||||||
|
// Redirect to URL without trailing slashes and "/index.html"
|
||||||
|
routes_array.push(json!({
|
||||||
|
"match": [{ "path": ["*/index.html", "*/"], "not": [{ "path": ["/"] }] }],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "rewrite",
|
||||||
|
"strip_path_suffix": "/index.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "rewrite",
|
||||||
|
"strip_path_suffix": "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"headers": {
|
||||||
|
"Location": ["{http.request.uri}"]
|
||||||
|
},
|
||||||
|
"status_code": 308
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
routes_array.push(json!({
|
||||||
|
"match": [{ "file": { "try_files": ["{http.request.uri.path}", "{http.request.uri.path}/index.html", "/index.html"]} }],
|
||||||
|
"handle": [{ "handler": "rewrite", "uri": "{http.matchers.file.relative}" }]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
routes_array.push(json!({
|
||||||
|
"handle": [{
|
||||||
|
"handler": "file_server",
|
||||||
|
"index_names": if config.paths_mode == SiteConfigPathsMode::Normal { json!(["index.html"]) } else { json!([]) },
|
||||||
|
"precompressed": {
|
||||||
|
"br": {},
|
||||||
|
"gzip": {}
|
||||||
|
},
|
||||||
|
"hide": [
|
||||||
|
content_path.join(SITE_VERSION_CONFIG_FILE_NAME).to_string()
|
||||||
|
],
|
||||||
|
"precompressed_order": ["br", "gzip"]
|
||||||
|
}]
|
||||||
|
}));
|
||||||
|
|
||||||
let configuration_object = json!({
|
let configuration_object = json!({
|
||||||
"@id": domain,
|
"@id": domain,
|
||||||
"match": [{ "host": [domain] }],
|
"match": [{ "host": [domain] }],
|
||||||
|
@ -42,14 +121,9 @@ impl CaddyController {
|
||||||
{
|
{
|
||||||
"group": "1",
|
"group": "1",
|
||||||
"handle": [
|
"handle": [
|
||||||
{ "handler": "vars", "root": content_path.to_string() },
|
|
||||||
{
|
{
|
||||||
"handler": "file_server",
|
"handler": "subroute",
|
||||||
"precompressed": {
|
"routes": routes_array
|
||||||
"br": {},
|
|
||||||
"gzip": {}
|
|
||||||
},
|
|
||||||
"precompressed_order": ["br", "gzip"]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
74
src/sites.rs
74
src/sites.rs
|
@ -7,12 +7,47 @@ use async_std::{fs, task};
|
||||||
use camino::{Utf8Path, Utf8PathBuf};
|
use camino::{Utf8Path, Utf8PathBuf};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use color_eyre::eyre::{WrapErr, eyre};
|
use color_eyre::eyre::{WrapErr, eyre};
|
||||||
use log::{debug, info};
|
use log::{debug, error, info};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::io::ErrorKind;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub const SITE_STATE_FILE_NAME: &str = "state.toml";
|
||||||
|
pub const SITE_VERSION_CONFIG_FILE_NAME: &str = "site.toml";
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default)]
|
||||||
|
pub struct SiteConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub paths_mode: SiteConfigPathsMode,
|
||||||
|
#[serde(default)]
|
||||||
|
pub redirects: Vec<SiteConfigRedirect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum SiteConfigPathsMode {
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
Spa,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SiteConfigRedirect {
|
||||||
|
pub from: String,
|
||||||
|
pub to: String,
|
||||||
|
pub kind: SiteConfigRedirectKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum SiteConfigRedirectKind {
|
||||||
|
Temporary,
|
||||||
|
Permanent,
|
||||||
|
Rewrite,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct SiteState {
|
pub struct SiteState {
|
||||||
active_version: Option<SiteStateVersion>,
|
active_version: Option<SiteStateVersion>,
|
||||||
|
@ -21,13 +56,13 @@ pub struct SiteState {
|
||||||
|
|
||||||
impl SiteState {
|
impl SiteState {
|
||||||
async fn read_from_site_directory(site_directory_path: &Utf8Path) -> Result<Self> {
|
async fn read_from_site_directory(site_directory_path: &Utf8Path) -> Result<Self> {
|
||||||
let string = fs::read_to_string(site_directory_path.join("site.toml").into_std_path_buf()).await?;
|
let string = fs::read_to_string(site_directory_path.join(SITE_STATE_FILE_NAME).into_std_path_buf()).await?;
|
||||||
Ok(toml::from_str::<SiteState>(&string)?)
|
Ok(toml::from_str::<SiteState>(&string)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn write_to_site_directory(&self, site_directory_path: &Utf8Path) -> Result<()> {
|
async fn write_to_site_directory(&self, site_directory_path: &Utf8Path) -> Result<()> {
|
||||||
fs::create_dir_all(site_directory_path.as_std_path()).await?;
|
fs::create_dir_all(site_directory_path.as_std_path()).await?;
|
||||||
let mut file = fs::File::create(site_directory_path.join("site.toml").into_std_path_buf()).await?;
|
let mut file = fs::File::create(site_directory_path.join(SITE_STATE_FILE_NAME).into_std_path_buf()).await?;
|
||||||
file.write_all(toml::to_string(&self).unwrap().as_bytes()).await?;
|
file.write_all(toml::to_string(&self).unwrap().as_bytes()).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -125,6 +160,15 @@ pub async fn load_sites(sites_directory_path: &Utf8Path) -> Result<Sites> {
|
||||||
Ok(RwLock::new(sites))
|
Ok(RwLock::new(sites))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn read_site_config(content_path: &Utf8Path) -> Result<Option<SiteConfig>> {
|
||||||
|
let path = content_path.join(SITE_VERSION_CONFIG_FILE_NAME).into_std_path_buf();
|
||||||
|
match fs::read_to_string(path).await {
|
||||||
|
Ok(config_string) => Ok(toml::from_str::<SiteConfig>(&config_string).ok()),
|
||||||
|
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Some(SiteConfig::default())),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_download(ureq_agent: Arc<ureq::Agent>, sites: &Sites, caddy_controller: &mut CaddyController, domain: String) -> Result<()> {
|
async fn handle_download(ureq_agent: Arc<ureq::Agent>, sites: &Sites, caddy_controller: &mut CaddyController, domain: String) -> Result<()> {
|
||||||
let site = {
|
let site = {
|
||||||
let sites = sites.read().await;
|
let sites = sites.read().await;
|
||||||
|
@ -200,6 +244,17 @@ async fn handle_download(ureq_agent: Arc<ureq::Agent>, sites: &Sites, caddy_cont
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
let site_config = if let Some(c) = read_site_config(&extraction_directory_path).await? {
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
error!(
|
||||||
|
"The configuration file of {domain} ({}) is invalid. The new version will be ignored.",
|
||||||
|
current_version.id
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
// Update and write state
|
// Update and write state
|
||||||
let current_version = current_version.clone();
|
let current_version = current_version.clone();
|
||||||
let old_active_version = state.active_version.replace(current_version);
|
let old_active_version = state.active_version.replace(current_version);
|
||||||
|
@ -207,7 +262,9 @@ async fn handle_download(ureq_agent: Arc<ureq::Agent>, sites: &Sites, caddy_cont
|
||||||
|
|
||||||
// Update Caddy configuration
|
// Update Caddy configuration
|
||||||
info!("Download for {domain} successful, now updating proxy configuration…");
|
info!("Download for {domain} successful, now updating proxy configuration…");
|
||||||
caddy_controller.upsert_site_configuration(&domain, &extraction_directory_path).await?;
|
caddy_controller
|
||||||
|
.upsert_site_configuration(&domain, &extraction_directory_path, &site_config)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
fs::remove_file(archive_file_path.as_std_path()).await?;
|
fs::remove_file(archive_file_path.as_std_path()).await?;
|
||||||
|
@ -253,9 +310,12 @@ pub async fn start_sites_worker(sites_directory_path: Utf8PathBuf, mut caddy_con
|
||||||
if let Some(active_version) = &state.active_version {
|
if let Some(active_version) = &state.active_version {
|
||||||
is_outdated = current_version.id != active_version.id;
|
is_outdated = current_version.id != active_version.id;
|
||||||
|
|
||||||
caddy_controller
|
let content_path = &site.path.join(&active_version.id);
|
||||||
.upsert_site_configuration(&site.domain, &site.path.join(&active_version.id))
|
if let Some(site_config) = read_site_config(&content_path).await? {
|
||||||
.await?;
|
caddy_controller.upsert_site_configuration(&site.domain, content_path, &site_config).await?;
|
||||||
|
} else {
|
||||||
|
error!("The configuration file of {} ({}) is invalid.", site.domain, current_version.id);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_outdated {
|
if is_outdated {
|
||||||
|
|
Loading…
Add table
Reference in a new issue