mirror of
https://github.com/YGGverse/nexy.git
synced 2026-03-31 17:25:27 +00:00
initial commit
This commit is contained in:
parent
d3661f8865
commit
ab625aa96a
14 changed files with 533 additions and 0 deletions
27
.github/workflows/build.yml
vendored
Normal file
27
.github/workflows/build.yml
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUSTFLAGS: -Dwarnings
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Run rustfmt
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
|
- name: Run clippy
|
||||||
|
run: cargo clippy --all-targets
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --verbose
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --verbose
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Cargo.lock
|
||||||
|
target
|
||||||
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "nexy"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license = "MIT"
|
||||||
|
readme = "README.md"
|
||||||
|
description = "Multi-network server for the Nex protocol"
|
||||||
|
keywords = ["nex", "protocol", "tcp", "socket", "server"]
|
||||||
|
categories = ["network-programming"]
|
||||||
|
repository = "https://github.com/YGGverse/nexy"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
urlencoding = "2.1"
|
||||||
55
README.md
Normal file
55
README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Nexy - Multi-network server for the [Nex protocol](https://nex.nightfall.city/nex/info/specification.txt)
|
||||||
|
|
||||||
|

|
||||||
|
[](https://deps.rs/repo/github/yggverse/nexy)
|
||||||
|
[](https://crates.io/crates/nexy)
|
||||||
|
|
||||||
|
Run server accessible to Internet IPv4/IPv6, [Yggdrasil](https://yggdrasil-network.github.io/), [Mycelium](https://github.com/threefoldtech/mycelium), and other networks simultaneously, as many as desired.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. `git clone https://github.com/yggverse/nexy.git && cd nexy`
|
||||||
|
2. `cargo build --release`
|
||||||
|
3. `sudo install target/release/nexy /usr/local/bin/nexy`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
nexy -p /path/to/public
|
||||||
|
```
|
||||||
|
* by default, server starts on localhost; change it with the `--bind` option(s)
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
-b, --bind <BIND>
|
||||||
|
Bind server(s) `host:port` to listen incoming connections
|
||||||
|
|
||||||
|
* use `[host]:port` notation for IPv6
|
||||||
|
|
||||||
|
[default: 127.0.0.1:1900 [::1]:1900]
|
||||||
|
|
||||||
|
-d, --debug <DEBUG>
|
||||||
|
Debug level
|
||||||
|
|
||||||
|
* `e` - error * `i` - info
|
||||||
|
|
||||||
|
[default: ei]
|
||||||
|
|
||||||
|
-t, --template <TEMPLATE>
|
||||||
|
Absolute path to the template files directory
|
||||||
|
|
||||||
|
-p, --public <PUBLIC>
|
||||||
|
Absolute path to the public files directory
|
||||||
|
|
||||||
|
-r, --read-chunk <READ_CHUNK>
|
||||||
|
Optimize memory usage on reading large files or stream
|
||||||
|
|
||||||
|
[default: 1024]
|
||||||
|
|
||||||
|
-h, --help
|
||||||
|
Print help (see a summary with '-h')
|
||||||
|
|
||||||
|
-V, --version
|
||||||
|
Print version
|
||||||
|
```
|
||||||
37
src/config.rs
Normal file
37
src/config.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
/// Default port
|
||||||
|
/// https://nex.nightfall.city/nex/info/specification.txt
|
||||||
|
const PORT: u16 = 1900;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
pub struct Config {
|
||||||
|
/// Bind server(s) `host:port` to listen incoming connections
|
||||||
|
///
|
||||||
|
/// * use `[host]:port` notation for IPv6
|
||||||
|
#[arg(short, long, default_values_t = vec![
|
||||||
|
std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, PORT).to_string(),
|
||||||
|
std::net::SocketAddrV6::new(std::net::Ipv6Addr::LOCALHOST, PORT, 0, 0).to_string()
|
||||||
|
])]
|
||||||
|
pub bind: Vec<String>,
|
||||||
|
|
||||||
|
/// Debug level
|
||||||
|
///
|
||||||
|
/// * `e` - error
|
||||||
|
/// * `i` - info
|
||||||
|
#[arg(short, long, default_value_t = String::from("ei"))]
|
||||||
|
pub debug: String,
|
||||||
|
|
||||||
|
/// Absolute path to the template files directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub template: Option<String>,
|
||||||
|
|
||||||
|
/// Absolute path to the public files directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub public: String,
|
||||||
|
|
||||||
|
/// Optimize memory usage on reading large files or stream
|
||||||
|
#[arg(short, long, default_value_t = 1024)]
|
||||||
|
pub read_chunk: usize,
|
||||||
|
}
|
||||||
26
src/main.rs
Normal file
26
src/main.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
mod config;
|
||||||
|
mod response;
|
||||||
|
mod server;
|
||||||
|
mod session;
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
use clap::Parser;
|
||||||
|
let a = config::Config::parse();
|
||||||
|
let s = std::sync::Arc::new(session::Session::init(&a)?);
|
||||||
|
for b in a.bind {
|
||||||
|
s.debug.info(&format!("start server on `{b}`..."));
|
||||||
|
match std::net::TcpListener::bind(&b) {
|
||||||
|
Ok(r) => {
|
||||||
|
std::thread::spawn({
|
||||||
|
let s = s.clone();
|
||||||
|
move || server::start(r, &s)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => s
|
||||||
|
.debug
|
||||||
|
.error(&format!("failed to start server on `{b}`: `{e}`")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::thread::park();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
11
src/response.rs
Normal file
11
src/response.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
/// Internal types
|
||||||
|
pub enum Response<'a> {
|
||||||
|
/// Includes reference to the original request
|
||||||
|
AccessDenied(&'a str),
|
||||||
|
/// Includes server-side error description
|
||||||
|
InternalServerError(String),
|
||||||
|
/// Includes reference to the original request
|
||||||
|
NotFound(&'a str),
|
||||||
|
/// Includes bytes array
|
||||||
|
Success(&'a [u8]),
|
||||||
|
}
|
||||||
26
src/server.rs
Normal file
26
src/server.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
mod connection;
|
||||||
|
|
||||||
|
use crate::session::Session;
|
||||||
|
use connection::Connection;
|
||||||
|
use std::{net::TcpListener, sync::Arc, thread};
|
||||||
|
|
||||||
|
pub fn start(server: TcpListener, session: &Arc<Session>) {
|
||||||
|
for i in server.incoming() {
|
||||||
|
match i {
|
||||||
|
Ok(stream) => {
|
||||||
|
thread::spawn({
|
||||||
|
let session = session.clone();
|
||||||
|
move || match Connection::init(&session, stream) {
|
||||||
|
Ok(connection) => connection.handle(),
|
||||||
|
Err(e) => session
|
||||||
|
.debug
|
||||||
|
.error(&format!("failed to init connection: `{e}`")),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => session
|
||||||
|
.debug
|
||||||
|
.error(&format!("failed to accept incoming connection: `{e}`")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/server/connection.rs
Normal file
108
src/server/connection.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
use crate::{response::Response, session::Session};
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::{
|
||||||
|
io::{Read, Write},
|
||||||
|
net::{SocketAddr, TcpStream},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Parsed once endpoint addresses for this `stream`
|
||||||
|
struct Address {
|
||||||
|
server: SocketAddr,
|
||||||
|
client: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Client/server connection with its features implementation
|
||||||
|
pub struct Connection {
|
||||||
|
address: Address,
|
||||||
|
session: Arc<Session>,
|
||||||
|
stream: TcpStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Connection {
|
||||||
|
pub fn init(session: &Arc<Session>, stream: TcpStream) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
address: Address {
|
||||||
|
server: stream.local_addr()?,
|
||||||
|
client: stream.peer_addr()?,
|
||||||
|
},
|
||||||
|
session: session.clone(),
|
||||||
|
stream,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle(mut self) {
|
||||||
|
match self.request() {
|
||||||
|
Ok(q) => {
|
||||||
|
self.session.debug.info(&format!(
|
||||||
|
"[{}] < [{}] request `{q}`...",
|
||||||
|
self.address.server, self.address.client
|
||||||
|
));
|
||||||
|
self.session
|
||||||
|
.clone()
|
||||||
|
.storage
|
||||||
|
.request(&q, |r| self.response(r))
|
||||||
|
}
|
||||||
|
Err(e) => self.response(Response::InternalServerError(format!(
|
||||||
|
"[{}] < [{}] failed to handle incoming request: `{e}`",
|
||||||
|
self.address.server, self.address.client
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
self.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request(&mut self) -> Result<String> {
|
||||||
|
let mut b = [0; 1024]; // @TODO unspecified len?
|
||||||
|
let n = self.stream.read(&mut b)?;
|
||||||
|
Ok(urlencoding::decode(std::str::from_utf8(&b[..n])?.trim())?.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response(&mut self, response: Response) {
|
||||||
|
let bytes = match response {
|
||||||
|
Response::Success(b) => b,
|
||||||
|
Response::InternalServerError(e) => {
|
||||||
|
self.session.debug.error(&e);
|
||||||
|
self.session.template.internal_server_error.as_bytes()
|
||||||
|
}
|
||||||
|
Response::AccessDenied(q) => {
|
||||||
|
self.session.debug.error(&format!(
|
||||||
|
"[{}] < [{}] access to `{q}` denied.",
|
||||||
|
self.address.server, self.address.client
|
||||||
|
));
|
||||||
|
self.session.template.access_denied.as_bytes()
|
||||||
|
}
|
||||||
|
Response::NotFound(q) => {
|
||||||
|
self.session.debug.error(&format!(
|
||||||
|
"[{}] < [{}] requested resource `{q}` not found.",
|
||||||
|
self.address.server, self.address.client
|
||||||
|
));
|
||||||
|
self.session.template.not_found.as_bytes()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match self.stream.write_all(bytes) {
|
||||||
|
Ok(()) => self.session.debug.info(&format!(
|
||||||
|
"[{}] > [{}] sent {} bytes response.",
|
||||||
|
self.address.server,
|
||||||
|
self.address.client,
|
||||||
|
bytes.len()
|
||||||
|
)),
|
||||||
|
Err(e) => self.session.debug.error(&format!(
|
||||||
|
"[{}] ! [{}] failed to response: `{e}`",
|
||||||
|
self.address.server, self.address.client,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shutdown(self) {
|
||||||
|
match self.stream.shutdown(std::net::Shutdown::Both) {
|
||||||
|
Ok(()) => self.session.debug.info(&format!(
|
||||||
|
"[{}] - [{}] connection closed by server.",
|
||||||
|
self.address.server, self.address.client,
|
||||||
|
)),
|
||||||
|
Err(e) => self.session.debug.error(&format!(
|
||||||
|
"[{}] > [{}] failed to close connection: `{e}`",
|
||||||
|
self.address.server, self.address.client,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/session.rs
Normal file
22
src/session.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
mod debug;
|
||||||
|
mod storage;
|
||||||
|
mod template;
|
||||||
|
|
||||||
|
use {debug::Debug, storage::Storage, template::Template};
|
||||||
|
|
||||||
|
/// Single container for the current session
|
||||||
|
pub struct Session {
|
||||||
|
pub debug: Debug,
|
||||||
|
pub storage: Storage,
|
||||||
|
pub template: Template,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
pub fn init(config: &crate::config::Config) -> anyhow::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
debug: Debug::init(&config.debug)?,
|
||||||
|
storage: Storage::init(&config.public, config.read_chunk)?,
|
||||||
|
template: Template::init(&config.template)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/session/debug.rs
Normal file
33
src/session/debug.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
mod level;
|
||||||
|
use level::Level;
|
||||||
|
|
||||||
|
pub struct Debug(Vec<Level>);
|
||||||
|
|
||||||
|
impl Debug {
|
||||||
|
pub fn init(levels: &str) -> anyhow::Result<Self> {
|
||||||
|
let mut l = Vec::with_capacity(levels.len());
|
||||||
|
for s in levels.to_lowercase().chars() {
|
||||||
|
l.push(Level::parse(s)?);
|
||||||
|
}
|
||||||
|
Ok(Self(l))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error(&self, message: &str) {
|
||||||
|
if self.0.contains(&Level::Error) {
|
||||||
|
eprintln!("[{}] [error] {message}", now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn info(&self, message: &str) {
|
||||||
|
if self.0.contains(&Level::Info) {
|
||||||
|
println!("[{}] [info] {message}", now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now() -> u128 {
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis()
|
||||||
|
}
|
||||||
17
src/session/debug/level.rs
Normal file
17
src/session/debug/level.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum Level {
|
||||||
|
Error,
|
||||||
|
Info,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Level {
|
||||||
|
pub fn parse(value: char) -> Result<Self> {
|
||||||
|
match value {
|
||||||
|
'e' => Ok(Self::Error),
|
||||||
|
'i' => Ok(Self::Info),
|
||||||
|
_ => bail!("unsupported debug level `{value}`!"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/session/storage.rs
Normal file
108
src/session/storage.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
use crate::response::Response;
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use std::{fs, io::Read, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
|
pub struct Storage {
|
||||||
|
public_dir: PathBuf,
|
||||||
|
read_chunk: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Storage {
|
||||||
|
pub fn init(path: &str, read_chunk: usize) -> Result<Self> {
|
||||||
|
let public_dir = PathBuf::from_str(path)?.canonicalize()?;
|
||||||
|
let t = fs::metadata(&public_dir)?;
|
||||||
|
if !t.is_dir() {
|
||||||
|
bail!("Storage destination is not directory!");
|
||||||
|
}
|
||||||
|
if t.is_symlink() {
|
||||||
|
bail!("Symlinks yet not supported!");
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
public_dir,
|
||||||
|
read_chunk,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request(&self, query: &str, mut callback: impl FnMut(Response)) {
|
||||||
|
let p = {
|
||||||
|
// access restriction zone, change carefully!
|
||||||
|
let mut p = PathBuf::from(&self.public_dir);
|
||||||
|
p.push(query.trim_matches('/'));
|
||||||
|
match p.canonicalize() {
|
||||||
|
Ok(c) => {
|
||||||
|
if !c.starts_with(&self.public_dir) {
|
||||||
|
return callback(Response::AccessDenied(query));
|
||||||
|
}
|
||||||
|
c
|
||||||
|
}
|
||||||
|
Err(_) => return callback(Response::NotFound(query)),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match fs::metadata(&p) {
|
||||||
|
Ok(t) => match (t.is_dir(), t.is_file()) {
|
||||||
|
(true, _) => callback(match self.list(&p) {
|
||||||
|
Ok(ref l) => Response::Success(l.as_bytes()),
|
||||||
|
Err(e) => Response::InternalServerError(e.to_string()),
|
||||||
|
}),
|
||||||
|
(_, true) => match fs::File::open(p) {
|
||||||
|
Ok(mut f) => loop {
|
||||||
|
let mut b = vec![0; self.read_chunk];
|
||||||
|
match f.read(&mut b) {
|
||||||
|
Ok(0) => break,
|
||||||
|
Ok(n) => callback(Response::Success(&b[..n])),
|
||||||
|
Err(e) => {
|
||||||
|
return callback(Response::InternalServerError(format!(
|
||||||
|
"failed to read response chunk for `{query}`: `{e}`"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => callback(Response::InternalServerError(format!(
|
||||||
|
"failed to read response for query`{query}`: `{e}`"
|
||||||
|
))),
|
||||||
|
},
|
||||||
|
_ => panic!(), // unexpected
|
||||||
|
},
|
||||||
|
Err(e) => callback(Response::InternalServerError(format!(
|
||||||
|
"failed to read storage for `{query}`: `{e}`"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build entries list for given `path`,
|
||||||
|
/// sort ASC, by directories first.
|
||||||
|
///
|
||||||
|
/// * make sure the `path` is allowed before call this method!
|
||||||
|
fn list(&self, path: &PathBuf) -> Result<String> {
|
||||||
|
use urlencoding::encode;
|
||||||
|
const C: usize = 25; // @TODO optional
|
||||||
|
let mut d = Vec::with_capacity(C);
|
||||||
|
let mut f = Vec::with_capacity(C);
|
||||||
|
for entry in fs::read_dir(path)? {
|
||||||
|
let e = entry?;
|
||||||
|
let t = fs::metadata(e.path())?;
|
||||||
|
match (t.is_dir(), t.is_file()) {
|
||||||
|
(true, _) => d.push(e.file_name()),
|
||||||
|
(_, true) => f.push(e.file_name()),
|
||||||
|
_ => {} // @TODO symlinks support?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut l = Vec::with_capacity(d.len() + f.len());
|
||||||
|
if &self.public_dir != path {
|
||||||
|
l.push("=> ../".to_string())
|
||||||
|
}
|
||||||
|
d.sort();
|
||||||
|
for dir in d {
|
||||||
|
if let Some(s) = dir.to_str() {
|
||||||
|
l.push(format!("=> {}/", encode(s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.sort();
|
||||||
|
for file in f {
|
||||||
|
if let Some(s) = file.to_str() {
|
||||||
|
l.push(format!("=> {}", encode(s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(l.join("\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/session/template.rs
Normal file
46
src/session/template.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
|
||||||
|
pub struct Template {
|
||||||
|
pub access_denied: String,
|
||||||
|
pub internal_server_error: String,
|
||||||
|
pub not_found: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Template {
|
||||||
|
pub fn init(directory: &Option<String>) -> Result<Self> {
|
||||||
|
use std::{fs::read_to_string, path::PathBuf};
|
||||||
|
|
||||||
|
const ACCESS_DENIED: (&str, &str) = ("access_denied.txt", "Access denied");
|
||||||
|
const INTERNAL_SERVER_ERROR: (&str, &str) =
|
||||||
|
("internal_server_error.txt", "Internal server error");
|
||||||
|
const NOT_FOUND: (&str, &str) = ("not_found.txt", "Not found");
|
||||||
|
|
||||||
|
fn path(directory: &str, file: &str) -> Result<PathBuf> {
|
||||||
|
let mut p = PathBuf::from(directory).canonicalize()?;
|
||||||
|
p.push(file);
|
||||||
|
if !p.exists() {
|
||||||
|
bail!("Template path `{}` does not exist", p.to_string_lossy())
|
||||||
|
}
|
||||||
|
if !p.is_file() {
|
||||||
|
bail!("Template path `{}` is not file", p.to_string_lossy())
|
||||||
|
}
|
||||||
|
if p.is_symlink() {
|
||||||
|
bail!("Symlinks yet not supported!");
|
||||||
|
}
|
||||||
|
Ok(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(match directory {
|
||||||
|
Some(d) => Self {
|
||||||
|
access_denied: read_to_string(path(d, ACCESS_DENIED.0)?)?,
|
||||||
|
internal_server_error: read_to_string(path(d, INTERNAL_SERVER_ERROR.0)?)?,
|
||||||
|
not_found: read_to_string(path(d, NOT_FOUND.0)?)?,
|
||||||
|
},
|
||||||
|
None => Self {
|
||||||
|
access_denied: ACCESS_DENIED.1.to_string(),
|
||||||
|
internal_server_error: INTERNAL_SERVER_ERROR.1.to_string(),
|
||||||
|
not_found: NOT_FOUND.1.to_string(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue