mirror of https://github.com/raftario/filite.git
A fresh start
This commit is contained in:
parent
3104abfd25
commit
54c6b33eb3
11
.env.example
11
.env.example
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
# Rust build artifacts
|
||||
target/
|
||||
**/*.rs.bk
|
||||
|
||||
# User specific files
|
||||
.env
|
||||
**/*.log
|
||||
|
||||
# JetBrains settings
|
||||
.vscode/
|
||||
.idea/
|
||||
|
|
File diff suppressed because it is too large
Load Diff
39
Cargo.toml
39
Cargo.toml
|
@ -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 = []
|
||||
|
|
|
@ -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)
|
||||
|
|
15
build.rs
15
build.rs
|
@ -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();
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
[print_schema]
|
||||
file = "src/schema.rs"
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE files
|
|
@ -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'))
|
||||
);
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE links
|
|
@ -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'))
|
||||
);
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE texts
|
|
@ -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'))
|
||||
);
|
|
@ -1,2 +0,0 @@
|
|||
ALTER TABLE texts
|
||||
DROP COLUMN highlight;
|
|
@ -1,2 +0,0 @@
|
|||
ALTER TABLE texts
|
||||
ADD highlight BOOLEAN NOT NULL DEFAULT false;
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||
})
|
||||
};
|
||||
}
|
107
src/main.rs
107
src/main.rs
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
219
src/queries.rs
219
src/queries.rs
|
@ -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)
|
||||
}
|
||||
}
|
565
src/routes.rs
565
src/routes.rs
|
@ -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("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
}
|
||||
|
||||
/// 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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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,);
|
323
src/setup.rs
323
src/setup.rs
|
@ -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);
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue