http-fs 2.0.1

HTTP File Service library
Documentation
//!# http-fs
//!
//!## Features
//!
//!- `tokio` - Enables `tokio` runtime integration. Enables `rt`.
//!- `hyper` - Enables `hyper` [integration](adaptors/hyper/index.html). Enables `http1` and `server` features.
//!
//!## Usage
//!
//!```rust
//!use http_fs::config::{self, StaticFileConfig, DummyWorker};
//!use http_fs::{StaticFiles};
//!
//!use std::path::Path;
//!
//!pub struct DirectoryConfig;
//!impl StaticFileConfig for DirectoryConfig {
//!    type FileService = config::DefaultConfig;
//!    type DirService = config::DefaultConfig;
//!
//!    fn handle_directory(&self, _path: &Path) -> bool {
//!        true
//!    }
//!}
//!
//!fn main() {
//!    let static_files = StaticFiles::new(DummyWorker, DirectoryConfig);
//!}
//!```

#![warn(missing_docs)]
#![cfg_attr(feature = "cargo-clippy", allow(clippy::style))]

#[cfg(feature = "hyper")]
pub extern crate hyper;
pub extern crate http;
pub extern crate etag;
pub extern crate httpdate;

pub mod config;
pub mod headers;
pub mod file;
mod body;
pub mod utils;
pub mod adaptors;

use http::header::{self, HeaderValue, HeaderMap};
use http::{Method, StatusCode, Uri};
use percent_encoding::percent_decode;

pub use config::{FileServeConfig, DirectoryListingConfig, StaticFileConfig, FsTaskSpawner};
pub use body::Body;

use core::fmt;
use std::{fs, io};
use std::path::{PathBuf, Path};
use std::borrow::Cow;

#[cold]
#[inline(never)]
fn unexpected_error<T: fmt::Display, W: FsTaskSpawner, C: FileServeConfig>(error: T, response: &mut http::Response<Body<W, C>>) {
    *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
    *response.body_mut() = Body::Full(error.to_string().into())
}

#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
///Entry's in `fs`
pub enum ServeEntry {
    ///Entry is not found
    NotFound,
    ///Error when looking up entry
    ///
    ///Likely because file either doesn't exist or you lacks permissions
    IoError(io::Error),
    ///File entry is found
    File(fs::File, fs::Metadata, PathBuf),
    ///Directory entry is found
    Directory(PathBuf, fs::ReadDir),
}

/// Static files service
pub struct StaticFiles<W, C> {
    worker: W,
    config: C,
}

impl<W: Clone, C: Clone> Clone for StaticFiles<W, C> {
    fn clone(&self) -> StaticFiles<W, C> {
        Self {
            worker: self.worker.clone(),
            config: self.config.clone(),
        }
    }
}

impl<W: FsTaskSpawner> StaticFiles<W, config::DefaultConfig> {
    ///Creates new instance with default config.
    pub fn default_with(worker: W) -> Self {
        Self {
            worker,
            config: config::DefaultConfig,
        }
    }
}

impl<W: FsTaskSpawner, C: StaticFileConfig> StaticFiles<W, C> {
    ///Creates new instance with provided config
    pub fn new(worker: W, config: C) -> Self {
        Self {
            worker,
            config,
        }
    }

    ///Serves file
    pub fn serve(&self, path: &Path) -> ServeEntry {
        let mut full_path = self.config.serve_dir().join(path);

        let mut meta = match full_path.metadata() {
            Ok(meta) => meta,
            Err(_) => return ServeEntry::NotFound,
        };

        if meta.is_dir() {
            if let Some(name) = self.config.index_file(path) {
                full_path = full_path.join(name);
                meta = match full_path.metadata() {
                    Ok(meta) => meta,
                    Err(_) => return ServeEntry::NotFound,
                };
            } else if self.config.handle_directory(path) {
                return match full_path.read_dir() {
                    Ok(dir) => ServeEntry::Directory(path.to_path_buf(), dir),
                    Err(error) => ServeEntry::IoError(error),
                }
            } else {
                return ServeEntry::NotFound
            }
        }

        match fs::File::open(&full_path) {
            Ok(file) => ServeEntry::File(file, meta, full_path),
            Err(error) => ServeEntry::IoError(error),
        }
    }

    ///Handles not found directory
    pub fn handle_not_found(&self, path: &Path, out_headers: &mut http::HeaderMap) -> (StatusCode, bytes::Bytes) {
        self.config.handle_not_found(path, out_headers)
    }

    ///Get HTML page with listing
    pub fn list_dir(&self, path: &Path, dir: fs::ReadDir) -> bytes::Bytes {
        C::DirService::create_body(self.config.serve_dir(), path, dir)
    }

    ///Serve directory routine
    pub fn handle_dir(&self, path: &Path, dir: fs::ReadDir, out_headers: &mut http::HeaderMap) -> bytes::Bytes {
        const HTML: HeaderValue = HeaderValue::from_static("text/html; charset=utf-8");

        let body = C::DirService::create_body(self.config.serve_dir(), path, dir);
        out_headers.insert(header::CONTENT_TYPE, HTML);
        out_headers.insert(header::CONTENT_LENGTH, body.len().into());
        body
    }

    ///Serves file routine
    pub fn serve_file(&self, path: &Path, file: fs::File, meta: fs::Metadata, method: http::Method, headers: &http::HeaderMap, out_headers: &mut http::HeaderMap) -> (StatusCode, Body<W, C::FileService>) {
        let file_name = match path.file_name().and_then(|file_name| file_name.to_str()) {
            Some(file_name) => file_name,
            None => return (StatusCode::NOT_FOUND, Body::empty())
        };

        file::ServeFile::<W, C::FileService>::from_parts_with_cfg(file_name, file, meta).prepare(path, method, headers, out_headers)
    }

    ///Serves `http` request
    pub fn serve_http(&self, method: &Method, uri: &Uri, headers: &HeaderMap) -> http::Response<Body<W, C::FileService>> {
        const ALLOWED: HeaderValue = HeaderValue::from_static("GET, HEAD");

        let mut response = http::Response::new(Body::empty());
        if !C::is_method_allowed(method) {
            *response.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
            response.headers_mut().insert(header::ALLOW, ALLOWED);
        } else {
            let path = uri.path().trim_start_matches('/');
            let path = match percent_decode(path.as_bytes()).decode_utf8() {
                Ok(path) => path,
                Err(unexpected) => {
                    unexpected_error(unexpected, &mut response);
                    return response;
                }
            };
            let path = &path;
            let path = match path {
                Cow::Borrowed(path) => Path::new(path),
                Cow::Owned(ref path) => Path::new(path),
            };

            match self.serve(&path) {
                ServeEntry::NotFound | ServeEntry::IoError(_) => {
                    let (code, body) = self.handle_not_found(&path, response.headers_mut());
                    *response.status_mut() = code;
                    *response.body_mut() = body.into();
                },
                ServeEntry::Directory(path, dir) => {
                    *response.status_mut() = StatusCode::OK;
                    let body = self.handle_dir(&path, dir, response.headers_mut());
                    *response.body_mut() = body.into();
                },
                ServeEntry::File(file, meta, path) => {
                    let (code, body) = self.serve_file(&path, file, meta, method.clone(), headers, response.headers_mut());
                    *response.status_mut() = code;
                    *response.body_mut() = body;
                }
            }
        }
        response
    }
}

#[cfg(feature = "tokio")]
impl Default for StaticFiles<config::TokioWorker, config::DefaultConfig> {
    #[inline]
    fn default() -> Self {
        Self::default_with(config::TokioWorker)
    }
}