1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
//! Vial can serve static files out of an asset directory and
//! optionally bundle them into your application in `--release` mode.
//!
//! By setting an asset directory, either through the
//! [`vial::asset_dir!()`][macro.asset_dir.html] or
//! [`vial::bundle_assets!()`][macro.bundle_assets.html] macro,
//! you can then use the methods in this module to work with them:
//!
//! - **[asset::etag()](#method.etag)**: Get the ETag for an asset.
//!   Used automatically by the Router if a web request matches an
//!   asset's path.
//! - **[asset::exists()](#method.exists)**: Does an asset exist?
//!   Works regardless of whether the asset is bundled or not.
//! - **[asset::is_bundled()](#method.is_bundled)**: Are assets
//!   bundled? Only true in `--release` mode and when used with the
//!   `vial::bundle_assets!()` macro.
//! - **[asset::to_string()](#method.to_string)**: Like
//!   `fs::read_to_string()`, delivers the content of an asset as a
//!   `String`.
//! - **[asset::as_reader()](#method.as_reader)**: Like
//!   `asset::to_string()` but provides an `io::Read` of an asset,
//!   whether or not it's bundled.
//!
//! To get started, put all your `.js` and `.css` and other static
//! assets into a directory in the root of your project, then
//! reference them in HTML  as if the root of your Vial web
//! application was that asset directory.
//!
//! Next call [`vial::asset_dir!()`][macro.asset_dir.html] with the
//! path to your asset directory (maybe `assets/`?) before starting
//! your application with [`vial::run!`](macro.run.html):
//!
//! If we had a directory structure like this:
//!     .
//!     ├── README.md
//!     ├── assets
//!     │   └── img
//!     │       ├── banker.png
//!     │       └── doctor.png
//!     └── src
//!         └── main.rs
//!
//! We could serve our images like so:
//!
//! ```no_run
//! vial::routes! {
//!     GET "/" => |_| "
//!         <p><img src='/img/doctor.png'/></p>
//!         <p><img src='/img/banker.png'/></p>
//!     ";
//! }
//!
//! fn main() {
//!     vial::asset_dir!("assets/");
//!     vial::run!().unwrap();
//! }
//! ```
//!
//!
use {
    crate::{util, Error, Result},
    std::{
        borrow::Cow,
        collections::{hash_map::DefaultHasher, HashMap},
        fs,
        hash::{Hash, Hasher},
        io::{self, BufReader, Read},
        str,
    },
};

/// Produce an etag for an asset.
pub fn etag(path: &str) -> Cow<str> {
    if is_bundled() {
        Cow::from(crate::BUILD_DATE)
    } else {
        let mut hasher = DefaultHasher::new();
        last_modified(path).hash(&mut hasher);
        Cow::from(format!("{:x}", hasher.finish()))
    }
}

/// The last modified time for an asset on disk.
/// Does nothing if `is_bundled()` is true.
fn last_modified(path: &str) -> Option<String> {
    if is_bundled() {
        return None;
    }

    let path = normalize_path(path)?;
    if let Ok(meta) = fs::metadata(path) {
        if let Ok(time) = meta.modified() {
            return Some(format!("{:?}", time));
        }
    }
    None
}

/// Cleans a path of tricky things like `..` and puts it in a format
/// we can use in other asset functions.
pub fn normalize_path(path: &str) -> Option<String> {
    if let Some(root) = asset_dir() {
        Some(format!(
            "{}/{}",
            root.trim_end_matches('/'),
            path.trim_start_matches(root)
                .trim_start_matches('.')
                .trim_start_matches('/')
                .replace("..", ".")
        ))
    } else {
        None
    }
}

/// Have assets been bundled into the binary?
pub fn is_bundled() -> bool {
    bundled_assets().is_some()
}

/// Access to read-only, in-memory assets in bundle mode.
fn bundled_assets() -> Option<&'static HashMap<String, &'static [u8]>> {
    unsafe { crate::BUNDLED_ASSETS.as_ref() }
}

/// Size of an asset in `asset_dir()`. `0` if the asset doesn't exist.
/// Works in bundled mode and regular mode.
pub fn size(path: &str) -> usize {
    if !exists(path) {
        return 0;
    }

    let path = match normalize_path(path) {
        Some(path) => path,
        None => return 0,
    };

    if is_bundled() {
        bundled_assets()
            .unwrap()
            .get(&path)
            .map(|a| a.len())
            .unwrap_or(0)
    } else {
        util::file_size(&path)
    }
}

/// The directory of the asset dir.
fn asset_dir() -> Option<&'static String> {
    unsafe { crate::ASSET_DIR.as_ref() }
}

/// Does the asset exist on disk? `path` is the path relative to
/// `ASSET_DIR` ex: asset::exists("index.html") checks for
/// "./static/index.html" if `ASSET_DIR` is set to `static`.
/// Works both in regular mode and bundle mode.
pub fn exists(path: &str) -> bool {
    if let Some(path) = normalize_path(path) {
        if is_bundled() {
            return bundled_assets().unwrap().contains_key(&path);
        } else if let Ok(file) = fs::File::open(path) {
            if let Ok(meta) = file.metadata() {
                return !meta.is_dir();
            }
        }
    }
    false
}

/// Like fs::read_to_string(), but with an asset.
pub fn to_string(path: &str) -> Result<String> {
    if let Some(bytes) = read(path) {
        if let Ok(utf8) = str::from_utf8(bytes.as_ref()) {
            return Ok(utf8.to_string());
        }
    }

    Err(Error::AssetNotFound(path.into()))
}

/// Produces a boxed `io::Read` for an asset.
pub fn as_reader(path: &str) -> Option<Box<dyn io::Read>> {
    let path = normalize_path(path)?;
    if is_bundled() {
        if let Some(v) = bundled_assets().unwrap().get(&path) {
            return Some(Box::new(*v));
        }
    } else if let Ok(file) = fs::File::open(path) {
        if let Ok(meta) = file.metadata() {
            if !meta.is_dir() {
                return Some(Box::new(BufReader::new(file)));
            }
        }
    }
    None
}

/// Read an asset to [u8].
pub fn read(path: &str) -> Option<Cow<'static, [u8]>> {
    let path = normalize_path(path)?;
    if is_bundled() {
        if let Some(v) = bundled_assets().unwrap().get(&path) {
            return Some(Cow::from(*v));
        }
    } else {
        let mut buf = vec![];
        if let Ok(mut file) = fs::File::open(path) {
            if let Ok(meta) = file.metadata() {
                if !meta.is_dir() && file.read_to_end(&mut buf).is_ok() {
                    return Some(Cow::from(buf));
                }
            }
        }
    }
    None
}