rust_embed_for_web_utils/file/
dynamic.rs

1use std::{
2    convert::TryInto,
3    fmt::Debug,
4    io::{BufReader, Read},
5    path::Path,
6    time::SystemTime,
7};
8
9use chrono::TimeZone;
10use new_mime_guess::MimeGuess;
11use sha2::{Digest, Sha256};
12
13use super::common::EmbedableFile;
14
15/// A file read from the file system dynamically.
16///
17/// `rust-embed-for-web` changes which type of file you get based on whether
18/// it's a debug or release build. In debug builds, you'll get `DynamicFile`s.
19///
20/// You should interface with this object using the `EmbedableFile` trait, which
21/// is implemented for both the embedded and dynamic files.
22#[derive(Clone)]
23pub struct DynamicFile {
24    name: String,
25    data: Vec<u8>,
26    hash: String,
27    last_modified_timestamp: Option<i64>,
28    mime_type: Option<String>,
29}
30
31impl EmbedableFile for DynamicFile {
32    type Data = Vec<u8>;
33    type Meta = String;
34
35    fn name(&self) -> Self::Meta {
36        self.name.clone()
37    }
38
39    fn data(&self) -> Self::Data {
40        self.data.clone()
41    }
42
43    fn data_gzip(&self) -> Option<Self::Data> {
44        None
45    }
46
47    fn data_br(&self) -> Option<Self::Data> {
48        None
49    }
50
51    #[cfg(feature = "compression-zstd")]
52    fn data_zstd(&self) -> Option<Self::Data> {
53        None
54    }
55
56    fn last_modified(&self) -> Option<Self::Meta> {
57        self.last_modified_timestamp()
58            .map(|v| chrono::Utc.timestamp_opt(v, 0).unwrap().to_rfc2822())
59    }
60
61    fn last_modified_timestamp(&self) -> Option<i64> {
62        self.last_modified_timestamp
63    }
64
65    fn hash(&self) -> Self::Meta {
66        self.hash.clone()
67    }
68
69    fn etag(&self) -> Self::Meta {
70        format!("\"{}\"", self.hash)
71    }
72
73    fn mime_type(&self) -> Option<Self::Meta> {
74        self.mime_type.clone()
75    }
76}
77
78fn modified_unix_timestamp(metadata: &std::fs::Metadata) -> Option<i64> {
79    metadata.modified().ok().and_then(|modified| {
80        modified
81            .duration_since(SystemTime::UNIX_EPOCH)
82            .ok()
83            .and_then(|v| v.as_secs().try_into().ok())
84            .or_else(|| {
85                SystemTime::UNIX_EPOCH
86                    .duration_since(modified)
87                    .ok()
88                    .and_then(|v| v.as_secs().try_into().ok().map(|v: i64| -v))
89            })
90    })
91}
92
93impl DynamicFile {
94    pub fn read_from_fs<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
95        let file = std::fs::OpenOptions::new().read(true).open(&path)?;
96
97        let last_modified_timestamp = modified_unix_timestamp(&file.metadata()?);
98
99        let mut data = Vec::new();
100        BufReader::new(file).read_to_end(&mut data)?;
101
102        let mut hasher = Sha256::new();
103        hasher.update(&data);
104        let hash = hasher.finalize();
105        let hash = base85rs::encode(&hash[..]);
106
107        let mime_type = MimeGuess::from_path(&path).first().map(|v| v.to_string());
108        let name = Path::file_name(path.as_ref())
109            .expect("Unable to parse the file name")
110            .to_string_lossy()
111            .to_string();
112
113        Ok(DynamicFile {
114            name,
115            data,
116            hash,
117            last_modified_timestamp,
118            mime_type,
119        })
120    }
121}
122
123impl Debug for DynamicFile {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        f.debug_struct("DynamicFile")
126            .field("name", &self.name)
127            .field("hash", &self.hash)
128            .field("last_modified", &self.last_modified())
129            .field("mime_type", &self.mime_type)
130            .finish()
131    }
132}
133
134impl PartialEq for DynamicFile {
135    fn eq(&self, other: &Self) -> bool {
136        self.hash.eq(&other.hash)
137    }
138}