vacuna 0.4.2

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

use actix_web::{
    HttpResponse,
    body::BoxBody,
    dev::{self, Service, ServiceRequest, ServiceResponse},
    error::Error,
    guard::Guard,
    http::{
        Method,
        header::{self, AcceptEncoding, ContentEncoding, Encoding, Header, Preference},
    },
};
use futures_core::future::LocalBoxFuture;

use super::named::NamedFile;
use tracing::trace;

use crate::{
    error::FilesError,
    files::compressed_files::CompressedFileTypes,
    files::directory::{Directory, DirectoryRenderer},
    files::path_buf::PathBufWrap,
};

use actix_service::boxed::{BoxService, BoxServiceFactory};
pub(crate) type HttpService = BoxService<ServiceRequest, ServiceResponse, Error>;
pub(crate) type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>;

/// Assembled file serving service.
#[derive(Clone)]
pub struct FilesService(pub(crate) Rc<FilesServiceInner>);

impl Deref for FilesService {
    type Target = FilesServiceInner;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn file_for_encoding(
    path: &Path,
    pre_compressed: &Option<CompressedFileTypes>,
    accept: &Option<AcceptEncoding>,
) -> Option<(PathBuf, ContentEncoding, Option<ContentEncoding>)> {
    if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) {
        if let Some(pre) = pre_compressed.as_ref() {
            if let Some(acc) = accept {
                for pref in acc.ranked().iter() {
                    trace!("pref: {:?}", pref);
                    if pref == &Preference::Specific(Encoding::identity()) {
                        let p = path.with_file_name(file_name);
                        if p.exists() {
                            return Some((p, ContentEncoding::Identity, None));
                        } else {
                            for (enc, suffix) in pre.iter() {
                                trace!("looking for:{}{}", file_name, suffix);
                                let p = path.with_file_name(format!("{}{}", file_name, suffix));
                                if p.exists() {
                                    return Some((p, ContentEncoding::Identity, Some(*enc)));
                                }
                            }
                        }
                    } else {
                        for (enc, suffix) in pre.iter() {
                            trace!("enc: {:?}", enc);
                            if pref == &Preference::Specific(Encoding::Known(*enc)) || pref.is_any() {
                                trace!("looking for:{}{}", file_name, suffix);
                                let p = path.with_file_name(format!("{}{}", file_name, suffix));
                                if p.exists() {
                                    return Some((p, *enc, None));
                                }
                            }
                        }
                    }
                }
            } else {
                for (enc, suffix) in pre.iter() {
                    let p = path.with_file_name(format!("{}{}", file_name, suffix));
                    if p.exists() {
                        return Some((p, *enc, None));
                    }
                }
            }
        }
    }

    None
}

pub struct FilesServiceInner {
    pub(crate) directory: PathBuf,
    pub(crate) index: Option<String>,
    pub(crate) show_index: bool,
    pub(crate) redirect_to_slash: bool,
    pub(crate) default: Option<HttpService>,
    pub(crate) renderer: Rc<DirectoryRenderer>,
    pub(crate) guards: Option<Rc<dyn Guard>>,
    pub(crate) hidden_files: bool,
    pub(crate) pre_compressed: Option<CompressedFileTypes>,
}

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

impl FilesService {
    async fn handle_err(&self, err: io::Error, req: ServiceRequest) -> Result<ServiceResponse, Error> {
        log::debug!("error handling {}: {}", req.path(), err);

        if let Some(ref default) = self.default {
            default.call(req).await
        } else {
            Ok(req.error_response(err))
        }
    }

    fn serve_named_file(&self, req: ServiceRequest, named_file: NamedFile) -> ServiceResponse {
        let (req, _) = req.into_parts();
        let res = named_file.into_response(&req);
        ServiceResponse::new(req, res)
    }

    fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse {
        let dir = Directory::new(self.directory.clone(), path, self.pre_compressed.clone());

        let (req, _) = req.into_parts();

        (self.renderer)(&dir, &req).unwrap_or_else(|e| ServiceResponse::from_err(e, req))
    }
}

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

impl Service<ServiceRequest> for FilesService {
    type Response = ServiceResponse<BoxBody>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    dev::always_ready!();

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let is_method_valid = if let Some(guard) = &self.guards {
            // execute user defined guards
            (**guard).check(&req.guard_ctx())
        } else {
            // default behavior
            matches!(*req.method(), Method::HEAD | Method::GET)
        };

        let accept = header::AcceptEncoding::parse(req.request()).ok().clone();
        let this = self.clone();

        Box::pin(async move {
            if !is_method_valid {
                return Ok(req.into_response(
                    HttpResponse::MethodNotAllowed()
                        .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
                        .body("Request did not meet this resource's requirements."),
                ));
            }

            let path_on_disk = match PathBufWrap::parse_path(req.match_info().unprocessed(), this.hidden_files) {
                Ok(item) => item,
                Err(err) => return Ok(req.error_response(err)),
            };

            let mut path = this.directory.join(&path_on_disk);

            if path.is_dir() {
                if let Err(err) = path.canonicalize() {
                    return this.handle_err(err, req).await;
                }
                if this.redirect_to_slash && !req.path().ends_with('/') && (this.index.is_some() || this.show_index) {
                    let redirect_to = format!("{}/", req.path());

                    return Ok(req.into_response(
                        HttpResponse::Found()
                            .insert_header((header::LOCATION, redirect_to))
                            .finish(),
                    ));
                }

                trace!(" looking for index  ");
                match this.index {
                    Some(ref index) => {
                        let mut named_path = path.join(index);
                        let mut encoding = None;
                        let mut source_enconding = None;
                        if let Some((path, enc, s_enc)) = file_for_encoding(&named_path, &this.pre_compressed, &accept)
                        {
                            named_path = path;
                            encoding = Some(enc);
                            source_enconding = s_enc;
                        }
                        match NamedFile::open_async(named_path).await {
                            Ok(mut named_file) => {
                                named_file = named_file.prefer_utf8(true);
                                if let Some(enc) = encoding {
                                    named_file = named_file.set_content_encoding(enc);
                                }
                                if let Some(s_enc) = source_enconding {
                                    named_file = named_file.set_source_content_encoding(s_enc);
                                }
                                Ok(this.serve_named_file(req, named_file))
                            }
                            Err(_) if this.show_index => Ok(this.show_index(req, path)),
                            Err(err) => this.handle_err(err, req).await,
                        }
                    }
                    None if this.show_index => Ok(this.show_index(req, path)),
                    None => Ok(ServiceResponse::from_err(FilesError::IsDirectory, req.into_parts().0)),
                }
            } else {
                let mut encoding = None;
                let mut source_enconding = None;
                if let Some((p, enc, s_enc)) = file_for_encoding(&path, &this.pre_compressed, &accept) {
                    path = p;
                    encoding = Some(enc);
                    source_enconding = s_enc;
                }
                if let Err(err) = path.canonicalize() {
                    return this.handle_err(err, req).await;
                }
                match NamedFile::open_async(&path).await {
                    Ok(mut named_file) => {
                        named_file = named_file.prefer_utf8(true);
                        if let Some(enc) = encoding {
                            named_file = named_file.set_content_encoding(enc);
                        }
                        if let Some(s_enc) = source_enconding {
                            named_file = named_file.set_source_content_encoding(s_enc);
                        }
                        let (req, _) = req.into_parts();
                        let res = named_file.into_response(&req);
                        Ok(ServiceResponse::new(req, res))
                    }
                    Err(err) => this.handle_err(err, req).await,
                }
            }
        })
    }
}