mail_core_ng/default_impl/
fs.rs

1use std::{
2    path::{Path, PathBuf},
3    fs::{self, File},
4    io::{self, Read},
5    env,
6    marker::PhantomData,
7};
8
9use checked_command::CheckedCommand;
10use failure::Fail;
11use futures::IntoFuture;
12
13use headers::header_components::{
14    MediaType,
15    FileMeta
16};
17
18use crate::{
19    iri::IRI,
20    utils::{
21        SendBoxFuture,
22        ConstSwitch, Enabled
23    },
24    error::{
25        ResourceLoadingError,
26        ResourceLoadingErrorKind
27    },
28    resource:: {
29        Data,
30        Source,
31        UseMediaType,
32        Metadata
33    },
34    context::{
35        Context,
36        ResourceLoaderComponent,
37        MaybeEncData
38    }
39};
40
41// have a scheme ignoring variant for Mux as the scheme is preset
42// allow a setup with different scheme path/file etc. the behavior stays the same!
43// do not handle sandboxing/security as such do not handle "file" only "path" ~use open_at if available?~
44
45//TODO more doc
46/// By setting SchemeValidation to Disabled the FsResourceLoader can be used to simple
47/// load a resource from a file based on a scheme tail as path independent of the rest,
48/// so e.g. it it is used in a `Mux` which selects a `ResourceLoader` impl based on a scheme
49/// the scheme would not be double validated.
50#[derive( Debug, Clone, PartialEq, Default )]
51pub struct FsResourceLoader<
52    SchemeValidation: ConstSwitch = Enabled,
53> {
54    root: PathBuf,
55    scheme: &'static str,
56    _marker: PhantomData<SchemeValidation>
57}
58
59impl<SVSw> FsResourceLoader<SVSw>
60    where SVSw: ConstSwitch
61{
62
63    const DEFAULT_SCHEME: &'static str = "path";
64
65    /// create a new file system based FileLoader, which will  "just" standard _blocking_ IO
66    /// to read a file from the file system into a buffer
67    pub fn new<P: Into<PathBuf>>( root: P ) -> Self {
68        Self::new_with_scheme(root.into(), Self::DEFAULT_SCHEME)
69    }
70
71    pub fn new_with_scheme<P: Into<PathBuf>>( root: P, scheme: &'static str ) -> Self {
72        FsResourceLoader { root: root.into(), scheme, _marker: PhantomData}
73    }
74
75    pub fn with_cwd_root() -> Result<Self, io::Error> {
76        let cwd = env::current_dir()?;
77        Ok(Self::new(cwd))
78    }
79
80    pub fn root(&self) -> &Path {
81        &self.root
82    }
83
84    pub fn scheme(&self) -> &'static str {
85        self.scheme
86    }
87
88    pub fn does_validate_scheme(&self) -> bool {
89        SVSw::ENABLED
90    }
91
92    pub fn iri_has_compatible_scheme(&self, iri: &IRI) -> bool {
93        iri.scheme() == self.scheme
94    }
95}
96
97
98impl<ValidateScheme> ResourceLoaderComponent for FsResourceLoader<ValidateScheme>
99    where ValidateScheme: ConstSwitch
100{
101
102    fn load_resource(&self, source: &Source, ctx: &impl Context)
103        -> SendBoxFuture<MaybeEncData, ResourceLoadingError>
104    {
105        if ValidateScheme::ENABLED && !self.iri_has_compatible_scheme(&source.iri) {
106            let err = ResourceLoadingError
107                ::from(ResourceLoadingErrorKind::NotFound)
108                .with_source_iri_or_else(|| Some(source.iri.clone()));
109
110            return Box::new(Err(err).into_future());
111        }
112
113        let path = self.root().join(path_from_tail(&source.iri));
114        let use_media_type = source.use_media_type.clone();
115        let use_file_name = source.use_file_name.clone();
116
117        load_data(
118            path,
119            use_media_type,
120            use_file_name,
121            ctx,
122            |data| Ok(MaybeEncData::EncData(data.transfer_encode(Default::default())))
123        )
124    }
125}
126
127
128//TODO add a PostProcess hook which can be any combination of
129// FixNewline, SniffMediaType and custom postprocessing
130// now this has new responsibilities
131// 2. get and create File Meta
132// 3. if source.media_type.is_none() do cautious mime sniffing
133pub fn load_data<R, F>(
134    path: PathBuf,
135    use_media_type: UseMediaType,
136    use_file_name: Option<String>,
137    ctx: &impl Context,
138    post_process: F,
139) -> SendBoxFuture<R, ResourceLoadingError>
140    where R: Send + 'static,
141          F: FnOnce(Data) -> Result<R, ResourceLoadingError> + Send + 'static
142{
143    let content_id = ctx.generate_content_id();
144    ctx.offload_fn(move || {
145        let mut fd = File::open(&path)
146            .map_err(|err| {
147                if err.kind() == io::ErrorKind::NotFound {
148                    err.context(ResourceLoadingErrorKind::NotFound)
149                } else {
150                    err.context(ResourceLoadingErrorKind::LoadingFailed)
151                }
152            })?;
153
154        let mut file_meta = file_meta_from_metadata(fd.metadata()?);
155
156        if let Some(name) = use_file_name {
157            file_meta.file_name = Some(name)
158        } else {
159            file_meta.file_name = path.file_name()
160                .map(|name| name.to_string_lossy().into_owned())
161        }
162
163        let mut buffer = Vec::new();
164        fd.read_to_end(&mut buffer)?;
165
166        let media_type =
167            match use_media_type {
168                UseMediaType::Auto => {
169                    sniff_media_type(&path)?
170                },
171                UseMediaType::Default(media_type) => {
172                    media_type
173                }
174            };
175
176        let data = Data::new(buffer, Metadata {
177            file_meta,
178            content_id,
179            media_type,
180        });
181
182        post_process(data)
183    })
184
185}
186
187fn sniff_media_type(path: impl AsRef<Path>) -> Result<MediaType, ResourceLoadingError> {
188    //TODO replace current  impl with conservative sniffing
189    let output = CheckedCommand
190        ::new("file")
191        .args(&["--brief", "--mime"])
192        .arg(path.as_ref())
193        .output()
194        .map_err(|err| err.context(ResourceLoadingErrorKind::MediaTypeDetectionFailed))?;
195
196    let raw_media_type = String
197        ::from_utf8(output.stdout)
198        .map_err(|err| err.context(ResourceLoadingErrorKind::MediaTypeDetectionFailed))?;
199
200    let media_type = MediaType
201        ::parse(raw_media_type.trim())
202        .map_err(|err| err.context(ResourceLoadingErrorKind::MediaTypeDetectionFailed))?;
203
204    Ok(media_type)
205}
206
207//TODO implement From<MetaDate> for FileMeta instead of this
208fn file_meta_from_metadata(meta: fs::Metadata) -> FileMeta {
209    FileMeta {
210        file_name: None,
211        creation_date: meta.created().ok().map(From::from),
212        modification_date: meta.modified().ok().map(From::from),
213        read_date: meta.accessed().ok().map(From::from),
214        //TODO make FileMeta.size a u64
215        size: get_file_size(&meta).map(|x|x as usize),
216    }
217}
218
219fn get_file_size(meta: &fs::Metadata) -> Option<u64> {
220    #[cfg(unix)]
221    {
222        use std::os::unix::fs::MetadataExt;
223        return Some(meta.size());
224    }
225    #[cfg(windows)]
226    {
227        use std::os::windows::fs::MetadataExt;
228        return Some(meta.file_size());
229    }
230    #[allow(unreachable_code)]
231    None
232}
233
234fn path_from_tail(path_iri: &IRI) -> &Path {
235    let tail = path_iri.tail();
236    let path = if tail.starts_with("///") {
237        &tail[2..]
238    } else {
239        &tail
240    };
241    Path::new(path)
242}
243
244
245#[cfg(test)]
246mod tests {
247
248
249    mod sniff_media_type {
250        use super::super::*;
251
252        #[test]
253        fn works_reasonable_for_cargo_files() {
254            let res = sniff_media_type("./Cargo.toml")
255                .unwrap();
256
257            // it currently doesn't take advantage of file endings so
258            // all pure "text" will be text/plain
259            assert_eq!(res.as_str_repr(), "text/plain; charset=us-ascii");
260        }
261    }
262}