Compare commits
4 Commits
3a961bc524
...
b07f076634
Author | SHA1 | Date |
---|---|---|
Aravinth Manivannan | b07f076634 | |
Aravinth Manivannan | 2d9d511bb8 | |
Aravinth Manivannan | ccb0ac9d09 | |
Aravinth Manivannan | 3c3ff0f8a7 |
|
@ -1611,9 +1611,11 @@ dependencies = [
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tera",
|
"tera",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-actix-web",
|
"tracing-actix-web",
|
||||||
"url",
|
"url",
|
||||||
|
@ -2158,6 +2160,19 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d232d893b10de3eb7258ff01974d6ee20663d8e833263c99409d4b13a0209da"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha-1"
|
name = "sha-1"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
@ -2732,6 +2747,12 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1e5fa573d8ac5f1a856f8d7be41d390ee973daf97c806b2c1a465e4e1406e68"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
|
|
@ -48,6 +48,8 @@ rust-embed = "6.3.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
tracing = { version = "0.1.37", features = ["log"]}
|
tracing = { version = "0.1.37", features = ["log"]}
|
||||||
tracing-actix-web = "0.6.2"
|
tracing-actix-web = "0.6.2"
|
||||||
|
toml = "0.5.9"
|
||||||
|
serde_yaml = "0.9.14"
|
||||||
|
|
||||||
[dependencies.cache-buster]
|
[dependencies.cache-buster]
|
||||||
git = "https://github.com/realaravinth/cache-buster"
|
git = "https://github.com/realaravinth/cache-buster"
|
||||||
|
|
|
@ -22,6 +22,7 @@ use crate::ctx::Ctx;
|
||||||
use crate::db::Site;
|
use crate::db::Site;
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
|
use crate::page_config;
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use crate::subdomains::get_random_subdomain;
|
use crate::subdomains::get_random_subdomain;
|
||||||
use crate::utils::get_random;
|
use crate::utils::get_random;
|
||||||
|
@ -54,6 +55,9 @@ impl Ctx {
|
||||||
self.db.add_site(&db_site).await?;
|
self.db.add_site(&db_site).await?;
|
||||||
let page = Page::from_site(&self.settings, db_site);
|
let page = Page::from_site(&self.settings, db_site);
|
||||||
page.update(&page.branch)?;
|
page.update(&page.branch)?;
|
||||||
|
if let Some(config) = page_config::Config::load(&page.path, &page.branch) {
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
Ok(page)
|
Ok(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,17 +65,22 @@ impl Ctx {
|
||||||
if let Ok(db_site) = self.db.get_site_from_secret(secret).await {
|
if let Ok(db_site) = self.db.get_site_from_secret(secret).await {
|
||||||
let page = Page::from_site(&self.settings, db_site);
|
let page = Page::from_site(&self.settings, db_site);
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
let page = page.clone();
|
{
|
||||||
web::block(move || {
|
let page = page.clone();
|
||||||
if let Some(branch) = branch {
|
web::block(move || {
|
||||||
tx.send(page.update(&branch)).unwrap();
|
if let Some(branch) = branch {
|
||||||
} else {
|
tx.send(page.update(&branch)).unwrap();
|
||||||
tx.send(page.update(&page.branch)).unwrap();
|
} else {
|
||||||
}
|
tx.send(page.update(&page.branch)).unwrap();
|
||||||
})
|
}
|
||||||
.await
|
})
|
||||||
.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
rx.await.unwrap()?;
|
rx.await.unwrap()?;
|
||||||
|
if let Some(config) = page_config::Config::load(&page.path, &page.branch) {
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(ServiceError::WebsiteNotFound)
|
Err(ServiceError::WebsiteNotFound)
|
||||||
|
|
|
@ -86,6 +86,10 @@ pub enum ServiceError {
|
||||||
/// website not found
|
/// website not found
|
||||||
WebsiteNotFound,
|
WebsiteNotFound,
|
||||||
|
|
||||||
|
#[display(fmt = "File not found")]
|
||||||
|
/// File not found
|
||||||
|
FileNotFound,
|
||||||
|
|
||||||
/// when the a path configured for a page is already taken
|
/// when the a path configured for a page is already taken
|
||||||
#[display(
|
#[display(
|
||||||
fmt = "Path already used for another website. lhs: {:?} rhs: {:?}",
|
fmt = "Path already used for another website. lhs: {:?} rhs: {:?}",
|
||||||
|
@ -236,6 +240,7 @@ impl ResponseError for ServiceError {
|
||||||
ServiceError::EmailTaken => StatusCode::BAD_REQUEST,
|
ServiceError::EmailTaken => StatusCode::BAD_REQUEST,
|
||||||
ServiceError::UsernameTaken => StatusCode::BAD_REQUEST,
|
ServiceError::UsernameTaken => StatusCode::BAD_REQUEST,
|
||||||
ServiceError::AccountNotFound => StatusCode::NOT_FOUND,
|
ServiceError::AccountNotFound => StatusCode::NOT_FOUND,
|
||||||
|
ServiceError::FileNotFound => StatusCode::NOT_FOUND,
|
||||||
|
|
||||||
ServiceError::ProfanityError => StatusCode::BAD_REQUEST, //BADREQUEST,
|
ServiceError::ProfanityError => StatusCode::BAD_REQUEST, //BADREQUEST,
|
||||||
ServiceError::BlacklistError => StatusCode::BAD_REQUEST, //BADREQUEST,
|
ServiceError::BlacklistError => StatusCode::BAD_REQUEST, //BADREQUEST,
|
||||||
|
|
56
src/git.rs
56
src/git.rs
|
@ -173,8 +173,6 @@ fn read_file_inner(
|
||||||
}
|
}
|
||||||
|
|
||||||
let inner = |repo: &git2::Repository, tree: &git2::Tree| -> ServiceResult<FileInfo> {
|
let inner = |repo: &git2::Repository, tree: &git2::Tree| -> ServiceResult<FileInfo> {
|
||||||
// let head = repo.head().unwrap();
|
|
||||||
// let tree = head.peel_to_tree().unwrap();
|
|
||||||
let mut path = path;
|
let mut path = path;
|
||||||
if path == "/" {
|
if path == "/" {
|
||||||
let content = get_index_file(tree.id(), repo);
|
let content = get_index_file(tree.id(), repo);
|
||||||
|
@ -187,8 +185,16 @@ fn read_file_inner(
|
||||||
if path.starts_with('/') {
|
if path.starts_with('/') {
|
||||||
path = path.trim_start_matches('/');
|
path = path.trim_start_matches('/');
|
||||||
}
|
}
|
||||||
let entry = tree.get_path(Path::new(path)).unwrap();
|
|
||||||
//FileType::Dir(items)
|
fn file_not_found(e: git2::Error) -> ServiceError {
|
||||||
|
if e.code() == ErrorCode::NotFound {
|
||||||
|
if e.class() == ErrorClass::Tree {
|
||||||
|
return ServiceError::FileNotFound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.into();
|
||||||
|
}
|
||||||
|
let entry = tree.get_path(Path::new(path)).map_err(file_not_found)?;
|
||||||
|
|
||||||
let mode: GitFileMode = entry.clone().into();
|
let mode: GitFileMode = entry.clone().into();
|
||||||
if let Some(name) = entry.name() {
|
if let Some(name) = entry.name() {
|
||||||
|
@ -212,17 +218,17 @@ fn read_file_inner(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//let repo = git2::Repository::open(repo_path).unwrap();
|
|
||||||
inner(repo, tree)
|
inner(repo, tree)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use mktemp::Temp;
|
||||||
|
|
||||||
const FILE_CONTENT: &str = "foobar";
|
const FILE_CONTENT: &str = "foobar";
|
||||||
|
|
||||||
fn write_file_util(path: &str) {
|
pub fn write_file_util(repo_path: &str, file_name: &str, content: Option<&str>) {
|
||||||
// TODO change updated in DB
|
// TODO change updated in DB
|
||||||
let inner = |repo: &mut Repository| -> ServiceResult<()> {
|
let inner = |repo: &mut Repository| -> ServiceResult<()> {
|
||||||
let mut tree_builder = match repo.head() {
|
let mut tree_builder = match repo.head() {
|
||||||
|
@ -233,10 +239,14 @@ pub mod tests {
|
||||||
|
|
||||||
let odb = repo.odb().unwrap();
|
let odb = repo.odb().unwrap();
|
||||||
|
|
||||||
let obj = odb
|
let content = if content.is_some() {
|
||||||
.write(ObjectType::Blob, FILE_CONTENT.as_bytes())
|
content.as_ref().unwrap()
|
||||||
.unwrap();
|
} else {
|
||||||
tree_builder.insert("README.txt", obj, 0o100644).unwrap();
|
FILE_CONTENT
|
||||||
|
};
|
||||||
|
|
||||||
|
let obj = odb.write(ObjectType::Blob, content.as_bytes()).unwrap();
|
||||||
|
tree_builder.insert(file_name, obj, 0o100644).unwrap();
|
||||||
let tree_hash = tree_builder.write().unwrap();
|
let tree_hash = tree_builder.write().unwrap();
|
||||||
let author = Signature::now("librepages", "admin@librepages.org").unwrap();
|
let author = Signature::now("librepages", "admin@librepages.org").unwrap();
|
||||||
let committer = Signature::now("librepages", "admin@librepages.org").unwrap();
|
let committer = Signature::now("librepages", "admin@librepages.org").unwrap();
|
||||||
|
@ -268,26 +278,34 @@ pub mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
};
|
};
|
||||||
|
|
||||||
if Repository::open(path).is_err() {
|
if Repository::open(repo_path).is_err() {
|
||||||
let _ = Repository::init(path);
|
let _ = Repository::init(repo_path);
|
||||||
}
|
}
|
||||||
let mut repo = Repository::open(path).unwrap();
|
let mut repo = Repository::open(repo_path).unwrap();
|
||||||
let _ = inner(&mut repo);
|
let _ = inner(&mut repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_git_write_read_works() {
|
fn test_git_write_read_works() {
|
||||||
const PATH: &str = "/tmp/librepges/test_git_write_read_works";
|
const FILENAME: &str = "README.txt";
|
||||||
|
|
||||||
write_file_util(PATH);
|
let tmp_dir = Temp::new_dir().unwrap();
|
||||||
let resp = read_file(&Path::new(PATH).into(), "README.txt").unwrap();
|
let path = tmp_dir.to_str().unwrap();
|
||||||
assert_eq!(resp.filename, "README.txt");
|
|
||||||
|
write_file_util(path, FILENAME, None);
|
||||||
|
let resp = read_file(&Path::new(path).into(), FILENAME).unwrap();
|
||||||
|
assert_eq!(resp.filename, FILENAME);
|
||||||
assert_eq!(resp.content.bytes(), FILE_CONTENT.as_bytes());
|
assert_eq!(resp.content.bytes(), FILE_CONTENT.as_bytes());
|
||||||
assert_eq!(resp.mime.first().unwrap(), "text/plain");
|
assert_eq!(resp.mime.first().unwrap(), "text/plain");
|
||||||
|
|
||||||
let resp = read_preview_file(&Path::new(PATH).into(), "master", "README.txt").unwrap();
|
let resp = read_preview_file(&Path::new(path).into(), "master", FILENAME).unwrap();
|
||||||
assert_eq!(resp.filename, "README.txt");
|
assert_eq!(resp.filename, FILENAME);
|
||||||
assert_eq!(resp.content.bytes(), FILE_CONTENT.as_bytes());
|
assert_eq!(resp.content.bytes(), FILE_CONTENT.as_bytes());
|
||||||
assert_eq!(resp.mime.first().unwrap(), "text/plain");
|
assert_eq!(resp.mime.first().unwrap(), "text/plain");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
read_preview_file(&Path::new(path).into(), "master", "file-does-not-exist.txt"),
|
||||||
|
Err(ServiceError::FileNotFound)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ mod errors;
|
||||||
mod git;
|
mod git;
|
||||||
mod meta;
|
mod meta;
|
||||||
mod page;
|
mod page;
|
||||||
|
mod page_config;
|
||||||
mod pages;
|
mod pages;
|
||||||
mod preview;
|
mod preview;
|
||||||
mod serve;
|
mod serve;
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::git::{ContentType, GitFileMode};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub source: Source,
|
||||||
|
pub domains: Option<Vec<String>>,
|
||||||
|
pub forms: Option<Forms>,
|
||||||
|
pub image_compression: Option<ImageCompression>,
|
||||||
|
pub redirects: Option<Vec<Redirects>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)]
|
||||||
|
pub struct Source {
|
||||||
|
production_branch: String,
|
||||||
|
staging: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)]
|
||||||
|
pub struct Forms {
|
||||||
|
pub enable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)]
|
||||||
|
pub struct ImageCompression {
|
||||||
|
pub enable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)]
|
||||||
|
pub struct Redirects {
|
||||||
|
pub from: String,
|
||||||
|
pub to: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Serialize, PartialEq, Eq)]
|
||||||
|
struct Policy<'a> {
|
||||||
|
rel_path: &'a str,
|
||||||
|
format: SupportedFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Policy<'a> {
|
||||||
|
const fn new(rel_path: &'a str, format: SupportedFormat) -> Self {
|
||||||
|
Self { rel_path, format }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Serialize, PartialEq, Eq)]
|
||||||
|
enum SupportedFormat {
|
||||||
|
Json,
|
||||||
|
Yaml,
|
||||||
|
Toml,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn load<P: AsRef<Path>>(repo_path: &P, branch: &str) -> Option<Config> {
|
||||||
|
const POLICIES: [Policy; 2] = [
|
||||||
|
Policy::new("librepages.toml", SupportedFormat::Toml),
|
||||||
|
Policy::new("librepages.json", SupportedFormat::Json),
|
||||||
|
];
|
||||||
|
|
||||||
|
if let Some(policy) = Self::discover(repo_path, branch, &POLICIES) {
|
||||||
|
// let path = p.repo.as_ref().join(policy.rel_path);
|
||||||
|
//let contents = fs::read_to_string(path).await.unwrap();
|
||||||
|
|
||||||
|
let file =
|
||||||
|
crate::git::read_preview_file(&repo_path.as_ref().into(), branch, policy.rel_path)
|
||||||
|
.unwrap();
|
||||||
|
if let ContentType::Text(contents) = file.content {
|
||||||
|
let res = match policy.format {
|
||||||
|
SupportedFormat::Json => Self::load_json(&contents),
|
||||||
|
SupportedFormat::Yaml => Self::load_yaml(&contents),
|
||||||
|
SupportedFormat::Toml => Self::load_toml(&contents),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Some(res);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn discover<'a, P: AsRef<Path>>(
|
||||||
|
repo_path: &P,
|
||||||
|
branch: &str,
|
||||||
|
policies: &'a [Policy<'a>],
|
||||||
|
) -> Option<&'a Policy<'a>> {
|
||||||
|
let repo = git2::Repository::open(&repo_path).unwrap();
|
||||||
|
|
||||||
|
let branch = repo.find_branch(&branch, git2::BranchType::Local).unwrap();
|
||||||
|
// let tree = head.peel_to_tree().unwrap();
|
||||||
|
let branch = branch.into_reference();
|
||||||
|
let tree = branch.peel_to_tree().unwrap();
|
||||||
|
|
||||||
|
for p in policies.iter() {
|
||||||
|
let file_exists = tree.iter().any(|x| {
|
||||||
|
if let Some(name) = x.name() {
|
||||||
|
if policies.iter().any(|p| p.rel_path == name) {
|
||||||
|
let mode: GitFileMode = x.into();
|
||||||
|
match mode {
|
||||||
|
GitFileMode::Executable | GitFileMode::Regular => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if file_exists {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_toml(c: &str) -> Config {
|
||||||
|
toml::from_str(c).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_yaml(c: &str) -> Config {
|
||||||
|
serde_yaml::from_str(c).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_json(c: &str) -> Config {
|
||||||
|
serde_json::from_str(&c).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::git::tests::write_file_util;
|
||||||
|
use mktemp::Temp;
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn page_config_test() {
|
||||||
|
let tmp_dir = Temp::new_dir().unwrap();
|
||||||
|
let repo_path = tmp_dir.join("page_config_test");
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(
|
||||||
|
&Path::new("./tests/cases/contains-everything/toml/librepages.toml")
|
||||||
|
.canonicalize()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
write_file_util(
|
||||||
|
repo_path.to_str().unwrap(),
|
||||||
|
"librepages.toml",
|
||||||
|
Some(&content),
|
||||||
|
);
|
||||||
|
|
||||||
|
let config = Config::load(&repo_path, "master").unwrap();
|
||||||
|
assert!(config.forms.as_ref().unwrap().enable);
|
||||||
|
assert!(config.image_compression.as_ref().unwrap().enable);
|
||||||
|
assert_eq!(config.source.production_branch, "librepages");
|
||||||
|
assert_eq!(config.source.staging.as_ref().unwrap(), "beta");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.redirects.as_ref().unwrap(),
|
||||||
|
&vec![
|
||||||
|
Redirects {
|
||||||
|
from: "/from1".into(),
|
||||||
|
to: "/to1".into()
|
||||||
|
},
|
||||||
|
Redirects {
|
||||||
|
from: "/from2".into(),
|
||||||
|
to: "/to2".into()
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.domains.as_ref().unwrap(),
|
||||||
|
&vec!["example.org".to_string(), "example.com".to_string(),]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
domains = [
|
||||||
|
"example.org",
|
||||||
|
"example.com",
|
||||||
|
]
|
||||||
|
redirects = [
|
||||||
|
{from = "/from1", to = "/to1"},
|
||||||
|
{from = "/from2", to = "/to2"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[source]
|
||||||
|
production_branch = "librepages"
|
||||||
|
staging = "beta"
|
||||||
|
|
||||||
|
[forms]
|
||||||
|
enable = true
|
||||||
|
|
||||||
|
[image_compression]
|
||||||
|
enable = true
|
Loading…
Reference in New Issue