A fresh start

This commit is contained in:
Raphaël Thériault 2020-06-17 00:11:47 -04:00
parent 3104abfd25
commit 54c6b33eb3
25 changed files with 595 additions and 3111 deletions

View File

@ -1,9 +1,8 @@
PORT=8080
DATABASE_URL=target/database.db
POOL_SIZE=4
DATABASE=target/database.db
FILES_DIR=target/static/
TEXT_DIR=target/static/
PASSWD=a1b2c3d4
RUST_LOG=actix_web=debug
LOG_FORMAT='[%r] (%D ms) : [%s] (%b B)'
FILITE_LOG=debug

View File

@ -1,4 +1,4 @@
name: Build
name: CI
on: [push, pull_request]
jobs:
build:
@ -7,8 +7,12 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
include:
- os: ubuntu-latest
bin-path: target/release/filite
- os: windows-latest
path: target/release/filite.exe
bin-path: target/release/filite.exe
- os: macOS-latest
bin-path: target/release/filite
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
@ -31,15 +35,49 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: build
args: --release
args: --release -p lilac-cli
- name: Strip binary
if: runner.os != 'Windows'
run: strip target/release/filite
run: strip ${{ matrix.bin-path }}
- name: Upload binary
uses: actions/upload-artifact@v1
with:
name: ${{ runner.os }}
path: ${{ matrix.path || 'target/release/filite' }}
path: ${{ matrix.bin-path }}
build-pi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: armv7-unknown-linux-musleabihf
override: true
- uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: pi-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- uses: actions/cache@v1
with:
path: ~/.cargo/git
key: pi-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- uses: actions/cache@v1
with:
path: target
key: pi-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Build project
uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --target armv7-unknown-linux-musleabihf
- name: Strip binary
run: strip target/armv7-unknown-linux-musleabihf/filite
- name: Upload binary
uses: actions/upload-artifact@v1
with:
name: Pi
path: target/armv7-unknown-linux-musleabihf/filite
checks:
runs-on: ubuntu-latest
steps:
@ -47,6 +85,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
components: clippy, rustfmt
override: true
- uses: actions/cache@v1
with:
@ -60,20 +99,16 @@ jobs:
with:
path: target
key: checks-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- run: rustup component add rustfmt
- run: rustup component add clippy
- name: Run tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all
- name: Check for clippy warnings
uses: actions-rs/cargo@v1
uses: actions-rs/clippy-check@v1
with:
command: clippy
args: --all -- -D warnings
token: ${{ secrets.GITHUB_TOKEN }}
- name: Check formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
args: -- --check

6
.gitignore vendored
View File

@ -1,10 +1,6 @@
# Rust build artifacts
target/
**/*.rs.bk
# User specific files
.env
**/*.log
# JetBrains settings
.vscode/
.idea/

1704
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -13,40 +13,19 @@ keywords = [
"pastebin"
]
license = "MIT"
build = "build.rs"
[dependencies]
actix-files = "0.2.1"
actix-identity = "0.2.1"
actix-rt = "1.0.0"
actix-multipart = "0.2.0"
actix-web = "2.0.0"
base64 = "0.11.0"
blake3 = "0.1.1"
chrono = "0.4.10"
dialoguer = "0.5.0"
diesel_migrations = "1.4.0"
dirs = "2.0.2"
dotenv = { version = "0.15.0", optional = true }
chrono = "0.4.11"
env_logger = "0.7.1"
futures = "0.3.1"
lazy_static = "1.4.0"
num_cpus = "1.11.1"
radix_fmt = "1.0.0"
rand = "0.7.3"
toml = "0.5.5"
[dependencies.diesel]
version = "1.4.3"
features = ["r2d2", "sqlite"]
[dependencies.libsqlite3-sys]
version = "0.16.0"
features = ["bundled"]
[dependencies.serde]
version = "1.0.104"
features = ["derive"]
[build-dependencies]
rand = "0.7.3"
envy = "0.4.1"
log = "0.4.8"
rusqlite = { version = "0.23.1", features = ["bundled", "chrono"] }
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 }
[features]
default = []

View File

@ -1,5 +1,7 @@
# filite
> 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.
[![GitHub Actions](https://github.com/raftario/filite/workflows/Build/badge.svg)](https://github.com/raftario/filite/actions?workflowID=Build)

View File

@ -1,15 +0,0 @@
use rand::Rng;
use std::{env, fs::File, io::Write, path::Path};
fn main() {
let mut key = [0; 32];
let mut rng = rand::thread_rng();
for b in key.iter_mut() {
*b = rng.gen();
}
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("key");
let mut f = File::create(&dest_path).unwrap();
f.write_all(&key).unwrap();
}

View File

@ -1,2 +0,0 @@
[print_schema]
file = "src/schema.rs"

View File

@ -1 +0,0 @@
DROP TABLE files

View File

@ -1,5 +0,0 @@
CREATE TABLE files (
id INTEGER NOT NULL PRIMARY KEY,
filepath TEXT NOT NULL,
created INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);

View File

@ -1 +0,0 @@
DROP TABLE links

View File

@ -1,5 +0,0 @@
CREATE TABLE links (
id INTEGER NOT NULL PRIMARY KEY,
forward TEXT NOT NULL,
created INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);

View File

@ -1 +0,0 @@
DROP TABLE texts

View File

@ -1,5 +0,0 @@
CREATE TABLE texts (
id INTEGER NOT NULL PRIMARY KEY,
contents TEXT NOT NULL,
created INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);

View File

@ -1,2 +0,0 @@
ALTER TABLE texts
DROP COLUMN highlight;

View File

@ -1,2 +0,0 @@
ALTER TABLE texts
ADD highlight BOOLEAN NOT NULL DEFAULT false;

View File

@ -1,47 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ title }}</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/styles/{{ theme }}.min.css"
/>
<style>
html,
body {
overflow: hidden;
}
html,
body,
pre {
margin: 0;
padding: 0;
}
code {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
max-width: 100%;
max-height: 100%;
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo,
monospace;
}
</style>
</head>
<body>
<pre><code>{{ contents }}</code></pre>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/highlight.min.js"
integrity="sha256-1zu+3BnLYV9LdiY85uXMzii3bdrkelyp37e0ZyTAQh0="
crossorigin="anonymous"
></script>
{{ languages }}
<script>
hljs.initHighlightingOnLoad();
</script>
</body>
</html>

View File

@ -1,447 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/spectre.css/0.5.8/spectre.min.css"
integrity="sha256-J24PZiunX9uL1Sdmbe6YT9kNuV5lfVxj3A6Kij5UP6k="
crossorigin="anonymous"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/spectre.css/0.5.8/spectre-icons.min.css"
integrity="sha256-LxdDS9G94ArUz2UYVPo5FhSeD4owwcBFAQv2Nl1dNUU="
crossorigin="anonymous"
/>
<style>
div[id$="-form"]:not(.active) {
display: none;
}
</style>
<title>filite</title>
</head>
<body>
<nav class="container mb-2 pb-2">
<div class="columns">
<div
class="column col-sm-12 col-md-10 col-lg-8 col-6 col-mx-auto"
>
<ul class="tab tab-block">
<li id="files-tab" class="tab-item">
<a href="#">Files</a>
</li>
<li id="links-tab" class="tab-item active">
<a href="#">Links</a>
</li>
<li id="texts-tab" class="tab-item">
<a href="#">Texts</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container mt-2 pt-2">
<div class="columns">
<div
id="files-form"
class="column col-sm-12 col-md-10 col-lg-8 col-6 col-mx-auto"
>
<div class="form-group">
<label class="form-label" for="files-url">URL</label>
<div class="input-group">
<span class="input-group-addon">/f/</span>
<input
id="files-url"
class="form-input"
type="text"
placeholder="a1b2c3"
required
/>
<button
class="btn btn-primary input-group-btn"
id="files-submit"
disabled
>
<i class="icon icon-upload"></i>
</button>
</div>
<p class="form-input-hint">Press space to randomize</p>
</div>
<div class="form-group">
<label class="form-label" for="files-file">File</label>
<input
class="form-input"
id="files-file"
type="file"
required
/>
</div>
</div>
<div
id="links-form"
class="column col-sm-12 col-md-10 col-lg-8 col-6 col-mx-auto active"
>
<div class="form-group">
<label class="form-label" for="links-url">URL</label>
<div class="input-group">
<span class="input-group-addon">/l/</span>
<input
id="links-url"
class="form-input"
type="text"
placeholder="a1b2c3"
required
/>
<button
class="btn btn-primary input-group-btn"
id="links-submit"
disabled
>
<i class="icon icon-upload"></i>
</button>
</div>
<p class="form-input-hint">Press space to randomize</p>
</div>
<div class="form-group">
<label class="form-label" for="links-forward"
>Forward</label
>
<input
id="links-forward"
class="form-input"
type="url"
placeholder="http://example.com/"
required
/>
</div>
</div>
<div
id="texts-form"
class="column col-sm-12 col-md-10 col-lg-8 col-6 col-mx-auto"
>
<div class="form-group">
<label class="form-label" for="texts-url">URL</label>
<div class="input-group">
<span class="input-group-addon">/t/</span>
<input
id="texts-url"
class="form-input"
type="text"
placeholder="a1b2c3"
required
/>
<button
class="btn btn-primary input-group-btn"
id="texts-submit"
disabled
>
<i class="icon icon-upload"></i>
</button>
</div>
<p class="form-input-hint">Press space to randomize</p>
</div>
<div class="form-group">
<label class="form-label" for="texts-contents"
>Contents</label
>
<textarea
id="texts-contents"
class="form-input"
placeholder="Hello, World!"
required
></textarea>
</div>
<div class="form-group">
<label class="form-switch">
<input id="texts-highlight" type="checkbox" />
<i class="form-icon"></i> Syntax highlighting
</label>
</div>
</div>
</div>
</main>
<div id="modal" class="modal">
<a id="modal-bg" href="#" class="modal-overlay"></a>
<div class="modal-container">
<div class="modal-header">
<div class="modal-title h6">Success</div>
</div>
<div class="modal-body">
<div class="content">
<div class="form-group">
<div class="has-icon-right">
<input
id="modal-input"
type="url"
class="form-input"
/>
<i class="form-icon icon icon-copy"></i>
</div>
<p class="form-input-hint" id="modal-hint">
Click to copy to clipboard
</p>
</div>
</div>
</div>
</div>
</div>
<script>
const tabs = {
files: [
document.querySelector("#files-tab"),
document.querySelector("#files-form"),
],
links: [
document.querySelector("#links-tab"),
document.querySelector("#links-form"),
],
texts: [
document.querySelector("#texts-tab"),
document.querySelector("#texts-form"),
],
};
const inputs = {
files: [
document.querySelector("#files-url"),
document.querySelector("#files-file"),
document.querySelector("#files-submit"),
],
links: [
document.querySelector("#links-url"),
document.querySelector("#links-forward"),
document.querySelector("#links-submit"),
],
texts: [
document.querySelector("#texts-url"),
document.querySelector("#texts-contents"),
document.querySelector("#texts-highlight"),
document.querySelector("#texts-submit"),
],
};
const used = {
files: [],
links: [],
texts: [],
};
let baseUrl = `${location.protocol}//${location.host}${location.pathname}`;
if (!baseUrl.endsWith("/")) {
baseUrl += "/";
}
const modal = {
self: document.querySelector("#modal"),
input: document.querySelector("#modal-input"),
bg: document.querySelector("#modal-bg"),
hint: document.querySelector("#modal-hint"),
};
const openModal = (text) => {
modal.input.value = text;
modal.hint.innerText = "Click to copy to clipboard";
modal.self.classList.add("active");
};
const closeModal = () => {
modal.hint.innerText = "Copied to clipboard";
setTimeout(() => {
modal.self.classList.remove("active");
modal.input.value = "";
}, 1000);
};
modal.input.onclick = (e) => {
e.preventDefault();
modal.input.select();
document.execCommand("copy");
closeModal();
};
modal.bg.onclick = closeModal;
const fetchUsed = () => {
fetch(`${baseUrl}f`)
.then((response) => response.json())
.then((json) => (used.files = json));
fetch(`${baseUrl}l`)
.then((response) => response.json())
.then((json) => (used.links = json));
fetch(`${baseUrl}t`)
.then((response) => response.json())
.then((json) => (used.texts = json));
};
fetchUsed();
const randomUrl = () => {
return Math.floor(Math.random() * 2147483647).toString(36);
};
for (const group in tabs) {
tabs[group][0].onclick = () => {
const active = document.querySelectorAll(".active");
for (const el of active) {
el.classList.remove("active");
}
for (const el of tabs[group]) {
el.classList.add("active");
}
};
}
for (const group in inputs) {
const submitButton = inputs[group][inputs[group].length - 1];
const urlInput = inputs[group][0];
urlInput.addEventListener("input", (e) => {
if (urlInput.value[urlInput.value.length - 1] === " ") {
urlInput.value = randomUrl();
checkValidity();
e.preventDefault();
return;
}
urlInput.value = urlInput.value
.replace(/[^0-9A-Za-z]/g, "")
.toLowerCase();
if (parseInt(urlInput.value, 36) > 2147483647) {
urlInput.setCustomValidity(
"Base 36 integer below or equal to zik0zj"
);
} else {
urlInput.setCustomValidity("");
}
});
const checkValidity = () => {
if (
used[group].some(
(x) => x.id === parseInt(urlInput.value, 36)
)
) {
urlInput.setCustomValidity("ID already in use");
} else {
urlInput.setCustomValidity("");
}
submitButton.disabled = inputs[group].some(
(input) =>
input.validity !== undefined &&
!input.validity.valid
);
};
checkValidity();
for (const input of inputs[group].filter(
(input) =>
input instanceof HTMLInputElement ||
input instanceof HTMLTextAreaElement
)) {
input.addEventListener("input", () => checkValidity());
input.addEventListener("change", () => checkValidity());
}
const clearInputs = () => {
for (const input of inputs[group].filter(
(input) =>
input instanceof HTMLInputElement ||
input instanceof HTMLTextAreaElement
)) {
input.value = "";
}
submitButton.disabled = true;
};
if (group === "files") {
submitButton.addEventListener("click", () => {
const filesFileInput = inputs.files[1];
const file = filesFileInput.files[0];
if (!file) {
alert(new Error("No file selected"));
return;
}
const fd = new FormData();
fd.append("file", file);
const id = urlInput.value;
const url = `${baseUrl}f/${id}`;
let status;
fetch(url, {
method: "PUT",
body: fd,
})
.then((response) => {
status = response.status;
return response.text();
})
.then((text) => {
if (status !== 201) {
throw new Error(text);
} else {
openModal(url);
clearInputs();
fetchUsed();
}
})
.catch((error) => alert(error));
});
} else if (group === "links") {
submitButton.addEventListener("click", () => {
const id = urlInput.value;
const forward = inputs.links[1].value;
const url = `${baseUrl}l/${id}`;
let status;
fetch(url, {
method: "PUT",
body: JSON.stringify({ forward }),
headers: { "Content-Type": "application/json" },
})
.then((response) => {
status = response.status;
return response.text();
})
.then((text) => {
if (status !== 201) {
throw new Error(text);
} else {
openModal(url);
clearInputs();
fetchUsed();
}
})
.catch((error) => alert(error));
});
} else if (group === "texts") {
submitButton.addEventListener("click", () => {
const id = urlInput.value;
const contents = inputs.texts[1].value;
const highlight = inputs.texts[2].checked;
const url = `${baseUrl}t/${id}`;
let status;
fetch(url, {
method: "PUT",
body: JSON.stringify({ contents, highlight }),
headers: { "Content-Type": "application/json" },
})
.then((response) => {
status = response.status;
return response.text();
})
.then((text) => {
if (status !== 201) {
throw new Error(text);
} else {
openModal(url);
clearInputs();
fetchUsed();
}
})
.catch((error) => alert(error));
});
}
}
</script>
</body>
</html>

View File

@ -1,36 +0,0 @@
pub const KEY: &[u8; 32] = include_bytes!(concat!(env!("OUT_DIR"), "/key"));
lazy_static! {
pub static ref EMPTY_HASH: Vec<u8> = crate::setup::hash(b"");
pub static ref POOL: crate::Pool =
crate::setup::create_pool(&CONFIG.database_url, CONFIG.pool_size);
}
#[cfg(feature = "dev")]
lazy_static! {
pub static ref CONFIG: crate::setup::Config = crate::setup::Config::debug();
pub static ref PASSWORD_HASH: Vec<u8> = {
dotenv::dotenv().ok();
let password = crate::get_env!("PASSWD");
crate::setup::hash(password.as_bytes())
};
}
#[cfg(not(feature = "dev"))]
lazy_static! {
pub static ref CONFIG: crate::setup::Config =
crate::setup::init(std::env::args().fold(0, |mode, a| if &a == "init" {
2
} else if &a == "passwd" {
1
} else {
mode
}));
pub static ref PASSWORD_HASH: Vec<u8> = {
let password_path = crate::setup::get_password_path();
std::fs::read(&password_path).unwrap_or_else(|e| {
eprintln!("Can't read password hash from disk: {}", e);
std::process::exit(1);
})
};
}

View File

@ -1,106 +1,9 @@
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate serde;
use std::env;
#[cfg_attr(not(feature = "dev"), macro_use)]
extern crate diesel_migrations;
use actix_identity::{CookieIdentityPolicy, IdentityService};
use actix_web::{web, App, HttpServer};
use diesel::{
r2d2::{self, ConnectionManager},
sqlite::SqliteConnection,
};
use std::process;
pub mod globals;
pub mod models;
pub mod queries;
pub mod routes;
pub mod schema;
pub mod setup;
/// SQLite database connection pool
pub type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
#[cfg(not(feature = "dev"))]
embed_migrations!();
use globals::{CONFIG, KEY};
#[actix_rt::main]
#[tokio::main]
async fn main() {
setup::init_logger();
#[cfg(not(feature = "dev"))]
{
embedded_migrations::run(&globals::POOL.get().unwrap()).unwrap_or_else(|e| {
eprintln!("Can't prepare database: {}", e);
process::exit(1);
});
if env::var_os("FILITE_LOG").is_none() {
env::set_var("FILITE_LOG", "INFO");
}
let port = CONFIG.port;
println!("Listening on port {}", port);
HttpServer::new(move || {
App::new()
.wrap(IdentityService::new(
CookieIdentityPolicy::new(KEY)
.name("filite-auth-cookie")
.secure(true),
))
.wrap(setup::logger_middleware())
.route("/", web::get().to(routes::index))
.route("/logout", web::get().to(routes::logout))
.route("/config", web::get().to(routes::get_config))
.route("/id/{id}", web::get().to(routes::id_to_str))
.service(
web::resource("/f")
.route(web::get().to(routes::files::select))
.route(web::post().to(routes::files::post)),
)
.service(
web::resource("/l")
.route(web::get().to(routes::links::select))
.route(web::post().to(routes::links::post)),
)
.service(
web::resource("/t")
.route(web::get().to(routes::texts::select))
.route(web::post().to(routes::texts::post)),
)
.service(
web::resource("/f/{id}")
.route(web::get().to(routes::files::get))
.route(web::put().to(routes::files::put))
.route(web::delete().to(routes::files::delete)),
)
.service(
web::resource("/l/{id}")
.route(web::get().to(routes::links::get))
.route(web::put().to(routes::links::put))
.route(web::delete().to(routes::links::delete)),
)
.service(
web::resource("/t/{id}")
.route(web::get().to(routes::texts::get))
.route(web::put().to(routes::texts::put))
.route(web::delete().to(routes::texts::delete)),
)
})
.bind(&format!("localhost:{}", port))
.unwrap_or_else(|e| {
eprintln!("Can't bind webserver to specified port: {}", e);
process::exit(1);
})
.run()
.await
.unwrap_or_else(|e| {
eprintln!("Can't start webserver: {}", e);
process::exit(1);
});
env_logger::init_from_env("FILITE_LOG");
}

View File

@ -1,76 +0,0 @@
//! Database models
/// Models from the `files` table
pub mod files {
use crate::schema::files;
/// An entry from the `files` table
#[derive(Queryable, Identifiable, Serialize)]
pub struct File {
/// Primary key, its radix 36 value is used as an url
pub id: i32,
/// Path to the file to serve relative to the static files root
pub filepath: String,
/// Creation date and time as a UNIX timestamp
pub created: i32,
}
/// A new entry to the `files` table
#[derive(Insertable)]
#[table_name = "files"]
pub struct NewFile<'a> {
pub id: i32,
pub filepath: &'a str,
}
}
/// Models from the `links` table
pub mod links {
use crate::schema::links;
/// An entry from the `links` table
#[derive(Queryable, Identifiable, Serialize)]
pub struct Link {
/// Primary key, its radix 36 value is used as an url
pub id: i32,
/// URL this link forwards to
pub forward: String,
/// Creation date and time as a UNIX timestamp
pub created: i32,
}
/// A new entry to the `links` table
#[derive(Insertable)]
#[table_name = "links"]
pub struct NewLink<'a> {
pub id: i32,
pub forward: &'a str,
}
}
/// Models from the `texts` table
pub mod texts {
use crate::schema::texts;
/// An entry from the `texts` table
#[derive(Queryable, Identifiable, Serialize)]
pub struct Text {
/// Primary key, its radix 36 value is used as an url
pub id: i32,
/// Text contents
pub contents: String,
/// Creation date and time as a UNIX timestamp
pub created: i32,
/// Whether to enable code highlighting or not for that text
pub highlight: bool,
}
/// A new entry to the `texts` table
#[derive(Insertable)]
#[table_name = "texts"]
pub struct NewText<'a> {
pub id: i32,
pub contents: &'a str,
pub highlight: bool,
}
}

View File

@ -1,219 +0,0 @@
//! Helper functions for SQL queries
/// Query string for SELECT queries
#[derive(Deserialize)]
pub struct SelectQuery {
/// Left creation bounder timestamp
pub from: Option<i32>,
/// Right creation bounder timestamp
pub to: Option<i32>,
/// Query size limit
pub limit: Option<i64>,
/// Whether to sort the results in ascending order
pub asc: Option<bool>,
}
/// Filters for SELECT queries
pub struct SelectFilters {
/// Creation and update date and time ranges
pub range: (Option<i32>, Option<i32>),
/// Query size limit
pub limit: Option<i64>,
/// Whether to sort the results in ascending order
pub asc: bool,
}
impl From<SelectQuery> for SelectFilters {
fn from(query: SelectQuery) -> Self {
SelectFilters {
range: (query.from, query.to),
limit: query.limit,
asc: query.asc.unwrap_or(false),
}
}
}
/// Code common to all select functions
macro_rules! common_select {
($q:expr, $f:expr) => {
if let Some(from) = $f.range.0 {
$q = $q.filter(created.ge(from));
}
if let Some(to) = $f.range.1 {
$q = $q.filter(created.lt(to));
}
if let Some(limit) = $f.limit {
$q = $q.limit(limit);
}
$q = if $f.asc {
$q.order(created.asc())
} else {
$q.order(created.desc())
};
};
}
/// SELECT a single entry given its id
macro_rules! find {
($n:ident, $t:ty) => {
pub fn find(f_id: i32) -> diesel::result::QueryResult<$t> {
let conn: &SqliteConnection = &crate::globals::POOL.get().unwrap();
$n.find(f_id).first::<$t>(conn)
}
};
}
/// DELETE an entry
macro_rules! delete {
($n:ident, $t:ty) => {
pub fn delete(d_id: i32) -> diesel::result::QueryResult<()> {
let conn: &SqliteConnection = &crate::globals::POOL.get().unwrap();
diesel::delete(&$n.find(d_id).first::<$t>(conn)?).execute(conn)?;
Ok(())
}
};
}
/// Queries affecting the `files` table
pub mod files {
use crate::{
globals::{CONFIG, POOL},
models::files::*,
queries::SelectFilters,
schema::files::{dsl::*, table},
};
use diesel::{
prelude::*,
result::{DatabaseErrorKind, Error, QueryResult},
};
use std::fs;
find!(files, File);
/// SELECT multiple file entries
pub fn select(filters: SelectFilters) -> QueryResult<Vec<File>> {
let conn: &SqliteConnection = &POOL.get().unwrap();
let mut query = files.into_boxed();
common_select!(query, filters);
query.load::<File>(conn)
}
/// Delete an existing file on disk
fn fs_del(fid: i32) -> QueryResult<()> {
let mut path = CONFIG.files_dir.clone();
path.push(match find(fid) {
Ok(f) => f.filepath,
Err(e) => {
return match e {
Error::NotFound => Ok(()),
_ => Err(e),
}
}
});
if !path.exists() {
return Ok(());
}
fs::remove_file(path).map_err(|e| {
Error::DatabaseError(
DatabaseErrorKind::UnableToSendCommand,
Box::new(format!("{}", e)),
)
})
}
/// REPLACE a file entry
pub fn replace(r_id: i32, r_filepath: &str) -> QueryResult<File> {
fs_del(r_id)?;
let conn: &SqliteConnection = &POOL.get().unwrap();
let new_file = NewFile {
id: r_id,
filepath: r_filepath,
};
diesel::replace_into(table)
.values(&new_file)
.execute(conn)?;
find(r_id)
}
/// DELETE an entry
pub fn delete(d_id: i32) -> QueryResult<()> {
fs_del(d_id)?;
let conn: &SqliteConnection = &POOL.get().unwrap();
diesel::delete(&files.find(d_id).first::<File>(conn)?).execute(conn)?;
Ok(())
}
}
/// Queries affecting the `links` table
pub mod links {
use crate::{
globals::POOL,
models::links::*,
queries::SelectFilters,
schema::links::{dsl::*, table},
};
use diesel::{prelude::*, result::QueryResult};
find!(links, Link);
delete!(links, Link);
/// SELECT multiple link entries
pub fn select(filters: SelectFilters) -> QueryResult<Vec<Link>> {
let conn: &SqliteConnection = &POOL.get().unwrap();
let mut query = links.into_boxed();
common_select!(query, filters);
query.load::<Link>(conn)
}
/// REPLACE a link entry
pub fn replace(r_id: i32, r_forward: &str) -> QueryResult<Link> {
let conn: &SqliteConnection = &POOL.get().unwrap();
let new_link = NewLink {
id: r_id,
forward: r_forward,
};
diesel::replace_into(table)
.values(&new_link)
.execute(conn)?;
find(r_id)
}
}
/// Queries affecting the `texts` table
pub mod texts {
use crate::{
globals::POOL,
models::texts::*,
queries::SelectFilters,
schema::texts::{dsl::*, table},
};
use diesel::{prelude::*, result::QueryResult};
find!(texts, Text);
delete!(texts, Text);
/// SELECT multiple text entries
pub fn select(filters: SelectFilters) -> QueryResult<Vec<Text>> {
let conn: &SqliteConnection = &POOL.get().unwrap();
let mut query = texts.into_boxed();
common_select!(query, filters);
query.load::<Text>(conn)
}
/// REPLACE a text entry
pub fn replace(r_id: i32, r_contents: &str, r_highlight: bool) -> QueryResult<Text> {
let conn: &SqliteConnection = &POOL.get().unwrap();
let new_text = NewText {
id: r_id,
contents: r_contents,
highlight: r_highlight,
};
diesel::replace_into(table)
.values(&new_text)
.execute(conn)?;
find(r_id)
}
}

View File

@ -1,565 +0,0 @@
//! Actix route handlers
use crate::{
globals::{CONFIG, EMPTY_HASH, PASSWORD_HASH},
setup,
};
use actix_identity::Identity;
use actix_web::{error::BlockingError, web, Error, HttpRequest, HttpResponse, Responder};
use chrono::{DateTime, NaiveDateTime, Utc};
use diesel;
use serde::Serialize;
use std::convert::Infallible;
#[cfg(feature = "dev")]
use crate::get_env;
#[cfg(feature = "dev")]
use std::{fs, path::PathBuf};
/// Parses an ID
fn parse_id(id: &str) -> Result<i32, HttpResponse> {
match i32::from_str_radix(id, 36) {
Ok(id) => Ok(id),
Err(_) => Err(HttpResponse::BadRequest().body("Invalid ID")),
}
}
/// Authenticates a user
async fn auth(identity: Identity, request: HttpRequest) -> Result<(), HttpResponse> {
if identity.identity().is_some() {
return Ok(());
}
if *PASSWORD_HASH == *EMPTY_HASH {
identity.remember("guest".into());
return Ok(());
}
let header = match request.headers().get("Authorization") {
Some(h) => match h.to_str() {
Ok(h) => h,
Err(_) => return Err(HttpResponse::BadRequest().body("Invalid Authorization header")),
},
None => {
return Err(HttpResponse::Unauthorized()
.header("WWW-Authenticate", "Basic realm=\"filite\"")
.body("Unauthorized"))
}
};
let connection_string = header.replace("Basic ", "");
let (user, password) = match base64::decode(&connection_string) {
Ok(c) => {
let credentials: Vec<Vec<u8>> = c
.splitn(2, |b| b == &b':')
.map(|s| s.to_vec())
.collect::<Vec<Vec<u8>>>();
match credentials.len() {
2 => (credentials[0].clone(), credentials[1].clone()),
_ => return Err(HttpResponse::BadRequest().body("Invalid Authorization header")),
}
}
Err(_) => return Err(HttpResponse::BadRequest().body("Invalid Authorization header")),
};
let infallible_hash = move || -> Result<Vec<u8>, Infallible> { Ok(setup::hash(&password)) };
if web::block(infallible_hash).await.unwrap() == *PASSWORD_HASH {
match String::from_utf8(user.to_vec()) {
Ok(u) => {
identity.remember(u);
Ok(())
}
Err(_) => Err(HttpResponse::BadRequest().body("Invalid Authorization header")),
}
} else {
Err(HttpResponse::Unauthorized()
.header("WWW-Authenticate", "Basic realm=\"filite\"")
.body("Unauthorized"))
}
}
/// Match result from REPLACE queries
fn match_replace_result<T: Serialize>(
result: Result<T, BlockingError<diesel::result::Error>>,
id: i32,
) -> Result<HttpResponse, Error> {
match result {
Ok(_) => Ok(HttpResponse::Created().body(format!("{}", radix_fmt::radix_36(id)))),
Err(_) => Err(HttpResponse::InternalServerError()
.body("Internal server error")
.into()),
}
}
/// Handles error from single GET queries using find
fn match_find_error<T>(error: BlockingError<diesel::result::Error>) -> Result<T, Error> {
match error {
BlockingError::Error(e) => match e {
diesel::result::Error::NotFound => {
Err(HttpResponse::NotFound().body("Not found").into())
}
_ => Err(HttpResponse::InternalServerError()
.body("Internal server error")
.into()),
},
BlockingError::Canceled => Err(HttpResponse::InternalServerError()
.body("Internal server error")
.into()),
}
}
/// Formats a timestamp to the "Last-Modified" header format
fn timestamp_to_last_modified(timestamp: i32) -> String {
let datetime =
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(i64::from(timestamp), 0), Utc);
datetime.format("%a, %d %b %Y %H:%M:%S GMT").to_string()
}
/// Escapes text to be inserted in a HTML element
fn escape_html(text: &str) -> String {
text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
}
/// GET multiple entries
macro_rules! select {
($m:ident) => {
pub async fn select(
request: HttpRequest,
query: actix_web::web::Query<SelectQuery>,
identity: actix_identity::Identity,
) -> Result<actix_web::HttpResponse, actix_web::Error> {
crate::routes::auth(identity, request).await?;
let filters = crate::queries::SelectFilters::from(query.into_inner());
match actix_web::web::block(move || crate::queries::$m::select(filters)).await {
Ok(x) => Ok(actix_web::HttpResponse::Ok().json(x)),
Err(_) => Err(actix_web::HttpResponse::InternalServerError()
.body("Internal server error")
.into()),
}
}
};
}
/// DELETE an entry
macro_rules! delete {
($m:ident) => {
pub async fn delete(
request: HttpRequest,
path: actix_web::web::Path<String>,
identity: actix_identity::Identity,
) -> Result<actix_web::HttpResponse, actix_web::Error> {
crate::routes::auth(identity, request).await?;
let id = crate::routes::parse_id(&path)?;
match actix_web::web::block(move || crate::queries::$m::delete(id)).await {
Ok(()) => Ok(actix_web::HttpResponse::Ok().body("Deleted")),
Err(e) => crate::routes::match_find_error(e),
}
}
};
}
/// Verify if an entry exists
macro_rules! random_id {
($m:ident) => {
use rand::distributions::Distribution;
pub async fn random_id() -> Result<i32, actix_web::Error> {
let mut rng = rand::thread_rng();
let distribution = rand::distributions::Uniform::from(0..i32::max_value());
loop {
let id = distribution.sample(&mut rng);
match actix_web::web::block(move || crate::queries::$m::find(id)).await {
Ok(_) => continue,
Err(e) => match e {
actix_web::error::BlockingError::Error(e) => match e {
diesel::result::Error::NotFound => return Ok(id),
_ => {
return Err(actix_web::HttpResponse::InternalServerError()
.body("Internal server error")
.into())
}
},
actix_web::error::BlockingError::Canceled => {
return Err(actix_web::HttpResponse::InternalServerError()
.body("Internal server error")
.into())
}
},
}
}
}
};
}
#[cfg(feature = "dev")]
lazy_static! {
static ref INDEX_PATH: PathBuf = {
let mut index_path = PathBuf::new();
index_path.push(get_env!("CARGO_MANIFEST_DIR"));
index_path.push("resources");
index_path.push("index.html");
index_path
};
}
#[cfg(not(feature = "dev"))]
static INDEX_CONTENTS: &str = include_str!("../resources/index.html");
static HIGHLIGHT_CONTENTS: &str = include_str!("../resources/highlight.html");
const HIGHLIGHT_LANGUAGE: &str = r#"<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/languages/{{ language }}.min.js"></script>"#;
/// Index page letting users upload via a UI
pub async fn index(request: HttpRequest, identity: Identity) -> impl Responder {
if let Err(response) = auth(identity, request).await {
return response;
}
let contents = {
#[cfg(feature = "dev")]
{
fs::read_to_string(&*INDEX_PATH).expect("Can't read index.html")
}
#[cfg(not(feature = "dev"))]
{
INDEX_CONTENTS.to_owned()
}
};
HttpResponse::Ok()
.header("Content-Type", "text/html")
.body(contents)
}
/// GET the config info
pub async fn get_config(request: HttpRequest, identity: Identity) -> impl Responder {
match auth(identity, request).await {
Ok(_) => HttpResponse::Ok().json(&*CONFIG),
Err(response) => response,
}
}
/// Logout route
pub async fn logout(identity: Identity) -> impl Responder {
if identity.identity().is_some() {
identity.forget();
HttpResponse::Ok().body("Logged out")
} else {
HttpResponse::Unauthorized()
.header("WWW-Authenticate", "Basic realm=\"filite\"")
.body("Unauthorized")
}
}
pub async fn id_to_str(path: web::Path<String>) -> impl Responder {
let id: i32 = match path.parse() {
Ok(id) => id,
Err(_) => return Err(HttpResponse::BadRequest().body("Invalid ID")),
};
Ok(HttpResponse::Ok().body(radix_fmt::radix_36(id).to_string()))
}
pub mod files {
use crate::routes::match_replace_result;
use crate::{
globals::CONFIG,
queries::{self, SelectQuery},
routes::{auth, match_find_error, parse_id},
};
use actix_files::NamedFile;
use actix_identity::Identity;
use actix_multipart::Multipart;
use actix_web::{web, Error, HttpRequest, HttpResponse};
use chrono::Utc;
use futures::StreamExt;
use std::{
fs::{self, File},
io::Write,
path::PathBuf,
};
select!(files);
delete!(files);
random_id!(files);
/// GET a file entry and statically serve it
pub async fn get(path: web::Path<String>) -> Result<NamedFile, Error> {
let id = parse_id(&path)?;
match web::block(move || queries::files::find(id)).await {
Ok(file) => {
let mut path = CONFIG.files_dir.clone();
path.push(file.filepath);
match NamedFile::open(&path) {
Ok(nf) => Ok(nf),
Err(_) => Err(HttpResponse::NotFound().body("Not found").into()),
}
}
Err(e) => match_find_error(e),
}
}
/// Common code for PUT and POST routes
async fn put_post(id: i32, mut body: Multipart) -> Result<HttpResponse, Error> {
let mut path = CONFIG.files_dir.clone();
let mut relative_path = PathBuf::new();
let dir_path = path.clone();
if web::block(move || fs::create_dir_all(dir_path))
.await
.is_err()
{
return Err(HttpResponse::InternalServerError()
.body("Internal server error")
.into());
}
let mut field = match body.next().await {
Some(f) => f?,
None => {
return Err(HttpResponse::BadRequest()
.body("Empty multipart body")
.into())
}
};
let content_disposition = match field.content_disposition() {
Some(cd) => cd,
None => {
return Err(HttpResponse::BadRequest()
.body("Missing content disposition")
.into())
}
};
let filename = match content_disposition.get_filename() {
Some(n) => n,
None => return Err(HttpResponse::BadRequest().body("Missing filename").into()),
};
let filename = format!(
"{}.{}",
radix_fmt::radix_36(Utc::now().timestamp()),
filename
);
path.push(&filename);
relative_path.push(&filename);
let relative_path = match path.to_str() {
Some(rp) => rp.to_owned(),
None => {
return Err(HttpResponse::InternalServerError()
.body("Internal server error")
.into())
}
};
let mut f = match web::block(move || File::create(&path)).await {
Ok(f) => f,
Err(_) => {
return Err(HttpResponse::InternalServerError()
.body("Internal server error")
.into())
}
};
while let Some(chunk) = field.next().await {
let data = match chunk {
Ok(c) => c,
Err(_) => {
return Err(HttpResponse::BadRequest()
.body("Invalid multipart data")
.into())
}
};
f = match web::block(move || match f.write_all(&data) {
Ok(_) => Ok(f),
Err(_) => Err(()),
})
.await
{
Ok(f) => f,
Err(_) => {
return Err(HttpResponse::InternalServerError()
.body("Internal server error")
.into())
}
};
}
match_replace_result(
web::block(move || queries::files::replace(id, &relative_path)).await,
id,
)
}
/// PUT a new file entry
pub async fn put(
request: HttpRequest,
path: web::Path<String>,
body: Multipart,
identity: Identity,
) -> Result<HttpResponse, Error> {
auth(identity, request).await?;
let id = parse_id(&path)?;
put_post(id, body).await
}
/// POST a new file entry using a multipart body
pub async fn post(
request: HttpRequest,
body: Multipart,
identity: Identity,
) -> Result<HttpResponse, Error> {
auth(identity, request).await?;
let id = random_id().await?;
put_post(id, body).await
}
}
pub mod links {
use crate::{
queries::{self, SelectQuery},
routes::{
auth, match_find_error, match_replace_result, parse_id, timestamp_to_last_modified,
},
};
use actix_identity::Identity;
use actix_web::{web, Error, HttpRequest, HttpResponse};
select!(links);
delete!(links);
random_id!(links);
/// GET a link entry and redirect to it
pub async fn get(path: web::Path<String>) -> Result<HttpResponse, Error> {
let id = parse_id(&path)?;
match web::block(move || queries::links::find(id)).await {
Ok(link) => Ok(HttpResponse::Found()
.header("Location", link.forward)
.header("Last-Modified", timestamp_to_last_modified(link.created))
.finish()),
Err(e) => match_find_error(e),
}
}
/// Request body when PUTting links
#[derive(Deserialize)]
pub struct PutPostLink {
pub forward: String,
}
/// PUT a new link entry
pub async fn put(
request: HttpRequest,
path: web::Path<String>,
body: web::Json<PutPostLink>,
identity: Identity,
) -> Result<HttpResponse, Error> {
auth(identity, request).await?;
let id = parse_id(&path)?;
match_replace_result(
web::block(move || queries::links::replace(id, &body.forward)).await,
id,
)
}
/// POST a new link entry
pub async fn post(
request: HttpRequest,
body: web::Json<PutPostLink>,
identity: Identity,
) -> Result<HttpResponse, Error> {
auth(identity, request).await?;
let id = random_id().await?;
match_replace_result(
web::block(move || queries::links::replace(id, &body.forward)).await,
id,
)
}
}
pub mod texts {
use crate::routes::escape_html;
use crate::{
globals::CONFIG,
routes::{HIGHLIGHT_CONTENTS, HIGHLIGHT_LANGUAGE},
};
use crate::{
queries::{self, SelectQuery},
routes::{
auth, match_find_error, match_replace_result, parse_id, timestamp_to_last_modified,
},
};
use actix_identity::Identity;
use actix_web::{web, Error, HttpRequest, HttpResponse};
select!(texts);
delete!(texts);
random_id!(texts);
/// GET a text entry and display it
pub async fn get(path: web::Path<String>) -> Result<HttpResponse, Error> {
let id = parse_id(&path)?;
match web::block(move || queries::texts::find(id)).await {
Ok(text) => {
let last_modified = timestamp_to_last_modified(text.created);
if text.highlight {
let languages: Vec<String> = CONFIG
.highlight
.languages
.iter()
.map(|l| HIGHLIGHT_LANGUAGE.replace("{{ language }}", l))
.collect();
let languages = languages.join("\n");
let contents = HIGHLIGHT_CONTENTS
.replace("{{ title }}", &path)
.replace("{{ theme }}", &CONFIG.highlight.theme)
.replace("{{ contents }}", &escape_html(&text.contents))
.replace("{{ languages }}", &languages);
Ok(HttpResponse::Ok()
.header("Last-Modified", last_modified)
.header("Content-Type", "text/html")
.body(contents))
} else {
Ok(HttpResponse::Ok()
.header("Last-Modified", last_modified)
.body(text.contents))
}
}
Err(e) => match_find_error(e),
}
}
/// Request body when PUTting texts
#[derive(Deserialize)]
pub struct PutPostText {
pub contents: String,
pub highlight: bool,
}
/// PUT a new text entry
pub async fn put(
request: HttpRequest,
path: web::Path<String>,
body: web::Json<PutPostText>,
identity: Identity,
) -> Result<HttpResponse, Error> {
auth(identity, request).await?;
let id = parse_id(&path)?;
match_replace_result(
web::block(move || queries::texts::replace(id, &body.contents, body.highlight)).await,
id,
)
}
/// POST a new text entry
pub async fn post(
request: HttpRequest,
body: web::Json<PutPostText>,
identity: Identity,
) -> Result<HttpResponse, Error> {
auth(identity, request).await?;
let id = random_id().await?;
match_replace_result(
web::block(move || queries::texts::replace(id, &body.contents, body.highlight)).await,
id,
)
}
}

View File

@ -1,26 +0,0 @@
table! {
files (id) {
id -> Integer,
filepath -> Text,
created -> Integer,
}
}
table! {
links (id) {
id -> Integer,
forward -> Text,
created -> Integer,
}
}
table! {
texts (id) {
id -> Integer,
contents -> Text,
created -> Integer,
highlight -> Bool,
}
}
allow_tables_to_appear_in_same_query!(files, links, texts,);

View File

@ -1,323 +0,0 @@
//! Utilities used during the initial setup
use crate::{globals::KEY, Pool};
use actix_web::middleware::Logger;
use diesel::{
r2d2::{self, ConnectionManager},
sqlite::SqliteConnection,
};
use std::{env, path::PathBuf};
#[cfg(not(feature = "dev"))]
use dialoguer::{Confirmation, PasswordInput};
#[cfg(not(feature = "dev"))]
use dirs;
#[cfg(feature = "dev")]
use dotenv;
#[cfg(feature = "dev")]
use std::str::FromStr;
#[cfg(not(feature = "dev"))]
use std::{fs, process};
#[cfg(not(feature = "dev"))]
use toml;
/// Returns a path to the directory storing application data
#[cfg(not(feature = "dev"))]
pub fn get_data_dir() -> PathBuf {
let base_dir = dirs::data_dir().expect("Unable to determine the data directory");
base_dir.join(env!("CARGO_PKG_NAME"))
}
/// Returns a path to the directory storing application config
#[cfg(not(feature = "dev"))]
pub fn get_config_dir() -> PathBuf {
let base_dir = dirs::config_dir().expect("Unable to determine the config directory");
base_dir.join(env!("CARGO_PKG_NAME"))
}
/// Returns a path to the configuration file
#[cfg(not(feature = "dev"))]
fn get_config_path() -> PathBuf {
get_config_dir().join("config.toml")
}
/// Returns a path to the bearer token hash
#[cfg(not(feature = "dev"))]
pub fn get_password_path() -> PathBuf {
get_config_dir().join("passwd")
}
/// Returns the BLAKE2b digest of the input string
pub fn hash(input: &[u8]) -> Vec<u8> {
blake3::keyed_hash(KEY, input).as_bytes().to_vec()
}
/// Returns an environment variable and panic if it isn't found
#[cfg(feature = "dev")]
#[macro_export]
macro_rules! get_env {
($k:literal) => {
std::env::var($k).expect(&format!("Can't find {} environment variable", $k));
};
}
/// Returns a parsed environment variable and panic if it isn't found or is not parsable
#[cfg(feature = "dev")]
macro_rules! parse_env {
($k:literal) => {
get_env!($k).parse().expect(&format!("Invalid {}", $k))
};
}
/// Application configuration
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(not(feature = "dev"), serde(default))]
pub struct Config {
/// Port to listen on
pub port: u16,
/// SQLite database connection url
pub database_url: String,
/// SQLite database connection pool size
pub pool_size: u32,
/// Directory where to store static files
pub files_dir: PathBuf,
/// Highlight.js configuration
pub highlight: HighlightConfig,
}
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(not(feature = "dev"), serde(default))]
pub struct HighlightConfig {
/// Theme to use
pub theme: String,
/// Additional languages to include
pub languages: Vec<String>,
}
#[cfg(not(feature = "dev"))]
impl Default for Config {
fn default() -> Self {
let port = 8080;
let database_url = {
let path = get_data_dir().join("database.db");
path.to_str()
.expect("Can't convert database path to string")
.to_owned()
};
let pool_size = std::cmp::max(1, num_cpus::get() as u32 / 2);
let files_dir = get_data_dir().join("files");
Self {
port,
database_url,
pool_size,
files_dir,
highlight: HighlightConfig::default(),
}
}
}
impl Default for HighlightConfig {
fn default() -> Self {
Self {
theme: "github".to_owned(),
languages: vec!["rust".to_owned()],
}
}
}
impl Config {
/// Deserialize the config file
#[cfg(not(feature = "dev"))]
pub fn read_file() -> Result<Self, &'static str> {
let path = get_config_path();
let contents = if let Ok(contents) = fs::read_to_string(&path) {
contents
} else {
return Err("Can't read config file.");
};
let result = toml::from_str(&contents);
if result.is_err() {
return Err("Invalid config file.");
}
let mut result: Config = result.unwrap();
if result.files_dir.is_absolute() {
if fs::create_dir_all(&result.files_dir).is_err() {
return Err("Can't create files_dir.");
}
result.files_dir = match result.files_dir.canonicalize() {
Ok(path) => path,
Err(_) => return Err("Invalid files_dir."),
}
} else {
let files_dir = get_data_dir().join(&result.files_dir);
if fs::create_dir_all(&files_dir).is_err() {
return Err("Can't create files_dir.");
}
result.files_dir = match files_dir.canonicalize() {
Ok(path) => path,
Err(_) => return Err("Invalid files_dir."),
}
}
Ok(result)
}
/// Serialize the config file
#[cfg(not(feature = "dev"))]
pub fn write_file(&self) -> Result<(), &'static str> {
let path = get_config_path();
let contents = toml::to_string(&self).expect("Can't serialize config.");
match fs::write(&path, &contents) {
Ok(_) => Ok(()),
Err(_) => Err("Can't write config file."),
}
}
/// Creates a config from environment variables
#[cfg(feature = "dev")]
pub fn debug() -> Self {
dotenv::dotenv().ok();
let port = parse_env!("PORT");
let database_url = get_env!("DATABASE_URL");
let pool_size = parse_env!("POOL_SIZE");
let files_dir = {
let files_dir = get_env!("FILES_DIR");
let path = PathBuf::from_str(&files_dir).expect("Can't convert files dir to path");
if path.is_absolute() {
path.canonicalize().expect("Invalid FILES_DIR")
} else {
let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR");
let mut cargo_manifest_dir = PathBuf::from_str(cargo_manifest_dir)
.expect("Can't convert cargo manifest dir to path");
cargo_manifest_dir.push(&path);
cargo_manifest_dir
.canonicalize()
.expect("Invalid FILES_DIR")
}
};
Self {
port,
database_url,
pool_size,
files_dir,
highlight: HighlightConfig::default(),
}
}
}
/// Creates a SQLite database connection pool
pub fn create_pool(url: &str, size: u32) -> Pool {
let manager = ConnectionManager::<SqliteConnection>::new(url);
r2d2::Pool::builder()
.max_size(size)
.build(manager)
.expect("Can't create pool")
}
/// Initializes the logger
pub fn init_logger() {
if cfg!(feature = "dev") && env::var_os("RUST_LOG").is_none() {
env::set_var("RUST_LOG", "actix_web=debug");
} else if !cfg!(feature = "dev") {
env::set_var("RUST_LOG", "actix_web=info");
}
env_logger::init();
}
/// Returns the logger middleware
pub fn logger_middleware() -> Logger {
#[cfg(feature = "dev")]
{
dotenv::dotenv().ok();
if let Ok(format) = env::var("LOG_FORMAT") {
Logger::new(&format)
} else {
Logger::default()
}
}
#[cfg(not(feature = "dev"))]
{
Logger::default()
}
}
/// Performs the initial setup
// mode: 0 = normal, 1 = reset password, 2 = reset everything
#[cfg(not(feature = "dev"))]
pub fn init(mode: u8) -> Config {
fs::create_dir_all(get_config_dir()).unwrap_or_else(|e| {
eprintln!("Can't create config directory: {}.", e);
process::exit(1);
});
let password_path = get_password_path();
if mode > 0 {
let mut password;
loop {
password = PasswordInput::new()
.with_prompt("Enter password")
.with_confirmation("Confirm password", "Mismatched passwords")
.allow_empty_password(true)
.interact()
.unwrap_or_else(|e| {
eprintln!("Can't read password: {}", e);
process::exit(1);
});
if !password.is_empty() {
break;
}
let keep_empty = Confirmation::new()
.with_text("Are you sure you want to leave an empty password? This will disable authentication.")
.default(false)
.interact()
.unwrap_or_else(|e| {
eprintln!("Can't read password: {}", e);
process::exit(1);
});
if keep_empty {
break;
}
}
let password_hash = hash(password.as_bytes());
fs::write(&password_path, password_hash.as_slice()).unwrap_or_else(|e| {
eprintln!("Can't write password: {}", e);
process::exit(1);
});
} else if !get_password_path().exists() {
eprintln!("No password file found. Try running `filite init` or `filite passwd`.");
process::exit(1);
}
let config_path = get_config_path();
if mode > 1 {
println!("Generating config file at {}", config_path.display());
let config = Config::default();
config.write_file().unwrap_or_else(|e| {
eprintln!("Can't write config file: {}", e);
process::exit(1);
});
} else if !config_path.exists() {
eprintln!("No config file found. Try running `filite init`.");
process::exit(1);
}
if mode > 0 {
process::exit(0);
}
Config::read_file().unwrap_or_else(|e| {
eprintln!("{}", e);
process::exit(1);
})
}