Heaver server V1
This commit is contained in:
		
							
								
								
									
										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>
 | 
			
		||||
		Reference in New Issue
	
	Block a user