├── .gitignore ├── src ├── macros.rs ├── middleware.rs ├── exit.rs ├── cli.rs └── main.rs ├── Cargo.toml ├── CHANGELOG.md ├── LICENSE-MIT ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── README.md ├── LICENSE-APACHE └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # rust build artifacts 2 | target/ 3 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! print_block { 2 | ( 3 | $($k:literal => $v:expr)+ 4 | ) => { 5 | { 6 | let mut lines = vec![]; 7 | let mut l = (0, 0); 8 | $( 9 | let (k,v) = ($k,format!("{}", $v)); 10 | l = (l.0.max(k.len()), l.1.max(v.len()).min(30)); 11 | lines.push((k, v.len() > 30, v)); 12 | )+ 13 | println!("┌{}┐", "─".repeat(l.0 + l.1 + 7)); 14 | for (k, q, v) in lines { 15 | println!("│ - {0:1$} : {2:3$} {4}", k, l.0, v, l.1, if q { "" } else { "│" }); 16 | } 17 | println!("└{}┘", "─".repeat(l.0 + l.1 + 7)); 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zy" 3 | version = "0.2.0" 4 | authors = ["Miraculous Owonubi "] 5 | edition = "2021" 6 | rust-version = "1.59.0" 7 | description = "Minimal and blazing-fast file server." 8 | repository = "https://github.com/miraclx/zy" 9 | license = "MIT OR Apache-2.0" 10 | keywords = ["static", "file", "server", "http", "cli"] 11 | categories = ["command-line-utilities", "web-programming::http-server"] 12 | 13 | [dependencies] 14 | clap = { version = "3.2", features = ["derive"] } 15 | mime = "0.3.16" 16 | tokio = { version = "1", features = ["macros", "signal"] } 17 | tracing = "0.1.26" 18 | actix-web = "4.2.1" 19 | humantime = "2.1.0" 20 | color-eyre = "0.6.2" 21 | actix-files = "0.6.2" 22 | tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } 23 | 24 | [profile.slim] 25 | inherits = "release" 26 | lto = true 27 | strip = true 28 | codegen-units = 1 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.2.0] - 2022-11-06 11 | 12 | - Support for the `PORT` environment variable. 13 | - Human cache time input (e.g. `1h`, `1year 6months`). 14 | - `--anonymize` flag to hide the `Server` and `X-Powered-By` headers. 15 | - Added `zstd` compression support. 16 | - Dynamic cache control (ETag, Last-Modified, Cache-Control). 17 | - Auto-served `index.html` files. 18 | - Use `println` over tracing for trivial logs. 19 | 20 | ## [0.1.1] - 2022-10-30 21 | 22 | > Release Page: 23 | 24 | [unreleased]: https://github.com/miraclx/zy/compare/v0.2.0...HEAD 25 | [0.2.0]: https://github.com/miraclx/zy/compare/v0.1.1...v0.2.0 26 | [0.1.1]: https://github.com/miraclx/zy/releases/tag/v0.1.1 27 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 NEAR 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | RUSTFLAGS: -D warnings 11 | RUST_BACKTRACE: short 12 | CARGO_NET_RETRY: 10 13 | CARGO_TERM_COLOR: always 14 | CARGO_INCREMENTAL: 0 15 | 16 | jobs: 17 | check: 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | rust-version: [1.59.0, stable] 23 | os: [ubuntu-latest, macos-latest, windows-latest] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v2 28 | 29 | - name: Install rust 30 | uses: dtolnay/rust-toolchain@master 31 | with: 32 | toolchain: ${{ matrix.rust-version }} 33 | target: ${{ matrix.target }} 34 | 35 | - uses: Swatinem/rust-cache@v2 36 | with: 37 | key: ${{ matrix.os }}-${{ matrix.rust-version }} 38 | 39 | - name: Run cargo check 40 | uses: actions-rs/cargo@v1 41 | with: 42 | command: check 43 | 44 | clippy: 45 | runs-on: ${{ matrix.os }} 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | rust-version: [1.59.0, stable] 50 | os: [ubuntu-latest, macos-latest, windows-latest] 51 | 52 | steps: 53 | - name: Checkout repository 54 | uses: actions/checkout@v2 55 | 56 | - name: Install rust 57 | uses: dtolnay/rust-toolchain@master 58 | with: 59 | toolchain: ${{ matrix.rust-version }} 60 | target: ${{ matrix.target }} 61 | components: clippy 62 | 63 | - uses: Swatinem/rust-cache@v2 64 | with: 65 | key: ${{ matrix.os }}-${{ matrix.rust-version }} 66 | 67 | - name: Check if clippy is installed 68 | shell: bash 69 | run: | 70 | rustup component list --installed | grep clippy 71 | if [ $? -eq 0 ]; then 72 | echo "CLIPPY_INSTALLED=true" >> $GITHUB_ENV 73 | else 74 | echo "CLIPPY_INSTALLED=false" >> $GITHUB_ENV 75 | fi 76 | 77 | - name: Run cargo clippy 78 | if: env.CLIPPY_INSTALLED == 'true' 79 | uses: actions-rs/cargo@v1 80 | with: 81 | command: clippy 82 | args: -- -D clippy::all 83 | 84 | fmt: 85 | runs-on: ubuntu-latest 86 | 87 | steps: 88 | - name: Checkout repository 89 | uses: actions/checkout@v2 90 | 91 | - name: Run cargo fmt 92 | run: cargo fmt --all -- --check 93 | -------------------------------------------------------------------------------- /src/middleware.rs: -------------------------------------------------------------------------------- 1 | use std::future::{ready, Future, Ready}; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; 6 | use actix_web::http::header; 7 | pub use actix_web::middleware::Compress; 8 | 9 | pub struct ZyServer { 10 | pub anonymize: bool, 11 | } 12 | 13 | impl Transform for ZyServer 14 | where 15 | S: Service>, 16 | S::Future: 'static, 17 | B: 'static, 18 | { 19 | type Response = S::Response; 20 | type Error = S::Error; 21 | type Transform = ZyServerMiddleware; 22 | type InitError = (); 23 | type Future = Ready>; 24 | 25 | fn new_transform(&self, service: S) -> Self::Future { 26 | ready(Ok(ZyServerMiddleware { 27 | service, 28 | anonymize: self.anonymize, 29 | })) 30 | } 31 | } 32 | 33 | pub struct ZyServerMiddleware { 34 | service: S, 35 | anonymize: bool, 36 | } 37 | 38 | impl Service for ZyServerMiddleware 39 | where 40 | S: Service>, 41 | S::Future: 'static, 42 | B: 'static, 43 | { 44 | type Response = S::Response; 45 | type Error = S::Error; 46 | type Future = Pin>>>; 47 | 48 | fn poll_ready(&self, ctx: &mut Context<'_>) -> Poll> { 49 | self.service.poll_ready(ctx) 50 | } 51 | 52 | fn call(&self, req: ServiceRequest) -> Self::Future { 53 | let fut = self.service.call(req); 54 | 55 | let anonymous = self.anonymize; 56 | Box::pin(async move { 57 | let mut res = fut.await?; 58 | 59 | if !anonymous { 60 | res.headers_mut().insert( 61 | header::SERVER, 62 | header::HeaderValue::from_static(concat!("Zy/", env!("CARGO_PKG_VERSION"))), 63 | ); 64 | 65 | res.headers_mut().insert( 66 | "X-Powered-By" 67 | .try_into() 68 | .expect("x-powered-by should be valid header"), 69 | header::HeaderValue::from_static(concat!("Zy/", env!("CARGO_PKG_VERSION"))), 70 | ); 71 | } 72 | 73 | Ok(res) 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/exit.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | 3 | use color_eyre::Result; 4 | use tokio::signal; 5 | use tracing::info; 6 | 7 | /// A future that never resolves, but will call the handler when it detects a shutdown signal. 8 | /// 9 | /// Signals: `SIGINT`, `SIGTERM`, `SIGHUP` or explicit `shutdown` call. 10 | /// 11 | /// A use-case of this, is to explicitly shutdown the server. Which then cancels this future. 12 | pub async fn on_signal(confirm_exit: bool, handler: F) -> Result<()> 13 | where 14 | F: FnOnce(bool) -> Fut, 15 | Fut: Future, 16 | { 17 | let sigint = async { 18 | if confirm_exit { 19 | let mut last_signal_timestamp = None; 20 | 21 | loop { 22 | signal::ctrl_c().await?; 23 | 24 | let now = std::time::Instant::now(); 25 | if let Some(last_signal_timestamp) = last_signal_timestamp { 26 | if now.duration_since(last_signal_timestamp) < std::time::Duration::from_secs(5) 27 | { 28 | info!("[signal] Ctrl-C received"); 29 | break; 30 | } 31 | } 32 | info!("[signal] Ctrl-C received, press again to exit"); 33 | last_signal_timestamp = Some(now); 34 | } 35 | } else { 36 | signal::ctrl_c().await?; 37 | } 38 | 39 | Result::<()>::Ok(()) 40 | }; 41 | 42 | let sigterm = async { 43 | #[cfg(unix)] 44 | { 45 | let mut sigterm = signal::unix::signal(signal::unix::SignalKind::terminate())?; 46 | sigterm.recv().await; 47 | info!("[signal] SIGTERM received"); 48 | } 49 | 50 | #[cfg(windows)] 51 | { 52 | let mut sigterm = signal::windows::ctrl_break()?; 53 | sigterm.recv().await; 54 | info!("[signal] Ctrl-Break received"); 55 | } 56 | 57 | Result::<()>::Ok(()) 58 | }; 59 | 60 | let sighup = async { 61 | #[cfg(unix)] 62 | { 63 | let mut sighup = signal::unix::signal(signal::unix::SignalKind::hangup())?; 64 | sighup.recv().await; 65 | info!("[signal] SIGHUP received"); 66 | } 67 | 68 | #[cfg(windows)] 69 | std::future::pending::<()>().await; 70 | 71 | Result::<()>::Ok(()) 72 | }; 73 | 74 | let res = tokio::select! { 75 | _ = sighup => handler(false), 76 | _ = sigint => handler(false), 77 | _ = sigterm => handler(true), 78 | }; 79 | 80 | tokio::select! { 81 | _ = async { 82 | res.await; 83 | std::future::pending::<()>().await; 84 | } => {}, 85 | _ = signal::ctrl_c() => { 86 | info!("[signal] Ctrl-C received while exiting, forcibly exiting..."); 87 | } 88 | } 89 | 90 | Ok(()) 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zy 2 | 3 | > Minimal and blazing-fast file server. For real, this time. 4 | 5 | [![Crates.io](https://img.shields.io/crates/v/zy?label=latest)](https://crates.io/crates/zy) 6 | [![CI](https://github.com/miraclx/zy/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/miraclx/zy/actions/workflows/ci.yml) 7 | [![MIT or Apache 2.0 Licensed](https://img.shields.io/crates/l/zy.svg)](#license) 8 | 9 | ## Features 10 | 11 | - [Single Page Application support](https://developer.mozilla.org/en-US/docs/Glossary/SPA) 12 | - Partial responses ([Range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range) support) 13 | - Cross-Origin Resource Sharing ([CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)) 14 | - [Automatic HTTP compression](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding) (Zstd, Gzip, Brotli, Deflate) 15 | - Dynamic [cache control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) (ETag, Last-Modified, Cache-Control) 16 | - Auto-served `index.html` files 17 | - Sane defaults 18 | - No access to hidden files 19 | - No access to content outside the base directory 20 | - No access to symbolic links outside the base directory 21 | 22 | ## Installation 23 | 24 | You can download any of the pre-compiled binaries from the [releases page](https://github.com/miraclx/zy/releases). 25 | 26 | Or if you already have Rust installed, you can install it with `cargo`: 27 | 28 | > - Please, note that the minimum supported version of Rust for `zy` is `1.59.0`. 29 | > - Also, that the binary may be bigger than expected because it contains debug symbols. This is intentional. To remove debug symbols and therefore reduce the file size, you can instead run it with the `--profile slim` or simply just run `strip` on it. 30 | 31 | ```console 32 | cargo install zy 33 | ``` 34 | 35 | Alternatively, you can also build the latest version of `zy` directly from GitHub. 36 | 37 | ```console 38 | cargo install --git https://github.com/miraclx/zy.git 39 | ``` 40 | 41 | ## Usage 42 | 43 | ```console 44 | zy 45 | ``` 46 | 47 | _This will start serving your current directory on by default._ 48 | 49 | _...you can also specify a different port or base directory:_ 50 | 51 | ```console 52 | zy /path/to/serve 53 | ``` 54 | 55 | _...or perhaps different addresses:_ 56 | 57 | ```console 58 | zy -l 5000 -l 127.0.0.1:8080 -l 192.168.1.25 59 | ``` 60 | 61 | ## Configuration 62 | 63 | You can run `zy --help` to see all available options. 64 | 65 | ```console 66 | $ zy --help 67 | Zy 0.2.0 68 | Minimal and blazing-fast file server. 69 | 70 | USAGE: 71 | zy [OPTIONS] [DIR] 72 | 73 | ARGS: 74 | Directory to serve [default: .] 75 | 76 | OPTIONS: 77 | -l, --listen Sets the address to listen on (repeatable) [default: 127.0.0.1:3000] 78 | Valid: `3000`, `127.0.0.1`, `127.0.0.1:3000` [env: PORT] 79 | -s, --spa Run as a Single Page Application 80 | -i, --index Index file to serve from the base directory [default: index.html] 81 | --404 404 file to serve from the base directory [default: 404.html] 82 | -c, --cache