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::os::unix::net::UnixStream;
|
||||
use async_std::process::Command;
|
||||
|
@ -18,7 +19,85 @@ pub struct 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!({
|
||||
"@id": domain,
|
||||
"match": [{ "host": [domain] }],
|
||||
|
@ -42,14 +121,9 @@ impl CaddyController {
|
|||
{
|
||||
"group": "1",
|
||||
"handle": [
|
||||
{ "handler": "vars", "root": content_path.to_string() },
|
||||
{
|
||||
"handler": "file_server",
|
||||
"precompressed": {
|
||||
"br": {},
|
||||
"gzip": {}
|
||||
},
|
||||
"precompressed_order": ["br", "gzip"]
|
||||
"handler": "subroute",
|
||||
"routes": routes_array
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
74
src/sites.rs
74
src/sites.rs
|
@ -7,12 +7,47 @@ use async_std::{fs, task};
|
|||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre::{WrapErr, eyre};
|
||||
use log::{debug, info};
|
||||
use log::{debug, error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::io::ErrorKind;
|
||||
use std::sync::Arc;
|
||||
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)]
|
||||
pub struct SiteState {
|
||||
active_version: Option<SiteStateVersion>,
|
||||
|
@ -21,13 +56,13 @@ pub struct SiteState {
|
|||
|
||||
impl SiteState {
|
||||
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)?)
|
||||
}
|
||||
|
||||
async fn write_to_site_directory(&self, site_directory_path: &Utf8Path) -> Result<()> {
|
||||
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?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -125,6 +160,15 @@ pub async fn load_sites(sites_directory_path: &Utf8Path) -> Result<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<()> {
|
||||
let site = {
|
||||
let sites = sites.read().await;
|
||||
|
@ -200,6 +244,17 @@ async fn handle_download(ureq_agent: Arc<ureq::Agent>, sites: &Sites, caddy_cont
|
|||
})
|
||||
.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
|
||||
let current_version = current_version.clone();
|
||||
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
|
||||
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
|
||||
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 {
|
||||
is_outdated = current_version.id != active_version.id;
|
||||
|
||||
caddy_controller
|
||||
.upsert_site_configuration(&site.domain, &site.path.join(&active_version.id))
|
||||
.await?;
|
||||
let content_path = &site.path.join(&active_version.id);
|
||||
if let Some(site_config) = read_site_config(&content_path).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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue