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