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    fn last_modified(&self) -> Option<Self::Meta> {
52        self.last_modified_timestamp()
53            .map(|v| chrono::Utc.timestamp_opt(v, 0).unwrap().to_rfc2822())
54    }
55
56    fn last_modified_timestamp(&self) -> Option<i64> {
57        self.last_modified_timestamp
58    }
59
60    fn hash(&self) -> Self::Meta {
61        self.hash.clone()
62    }
63
64    fn etag(&self) -> Self::Meta {
65        format!("\"{}\"", self.hash)
66    }
67
68    fn mime_type(&self) -> Option<Self::Meta> {
69        self.mime_type.clone()
70    }
71}
72
73fn modified_unix_timestamp(metadata: &std::fs::Metadata) -> Option<i64> {
74    metadata.modified().ok().and_then(|modified| {
75        modified
76            .duration_since(SystemTime::UNIX_EPOCH)
77            .ok()
78            .and_then(|v| v.as_secs().try_into().ok())
79            .or_else(|| {
80                SystemTime::UNIX_EPOCH
81                    .duration_since(modified)
82                    .ok()
83                    .and_then(|v| v.as_secs().try_into().ok().map(|v: i64| -v))
84            })
85    })
86}
87
88impl DynamicFile {
89    pub fn read_from_fs<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
90        let file = std::fs::OpenOptions::new().read(true).open(&path)?;
91
92        let last_modified_timestamp = modified_unix_timestamp(&file.metadata()?);
93
94        let mut data = Vec::new();
95        BufReader::new(file).read_to_end(&mut data)?;
96
97        let mut hasher = Sha256::new();
98        hasher.update(&data);
99        let hash = hasher.finalize();
100        let hash = base85rs::encode(&hash[..]);
101
102        let mime_type = MimeGuess::from_path(&path).first().map(|v| v.to_string());
103        let name = Path::file_name(path.as_ref())
104            .expect("Unable to parse the file name")
105            .to_string_lossy()
106            .to_string();
107
108        Ok(DynamicFile {
109            name,
110            data,
111            hash,
112            last_modified_timestamp,
113            mime_type,
114        })
115    }
116}
117
118impl Debug for DynamicFile {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        f.debug_struct("DynamicFile")
121            .field("name", &self.name)
122            .field("hash", &self.hash)
123            .field("last_modified", &self.last_modified())
124            .field("mime_type", &self.mime_type)
125            .finish()
126    }
127}
128
129impl PartialEq for DynamicFile {
130    fn eq(&self, other: &Self) -> bool {
131        self.hash.eq(&other.hash)
132    }
133}