Compare commits
26 Commits
Author | SHA1 | Date |
---|---|---|
blek | a685f5db84 | |
blek | 72abd4d5bd | |
blek | 57ea39ceda | |
blek | 575f7d31e8 | |
blek | 79ef0634f9 | |
blek | aca37bc52b | |
blek | 164af11b8b | |
blek | 9ccd3d6212 | |
blek | bae978d1ec | |
blek | 7b607c5ba6 | |
blek | 6120ce7a30 | |
blek | 6302d11acd | |
blek | b1463519d7 | |
blek | a0c6c27e8c | |
blek | 12614078ad | |
blek | 99807b9722 | |
blek | 32375127a9 | |
blek | 8d5d739568 | |
blek | b9f0d80dc3 | |
blek | d09f88f7fa | |
blek | b7b303afb3 | |
blek | ac393b2b7b | |
blek | 0d02c91626 | |
blek | a6d69c319e | |
blek | 8a41d4bef2 | |
blek | b0cc93c488 |
|
@ -0,0 +1,33 @@
|
||||||
|
# Deploying a production instance
|
||||||
|
Hi fellow sysadmins!
|
||||||
|
First of all, I want to thank you for using my piece of software.
|
||||||
|
The instructions can be found below
|
||||||
|
|
||||||
|
## Deploying a basic instance
|
||||||
|
To deploy a basic instance for general public use, follow these simple steps:
|
||||||
|
1. Clone this repo
|
||||||
|
2. Copy `docker-compose.prod.yml` to `docker-compose.yml` and edit it to fit your environment
|
||||||
|
3. Now, there are a few config files that need to be edited by you: `.env`, `filed/.env` and `janitord/.env`. Each directory contains an `.env.example`, and the configuration is pretty straightforward. However, if you are lost check this out: [filed config](#filed-configuration), [janitord config](#janitord-configuration).
|
||||||
|
4. Configure fileD using `filed/config/filed.toml`. The example is in the same folder. Example contains a lot of self-documenting comments, so it should be pretty simple too.
|
||||||
|
5. Set `REDIS_PASS` to a secure long string. Not exactly required, but this is something you would want to do
|
||||||
|
6. Create and start containers with `docker-compose up -d`
|
||||||
|
7. Route your top level reverse proxy to the `caddy` service or to the port that you opened via the docker compose file.
|
||||||
|
|
||||||
|
## More extensive configuration
|
||||||
|
Well, generally, time-wise, it is not really a good idea to create a custom services configuration.
|
||||||
|
However, I will guide you through the basic minimal configuration.
|
||||||
|
|
||||||
|
Basically, the most minimal blek!File is a fileD service connected to a redis database.
|
||||||
|
I think that if you are clinically insance, you can set these up as a systemd services or a `screen`ed program.
|
||||||
|
|
||||||
|
However, its not really recommended to run this without janitorD as unused files will just clog up your filesystem.
|
||||||
|
The two requirements for janitorD are to have access to the fileD's usercontent directory and the Redis database.
|
||||||
|
The default docker configuration mounts `/opt/user_content` to the same volume for both fileD and janitorD.
|
||||||
|
|
||||||
|
## FileD configuration
|
||||||
|
Unless you are running in some kind of super customized docker compose environment, just copying the `.env.example` to `.env` should be enough to get it to run.
|
||||||
|
|
||||||
|
Don't forget to set the `REDIS_PASS` to the same value across all services
|
||||||
|
|
||||||
|
## JanitorD configuration
|
||||||
|
Same as [filed config](#filed-configuration), don't forget to set `REDIS_PASS` to a valid value
|
22
README.md
22
README.md
|
@ -1,10 +1,3 @@
|
||||||
| ⚠️ This is in a rather early stage of development and shouldn't be deployed |
|
|
||||||
| --------------------------------------------------------------------------- |
|
|
||||||
|
|
||||||
Even though this project is mature enough to be deployed in a public instance,
|
|
||||||
this is highly discouraged.
|
|
||||||
However, if you do this, be prepared for [DOS](https://en.wikipedia.org/wiki/Denial-of-service_attack) issues and API changes.
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<h1 align='center'>
|
<h1 align='center'>
|
||||||
<img src="./filed/static/android-chrome-192x192.png"/>
|
<img src="./filed/static/android-chrome-192x192.png"/>
|
||||||
|
@ -16,6 +9,21 @@ blek! File is a free service that would help you with file sharing.
|
||||||
|
|
||||||
The principle is very simple: you upload a file, then download it from another device. The file will be deleted after 1 download or 30 minutes.
|
The principle is very simple: you upload a file, then download it from another device. The file will be deleted after 1 download or 30 minutes.
|
||||||
|
|
||||||
|
## Public instances
|
||||||
|
List of official instances
|
||||||
|
|
||||||
|
| Name | Administrator | URL |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 🌠 blek! File | b1ek <me@blek.codes> | [https://file.blek.codes](file.blek.codes) |
|
||||||
|
|
||||||
|
To add your instance in this list, fork and open a PR.
|
||||||
|
|
||||||
|
To qualify, your instance must be having:
|
||||||
|
1. Uploads without a password turned on
|
||||||
|
2. Have proper ToS
|
||||||
|
3. Come up with a unique name
|
||||||
|
4. Have a public administrator email
|
||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
This software is released under GPL3 license, a copyleft license that protects users' freedom by ensuring that all future copies of this software are open source as well.
|
This software is released under GPL3 license, a copyleft license that protects users' freedom by ensuring that all future copies of this software are open source as well.
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ services:
|
||||||
networks:
|
networks:
|
||||||
bfile:
|
bfile:
|
||||||
volumes:
|
volumes:
|
||||||
|
- './.git:/opt/code/.git'
|
||||||
- './filed:/opt/code'
|
- './filed:/opt/code'
|
||||||
- './filed/config:/etc/filed'
|
- './filed/config:/etc/filed'
|
||||||
- '/opt/code/target'
|
- '/opt/code/target'
|
||||||
|
|
|
@ -2,8 +2,8 @@ version: '3.7'
|
||||||
services:
|
services:
|
||||||
filed:
|
filed:
|
||||||
build:
|
build:
|
||||||
context: filed
|
context: .
|
||||||
dockerfile: Dockerfile.prod
|
dockerfile: filed/Dockerfile.prod
|
||||||
networks:
|
networks:
|
||||||
bfile:
|
bfile:
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -358,7 +358,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filed"
|
name = "filed"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"askama",
|
"askama",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "filed"
|
name = "filed"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
|
@ -2,16 +2,17 @@
|
||||||
FROM rust:alpine as builder
|
FROM rust:alpine as builder
|
||||||
|
|
||||||
WORKDIR /opt/build
|
WORKDIR /opt/build
|
||||||
COPY . .
|
COPY filed .
|
||||||
|
COPY ./.git ./.git
|
||||||
|
|
||||||
RUN apk add --no-cache musl-dev upx nodejs yarn && \
|
RUN apk add --no-cache git musl-dev upx nodejs yarn && \
|
||||||
yarn global add uglify-js
|
yarn global add uglify-js@3.17.4
|
||||||
|
|
||||||
RUN cargo b -r
|
RUN cargo b -r
|
||||||
RUN strip target/release/filed && upx --best target/release/filed
|
RUN strip target/release/filed && upx --best target/release/filed
|
||||||
|
|
||||||
# --- deploy ---
|
# --- deploy ---
|
||||||
FROM busybox:musl
|
FROM alpine:3.17
|
||||||
|
|
||||||
RUN mkdir /config
|
RUN mkdir /config
|
||||||
WORKDIR /config
|
WORKDIR /config
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
use std::{fs, path::PathBuf, ffi::OsStr, process::Command};
|
use std::{fs, path::PathBuf, ffi::OsStr, process::Command, error::Error};
|
||||||
|
|
||||||
use css_minify::optimizations::{Minifier, Level};
|
use css_minify::optimizations::{Minifier, Level};
|
||||||
|
|
||||||
|
@ -17,6 +17,19 @@ fn extfilter(valid: String, x: Option<&OsStr>) -> bool {
|
||||||
ext == valid
|
ext == valid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn system(cmd: &str, args: &[&str]) -> Result<String, Box<dyn Error>> {
|
||||||
|
let out = Command::new(cmd)
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
?;
|
||||||
|
|
||||||
|
if out.stderr.len() != 0 {
|
||||||
|
panic!("Got this while running {cmd} with \"{}\": {}", args.join(" "), String::from_utf8(out.stderr).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(String::from_utf8(out.stdout)?)
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=static/assets");
|
println!("cargo:rerun-if-changed=static/assets");
|
||||||
|
@ -52,11 +65,24 @@ fn main() {
|
||||||
|
|
||||||
scripts.iter().for_each(|asset| {
|
scripts.iter().for_each(|asset| {
|
||||||
Command::new("uglifyjs")
|
Command::new("uglifyjs")
|
||||||
.arg("-c")
|
|
||||||
.arg(asset)
|
.arg(asset)
|
||||||
.arg("-o")
|
.arg("-o")
|
||||||
.arg(asset_path(asset))
|
.arg(asset_path(asset))
|
||||||
|
.arg("-c")
|
||||||
.spawn()
|
.spawn()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
});
|
||||||
|
|
||||||
|
let commit = system("git", &["rev-parse", "HEAD"]).map_err(|x| x.to_string());
|
||||||
|
let branch = system("git", &["rev-parse", "--abbrev-ref", "HEAD"]).map_err(|x| x.to_string());
|
||||||
|
|
||||||
|
match commit {
|
||||||
|
Err(err) => panic!("Can't get commit: {}", err),
|
||||||
|
Ok(commit) => println!("cargo:rustc-env=COMMIT_HASH={commit}")
|
||||||
|
}
|
||||||
|
|
||||||
|
match branch {
|
||||||
|
Err(err) => panic!("Can't get commit: {}", err),
|
||||||
|
Ok(branch) => println!("cargo:rustc-env=COMMIT_BRANCH={branch}")
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -84,3 +84,7 @@ sudo_delete=false
|
||||||
# Whether /api/upload is enabled
|
# Whether /api/upload is enabled
|
||||||
# It is not recommended to enable it if API key auth is not enabled
|
# It is not recommended to enable it if API key auth is not enabled
|
||||||
upload=false
|
upload=false
|
||||||
|
|
||||||
|
# Whether curlapi is enabled
|
||||||
|
# curl {url}/curlapi/help for more info
|
||||||
|
curlapi=true
|
||||||
|
|
|
@ -112,6 +112,10 @@ pub struct APISettings {
|
||||||
/// Whether /api/upload is enabled
|
/// Whether /api/upload is enabled
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub upload: bool,
|
pub upload: bool,
|
||||||
|
|
||||||
|
/// Whether curlapi is enabled
|
||||||
|
#[serde(default)]
|
||||||
|
pub curlapi: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for APISettings {
|
impl Default for APISettings {
|
||||||
|
@ -123,7 +127,8 @@ impl Default for APISettings {
|
||||||
get_all_own_only: true,
|
get_all_own_only: true,
|
||||||
delete: false,
|
delete: false,
|
||||||
sudo_delete: false,
|
sudo_delete: false,
|
||||||
upload: false
|
upload: false,
|
||||||
|
curlapi: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,23 @@ pub struct Redis {
|
||||||
pub prefix: String
|
pub prefix: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VersionData {
|
||||||
|
pub commit: String,
|
||||||
|
pub short_commit: String,
|
||||||
|
pub branch: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VersionData {
|
||||||
|
fn default() -> Self {
|
||||||
|
VersionData {
|
||||||
|
commit: env!("COMMIT_HASH").to_string(),
|
||||||
|
short_commit: env!("COMMIT_HASH").to_string().chars().take(6).collect(),
|
||||||
|
branch: env!("COMMIT_BRANCH").to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Env {
|
pub struct Env {
|
||||||
pub logging: bool,
|
pub logging: bool,
|
||||||
|
@ -24,7 +41,8 @@ pub struct Env {
|
||||||
pub filedir: String,
|
pub filedir: String,
|
||||||
pub instanceurl: String,
|
pub instanceurl: String,
|
||||||
pub uploadspath: String,
|
pub uploadspath: String,
|
||||||
pub confpath: String
|
pub confpath: String,
|
||||||
|
pub version: VersionData
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_var<T: Into<String>, O: From<String>>(name: T) -> Result<O, String> {
|
fn get_var<T: Into<String>, O: From<String>>(name: T) -> Result<O, String> {
|
||||||
|
@ -140,7 +158,8 @@ pub fn loadenv() -> Result<Env, Box<dyn std::error::Error>> {
|
||||||
return Err(format!("CONF_FILE is {}, which is not a file!", spath).into())
|
return Err(format!("CONF_FILE is {}, which is not a file!", spath).into())
|
||||||
}
|
}
|
||||||
spath
|
spath
|
||||||
}
|
},
|
||||||
|
version: VersionData::default()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
use warp::{Filter, reply::Reply, reject::Rejection};
|
||||||
|
|
||||||
|
use super::state::SharedState;
|
||||||
|
|
||||||
|
mod upload;
|
||||||
|
mod help;
|
||||||
|
|
||||||
|
pub fn get_routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
||||||
|
upload::get_routes(state.clone())
|
||||||
|
.or(help::get_routes(state))
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
use askama::Template;
|
||||||
|
use warp::{Filter, reply::{Reply, html}, reject::Rejection};
|
||||||
|
|
||||||
|
use crate::web::{state::SharedState, pages::CurlHelpPage, rejection::HttpReject};
|
||||||
|
|
||||||
|
pub async fn help(state: SharedState, ua: String) -> Result<Box<dyn Reply>, Rejection> {
|
||||||
|
|
||||||
|
if ! ua.starts_with("curl/") {
|
||||||
|
let page = CurlHelpPage { conf: state.config.clone(), env: state.env.clone() };
|
||||||
|
return Ok(
|
||||||
|
Box::new(
|
||||||
|
html(page.render().map_err(|x| HttpReject::AskamaError(x))?)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let brand = format!(
|
||||||
|
"{} \x1b[1m{}\x1b[0m {}",
|
||||||
|
state.config.brand.instance_emoji,
|
||||||
|
state.config.brand.instance_name,
|
||||||
|
{
|
||||||
|
if state.config.brand.instance_name != "blek! File" {
|
||||||
|
"\n\x1b[90mPowered by blek! File\x1b[0m"
|
||||||
|
} else { "" }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut warns: String = String::new();
|
||||||
|
if ! state.config.api.curlapi {
|
||||||
|
warns += "\x1b[1;31mWarning: curl API is disabled on this instance.\nYou can use the web UI to upload files.\x1b[0m\n\n"
|
||||||
|
}
|
||||||
|
if ! state.config.files.allow_uploads {
|
||||||
|
warns += {
|
||||||
|
format!(
|
||||||
|
"\x1b[1;31mWarning: all uploads are disabled on this instance{}\x1b[0m",
|
||||||
|
{
|
||||||
|
if let Some(reason) = state.config.files.upload_disable_reason {
|
||||||
|
format!(" for this reason:\n\"{}\"", reason)
|
||||||
|
} else { ".".to_string() }
|
||||||
|
}
|
||||||
|
).as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let instance = state.env.instanceurl;
|
||||||
|
let help =
|
||||||
|
format!(
|
||||||
|
"To upload a new file, you can POST it like this:
|
||||||
|
\x1b[32mcurl\x1b[0m \x1b[33m-X POST\x1b[0m \x1b[34m{instance}/curlapi/upload\x1b[0m \x1b[33m-F'file=@file.txt'\x1b[0m \x1b[33m-F'tos_consent=on'\x1b[0m
|
||||||
|
You can also add a password:
|
||||||
|
\x1b[32mcurl\x1b[0m \x1b[33m-X POST\x1b[0m \x1b[34m{instance}/curlapi/upload\x1b[0m \x1b[33m-F'file=@file.txt'\x1b[0m \x1b[33m-F'filename=uwu'\x1b[0m \x1b[33m-F'tos_consent=on'\x1b[0m \x1b[33m-F'named=on'\x1b[0m
|
||||||
|
The `named=on` switch is neede because this API is basically
|
||||||
|
the HTML used at the regular web UI form wrapped into this URL
|
||||||
|
|
||||||
|
\x1b[1;32mIMPORTANT:\x1b[0m Read the terms of service \x1b[1mbefore\x1b[0m uploading the file!
|
||||||
|
The ToS can be found here: \x1b[34m{instance}/tos\x1b[0m .
|
||||||
|
|
||||||
|
{warns}"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
Box::new(
|
||||||
|
format!("
|
||||||
|
\x1b[31m┓ ╻\x1b[0m \x1b[35m┏┓•┓ \x1b[0m
|
||||||
|
\x1b[32m┣┓┃\x1b[0m \x1b[95m┣ ┓┃┏┓\x1b[0m
|
||||||
|
\x1b[34m┗┛•\x1b[0m \x1b[35m┻ ┗┗┗━\x1b[0m
|
||||||
|
|
||||||
|
{brand}
|
||||||
|
{help}
|
||||||
|
").to_string())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
||||||
|
warp::any()
|
||||||
|
.and(warp::path!("curlapi" / "help"))
|
||||||
|
.and(
|
||||||
|
warp::any()
|
||||||
|
.map(move || state.clone())
|
||||||
|
)
|
||||||
|
.and(
|
||||||
|
warp::header::<String>("user-agent")
|
||||||
|
)
|
||||||
|
.and_then(help)
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
use warp::{filters::multipart::FormData, reply::{Reply, with_status}, reject::Rejection, Filter};
|
||||||
|
use warp_real_ip::real_ip;
|
||||||
|
|
||||||
|
use crate::{web::{state::SharedState, forms::{FormElement, UploadFormData}, rejection::HttpReject}, files::File};
|
||||||
|
|
||||||
|
pub async fn upload(form: FormData, ip: Option<IpAddr>, state: SharedState) -> Result<Box<dyn Reply>, Rejection> {
|
||||||
|
if ! state.config.files.allow_uploads {
|
||||||
|
return Ok(
|
||||||
|
Box::new(
|
||||||
|
with_status(
|
||||||
|
match state.config.files.upload_disable_reason {
|
||||||
|
Some(reason) => format!("Uploads are disabled for the following reason:\n{reason}"),
|
||||||
|
None => "Uploads are disabled.".into()
|
||||||
|
},
|
||||||
|
warp::http::StatusCode::SERVICE_UNAVAILABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = FormElement::from_formdata(form)
|
||||||
|
.await
|
||||||
|
.map_err(|x| HttpReject::WarpError(x))?;
|
||||||
|
|
||||||
|
if let Some(consent) = params.get("tos_consent") {
|
||||||
|
if consent.data != "on".bytes().collect::<Vec<u8>>() {
|
||||||
|
return Ok(
|
||||||
|
Box::new(
|
||||||
|
format!("You need to agree to the ToS to upload a file.\nSee {}/curlapi/help for details\n\nTo agree to the ToS, add a -F'tos_consent=on'\n", state.env.instanceurl)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Ok(
|
||||||
|
Box::new(
|
||||||
|
format!("You need to agree to the ToS to upload a file.\nSee {}/curlapi/help for details\n\nTo agree to the ToS, add a -F'tos_consent=on'\n", state.env.instanceurl)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let formdata = UploadFormData::from_formdata(params, true);
|
||||||
|
if let Some(formdata) = formdata {
|
||||||
|
|
||||||
|
let mut breaks_conf = false;
|
||||||
|
if (!state.config.files.allow_custom_names) && formdata.filename.is_some() {
|
||||||
|
breaks_conf = true;
|
||||||
|
}
|
||||||
|
if (!state.config.files.allow_pass_protection) && formdata.password.is_some() {
|
||||||
|
breaks_conf = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if breaks_conf {
|
||||||
|
return Ok(
|
||||||
|
Box::new(
|
||||||
|
with_status(
|
||||||
|
"Attempt to set name or password when they are disabled".to_string(),
|
||||||
|
warp::http::StatusCode::BAD_REQUEST
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pass) = state.config.files.upload_pass {
|
||||||
|
let pass_valid: bool;
|
||||||
|
if let Some(upass) = formdata.instancepass {
|
||||||
|
pass_valid = upass == pass;
|
||||||
|
} else {
|
||||||
|
pass_valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! pass_valid {
|
||||||
|
return Ok(
|
||||||
|
Box::new(
|
||||||
|
with_status(
|
||||||
|
"Invalid instance password".to_string(),
|
||||||
|
warp::http::StatusCode::BAD_REQUEST
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = File::create(
|
||||||
|
formdata.file,
|
||||||
|
formdata.mime,
|
||||||
|
formdata.filename.clone(),
|
||||||
|
state.env.clone(),
|
||||||
|
formdata.delmode,
|
||||||
|
formdata.password,
|
||||||
|
ip
|
||||||
|
).await.map_err(|x| HttpReject::StringError(x.to_string()))?;
|
||||||
|
|
||||||
|
state.file_mgr.save(&file, formdata.lookup_kind).map_err(|x| HttpReject::StringError(x.to_string()))?;
|
||||||
|
|
||||||
|
return Ok(
|
||||||
|
Box::new(
|
||||||
|
format!(
|
||||||
|
concat!(
|
||||||
|
"File uploaded successfully.\n",
|
||||||
|
"It is available via this link:\n\n",
|
||||||
|
|
||||||
|
"{}/upload/{}\n"
|
||||||
|
),
|
||||||
|
state.env.instanceurl,
|
||||||
|
urlencoding::encode(
|
||||||
|
match formdata.filename {
|
||||||
|
Some(name) => name,
|
||||||
|
None => file.hash()
|
||||||
|
}.as_str()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Ok(
|
||||||
|
Box::new(
|
||||||
|
with_status(
|
||||||
|
"Invalid form".to_string(),
|
||||||
|
warp::http::StatusCode::BAD_REQUEST
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
||||||
|
warp::post()
|
||||||
|
.and(warp::path!("curlapi" / "upload"))
|
||||||
|
.and(warp::multipart::form())
|
||||||
|
.and(real_ip(vec![state.env.proxy_addr]))
|
||||||
|
.and(
|
||||||
|
warp::any()
|
||||||
|
.map(move || state.clone())
|
||||||
|
)
|
||||||
|
.and_then(upload)
|
||||||
|
}
|
|
@ -17,9 +17,9 @@ use crate::files::{File, lookup::LookupKind, DeleteMode};
|
||||||
use super::{state::SharedState, pages::{UploadSuccessPage, ErrorPage}, rejection::HttpReject};
|
use super::{state::SharedState, pages::{UploadSuccessPage, ErrorPage}, rejection::HttpReject};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Debug, Serialize, Clone)]
|
||||||
struct FormElement {
|
pub struct FormElement {
|
||||||
data: Vec<u8>,
|
pub data: Vec<u8>,
|
||||||
mime: String
|
pub mime: String
|
||||||
}
|
}
|
||||||
impl FormElement {
|
impl FormElement {
|
||||||
|
|
||||||
|
@ -58,15 +58,15 @@ impl FormElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UploadFormData {
|
pub struct UploadFormData {
|
||||||
filename: Option<String>,
|
pub filename: Option<String>,
|
||||||
password: Option<String>,
|
pub password: Option<String>,
|
||||||
instancepass: Option<String>,
|
pub instancepass: Option<String>,
|
||||||
lookup_kind: LookupKind,
|
pub lookup_kind: LookupKind,
|
||||||
delmode: DeleteMode,
|
pub delmode: DeleteMode,
|
||||||
file: Vec<u8>,
|
pub file: Vec<u8>,
|
||||||
mime: String,
|
pub mime: String,
|
||||||
tos_consent: bool
|
pub tos_consent: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UploadFormData {
|
impl Default for UploadFormData {
|
||||||
|
@ -86,7 +86,7 @@ impl Default for UploadFormData {
|
||||||
|
|
||||||
impl UploadFormData {
|
impl UploadFormData {
|
||||||
|
|
||||||
pub fn from_formdata(data: HashMap<String, FormElement>) -> Option<UploadFormData> {
|
pub fn from_formdata(data: HashMap<String, FormElement>, use_defaults: bool) -> Option<UploadFormData> {
|
||||||
let mut out = Self::default();
|
let mut out = Self::default();
|
||||||
|
|
||||||
// Add a name
|
// Add a name
|
||||||
|
@ -125,9 +125,11 @@ impl UploadFormData {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
|
if ! use_defaults {
|
||||||
return None
|
return None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match data.get("instancepass") {
|
match data.get("instancepass") {
|
||||||
Some(val) => {
|
Some(val) => {
|
||||||
|
@ -169,7 +171,7 @@ pub async fn upload(form: FormData, ip: Option<IpAddr>, state: SharedState) -> R
|
||||||
}
|
}
|
||||||
|
|
||||||
let params: HashMap<String, FormElement> = FormElement::from_formdata(form).await.map_err(|x| HttpReject::WarpError(x))?;
|
let params: HashMap<String, FormElement> = FormElement::from_formdata(form).await.map_err(|x| HttpReject::WarpError(x))?;
|
||||||
let formdata = UploadFormData::from_formdata(params.clone());
|
let formdata = UploadFormData::from_formdata(params.clone(), false);
|
||||||
|
|
||||||
if let Some(formdata) = formdata {
|
if let Some(formdata) = formdata {
|
||||||
|
|
||||||
|
|
|
@ -2,32 +2,28 @@
|
||||||
web - The part of filed that handles everything related to HTTP
|
web - The part of filed that handles everything related to HTTP
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::env::current_dir;
|
|
||||||
|
|
||||||
use static_dir::static_dir;
|
use static_dir::static_dir;
|
||||||
use warp::{Filter, reply::Reply, reject::Rejection};
|
use warp::{Filter, reply::Reply, reject::Rejection};
|
||||||
|
|
||||||
use crate::{env::Env, files::lookup::FileManager, config::types::Config};
|
use crate::{env::Env, files::lookup::FileManager, config::types::Config};
|
||||||
|
|
||||||
mod pages;
|
mod pages;
|
||||||
mod forms;
|
pub mod forms;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
mod rejection;
|
mod rejection;
|
||||||
mod api;
|
mod api;
|
||||||
mod uploaded;
|
mod uploaded;
|
||||||
|
mod curlapi;
|
||||||
|
|
||||||
use state::SharedState;
|
use state::SharedState;
|
||||||
|
|
||||||
pub fn routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
pub fn routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
||||||
let staticpath = current_dir().unwrap();
|
static_dir!("static")
|
||||||
let staticpath = staticpath.to_str().unwrap().to_string() + "/static";
|
.or(curlapi::get_routes(state.clone()))
|
||||||
|
.or(pages::get_routes(state.clone()))
|
||||||
pages::get_routes(state.clone())
|
|
||||||
.or(forms::get_routes(state.clone()))
|
.or(forms::get_routes(state.clone()))
|
||||||
.or(api::get_routes(state.clone()))
|
.or(api::get_routes(state.clone()))
|
||||||
.or(uploaded::get_uploaded(state))
|
.or(uploaded::get_uploaded(state))
|
||||||
.or(static_dir!("static"))
|
|
||||||
.or(warp::fs::dir(staticpath.to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -88,6 +88,14 @@ pub struct ErrorPage {
|
||||||
pub link_text: Option<String>
|
pub link_text: Option<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template( path = "curlapi_help.html" )]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct CurlHelpPage {
|
||||||
|
pub env: Env,
|
||||||
|
pub conf: Config
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn uploaded(query: HashMap<String, String>, state: SharedState) -> Result<Html<String>, Rejection> {
|
pub async fn uploaded(query: HashMap<String, String>, state: SharedState) -> Result<Html<String>, Rejection> {
|
||||||
|
|
||||||
if ! query.contains_key("file") {
|
if ! query.contains_key("file") {
|
||||||
|
|
|
@ -25,3 +25,6 @@
|
||||||
.alert.blue .alert-title {
|
.alert.blue .alert-title {
|
||||||
background: #203050;
|
background: #203050;
|
||||||
}
|
}
|
||||||
|
.alert.green .alert-title {
|
||||||
|
background: #205030;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
.code {
|
||||||
|
display: block;
|
||||||
|
padding: 1em;
|
||||||
|
border: 1px solid var(--header-sec-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.code .inner {
|
||||||
|
width: max-content;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.code-inline {
|
||||||
|
display: inline;
|
||||||
|
background: #00000010;
|
||||||
|
border: 1px solid #c2c4c210;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code, .code-inline {
|
||||||
|
font-family: monospace;
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
|
@ -6,6 +6,11 @@
|
||||||
|
|
||||||
/** @type {HTMLElement} */
|
/** @type {HTMLElement} */
|
||||||
const root_drag_rop = document.getElementsByClassName('file-drag-n-drop')[0];
|
const root_drag_rop = document.getElementsByClassName('file-drag-n-drop')[0];
|
||||||
|
|
||||||
|
// make the root drag&drop element an ideal circle
|
||||||
|
root_drag_rop.style.width = root_drag_rop.offsetWidth + 'px';
|
||||||
|
root_drag_rop.style.height = root_drag_rop.offsetWidth + 'px';
|
||||||
|
|
||||||
/** @type {HTMLElement} */
|
/** @type {HTMLElement} */
|
||||||
const drag_rop = document.getElementsByClassName('file-drag-n-drop-inside')[0];
|
const drag_rop = document.getElementsByClassName('file-drag-n-drop-inside')[0];
|
||||||
const dr_rop_t = document.getElementsByClassName('file-drag-n-drop-inside-text')[0];
|
const dr_rop_t = document.getElementsByClassName('file-drag-n-drop-inside-text')[0];
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
.file-drag-n-drop {
|
.file-drag-n-drop {
|
||||||
display: block;
|
display: block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 440px;
|
width: 100%;
|
||||||
height: 440px;
|
height: 440px;
|
||||||
background: var(--header-sec-color);
|
background: var(--header-sec-color);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
|
|
@ -67,6 +67,13 @@
|
||||||
<td>
|
<td>
|
||||||
<small>Made with Rust and <3</small>
|
<small>Made with Rust and <3</small>
|
||||||
|
|
||||||
|
<small style="display:block">
|
||||||
|
Version
|
||||||
|
<a href="https://git.blek.codes/blek/bfile/commit/{{ env.version.commit }}" target="_blank">
|
||||||
|
{{ env!("CARGO_PKG_VERSION") }} ({{ env.version.branch -}}/{{- env.version.short_commit }})
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
|
||||||
<ul style='margin:10px 0'>
|
<ul style='margin:10px 0'>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://git.blek.codes/blek/bfile">
|
<a href="https://git.blek.codes/blek/bfile">
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link rel='stylesheet' href="/code.css" />
|
||||||
|
<link rel='stylesheet' href="/alert.css" />
|
||||||
|
<link rel='stylesheet' href="/js-only.css" />
|
||||||
|
<style>
|
||||||
|
.copy-btn { font-size: 70%; transform: translateY(-25%); display: inline-block }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<div style="max-width:95vw;width:fit-content;margin:0 auto">
|
||||||
|
<h1 style="text-align:center">Curl API</h1>
|
||||||
|
<p>
|
||||||
|
blek! File has an API for uploading files via cURL.
|
||||||
|
To upload a file via cURL, follow these instructions:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
To upload a file, POST it like this:
|
||||||
|
<a href="#" class="copy-btn" data-clipboard-text="curl -X POST {{env.instanceurl}}/curlapi/upload -F'file=@file.txt' -F'tos_consent=on'">
|
||||||
|
Copy!
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<div class='code'>
|
||||||
|
<span class='inner'>
|
||||||
|
<span style='color:green'>curl</span>
|
||||||
|
<span style='color:orange'>-X POST</span>
|
||||||
|
<span style='color:darkcyan'>{{env.instanceurl}}/curlapi/upload</span>
|
||||||
|
<span style='color:orange'>-F'file=@file.txt'</span>
|
||||||
|
<span style='color:orange'>-F'tos_consent=on'</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
To add a password, do it like this:
|
||||||
|
<a href="#" class="copy-btn" data-clipboard-text="curl -X POST {{env.instanceurl}}/curlapi/upload -F'file=@file.txt' -F'filename=uwu' -F'tos_consent=on' -F'named=on'">
|
||||||
|
Copy!
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<div class='code'>
|
||||||
|
<span class='inner'>
|
||||||
|
<span style='color:green'>curl</span>
|
||||||
|
<span style='color:orange'>-X POST</span>
|
||||||
|
<span style='color:darkcyan'>{{env.instanceurl}}/curlapi/upload</span>
|
||||||
|
<span style='color:orange'>-F'file=@file.txt'</span>
|
||||||
|
<span style='color:orange'>-F'tos_consent=on'</span>
|
||||||
|
<span style='color:orange'>-F'named=on'</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Note that the
|
||||||
|
<span class='code-inline'>named=on</span>
|
||||||
|
switch is required.
|
||||||
|
Its needed because the curl API is basically a wrapper of
|
||||||
|
<a href="/">this</a>
|
||||||
|
HTML form.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="alert green">
|
||||||
|
<h1 class="alert-title">
|
||||||
|
Important
|
||||||
|
</h1>
|
||||||
|
<p class="alert-text">
|
||||||
|
Read the
|
||||||
|
<a href="/tos">Terms of Service</a>
|
||||||
|
<b>before</b>
|
||||||
|
uploading a file.
|
||||||
|
<br/>
|
||||||
|
You agree to them by adding the
|
||||||
|
<span class="code-inline">tos_consent=on</span>
|
||||||
|
switch.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='alert blue'>
|
||||||
|
<h1 class='alert-title'>Web UI</h1>
|
||||||
|
<div class='alert-text'>
|
||||||
|
<p>
|
||||||
|
Hey, it looks like you are viewing this page from a browser!<br/>
|
||||||
|
You can use the Web UI as well to upload a file!
|
||||||
|
</p>
|
||||||
|
<p style='margin:32px 0'>
|
||||||
|
<a href='/' role='button' class='btn'>
|
||||||
|
Go to the web UI
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="https://unpkg.com/clipboard@2/dist/clipboard.min.js"></script>
|
||||||
|
<script>
|
||||||
|
new ClipboardJS('.copy-btn');
|
||||||
|
{#- -#}
|
||||||
|
( {#- -#}
|
||||||
|
() => { {#- -#}
|
||||||
|
let btns = document.getElementsByClassName('copy-btn') {#- -#}
|
||||||
|
for (const button of btns) { {#- -#}
|
||||||
|
button.onclick = () => { {#- -#}
|
||||||
|
let old = button.innerHTML; {#- -#}
|
||||||
|
button.innerHTML = 'Copied!'; {#- -#}
|
||||||
|
setTimeout(() => { button.innerHTML = old }, 500); {#- -#}
|
||||||
|
} {#- -#}
|
||||||
|
} {#- -#}
|
||||||
|
} {#- -#}
|
||||||
|
)() {#- -#}
|
||||||
|
</script>
|
||||||
|
<script src='/js-only.js'></script>
|
||||||
|
{% endblock %}
|
|
@ -5,5 +5,5 @@ REDIS_PREFIX=bfile-
|
||||||
|
|
||||||
USERCONTENT_DIR=/opt/user_uploads
|
USERCONTENT_DIR=/opt/user_uploads
|
||||||
|
|
||||||
CLEAN_DEL=30 minutes
|
CLEAN_DEL=30min
|
||||||
CLEAN_ERRDEL=2 minutes
|
CLEAN_ERRDEL=2min
|
||||||
|
|
|
@ -10,7 +10,7 @@ RUN cargo b -r
|
||||||
RUN strip target/release/janitor && upx --best target/release/janitor
|
RUN strip target/release/janitor && upx --best target/release/janitor
|
||||||
|
|
||||||
# --- deploy ---
|
# --- deploy ---
|
||||||
FROM busybox:musl
|
FROM alpine:3.17
|
||||||
|
|
||||||
RUN mkdir /config
|
RUN mkdir /config
|
||||||
WORKDIR /config
|
WORKDIR /config
|
||||||
|
|
Loading…
Reference in New Issue