Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_asset/src/io/web.rs
6600 views
1
use crate::io::{AssetReader, AssetReaderError, Reader};
2
use crate::io::{AssetSource, PathStream};
3
use crate::{AssetApp, AssetPlugin};
4
use alloc::{borrow::ToOwned, boxed::Box};
5
use bevy_app::{App, Plugin};
6
use bevy_tasks::ConditionalSendFuture;
7
use blocking::unblock;
8
use std::path::{Path, PathBuf};
9
use tracing::warn;
10
11
/// Adds the `http` and `https` asset sources to the app.
12
///
13
/// NOTE: Make sure to add this plugin *before* `AssetPlugin` to properly register http asset sources.
14
///
15
/// WARNING: be careful about where your URLs are coming from! URLs can potentially be exploited by an
16
/// attacker to trigger vulnerabilities in our asset loaders, or DOS by downloading enormous files. We
17
/// are not aware of any such vulnerabilities at the moment, just be careful!
18
///
19
/// Any asset path that begins with `http` (when the `http` feature is enabled) or `https` (when the
20
/// `https` feature is enabled) will be loaded from the web via `fetch` (wasm) or `ureq` (native).
21
///
22
/// Example usage:
23
///
24
/// ```rust
25
/// # use bevy_app::{App, Startup};
26
/// # use bevy_ecs::prelude::{Commands, Res};
27
/// # use bevy_asset::web::{WebAssetPlugin, AssetServer};
28
/// # struct DefaultPlugins;
29
/// # impl DefaultPlugins { fn set(plugin: WebAssetPlugin) -> WebAssetPlugin { plugin } }
30
/// # use bevy_asset::web::AssetServer;
31
/// # #[derive(Asset, TypePath, Default)]
32
/// # struct Image;
33
/// # #[derive(Component)]
34
/// # struct Sprite;
35
/// # impl Sprite { fn from_image(_: Handle<Image>) -> Self { Sprite } }
36
/// # fn main() {
37
/// App::new()
38
/// .add_plugins(DefaultPlugins.set(WebAssetPlugin {
39
/// silence_startup_warning: true,
40
/// }))
41
/// # .add_systems(Startup, setup).run();
42
/// # }
43
/// // ...
44
/// # fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
45
/// commands.spawn(Sprite::from_image(asset_server.load("https://example.com/favicon.png")));
46
/// # }
47
/// ```
48
///
49
/// By default, `ureq`'s HTTP compression is disabled. To enable gzip and brotli decompression, add
50
/// the following dependency and features to your Cargo.toml. This will improve bandwidth
51
/// utilization when its supported by the server.
52
///
53
/// ```toml
54
/// [target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
55
/// ureq = { version = "3", default-features = false, features = ["gzip", "brotli"] }
56
/// ```
57
#[derive(Default)]
58
pub struct WebAssetPlugin {
59
pub silence_startup_warning: bool,
60
}
61
62
impl Plugin for WebAssetPlugin {
63
fn build(&self, app: &mut App) {
64
if !self.silence_startup_warning {
65
warn!("WebAssetPlugin is potentially insecure! Make sure to verify asset URLs are safe to load before loading them. \
66
If you promise you know what you're doing, you can silence this warning by setting silence_startup_warning: true \
67
in the WebAssetPlugin construction.");
68
}
69
if app.is_plugin_added::<AssetPlugin>() {
70
warn!("WebAssetPlugin must be added before AssetPlugin for it to work!");
71
}
72
#[cfg(feature = "http")]
73
app.register_asset_source(
74
"http",
75
AssetSource::build()
76
.with_reader(move || Box::new(WebAssetReader::Http))
77
.with_processed_reader(move || Box::new(WebAssetReader::Http)),
78
);
79
80
#[cfg(feature = "https")]
81
app.register_asset_source(
82
"https",
83
AssetSource::build()
84
.with_reader(move || Box::new(WebAssetReader::Https))
85
.with_processed_reader(move || Box::new(WebAssetReader::Https)),
86
);
87
}
88
}
89
90
/// Asset reader that treats paths as urls to load assets from.
91
pub enum WebAssetReader {
92
/// Unencrypted connections.
93
Http,
94
/// Use TLS for setting up connections.
95
Https,
96
}
97
98
impl WebAssetReader {
99
fn make_uri(&self, path: &Path) -> PathBuf {
100
let prefix = match self {
101
Self::Http => "http://",
102
Self::Https => "https://",
103
};
104
PathBuf::from(prefix).join(path)
105
}
106
107
/// See [`io::get_meta_path`](`crate::io::get_meta_path`)
108
fn make_meta_uri(&self, path: &Path) -> PathBuf {
109
let meta_path = crate::io::get_meta_path(path);
110
self.make_uri(&meta_path)
111
}
112
}
113
114
#[cfg(target_arch = "wasm32")]
115
async fn get<'a>(path: PathBuf) -> Result<Box<dyn Reader>, AssetReaderError> {
116
use crate::io::wasm::HttpWasmAssetReader;
117
118
HttpWasmAssetReader::new("")
119
.fetch_bytes(path)
120
.await
121
.map(|r| Box::new(r) as Box<dyn Reader>)
122
}
123
124
#[cfg(not(target_arch = "wasm32"))]
125
async fn get(path: PathBuf) -> Result<Box<dyn Reader>, AssetReaderError> {
126
use crate::io::VecReader;
127
use alloc::{boxed::Box, vec::Vec};
128
use bevy_platform::sync::LazyLock;
129
use std::io::{self, BufReader, Read};
130
131
let str_path = path.to_str().ok_or_else(|| {
132
AssetReaderError::Io(
133
io::Error::other(std::format!("non-utf8 path: {}", path.display())).into(),
134
)
135
})?;
136
137
#[cfg(all(not(target_arch = "wasm32"), feature = "web_asset_cache"))]
138
if let Some(data) = web_asset_cache::try_load_from_cache(str_path).await? {
139
return Ok(Box::new(VecReader::new(data)));
140
}
141
use ureq::Agent;
142
143
static AGENT: LazyLock<Agent> = LazyLock::new(|| Agent::config_builder().build().new_agent());
144
145
let uri = str_path.to_owned();
146
// Use [`unblock`] to run the http request on a separately spawned thread as to not block bevy's
147
// async executor.
148
let response = unblock(|| AGENT.get(uri).call()).await;
149
150
match response {
151
Ok(mut response) => {
152
let mut reader = BufReader::new(response.body_mut().with_config().reader());
153
154
let mut buffer = Vec::new();
155
reader.read_to_end(&mut buffer)?;
156
157
#[cfg(all(not(target_arch = "wasm32"), feature = "web_asset_cache"))]
158
web_asset_cache::save_to_cache(str_path, &buffer).await?;
159
160
Ok(Box::new(VecReader::new(buffer)))
161
}
162
// ureq considers all >=400 status codes as errors
163
Err(ureq::Error::StatusCode(code)) => {
164
if code == 404 {
165
Err(AssetReaderError::NotFound(path))
166
} else {
167
Err(AssetReaderError::HttpError(code))
168
}
169
}
170
Err(err) => Err(AssetReaderError::Io(
171
io::Error::other(std::format!(
172
"unexpected error while loading asset {}: {}",
173
path.display(),
174
err
175
))
176
.into(),
177
)),
178
}
179
}
180
181
impl AssetReader for WebAssetReader {
182
fn read<'a>(
183
&'a self,
184
path: &'a Path,
185
) -> impl ConditionalSendFuture<Output = Result<Box<dyn Reader>, AssetReaderError>> {
186
get(self.make_uri(path))
187
}
188
189
async fn read_meta<'a>(&'a self, path: &'a Path) -> Result<Box<dyn Reader>, AssetReaderError> {
190
let uri = self.make_meta_uri(path);
191
get(uri).await
192
}
193
194
async fn is_directory<'a>(&'a self, _path: &'a Path) -> Result<bool, AssetReaderError> {
195
Ok(false)
196
}
197
198
async fn read_directory<'a>(
199
&'a self,
200
path: &'a Path,
201
) -> Result<Box<PathStream>, AssetReaderError> {
202
Err(AssetReaderError::NotFound(self.make_uri(path)))
203
}
204
}
205
206
/// A naive implementation of a cache for assets downloaded from the web that never invalidates.
207
/// `ureq` currently does not support caching, so this is a simple workaround.
208
/// It should eventually be replaced by `http-cache` or similar, see [tracking issue](https://github.com/06chaynes/http-cache/issues/91)
209
#[cfg(all(not(target_arch = "wasm32"), feature = "web_asset_cache"))]
210
mod web_asset_cache {
211
use alloc::string::String;
212
use alloc::vec::Vec;
213
use core::hash::{Hash, Hasher};
214
use futures_lite::AsyncWriteExt;
215
use std::collections::hash_map::DefaultHasher;
216
use std::io;
217
use std::path::PathBuf;
218
219
use crate::io::Reader;
220
221
const CACHE_DIR: &str = ".web-asset-cache";
222
223
fn url_to_hash(url: &str) -> String {
224
let mut hasher = DefaultHasher::new();
225
url.hash(&mut hasher);
226
std::format!("{:x}", hasher.finish())
227
}
228
229
pub async fn try_load_from_cache(url: &str) -> Result<Option<Vec<u8>>, io::Error> {
230
let filename = url_to_hash(url);
231
let cache_path = PathBuf::from(CACHE_DIR).join(&filename);
232
233
if cache_path.exists() {
234
let mut file = async_fs::File::open(&cache_path).await?;
235
let mut buffer = Vec::new();
236
file.read_to_end(&mut buffer).await?;
237
Ok(Some(buffer))
238
} else {
239
Ok(None)
240
}
241
}
242
243
pub async fn save_to_cache(url: &str, data: &[u8]) -> Result<(), io::Error> {
244
let filename = url_to_hash(url);
245
let cache_path = PathBuf::from(CACHE_DIR).join(&filename);
246
247
async_fs::create_dir_all(CACHE_DIR).await.ok();
248
249
let mut cache_file = async_fs::File::create(&cache_path).await?;
250
cache_file.write_all(data).await?;
251
252
Ok(())
253
}
254
}
255
256
#[cfg(test)]
257
mod tests {
258
use super::*;
259
260
#[test]
261
fn make_http_uri() {
262
assert_eq!(
263
WebAssetReader::Http
264
.make_uri(Path::new("example.com/favicon.png"))
265
.to_str()
266
.unwrap(),
267
"http://example.com/favicon.png"
268
);
269
}
270
271
#[test]
272
fn make_https_uri() {
273
assert_eq!(
274
WebAssetReader::Https
275
.make_uri(Path::new("example.com/favicon.png"))
276
.to_str()
277
.unwrap(),
278
"https://example.com/favicon.png"
279
);
280
}
281
282
#[test]
283
fn make_http_meta_uri() {
284
assert_eq!(
285
WebAssetReader::Http
286
.make_meta_uri(Path::new("example.com/favicon.png"))
287
.to_str()
288
.unwrap(),
289
"http://example.com/favicon.png.meta"
290
);
291
}
292
293
#[test]
294
fn make_https_meta_uri() {
295
assert_eq!(
296
WebAssetReader::Https
297
.make_meta_uri(Path::new("example.com/favicon.png"))
298
.to_str()
299
.unwrap(),
300
"https://example.com/favicon.png.meta"
301
);
302
}
303
304
#[test]
305
fn make_https_without_extension_meta_uri() {
306
assert_eq!(
307
WebAssetReader::Https
308
.make_meta_uri(Path::new("example.com/favicon"))
309
.to_str()
310
.unwrap(),
311
"https://example.com/favicon.meta"
312
);
313
}
314
}
315
316