diff options
Diffstat (limited to 'core/src/default_impl/fs.rs')
-rw-r--r-- | core/src/default_impl/fs.rs | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/core/src/default_impl/fs.rs b/core/src/default_impl/fs.rs new file mode 100644 index 0000000..bab8af7 --- /dev/null +++ b/core/src/default_impl/fs.rs @@ -0,0 +1,262 @@ +use std::{ + path::{Path, PathBuf}, + fs::{self, File}, + io::{self, Read}, + env, + marker::PhantomData, +}; + +use checked_command::CheckedCommand; +use failure::Fail; +use futures::IntoFuture; + +use headers::header_components::{ + MediaType, + FileMeta +}; + +use crate::{ + iri::IRI, + utils::{ + SendBoxFuture, + ConstSwitch, Enabled + }, + error::{ + ResourceLoadingError, + ResourceLoadingErrorKind + }, + resource:: { + Data, + Source, + UseMediaType, + Metadata + }, + context::{ + Context, + ResourceLoaderComponent, + MaybeEncData + } +}; + +// have a scheme ignoring variant for Mux as the scheme is preset +// allow a setup with different scheme path/file etc. the behavior stays the same! +// do not handle sandboxing/security as such do not handle "file" only "path" ~use open_at if available?~ + +//TODO more doc +/// By setting SchemeValidation to Disabled the FsResourceLoader can be used to simple +/// load a resource from a file based on a scheme tail as path independent of the rest, +/// so e.g. it it is used in a `Mux` which selects a `ResourceLoader` impl based on a scheme +/// the scheme would not be double validated. +#[derive( Debug, Clone, PartialEq, Default )] +pub struct FsResourceLoader< + SchemeValidation: ConstSwitch = Enabled, +> { + root: PathBuf, + scheme: &'static str, + _marker: PhantomData<SchemeValidation> +} + +impl<SVSw> FsResourceLoader<SVSw> + where SVSw: ConstSwitch +{ + + const DEFAULT_SCHEME: &'static str = "path"; + + /// create a new file system based FileLoader, which will "just" standard _blocking_ IO + /// to read a file from the file system into a buffer + pub fn new<P: Into<PathBuf>>( root: P ) -> Self { + Self::new_with_scheme(root.into(), Self::DEFAULT_SCHEME) + } + + pub fn new_with_scheme<P: Into<PathBuf>>( root: P, scheme: &'static str ) -> Self { + FsResourceLoader { root: root.into(), scheme, _marker: PhantomData} + } + + pub fn with_cwd_root() -> Result<Self, io::Error> { + let cwd = env::current_dir()?; + Ok(Self::new(cwd)) + } + + pub fn root(&self) -> &Path { + &self.root + } + + pub fn scheme(&self) -> &'static str { + self.scheme + } + + pub fn does_validate_scheme(&self) -> bool { + SVSw::ENABLED + } + + pub fn iri_has_compatible_scheme(&self, iri: &IRI) -> bool { + iri.scheme() == self.scheme + } +} + + +impl<ValidateScheme> ResourceLoaderComponent for FsResourceLoader<ValidateScheme> + where ValidateScheme: ConstSwitch +{ + + fn load_resource(&self, source: &Source, ctx: &impl Context) + -> SendBoxFuture<MaybeEncData, ResourceLoadingError> + { + if ValidateScheme::ENABLED && !self.iri_has_compatible_scheme(&source.iri) { + let err = ResourceLoadingError + ::from(ResourceLoadingErrorKind::NotFound) + .with_source_iri_or_else(|| Some(source.iri.clone())); + + return Box::new(Err(err).into_future()); + } + + let path = self.root().join(path_from_tail(&source.iri)); + let use_media_type = source.use_media_type.clone(); + let use_file_name = source.use_file_name.clone(); + + load_data( + path, + use_media_type, + use_file_name, + ctx, + |data| Ok(MaybeEncData::EncData(data.transfer_encode(Default::default()))) + ) + } +} + + +//TODO add a PostProcess hook which can be any combination of +// FixNewline, SniffMediaType and custom postprocessing +// now this has new responsibilities +// 2. get and create File Meta +// 3. if source.media_type.is_none() do cautious mime sniffing +pub fn load_data<R, F>( + path: PathBuf, + use_media_type: UseMediaType, + use_file_name: Option<String>, + ctx: &impl Context, + post_process: F, +) -> SendBoxFuture<R, ResourceLoadingError> + where R: Send + 'static, + F: FnOnce(Data) -> Result<R, ResourceLoadingError> + Send + 'static +{ + let content_id = ctx.generate_content_id(); + ctx.offload_fn(move || { + let mut fd = File::open(&path) + .map_err(|err| { + if err.kind() == io::ErrorKind::NotFound { + err.context(ResourceLoadingErrorKind::NotFound) + } else { + err.context(ResourceLoadingErrorKind::LoadingFailed) + } + })?; + + let mut file_meta = file_meta_from_metadata(fd.metadata()?); + + if let Some(name) = use_file_name { + file_meta.file_name = Some(name) + } else { + file_meta.file_name = path.file_name() + .map(|name| name.to_string_lossy().into_owned()) + } + + let mut buffer = Vec::new(); + fd.read_to_end(&mut buffer)?; + + let media_type = + match use_media_type { + UseMediaType::Auto => { + sniff_media_type(&path)? + }, + UseMediaType::Default(media_type) => { + media_type + } + }; + + let data = Data::new(buffer, Metadata { + file_meta, + content_id, + media_type, + }); + + post_process(data) + }) + +} + +fn sniff_media_type(path: impl AsRef<Path>) -> Result<MediaType, ResourceLoadingError> { + //TODO replace current impl with conservative sniffing + let output = CheckedCommand + ::new("file") + .args(&["--brief", "--mime"]) + .arg(path.as_ref()) + .output() + .map_err(|err| err.context(ResourceLoadingErrorKind::MediaTypeDetectionFailed))?; + + let raw_media_type = String + ::from_utf8(output.stdout) + .map_err(|err| err.context(ResourceLoadingErrorKind::MediaTypeDetectionFailed))?; + + let media_type = MediaType + ::parse(raw_media_type.trim()) + .map_err(|err| err.context(ResourceLoadingErrorKind::MediaTypeDetectionFailed))?; + + Ok(media_type) +} + +//TODO implement From<MetaDate> for FileMeta instead of this +fn file_meta_from_metadata(meta: fs::Metadata) -> FileMeta { + FileMeta { + file_name: None, + creation_date: meta.created().ok().map(From::from), + modification_date: meta.modified().ok().map(From::from), + read_date: meta.accessed().ok().map(From::from), + //TODO make FileMeta.size a u64 + size: get_file_size(&meta).map(|x|x as usize), + } +} + +fn get_file_size(meta: &fs::Metadata) -> Option<u64> { + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + return Some(meta.size()); + } + #[cfg(windows)] + { + use std::os::windows::fs::MetadataExt; + return Some(meta.file_size()); + } + #[allow(unreachable_code)] + None +} + +fn path_from_tail(path_iri: &IRI) -> &Path { + let tail = path_iri.tail(); + let path = if tail.starts_with("///") { + &tail[2..] + } else { + &tail + }; + Path::new(path) +} + + +#[cfg(test)] +mod tests { + + + mod sniff_media_type { + use super::super::*; + + #[test] + fn works_reasonable_for_cargo_files() { + let res = sniff_media_type("./Cargo.toml") + .unwrap(); + + // it currently doesn't take advantage of file endings so + // all pure "text" will be text/plain + assert_eq!(res.as_str_repr(), "text/plain; charset=us-ascii"); + } + } +}
\ No newline at end of file |