Heaver server V1
This commit is contained in:
parent
2c0c67de63
commit
75d69de86c
1569
Cargo.lock
generated
Normal file
1569
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@ -6,3 +6,13 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.0.29", features = ["derive", "env"] }
|
||||
tar = "0.4.38"
|
||||
actix-web = "4"
|
||||
actix = "0.13.0"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.17"
|
||||
env_logger = "0.10.0"
|
||||
actix-web-httpauth = "0.8.0"
|
||||
askama = "0.11.1"
|
||||
tokio = "1.23.0"
|
12
assets/bootstrap.min.css
vendored
Normal file
12
assets/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
185
src/main.rs
185
src/main.rs
@ -1,3 +1,184 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
use std::fs::File;
|
||||
use std::io::{ErrorKind, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::sync::mpsc;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix::dev::Stream;
|
||||
use actix_web::dev::ServiceRequest;
|
||||
use actix_web::middleware::Logger;
|
||||
use actix_web::web::Bytes;
|
||||
use actix_web::HttpServer;
|
||||
use actix_web::{web, App, HttpResponse};
|
||||
use actix_web_httpauth::extractors;
|
||||
use actix_web_httpauth::extractors::basic::BasicAuth;
|
||||
use actix_web_httpauth::extractors::AuthenticationError;
|
||||
use actix_web_httpauth::middleware::HttpAuthentication;
|
||||
use askama::Template;
|
||||
use clap::Parser;
|
||||
|
||||
/// Simple heavy file server
|
||||
#[derive(Parser, Debug)]
|
||||
struct Args {
|
||||
/// The URL this service will listen to
|
||||
#[arg(short, long, env, default_value = "0.0.0.0:5000")]
|
||||
listen_url: String,
|
||||
|
||||
/// Directory that contains served files
|
||||
#[arg(short, long, env)]
|
||||
target_dir: String,
|
||||
|
||||
/// Access token used to secure access to this service
|
||||
#[arg(short, long, env)]
|
||||
access_token: Option<String>,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref ARGS: Args = Args::parse();
|
||||
}
|
||||
|
||||
async fn validator(
|
||||
req: ServiceRequest,
|
||||
creds: BasicAuth,
|
||||
) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
|
||||
if creds.password().eq(&ARGS.access_token.as_deref()) {
|
||||
Ok(req)
|
||||
} else {
|
||||
let config = extractors::basic::Config::default();
|
||||
Err((AuthenticationError::from(config).into(), req))
|
||||
}
|
||||
}
|
||||
|
||||
fn recurse_scan<B: AsRef<Path>>(dir: B) -> Vec<PathBuf> {
|
||||
let dir = dir.as_ref();
|
||||
|
||||
if dir.is_file() {
|
||||
return vec![dir.to_path_buf()];
|
||||
}
|
||||
|
||||
let mut list = vec![];
|
||||
for file in dir.read_dir().unwrap() {
|
||||
let file = file.unwrap();
|
||||
list.append(&mut recurse_scan(&file.path()));
|
||||
}
|
||||
|
||||
list
|
||||
}
|
||||
|
||||
/// Get the list of files to download
|
||||
fn files_list() -> Vec<PathBuf> {
|
||||
recurse_scan(&ARGS.target_dir)
|
||||
}
|
||||
|
||||
async fn bootstrap_css() -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("content-type", "text/css"))
|
||||
.body(include_str!("../assets/bootstrap.min.css"))
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "../templates/index.html")]
|
||||
struct IndexTemplate {
|
||||
files: Vec<PathBuf>,
|
||||
app_title: &'static str,
|
||||
}
|
||||
|
||||
async fn index() -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("content-type", "text/html"))
|
||||
.body(
|
||||
IndexTemplate {
|
||||
files: files_list(),
|
||||
app_title: "Heavy file server",
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
struct SendWrapper(mpsc::Sender<Vec<u8>>);
|
||||
|
||||
impl Write for SendWrapper {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
if let Err(e) = self.0.send(buf.to_vec()) {
|
||||
log::error!("Failed to send a chunk of data! {}", e);
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
"Failed to send a chunk of data!",
|
||||
));
|
||||
}
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct FileStreamer {
|
||||
receive: mpsc::Receiver<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl FileStreamer {
|
||||
pub fn start() -> Self {
|
||||
let (send, receive) = mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let mut tar = tar::Builder::new(SendWrapper(send));
|
||||
|
||||
for file in files_list() {
|
||||
let file_path = &file.to_str().unwrap().replace(&ARGS.target_dir, "")[1..];
|
||||
log::debug!("Add {} to archive", file_path);
|
||||
tar.append_file(
|
||||
file_path,
|
||||
&mut File::open(&file).expect("Failed to open file"),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
tar.finish().unwrap();
|
||||
});
|
||||
|
||||
Self { receive }
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for FileStreamer {
|
||||
type Item = Result<Bytes, std::io::Error>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match self.receive.recv() {
|
||||
Ok(d) => Poll::Ready(Some(Ok(Bytes::copy_from_slice(&d)))),
|
||||
Err(e) => {
|
||||
log::error!("Recv error: {}", e);
|
||||
Poll::Ready(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn download() -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("Content-Disposition", " attachment; filename=\"files.tar\""))
|
||||
.streaming(FileStreamer::start())
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
log::info!("Start to listen on {}", ARGS.listen_url);
|
||||
log::info!("File are served from {}", ARGS.target_dir);
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.wrap(HttpAuthentication::basic(validator))
|
||||
.wrap(Logger::default())
|
||||
.route("/assets/bootstrap.min.css", web::get().to(bootstrap_css))
|
||||
.route("/", web::get().to(index))
|
||||
.route("/download", web::get().to(download))
|
||||
})
|
||||
.bind(ARGS.listen_url.to_string())?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
29
templates/index.html
Normal file
29
templates/index.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ app_title }}</title>
|
||||
|
||||
<link rel="stylesheet" href="/assets/bootstrap.min.css">
|
||||
</head>
|
||||
<body style="margin-top: 80px">
|
||||
<div class="navbar navbar-expand-lg fixed-top navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand">{{ app_title }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div style="text-align: center">
|
||||
<a type="button" class="btn btn-outline-primary btn-lg" href="/download">Download files</a>
|
||||
</div>
|
||||
|
||||
<h3>Files list:</h3>
|
||||
<ul>
|
||||
{% for f in files -%}
|
||||
<li>{{ f.to_str().unwrap() }}</li>
|
||||
{% endfor -%}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user