Compare commits
2 Commits
95c867eb38
...
0a469ae367
Author | SHA1 | Date |
---|---|---|
neri | 0a469ae367 | |
neri | 4cd3cead88 |
|
@ -424,7 +424,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "datatrash"
|
name = "datatrash"
|
||||||
version = "2.1.1"
|
version = "2.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-files",
|
"actix-files",
|
||||||
"actix-governor",
|
"actix-governor",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "datatrash"
|
name = "datatrash"
|
||||||
version = "2.1.1"
|
version = "2.2.0"
|
||||||
authors = ["neri"]
|
authors = ["neri"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
A file and text uploading service with configurable time limit
|
A file and text uploading service with configurable time limit
|
||||||
|
|
||||||
![Application screenshot](./screenshot.png)
|
![Application screenshot showing the web form for file or text upload](./screenshot.png)
|
||||||
|
|
||||||
## running
|
## running
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ To run the software directly, use the compiling instructions below.
|
||||||
| RATE_LIMIT_PROXIED | false | whether rate limit should read x-forwarded-for |
|
| RATE_LIMIT_PROXIED | false | whether rate limit should read x-forwarded-for |
|
||||||
| RATE_LIMIT_REPLENISH_SECONDS | 60 | seconds to wait between requests |
|
| RATE_LIMIT_REPLENISH_SECONDS | 60 | seconds to wait between requests |
|
||||||
| RATE_LIMIT_BURST | 480 | allowed request burst |
|
| RATE_LIMIT_BURST | 480 | allowed request burst |
|
||||||
|
| ABUSE_MAIL | | email address to report abuse to |
|
||||||
|
|
||||||
### Database configuration
|
### Database configuration
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="mailto:{abusemail}?subject=Inhaltsmeldung%20datatrash&body=Dies%20ist%20eine%20Inhaltsmeldung%20f%C3%BCr%20eine%20Datei%20auf%20deiner%20datatrash-Instanz.%0ADie%20illegale%20Datei%20ist%20aktuell%20unter%20folgender%20URL%20abrufbar%3A%0A%0A{url}"
|
||||||
|
>
|
||||||
|
inhalt melden
|
||||||
|
</a>
|
|
@ -16,6 +16,7 @@ pub struct Config {
|
||||||
pub proxied: bool,
|
pub proxied: bool,
|
||||||
pub rate_limit_replenish_seconds: u64,
|
pub rate_limit_replenish_seconds: u64,
|
||||||
pub rate_limit_burst: u32,
|
pub rate_limit_burst: u32,
|
||||||
|
pub abuse_mail: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -53,6 +54,7 @@ pub async fn from_env() -> Config {
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|burst| burst.parse().ok())
|
.and_then(|burst| burst.parse().ok())
|
||||||
.unwrap_or(480);
|
.unwrap_or(480);
|
||||||
|
let abuse_mail = env::var("ABUSE_MAIL").ok();
|
||||||
|
|
||||||
Config {
|
Config {
|
||||||
static_dir,
|
static_dir,
|
||||||
|
@ -63,6 +65,7 @@ pub async fn from_env() -> Config {
|
||||||
proxied,
|
proxied,
|
||||||
rate_limit_replenish_seconds,
|
rate_limit_replenish_seconds,
|
||||||
rate_limit_burst,
|
rate_limit_burst,
|
||||||
|
abuse_mail,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,12 +15,8 @@ use sqlx::postgres::PgPool;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::{config::Config, deleter, mime_relations};
|
use crate::{config::Config, deleter, mime_relations, template};
|
||||||
|
|
||||||
const TEXT_VIEW_HTML: &str = include_str!("../template/text-view.html");
|
|
||||||
const URL_VIEW_HTML: &str = include_str!("../template/url-view.html");
|
|
||||||
|
|
||||||
const TEXT_VIEW_SIZE_LIMIT: u64 = 512 * 1024; // 512KiB
|
const TEXT_VIEW_SIZE_LIMIT: u64 = 512 * 1024; // 512KiB
|
||||||
|
|
||||||
|
@ -43,7 +39,7 @@ pub async fn download(
|
||||||
let mut response = match get_view_type(&req, &mime, &path, delete).await {
|
let mut response = match get_view_type(&req, &mime, &path, delete).await {
|
||||||
ViewType::Raw => build_file_response(false, &file_name, path, mime, &req),
|
ViewType::Raw => build_file_response(false, &file_name, path, mime, &req),
|
||||||
ViewType::Download => build_file_response(true, &file_name, path, mime, &req),
|
ViewType::Download => build_file_response(true, &file_name, path, mime, &req),
|
||||||
ViewType::Html => build_html_response(&path).await,
|
ViewType::Html => build_html_response(&path, &config, &req).await,
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
insert_cache_headers(&mut response, valid_till);
|
insert_cache_headers(&mut response, valid_till);
|
||||||
|
@ -121,23 +117,19 @@ async fn get_file_size(file_path: &Path) -> u64 {
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn build_html_response(path: &Path) -> Result<HttpResponse, Error> {
|
async fn build_html_response(
|
||||||
|
path: &Path,
|
||||||
|
config: &Config,
|
||||||
|
req: &HttpRequest,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
let content = fs::read_to_string(path).await.map_err(|file_err| {
|
let content = fs::read_to_string(path).await.map_err(|file_err| {
|
||||||
log::error!("file could not be read {:?}", file_err);
|
log::error!("file could not be read {:?}", file_err);
|
||||||
error::ErrorInternalServerError("this file should be here but could not be found")
|
error::ErrorInternalServerError("this file should be here but could not be found")
|
||||||
})?;
|
})?;
|
||||||
let encoded = htmlescape::encode_minimal(&content);
|
let html_view = template::build_html_view_template(&content, req, config);
|
||||||
let html = if !content.trim().contains(['\n', '\r']) && Url::from_str(content.trim()).is_ok() {
|
|
||||||
let attribute_encoded = htmlescape::encode_attribute(&content);
|
|
||||||
URL_VIEW_HTML
|
|
||||||
.replace("{link_content}", &encoded)
|
|
||||||
.replace("{link_attribute}", &attribute_encoded)
|
|
||||||
} else {
|
|
||||||
TEXT_VIEW_HTML.replace("{text}", &encoded)
|
|
||||||
};
|
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
.content_type(TEXT_HTML.to_string())
|
.content_type(TEXT_HTML.to_string())
|
||||||
.body(html))
|
.body(html_view))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_file_response(
|
fn build_file_response(
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use std::{cmp, io::ErrorKind};
|
use std::{cmp, io::ErrorKind, str::FromStr};
|
||||||
|
|
||||||
use actix_web::HttpRequest;
|
use actix_web::HttpRequest;
|
||||||
use time::Duration;
|
use time::Duration;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
||||||
|
@ -11,6 +12,53 @@ const AUTH_HIDE_JS: &str = include_str!("../template/auth-hide.js");
|
||||||
const AUTH_SNIPPET_HTML: &str = include_str!("../snippet/auth.html.snippet");
|
const AUTH_SNIPPET_HTML: &str = include_str!("../snippet/auth.html.snippet");
|
||||||
const MAX_SIZE_SNIPPET_HTML: &str = include_str!("../snippet/max_size.html.snippet");
|
const MAX_SIZE_SNIPPET_HTML: &str = include_str!("../snippet/max_size.html.snippet");
|
||||||
|
|
||||||
|
const ABUSE_SNIPPET_HTML: &str = include_str!("../snippet/abuse.html.snippet");
|
||||||
|
|
||||||
|
const UPLOAD_HTML: &str = include_str!("../template/upload.html");
|
||||||
|
const UPLOAD_SHORT_HTML: &str = include_str!("../template/upload-short.html");
|
||||||
|
|
||||||
|
const TEXT_VIEW_HTML: &str = include_str!("../template/text-view.html");
|
||||||
|
const URL_VIEW_HTML: &str = include_str!("../template/url-view.html");
|
||||||
|
|
||||||
|
pub fn build_uploaded_html(
|
||||||
|
req: &HttpRequest,
|
||||||
|
id: &str,
|
||||||
|
name: Option<&str>,
|
||||||
|
config: &Config,
|
||||||
|
) -> String {
|
||||||
|
let upload_html = if name.is_some() {
|
||||||
|
UPLOAD_SHORT_HTML
|
||||||
|
.replace("{link}", &get_file_url(req, id, name))
|
||||||
|
.replace("{shortlink}", &get_file_url(req, id, None))
|
||||||
|
} else {
|
||||||
|
UPLOAD_HTML.replace("{link}", &get_file_url(req, id, name))
|
||||||
|
};
|
||||||
|
insert_abuse_template(upload_html, None, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_file_url(req: &HttpRequest, id: &str, name: Option<&str>) -> String {
|
||||||
|
let host = get_host_url(req);
|
||||||
|
if let Some(name) = name {
|
||||||
|
let encoded_name = urlencoding::encode(name);
|
||||||
|
format!("{host}/{id}/{encoded_name}")
|
||||||
|
} else {
|
||||||
|
format!("{host}/{id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_html_view_template(content: &str, req: &HttpRequest, config: &Config) -> String {
|
||||||
|
let encoded = htmlescape::encode_minimal(content);
|
||||||
|
let html = if !content.trim().contains(['\n', '\r']) && Url::from_str(content.trim()).is_ok() {
|
||||||
|
let attribute_encoded = htmlescape::encode_attribute(content);
|
||||||
|
URL_VIEW_HTML
|
||||||
|
.replace("{link_content}", &encoded)
|
||||||
|
.replace("{link_attribute}", &attribute_encoded)
|
||||||
|
} else {
|
||||||
|
TEXT_VIEW_HTML.replace("{text}", &encoded)
|
||||||
|
};
|
||||||
|
insert_abuse_template(html, Some(req), config)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn write_prefillable_templates(config: &Config) {
|
pub async fn write_prefillable_templates(config: &Config) {
|
||||||
let index_path = config.static_dir.join("index.html");
|
let index_path = config.static_dir.join("index.html");
|
||||||
fs::write(index_path, build_index_html(config))
|
fs::write(index_path, build_index_html(config))
|
||||||
|
@ -47,6 +95,7 @@ fn build_index_html(config: &Config) -> String {
|
||||||
} else {
|
} else {
|
||||||
html = html.replace("{auth_snippet}", "");
|
html = html.replace("{auth_snippet}", "");
|
||||||
}
|
}
|
||||||
|
html = insert_abuse_template(html, None, config);
|
||||||
if let Some(max_file_size) = config.max_file_size {
|
if let Some(max_file_size) = config.max_file_size {
|
||||||
html = html
|
html = html
|
||||||
.replace("{max_size_snippet}", MAX_SIZE_SNIPPET_HTML.trim_end())
|
.replace("{max_size_snippet}", MAX_SIZE_SNIPPET_HTML.trim_end())
|
||||||
|
@ -57,6 +106,22 @@ fn build_index_html(config: &Config) -> String {
|
||||||
html
|
html
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_abuse_template(html: String, req: Option<&HttpRequest>, config: &Config) -> String {
|
||||||
|
if let Some(abuse_mail) = &config.abuse_mail {
|
||||||
|
let url = if let Some(req) = req {
|
||||||
|
format!("{host}{path}", host = get_host_url(req), path = req.uri())
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
// urlencoding::encode(data)
|
||||||
|
html.replace("{abuse}", ABUSE_SNIPPET_HTML.trim_end())
|
||||||
|
.replace("{abusemail}", abuse_mail)
|
||||||
|
.replace("{url}", &urlencoding::encode(&url))
|
||||||
|
} else {
|
||||||
|
html.replace("{abuse}", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn render_file_size(size: u64) -> String {
|
fn render_file_size(size: u64) -> String {
|
||||||
let magnitude = cmp::min((size as f64).log(1024.0) as u32, 5);
|
let magnitude = cmp::min((size as f64).log(1024.0) as u32, 5);
|
||||||
let prefix = ["", "ki", "Mi", "Gi", "Ti", "Pi"][magnitude as usize];
|
let prefix = ["", "ki", "Mi", "Gi", "Ti", "Pi"][magnitude as usize];
|
||||||
|
|
|
@ -13,9 +13,6 @@ use std::path::PathBuf;
|
||||||
use tokio::fs::{self, OpenOptions};
|
use tokio::fs::{self, OpenOptions};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
|
|
||||||
const UPLOAD_HTML: &str = include_str!("../template/upload.html");
|
|
||||||
const UPLOAD_SHORT_HTML: &str = include_str!("../template/upload-short.html");
|
|
||||||
|
|
||||||
const ID_CHARS: &[char] = &[
|
const ID_CHARS: &[char] = &[
|
||||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u',
|
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u',
|
||||||
'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||||
|
@ -63,7 +60,7 @@ pub async fn upload(
|
||||||
expiry_watch_sender.send(()).await.unwrap();
|
expiry_watch_sender.send(()).await.unwrap();
|
||||||
|
|
||||||
let redirect = get_redirect_url(&file_id, upload_config.original_name.as_deref());
|
let redirect = get_redirect_url(&file_id, upload_config.original_name.as_deref());
|
||||||
let url = get_file_url(&req, &file_id, upload_config.original_name.as_deref());
|
let url = template::get_file_url(&req, &file_id, upload_config.original_name.as_deref());
|
||||||
Ok(HttpResponse::SeeOther()
|
Ok(HttpResponse::SeeOther()
|
||||||
.insert_header((LOCATION, redirect))
|
.insert_header((LOCATION, redirect))
|
||||||
.body(format!("{url}\n")))
|
.body(format!("{url}\n")))
|
||||||
|
@ -125,16 +122,6 @@ fn gen_file_id() -> String {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_file_url(req: &HttpRequest, id: &str, name: Option<&str>) -> String {
|
|
||||||
let host = template::get_host_url(req);
|
|
||||||
if let Some(name) = name {
|
|
||||||
let encoded_name = urlencoding::encode(name);
|
|
||||||
format!("{host}/{id}/{encoded_name}")
|
|
||||||
} else {
|
|
||||||
format!("{host}/{id}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_redirect_url(id: &str, name: Option<&str>) -> String {
|
fn get_redirect_url(id: &str, name: Option<&str>) -> String {
|
||||||
if let Some(name) = name {
|
if let Some(name) = name {
|
||||||
let encoded_name = urlencoding::encode(name);
|
let encoded_name = urlencoding::encode(name);
|
||||||
|
@ -144,7 +131,7 @@ fn get_redirect_url(id: &str, name: Option<&str>) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn uploaded(req: HttpRequest) -> Result<HttpResponse, Error> {
|
pub async fn uploaded(req: HttpRequest, config: web::Data<Config>) -> Result<HttpResponse, Error> {
|
||||||
let id = req.match_info().query("id");
|
let id = req.match_info().query("id");
|
||||||
let name = req
|
let name = req
|
||||||
.match_info()
|
.match_info()
|
||||||
|
@ -152,14 +139,8 @@ pub async fn uploaded(req: HttpRequest) -> Result<HttpResponse, Error> {
|
||||||
.map(urlencoding::decode)
|
.map(urlencoding::decode)
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(|_| error::ErrorBadRequest("name is invalid utf-8"))?;
|
.map_err(|_| error::ErrorBadRequest("name is invalid utf-8"))?;
|
||||||
let upload_html = if name.is_some() {
|
let uploaded_html = template::build_uploaded_html(&req, id, name.as_deref(), &config);
|
||||||
UPLOAD_SHORT_HTML
|
|
||||||
.replace("{link}", &get_file_url(&req, id, name.as_deref()))
|
|
||||||
.replace("{shortlink}", &get_file_url(&req, id, None))
|
|
||||||
} else {
|
|
||||||
UPLOAD_HTML.replace("{link}", &get_file_url(&req, id, name.as_deref()))
|
|
||||||
};
|
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
.content_type("text/html")
|
.content_type("text/html")
|
||||||
.body(upload_html))
|
.body(uploaded_html))
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ h1 + textarea {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type='checkbox'] {
|
input[type="checkbox"] {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
margin-bottom: 1.5em;
|
margin-bottom: 1.5em;
|
||||||
}
|
}
|
||||||
|
@ -167,16 +167,16 @@ footer {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo {
|
footer > * {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
color: var(--fg-light);
|
color: var(--fg-light);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo:visited {
|
footer > *:visited {
|
||||||
color: var(--fg-light);
|
color: var(--fg-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo:hover {
|
footer > *:hover {
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,8 +59,8 @@ authentifizieren
|
||||||
</details>
|
</details>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
|
{abuse}
|
||||||
<a
|
<a
|
||||||
class="repo"
|
|
||||||
href="https://git.ctdo.de/neri/datatrash"
|
href="https://git.ctdo.de/neri/datatrash"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
<a class="button" href="?raw">roh anzeigen</a>
|
<a class="button" href="?raw">roh anzeigen</a>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
|
{abuse}
|
||||||
<a
|
<a
|
||||||
class="repo"
|
|
||||||
href="https://git.ctdo.de/neri/datatrash"
|
href="https://git.ctdo.de/neri/datatrash"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|
|
@ -19,8 +19,8 @@
|
||||||
</button>
|
</button>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
|
{abuse}
|
||||||
<a
|
<a
|
||||||
class="repo"
|
|
||||||
href="https://git.ctdo.de/neri/datatrash"
|
href="https://git.ctdo.de/neri/datatrash"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|
|
@ -15,8 +15,8 @@
|
||||||
</button>
|
</button>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
|
{abuse}
|
||||||
<a
|
<a
|
||||||
class="repo"
|
|
||||||
href="https://git.ctdo.de/neri/datatrash"
|
href="https://git.ctdo.de/neri/datatrash"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|
|
@ -27,8 +27,8 @@
|
||||||
<a class="button" href="?raw">roh anzeigen</a>
|
<a class="button" href="?raw">roh anzeigen</a>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
|
{abuse}
|
||||||
<a
|
<a
|
||||||
class="repo"
|
|
||||||
href="https://git.ctdo.de/neri/datatrash"
|
href="https://git.ctdo.de/neri/datatrash"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|
Loading…
Reference in New Issue