1use crate::io::{AssetReader, AssetReaderError, Reader};
2use crate::io::{AssetSource, PathStream};
3use crate::{AssetApp, AssetPlugin};
4use alloc::boxed::Box;
5use bevy_app::{App, Plugin};
6use bevy_tasks::ConditionalSendFuture;
7use std::path::{Path, PathBuf};
8use tracing::warn;
9
10#[derive(Default)]
57pub struct WebAssetPlugin {
58 pub silence_startup_warning: bool,
59}
60
61impl Plugin for WebAssetPlugin {
62 fn build(&self, app: &mut App) {
63 if !self.silence_startup_warning {
64 warn!("WebAssetPlugin is potentially insecure! Make sure to verify asset URLs are safe to load before loading them. \
65 If you promise you know what you're doing, you can silence this warning by setting silence_startup_warning: true \
66 in the WebAssetPlugin construction.");
67 }
68 if app.is_plugin_added::<AssetPlugin>() {
69 warn!("WebAssetPlugin must be added before AssetPlugin for it to work!");
70 }
71 #[cfg(feature = "http")]
72 app.register_asset_source(
73 "http",
74 AssetSource::build()
75 .with_reader(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 AssetSource::build()
83 .with_reader(move || Box::new(WebAssetReader::Https))
84 .with_processed_reader(move || Box::new(WebAssetReader::Https)),
85 );
86 }
87}
88
89pub enum WebAssetReader {
91 Http,
93 Https,
95}
96
97impl WebAssetReader {
98 fn make_uri(&self, path: &Path) -> PathBuf {
99 let prefix = match self {
100 Self::Http => "http://",
101 Self::Https => "https://",
102 };
103 PathBuf::from(prefix).join(path)
104 }
105
106 fn make_meta_uri(&self, path: &Path) -> PathBuf {
108 let meta_path = crate::io::get_meta_path(path);
109 self.make_uri(&meta_path)
110 }
111}
112
113#[cfg(target_arch = "wasm32")]
114async fn get<'a>(path: PathBuf) -> Result<Box<dyn Reader>, AssetReaderError> {
115 use crate::io::wasm::HttpWasmAssetReader;
116
117 HttpWasmAssetReader::new("")
118 .fetch_bytes(path)
119 .await
120 .map(|r| Box::new(r) as Box<dyn Reader>)
121}
122
123#[cfg(not(target_arch = "wasm32"))]
124async fn get(path: PathBuf) -> Result<Box<dyn Reader>, AssetReaderError> {
125 use crate::io::VecReader;
126 use alloc::{borrow::ToOwned, boxed::Box, vec::Vec};
127 use bevy_platform::sync::LazyLock;
128 use blocking::unblock;
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 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 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
181impl 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#[cfg(all(not(target_arch = "wasm32"), feature = "web_asset_cache"))]
210mod 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)]
257mod 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}