mail_core_ng/default_impl/
fs.rs1use 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#[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 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
128pub 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 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
207fn 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 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 assert_eq!(res.as_str_repr(), "text/plain; charset=us-ascii");
260 }
261 }
262}