~repos /rust-embed

#rust#proc-macro#http

git clone https://pyrossh.dev/repos/rust-embed.git

rust macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev.


59dad3da pyros2097

4 years ago
Merge pull request #142 from pyros2097/feature/file-metadata
Cargo.toml CHANGED
@@ -43,6 +43,9 @@ tokio = { version = "0.2", optional = true, features = ["macros"] }
43
43
  warp = { version = "0.2", default-features = false, optional = true }
44
44
  rocket = { version = "0.4.5", default-features = false, optional = true }
45
45
 
46
+ [dev-dependencies]
47
+ sha2 = "0.9"
48
+
46
49
  [features]
47
50
  debug-embed = ["rust-embed-impl/debug-embed", "rust-embed-utils/debug-embed"]
48
51
  interpolate-folder-path = ["rust-embed-impl/interpolate-folder-path"]
examples/actix.rs CHANGED
@@ -12,7 +12,7 @@ struct Asset;
12
12
  fn handle_embedded_file(path: &str) -> HttpResponse {
13
13
  match Asset::get(path) {
14
14
  Some(content) => {
15
- let body: Body = match content {
15
+ let body: Body = match content.data {
16
16
  Cow::Borrowed(bytes) => bytes.into(),
17
17
  Cow::Owned(bytes) => bytes.into(),
18
18
  };
examples/basic.rs CHANGED
@@ -6,5 +6,5 @@ struct Asset;
6
6
 
7
7
  fn main() {
8
8
  let index_html = Asset::get("index.html").unwrap();
9
- println!("{:?}", std::str::from_utf8(index_html.as_ref()));
9
+ println!("{:?}", std::str::from_utf8(index_html.data.as_ref()));
10
10
  }
examples/rocket.rs CHANGED
@@ -18,7 +18,7 @@ struct Asset;
18
18
  fn index<'r>() -> response::Result<'r> {
19
19
  Asset::get("index.html").map_or_else(
20
20
  || Err(Status::NotFound),
21
- |d| response::Response::build().header(ContentType::HTML).sized_body(Cursor::new(d)).ok(),
21
+ |d| response::Response::build().header(ContentType::HTML).sized_body(Cursor::new(d.data)).ok(),
22
22
  )
23
23
  }
24
24
 
@@ -34,7 +34,7 @@ fn dist<'r>(file: PathBuf) -> response::Result<'r> {
34
34
  .and_then(OsStr::to_str)
35
35
  .ok_or_else(|| Status::new(400, "Could not get file extension"))?;
36
36
  let content_type = ContentType::from_extension(ext).ok_or_else(|| Status::new(400, "Could not get file content type"))?;
37
- response::Response::build().header(content_type).sized_body(Cursor::new(d)).ok()
37
+ response::Response::build().header(content_type).sized_body(Cursor::new(d.data)).ok()
38
38
  },
39
39
  )
40
40
  }
examples/warp.rs CHANGED
@@ -26,7 +26,7 @@ fn serve_impl(path: &str) -> Result<impl Reply, Rejection> {
26
26
  let asset = Asset::get(path).ok_or_else(warp::reject::not_found)?;
27
27
  let mime = mime_guess::from_path(path).first_or_octet_stream();
28
28
 
29
- let mut res = Response::new(asset.into());
29
+ let mut res = Response::new(asset.data.into());
30
30
  res.headers_mut().insert("content-type", HeaderValue::from_str(mime.as_ref()).unwrap());
31
31
  Ok(res)
32
32
  }
impl/src/lib.rs CHANGED
@@ -46,7 +46,7 @@ fn embedded(ident: &syn::Ident, folder_path: String, prefix: Option<&str>) -> To
46
46
  quote! {
47
47
  #not_debug_attr
48
48
  impl #ident {
49
- pub fn get(file_path: &str) -> Option<std::borrow::Cow<'static, [u8]>> {
49
+ pub fn get(file_path: &str) -> Option<rust_embed::EmbeddedFile> {
50
50
  #handle_prefix
51
51
  match file_path.replace("\\", "/").as_str() {
52
52
  #(#match_values)*
@@ -66,7 +66,7 @@ fn embedded(ident: &syn::Ident, folder_path: String, prefix: Option<&str>) -> To
66
66
 
67
67
  #not_debug_attr
68
68
  impl rust_embed::RustEmbed for #ident {
69
- fn get(file_path: &str) -> Option<std::borrow::Cow<'static, [u8]>> {
69
+ fn get(file_path: &str) -> Option<rust_embed::EmbeddedFile> {
70
70
  #ident::get(file_path)
71
71
  }
72
72
  fn iter() -> rust_embed::Filenames {
@@ -89,19 +89,11 @@ fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>) -> Tok
89
89
  quote! {
90
90
  #[cfg(debug_assertions)]
91
91
  impl #ident {
92
- pub fn get(file_path: &str) -> Option<std::borrow::Cow<'static, [u8]>> {
92
+ pub fn get(file_path: &str) -> Option<rust_embed::EmbeddedFile> {
93
- use std::fs;
94
- use std::path::Path;
95
-
96
93
  #handle_prefix
97
94
 
98
- let file_path = Path::new(#folder_path).join(file_path.replace("\\", "/"));
95
+ let file_path = std::path::Path::new(#folder_path).join(file_path.replace("\\", "/"));
99
- match fs::read(file_path) {
96
+ rust_embed::utils::read_file_from_fs(&file_path).ok()
100
- Ok(contents) => Some(std::borrow::Cow::from(contents)),
101
- Err(_e) => {
102
- return None
103
- }
104
- }
105
97
  }
106
98
 
107
99
  pub fn iter() -> impl Iterator<Item = std::borrow::Cow<'static, str>> {
@@ -113,7 +105,7 @@ fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>) -> Tok
113
105
 
114
106
  #[cfg(debug_assertions)]
115
107
  impl rust_embed::RustEmbed for #ident {
116
- fn get(file_path: &str) -> Option<std::borrow::Cow<'static, [u8]>> {
108
+ fn get(file_path: &str) -> Option<rust_embed::EmbeddedFile> {
117
109
  #ident::get(file_path)
118
110
  }
119
111
  fn iter() -> rust_embed::Filenames {
@@ -138,25 +130,34 @@ fn generate_assets(ident: &syn::Ident, folder_path: String, prefix: Option<Strin
138
130
  }
139
131
  }
140
132
 
141
- #[cfg(not(feature = "compression"))]
142
133
  fn embed_file(rel_path: &str, full_canonical_path: &str) -> TokenStream2 {
134
+ let file = rust_embed_utils::read_file_from_fs(Path::new(full_canonical_path)).expect("File should be readable");
135
+ let hash = file.metadata.sha256_hash();
136
+ let last_modified = match file.metadata.last_modified() {
137
+ Some(last_modified) => quote! { Some(#last_modified) },
138
+ None => quote! { None },
139
+ };
140
+
141
+ let embedding_code = if cfg!(feature = "compression") {
143
- quote! {
142
+ quote! {
143
+ rust_embed::flate!(static FILE: [u8] from #full_canonical_path);
144
- #rel_path => {
144
+ let bytes = &FILE[..];
145
+ }
146
+ } else {
147
+ quote! {
145
- let bytes = &include_bytes!(#full_canonical_path)[..];
148
+ let bytes = &include_bytes!(#full_canonical_path)[..];
146
- Some(std::borrow::Cow::from(bytes))
147
- },
148
- }
149
+ }
149
- }
150
+ };
150
151
 
151
- #[cfg(feature = "compression")]
152
- fn embed_file(rel_path: &str, full_canonical_path: &str) -> TokenStream2 {
153
152
  quote! {
154
- #rel_path => {
153
+ #rel_path => {
155
- rust_embed::flate!(static FILE: [u8] from #full_canonical_path);
154
+ #embedding_code
156
155
 
157
- let bytes = &FILE[..];
156
+ Some(rust_embed::EmbeddedFile {
158
- Some(std::borrow::Cow::from(bytes))
157
+ data: std::borrow::Cow::from(bytes),
158
+ metadata: rust_embed::Metadata::__rust_embed_new([#(#hash),*], #last_modified)
159
- },
159
+ })
160
+ }
160
161
  }
161
162
  }
162
163
 
@@ -213,7 +214,7 @@ fn impl_rust_embed(ast: &syn::DeriveInput) -> TokenStream2 {
213
214
  when the `interpolate-folder-path` feature is enabled.";
214
215
  }
215
216
 
216
- panic!(message);
217
+ panic!("{}", message);
217
218
  };
218
219
 
219
220
  generate_assets(&ast.ident, folder_path, prefix)
src/lib.rs CHANGED
@@ -8,8 +8,9 @@ pub use include_flate::flate;
8
8
  extern crate rust_embed_impl;
9
9
  pub use rust_embed_impl::*;
10
10
 
11
+ pub use rust_embed_utils::{EmbeddedFile, Metadata};
12
+
11
13
  #[doc(hidden)]
12
- #[cfg(all(debug_assertions, not(feature = "debug-embed")))]
13
14
  pub extern crate rust_embed_utils as utils;
14
15
 
15
16
  /// A directory of binary assets.
@@ -37,7 +38,7 @@ pub trait RustEmbed {
37
38
  ///
38
39
  /// Otherwise, the bytes are read from the file system on each call and a
39
40
  /// `Cow::Owned(Vec<u8>)` is returned.
40
- fn get(file_path: &str) -> Option<std::borrow::Cow<'static, [u8]>>;
41
+ fn get(file_path: &str) -> Option<EmbeddedFile>;
41
42
 
42
43
  /// Iterates the files in this assets folder.
43
44
  ///
tests/interpolated_path.rs CHANGED
@@ -7,15 +7,9 @@ struct Asset;
7
7
 
8
8
  #[test]
9
9
  fn get_works() {
10
- if Asset::get("index.html").is_none() {
10
+ assert!(Asset::get("index.html").is_some(), "index.html should exist");
11
- panic!("index.html should exist");
12
- }
13
- if Asset::get("gg.html").is_some() {
11
+ assert!(Asset::get("gg.html").is_none(), "gg.html should not exist");
14
- panic!("gg.html should not exist");
15
- }
16
- if Asset::get("images/llama.png").is_none() {
12
+ assert!(Asset::get("images/llama.png").is_some(), "llama.png should exist");
17
- panic!("llama.png should exist");
18
- }
19
13
  }
20
14
 
21
15
  #[test]
tests/lib.rs CHANGED
@@ -7,15 +7,9 @@ struct Asset;
7
7
 
8
8
  #[test]
9
9
  fn get_works() {
10
- if Asset::get("index.html").is_none() {
10
+ assert!(Asset::get("index.html").is_some(), "index.html should exist");
11
- panic!("index.html should exist");
12
- }
13
- if Asset::get("gg.html").is_some() {
11
+ assert!(Asset::get("gg.html").is_none(), "gg.html should not exist");
14
- panic!("gg.html should not exist");
15
- }
16
- if Asset::get("images/llama.png").is_none() {
12
+ assert!(Asset::get("images/llama.png").is_some(), "llama.png should exist");
17
- panic!("llama.png should exist");
18
- }
19
13
  }
20
14
 
21
15
  /// Using Windows-style path separators (`\`) is acceptable
tests/metadata.rs ADDED
@@ -0,0 +1,24 @@
1
+ use rust_embed::{EmbeddedFile, RustEmbed};
2
+ use sha2::Digest;
3
+
4
+ #[derive(RustEmbed)]
5
+ #[folder = "examples/public/"]
6
+ struct Asset;
7
+
8
+ #[test]
9
+ fn hash_is_accurate() {
10
+ let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
11
+ let mut hasher = sha2::Sha256::new();
12
+ hasher.update(index_file.data);
13
+ let expected_hash: [u8; 32] = hasher.finalize().into();
14
+
15
+ assert_eq!(index_file.metadata.sha256_hash(), expected_hash);
16
+ }
17
+
18
+ #[test]
19
+ fn last_modified_is_accurate() {
20
+ let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
21
+ let expected_datetime_utc = 1527818165;
22
+
23
+ assert_eq!(index_file.metadata.last_modified(), Some(expected_datetime_utc));
24
+ }
utils/Cargo.toml CHANGED
@@ -13,6 +13,7 @@ edition = "2018"
13
13
 
14
14
  [dependencies]
15
15
  walkdir = "2.3.1"
16
+ sha2 = "0.9"
16
17
 
17
18
  [features]
18
19
  debug-embed = []
utils/src/lib.rs CHANGED
@@ -1,4 +1,11 @@
1
1
  #![forbid(unsafe_code)]
2
+
3
+ use sha2::Digest;
4
+ use std::borrow::Cow;
5
+ use std::path::Path;
6
+ use std::time::SystemTime;
7
+ use std::{fs, io};
8
+
2
9
  #[cfg_attr(all(debug_assertions, not(feature = "debug-embed")), allow(unused))]
3
10
  pub struct FileEntry {
4
11
  pub rel_path: String,
@@ -26,6 +33,57 @@ pub fn get_files(folder_path: String) -> impl Iterator<Item = FileEntry> {
26
33
  })
27
34
  }
28
35
 
36
+ /// A file embedded into the binary
37
+ pub struct EmbeddedFile {
38
+ pub data: Cow<'static, [u8]>,
39
+ pub metadata: Metadata,
40
+ }
41
+
42
+ /// Metadata about an embedded file
43
+ pub struct Metadata {
44
+ hash: [u8; 32],
45
+ last_modified: Option<u64>,
46
+ }
47
+
48
+ impl Metadata {
49
+ #[doc(hidden)]
50
+ pub fn __rust_embed_new(hash: [u8; 32], last_modified: Option<u64>) -> Self {
51
+ Self { hash, last_modified }
52
+ }
53
+
54
+ /// The SHA256 hash of the file
55
+ pub fn sha256_hash(&self) -> [u8; 32] {
56
+ self.hash
57
+ }
58
+
59
+ /// The last modified date in seconds since the UNIX epoch. If the underlying
60
+ /// platform/file-system does not support this, None is returned.
61
+ pub fn last_modified(&self) -> Option<u64> {
62
+ self.last_modified
63
+ }
64
+ }
65
+
66
+ pub fn read_file_from_fs(file_path: &Path) -> io::Result<EmbeddedFile> {
67
+ let data = fs::read(file_path)?;
68
+ let data = Cow::from(data);
69
+
70
+ let mut hasher = sha2::Sha256::new();
71
+ hasher.update(&data);
72
+ let hash: [u8; 32] = hasher.finalize().into();
73
+
74
+ let last_modified = fs::metadata(file_path)?.modified().ok().map(|last_modified| {
75
+ last_modified
76
+ .duration_since(SystemTime::UNIX_EPOCH)
77
+ .expect("Time before the UNIX epoch is unsupported")
78
+ .as_secs()
79
+ });
80
+
81
+ Ok(EmbeddedFile {
82
+ data,
83
+ metadata: Metadata { hash, last_modified },
84
+ })
85
+ }
86
+
29
87
  fn path_to_str<P: AsRef<std::path::Path>>(p: P) -> String {
30
88
  p.as_ref().to_str().expect("Path does not have a string representation").to_owned()
31
89
  }