vacuna 0.4.3

Simple web server for static files
Documentation
use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc};

use actix_service::ServiceFactory;
use actix_web::{
    HttpRequest,
    dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse},
    error::Error,
    guard::Guard,
    http::header::ContentEncoding,
};

use futures_core::future::LocalBoxFuture;

use crate::{
    config::{PreCompType, PreCompressed},
    files::directory::{Directory, DirectoryRenderer, directory_listing},
    files::service::{FilesService, FilesServiceInner},
};

use crate::files::service::HttpNewService;

#[derive(Clone, Debug)]
pub struct CompressedFileTypes {
    pre: Vec<(ContentEncoding, String)>,
}
impl From<&Vec<PreCompressed>> for CompressedFileTypes {
    fn from(value: &Vec<PreCompressed>) -> Self {
        let source = if value.is_empty() {
            PreCompressed::defaults()
        } else {
            value.clone()
        };

        let mut pre = Vec::new();
        for p in source {
            let enc = match p.compression_type {
                PreCompType::Gzip => ContentEncoding::Gzip,
                PreCompType::Brotli => ContentEncoding::Brotli,
                PreCompType::Zstd => ContentEncoding::Zstd,
                PreCompType::None => ContentEncoding::Identity,
            };
            pre.push((enc, p.extension.to_owned()));
        }
        Self { pre }
    }
}

impl CompressedFileTypes {
    pub fn iter(&self) -> std::slice::Iter<'_, (ContentEncoding, String)> {
        self.pre.iter()
    }
}

/// Static files handling service.
///
/// `Files` service must be registered with `App::service()` method.
///
/// # Examples
/// ```
/// use actix_web::App;
/// use actix_files::Files;
///
/// let app = App::new()
///     .service(Files::new("/static", "."));
/// ```
pub struct CompressedFiles {
    mount_path: String,
    directory: PathBuf,
    index: Option<String>,
    show_index: bool,
    redirect_to_slash: bool,
    default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
    renderer: Rc<DirectoryRenderer>,
    use_guards: Option<Rc<dyn Guard>>,
    guards: Vec<Rc<dyn Guard>>,
    hidden_files: bool,
    pre_compressed: Option<CompressedFileTypes>,
}

impl fmt::Debug for CompressedFiles {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("Files")
    }
}

impl Clone for CompressedFiles {
    fn clone(&self) -> Self {
        Self {
            directory: self.directory.clone(),
            index: self.index.clone(),
            show_index: self.show_index,
            redirect_to_slash: self.redirect_to_slash,
            default: self.default.clone(),
            renderer: self.renderer.clone(),
            mount_path: self.mount_path.clone(),
            use_guards: self.use_guards.clone(),
            guards: self.guards.clone(),
            hidden_files: self.hidden_files,
            pre_compressed: self.pre_compressed.clone(),
        }
    }
}

impl CompressedFiles {
    pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> CompressedFiles {
        let orig_dir = serve_from.into();
        let dir = match orig_dir.canonicalize() {
            Ok(canon_dir) => canon_dir,
            Err(_) => {
                log::error!("Specified path is not a directory: {:?}", orig_dir);
                PathBuf::new()
            }
        };

        CompressedFiles {
            mount_path: mount_path.trim_end_matches('/').to_owned(),
            directory: dir,
            index: None,
            show_index: false,
            redirect_to_slash: false,
            default: Rc::new(RefCell::new(None)),
            renderer: Rc::new(directory_listing),
            use_guards: None,
            guards: Vec::new(),
            hidden_files: false,
            pre_compressed: None,
        }
    }

    /// Show files listing for directories.
    ///
    /// By default show files listing is disabled.
    ///
    /// When used with [`Files::index_file()`], files listing is shown as a fallback
    /// when the index file is not found.
    pub fn show_files_listing(mut self) -> Self {
        self.show_index = true;
        self
    }

    /// Redirects to a slash-ended path when browsing a directory.
    ///
    /// By default never redirect.
    #[allow(unused)]
    pub fn redirect_to_slash_directory(mut self) -> Self {
        self.redirect_to_slash = true;
        self
    }

    /// Set custom directory renderer.
    #[allow(unused)]
    pub fn files_listing_renderer<F>(mut self, f: F) -> Self
    where
        for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> + 'static,
    {
        self.renderer = Rc::new(f);
        self
    }

    /// Set index file
    ///
    /// Shows specific index file for directories instead of
    /// showing files listing.
    ///
    /// If the index file is not found, files listing is shown as a fallback if
    /// [`Files::show_files_listing()`] is set.
    pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
        self.index = Some(index.into());
        self
    }

    /// Adds a routing guard.
    ///
    /// Use this to allow multiple chained file services that respond to strictly different
    /// properties of a request. Due to the way routing works, if a guard check returns true and the
    /// request starts being handled by the file service, it will not be able to back-out and try
    /// the next service, you will simply get a 404 (or 405) error response.
    ///
    /// To allow `POST` requests to retrieve files, see [`Files::method_guard()`].
    ///
    /// # Examples
    /// ```
    /// use actix_web::{guard::Header, App};
    /// use actix_files::Files;
    ///
    /// App::new().service(
    ///     Files::new("/","/my/site/files")
    ///         .guard(Header("Host", "example.com"))
    /// );
    /// ```
    #[allow(unused)]
    pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
        self.guards.push(Rc::new(guard));
        self
    }

    /// Specifies guard to check before fetching directory listings or files.
    ///
    /// Note that this guard has no effect on routing; it's main use is to guard on the request's
    /// method just before serving the file, only allowing `GET` and `HEAD` requests by default.
    /// See [`Files::guard`] for routing guards.
    #[allow(unused)]
    pub fn method_guard<G: Guard + 'static>(mut self, guard: G) -> Self {
        self.use_guards = Some(Rc::new(guard));
        self
    }

    /// Enables serving hidden files and directories, allowing a leading dots in url fragments.
    #[allow(unused)]
    pub fn use_hidden_files(mut self) -> Self {
        self.hidden_files = true;
        self
    }

    /// Enables serving pre_compressed files.
    pub fn pre_compressed(mut self, pre: CompressedFileTypes) -> Self {
        self.pre_compressed = Some(pre);
        self
    }
}

impl HttpServiceFactory for CompressedFiles {
    fn register(mut self, config: &mut AppService) {
        let guards = if self.guards.is_empty() {
            None
        } else {
            let guards = std::mem::take(&mut self.guards);
            Some(
                guards
                    .into_iter()
                    .map(|guard| -> Box<dyn Guard> { Box::new(guard) })
                    .collect::<Vec<_>>(),
            )
        };

        if self.default.borrow().is_none() {
            *self.default.borrow_mut() = Some(config.default_service());
        }

        let rdef = if config.is_root() {
            ResourceDef::root_prefix(&self.mount_path)
        } else {
            ResourceDef::prefix(&self.mount_path)
        };

        config.register_service(rdef, guards, self, None)
    }
}

impl ServiceFactory<ServiceRequest> for CompressedFiles {
    type Response = ServiceResponse;
    type Error = Error;
    type Config = ();
    type Service = FilesService;
    type InitError = ();
    type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;

    fn new_service(&self, _: ()) -> Self::Future {
        let mut inner = FilesServiceInner {
            directory: self.directory.clone(),
            index: self.index.clone(),
            show_index: self.show_index,
            redirect_to_slash: self.redirect_to_slash,
            default: None,
            renderer: self.renderer.clone(),
            guards: self.use_guards.clone(),
            hidden_files: self.hidden_files,
            pre_compressed: self.pre_compressed.clone(),
        };

        if let Some(ref default) = *self.default.borrow() {
            let fut = default.new_service(());
            Box::pin(async {
                match fut.await {
                    Ok(default) => {
                        inner.default = Some(default);
                        Ok(FilesService(Rc::new(inner)))
                    }
                    Err(_) => Err(()),
                }
            })
        } else {
            Box::pin(async move { Ok(FilesService(Rc::new(inner))) })
        }
    }
}