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
 | 
					# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[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() {
 | 
					use std::fs::File;
 | 
				
			||||||
    println!("Hello, world!");
 | 
					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