Config and base structure

This commit is contained in:
Raphaël Thériault 2020-06-24 00:40:42 -04:00
parent 47abc2bc3d
commit cf79946c8e
10 changed files with 1035 additions and 274 deletions

View File

@ -1,8 +0,0 @@
PORT=8080
DATABASE=target/database.db
FILES_DIR=target/static/
TEXT_DIR=target/static/
FILITE_LOG=debug

View File

@ -71,8 +71,6 @@ jobs:
use-cross: true
command: build
args: --release --target armv7-unknown-linux-musleabihf
- name: Strip binary
run: strip target/armv7-unknown-linux-musleabihf/release/filite
- name: Upload binary
uses: actions/upload-artifact@v1
with:

3
.gitignore vendored
View File

@ -4,3 +4,6 @@ target/
.vscode/
.idea/
filite.toml
filite.json

888
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,30 +3,64 @@ name = "filite"
version = "0.3.0"
authors = ["Raphaël Thériault <raphael_theriault@outlook.com>"]
edition = "2018"
description = "A simple, light and standalone pastebin, URL shortener and file-sharing service"
description = "A modular and standalone pastebin, URL shortener and file-sharing service"
homepage = "https://github.com/raftario/filite"
repository = "https://github.com/raftario/filite"
readme = "README.md"
keywords = [
"file-sharing",
"url-shortener",
"pastebin"
"pastebin",
]
license = "MIT"
[dependencies]
dotenv = { version = "0.15.0", optional = true }
anyhow = "1.0.31"
cfg-if = "0.1.10"
chrono = "0.4.11"
env_logger = "0.7.1"
envy = "0.4.1"
log = "0.4.8"
rusqlite = { version = "0.23.1", features = ["bundled", "chrono"] }
log = { version = "0.4.8", features = ["serde"] }
rust-argon2 = "0.8.2"
serde = { version = "1.0.112", features = ["derive"] }
tokio = { version = "0.2.21", features = ["blocking", "fs", "macros"] }
toml = "0.5.6"
warp = { version = "0.2.3", features = ["multipart", "tls"], default-features = false }
serde = { version = "1.0.114", features = ["derive"] }
serde_json = "1.0.55"
sqlx = { version = "0.3.5", features = ["chrono", "runtime-tokio"], default-features = false }
structopt = "0.3.15"
tokio = { version = "0.2.21", features = ["blocking", "fs"] }
warp = { version = "0.2.3", features = ["multipart"], default-features = false }
[features]
default = []
dev = ["dotenv"]
default = ["full"]
lite = ["sqlite"]
classic = [
"sqlite",
"threaded",
"highlight",
]
full = [
"sqlite",
"postgres",
"mysql",
"tls",
"threaded",
"analytics",
"highlight",
]
# Database backends
sqlite = ["sqlx/sqlite"]
postgres = ["sqlx/postgres"]
mysql = ["sqlx/mysql"]
# TLS support for the database and web server
tls = [
"sqlx/tls",
"warp/tls",
]
# Threaded runtime
threaded = ["tokio/rt-threaded"]
# Various analytics (no external services)
analytics = []
# Syntax highlighting
highlight = []

View File

@ -2,7 +2,7 @@
> The README isn't representative of the current status of the `next` branch and will only be updated once the changes are stabilised.
A simple, light and standalone pastebin, URL shortener and file-sharing service that hosts **fi**les, redirects **li**nks and stores **te**xts.
A modular pastebin, URL shortener and file-sharing service that hosts **fi**les, redirects **li**nks and stores **te**xts.
[![GitHub Actions](https://github.com/raftario/filite/workflows/Build/badge.svg)](https://github.com/raftario/filite/actions?workflowID=Build)
[![Crates.io](https://img.shields.io/crates/v/filite.svg)](https://crates.io/crates/filite)

147
src/config.rs Normal file
View File

@ -0,0 +1,147 @@
use anyhow::Error;
use log::LevelFilter;
use serde::{Deserialize, Serialize};
use std::{
fs::File,
io::{BufReader, BufWriter},
path::{Path, PathBuf},
};
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
pub port: u16,
pub database_url: String,
#[serde(default)]
pub logger: LoggerConfig,
#[serde(default)]
pub pool: PoolConfig,
#[cfg(feature = "tls")]
#[serde(default)]
pub tls: TlsConfig,
#[cfg(feature = "threaded")]
#[serde(default)]
pub threads: ThreadsConfig,
#[cfg(feature = "analytics")]
#[serde(default)]
pub analytics: AnalyticsConfig,
#[cfg(feature = "highlight")]
#[serde(default)]
pub highlight: HighlightConfig,
}
impl Config {
pub fn read(path: impl AsRef<Path>) -> Result<Self, Error> {
let file = File::open(path)?;
let config = serde_json::from_reader(BufReader::new(file))?;
Ok(config)
}
pub fn write(path: impl AsRef<Path>) -> Result<(), Error> {
let config = Self {
port: 80,
database_url: {
cfg_if::cfg_if! {
if #[cfg(feature = "sqlite")] {
"filite.db"
} else if #[cfg(feature = "postgres")] {
"postgresql://localhost:5432/filite"
} else if #[cfg(feature = "mysql")] {
"mysql://localhost:3306/filite"
}
}
}
.to_owned(),
logger: Default::default(),
pool: Default::default(),
#[cfg(feature = "tls")]
tls: Default::default(),
#[cfg(feature = "threaded")]
threads: Default::default(),
#[cfg(feature = "analytics")]
analytics: Default::default(),
#[cfg(feature = "highlight")]
highlight: Default::default(),
};
let file = File::create(path)?;
serde_json::to_writer_pretty(BufWriter::new(file), &config)?;
Ok(())
}
}
#[derive(Deserialize, Serialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct LoggerConfig {
pub console: LogLevel,
pub file: Option<FileLoggerConfig>,
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FileLoggerConfig {
#[serde(default)]
pub level: LogLevel,
pub path: PathBuf,
}
#[derive(Deserialize, Serialize)]
pub struct LogLevel(pub LevelFilter);
impl Default for LogLevel {
fn default() -> Self {
Self(LevelFilter::Info)
}
}
#[derive(Deserialize, Serialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct PoolConfig {
pub min_size: Option<u32>,
pub max_size: Option<u32>,
pub connect_timeout: Option<u64>,
pub idle_timeout: Option<u64>,
pub max_lifetime: Option<u64>,
}
#[cfg(feature = "tls")]
#[derive(Deserialize, Serialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct TlsConfig {
pub cert: Option<PathBuf>,
pub key: Option<PathBuf>,
}
#[cfg(feature = "threaded")]
#[derive(Deserialize, Serialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct ThreadsConfig {
pub core_threads: Option<usize>,
pub max_threads: Option<usize>,
}
#[cfg(feature = "analytics")]
#[derive(Deserialize, Serialize, Default)]
#[serde(default)]
pub struct AnalyticsConfig {
pub views: bool,
}
#[cfg(feature = "highlight")]
#[derive(Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct HighlightConfig {
pub theme: String,
pub languages: Vec<String>,
}
#[cfg(feature = "highlight")]
impl Default for HighlightConfig {
fn default() -> Self {
Self {
theme: "default".to_owned(),
languages: Default::default(),
}
}
}

88
src/logger.rs Normal file
View File

@ -0,0 +1,88 @@
use crate::config::LoggerConfig;
use anyhow::Error;
use log::{self, Level, LevelFilter, Log, Metadata, Record};
use std::{
fs::{File, OpenOptions},
io::{self, BufWriter, Stderr, Stdout, Write},
sync::Mutex,
};
pub fn init(config: &LoggerConfig) -> Result<(), Error> {
let logger = Logger {
console: (
config.console.0,
Mutex::new(io::stderr()),
Mutex::new(io::stdout()),
),
file: match &config.file {
Some(fc) => Some((
fc.level.0,
Mutex::new(BufWriter::new(
OpenOptions::new()
.append(true)
.create(true)
.open(&fc.path)?,
)),
)),
None => None,
},
};
log::set_boxed_logger(Box::new(logger))?;
Ok(())
}
struct Logger {
console: (LevelFilter, Mutex<Stderr>, Mutex<Stdout>),
file: Option<(LevelFilter, Mutex<BufWriter<File>>)>,
}
impl Log for Logger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= self.console.0
|| self
.file
.as_ref()
.map_or(false, |(lf, ..)| metadata.level() <= *lf)
}
fn log(&self, record: &Record) {
let target = record.target();
let level = record.level();
let args = record.args();
if let Some((lf, bw)) = &self.file {
if level <= *lf {
bw.lock()
.unwrap()
.write_fmt(format_args!("[{}]::[{}] {}\n", target, level, args))
.ok();
}
}
if level <= self.console.0 {
if level <= Level::Warn {
self.console
.1
.lock()
.unwrap()
.write_fmt(format_args!("[{}]::[{}] {}\n", target, level, args))
.ok();
} else {
self.console
.2
.lock()
.unwrap()
.write_fmt(format_args!("[{}]::[{}] {}\n", target, level, args))
.ok();
}
}
}
fn flush(&self) {
self.console.1.lock().unwrap().flush().ok();
self.console.2.lock().unwrap().flush().ok();
if let Some((_, bw)) = &self.file {
bw.lock().unwrap().flush().ok();
}
}
}

View File

@ -1,9 +1,80 @@
use std::env;
#[cfg(not(any(feature = "sqlite", feature = "postgres", feature = "mysql")))]
compile_error!("You need to select at least one database backend");
#[tokio::main]
async fn main() {
if env::var_os("FILITE_LOG").is_none() {
env::set_var("FILITE_LOG", "INFO");
}
env_logger::init_from_env("FILITE_LOG");
mod config;
mod logger;
mod runtime;
use anyhow::Error;
use config::Config;
use std::path::PathBuf;
use structopt::StructOpt;
#[derive(StructOpt)]
#[structopt(author, about)]
struct Opt {
/// Configuration file to use
///
/// If unspecified, will look for a filite.json
/// file in the current working directory.
#[structopt(short, long, name = "FILE")]
config: Option<PathBuf>,
#[structopt(subcommand)]
command: Option<Command>,
}
#[derive(StructOpt)]
enum Command {
/// Initialises the configuration file with default values
InitConfig {
/// File to write
///
/// If unspecified, will write to a filite.json
/// file in the current working directory.
#[structopt(name = "FILE")]
path: Option<PathBuf>,
},
/// Initialises the database tables
InitDatabase {
/// Database connection URL
#[structopt(name = "URL")]
url: String,
},
}
fn main() -> Result<(), Error> {
let args: Opt = Opt::from_args();
match &args.command {
Some(Command::InitConfig { path }) => {
return init_config(match path {
Some(_) => path.as_ref(),
None => args.config.as_ref(),
})
}
Some(Command::InitDatabase { url }) => return init_database(url),
None => (),
}
let config = Config::read(args.config.unwrap_or_else(|| PathBuf::from("filite.json")))?;
logger::init(&config.logger)?;
let mut runtime = runtime::build(&config)?;
runtime.block_on(run(&config))?;
Ok(())
}
async fn run(_config: &Config) -> Result<(), Error> {
Ok(())
}
fn init_config(path: Option<&PathBuf>) -> Result<(), Error> {
config::Config::write(path.unwrap_or(&PathBuf::from("filite.json")))?;
println!("Default config written");
Ok(())
}
fn init_database(_url: &str) -> Result<(), Error> {
Ok(())
}

26
src/runtime.rs Normal file
View File

@ -0,0 +1,26 @@
use crate::config::Config;
use anyhow::Error;
use log::debug;
use tokio::runtime::{Builder, Runtime};
#[cfg_attr(not(feature = "threaded"), allow(unused_variables))]
pub fn build(config: &Config) -> Result<Runtime, Error> {
let mut builder = Builder::new();
builder.basic_scheduler().enable_io();
#[cfg(feature = "threaded")]
{
builder.threaded_scheduler();
let config = &config.threads;
if let Some(ct) = config.core_threads {
builder.core_threads(ct);
}
if let Some(mt) = config.max_threads {
builder.max_threads(mt);
}
}
debug!("Building Tokio runtime");
Ok(builder.build()?)
}