Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ http = ["bevy_internal/http"]
# Enables downloading assets from HTTPS sources. Warning: there are security implications. Read the docs on WebAssetPlugin.
https = ["bevy_internal/https"]

# Enable caching downloaded assets on the filesystem. NOTE: this cache currently never invalidates entries!
# Enable http caching for downloaded assets on the filesystem.
web_asset_cache = ["bevy_internal/web_asset_cache"]

# Enable stepping-based debugging of Bevy systems
Expand Down
8 changes: 7 additions & 1 deletion crates/bevy_asset/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ embedded_watcher = ["file_watcher"]
multi_threaded = ["bevy_tasks/multi_threaded"]
http = ["blocking", "ureq"]
https = ["blocking", "ureq", "ureq/rustls", "ureq/platform-verifier"]
web_asset_cache = []
web_asset_cache = ["http-cache-ureq"]
asset_processor = []
watch = []
trace = []
Expand All @@ -39,6 +39,12 @@ bevy_platform = { path = "../bevy_platform", version = "0.19.0-dev", default-fea
"std",
] }

# http-cache-ureq = { version = "1.0.0-alpha.4", optional = true, default-features = false, features = [
http-cache-ureq = { git = "https://github.com/06chaynes/http-cache", branch = "main", optional = true, default-features = false, features = [
"manager-cacache",
"url-ada",
] }

stackfuture = { version = "0.3", default-features = false }
atomicow = { version = "1.1", default-features = false, features = ["std"] }
async-broadcast = { version = "0.7.2", default-features = false }
Expand Down
193 changes: 91 additions & 102 deletions crates/bevy_asset/src/io/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use crate::io::{AssetReader, AssetReaderError, AssetSourceBuilder, PathStream, R
use crate::{AssetApp, AssetPlugin};
use alloc::boxed::Box;
use bevy_app::{App, Plugin};
use bevy_tasks::ConditionalSendFuture;
use std::path::{Path, PathBuf};
use tracing::warn;

Expand Down Expand Up @@ -122,76 +121,116 @@ async fn get<'a>(path: PathBuf) -> Result<Box<dyn Reader>, AssetReaderError> {
#[cfg(not(target_arch = "wasm32"))]
async fn get(path: PathBuf) -> Result<Box<dyn Reader>, AssetReaderError> {
use crate::io::VecReader;
use alloc::{borrow::ToOwned, boxed::Box, vec::Vec};
use alloc::{borrow::ToOwned, boxed::Box};
use bevy_platform::sync::LazyLock;
use blocking::unblock;
use std::io::{self, BufReader, Read};
use std::io;

let str_path = path.to_str().ok_or_else(|| {
AssetReaderError::Io(
io::Error::other(std::format!("non-utf8 path: {}", path.display())).into(),
)
})?;

#[cfg(all(not(target_arch = "wasm32"), feature = "web_asset_cache"))]
if let Some(data) = web_asset_cache::try_load_from_cache(str_path).await? {
return Ok(Box::new(VecReader::new(data)));
// When the "web_asset_cache" feature is enabled, use http-cache's ureq integration.
#[cfg(feature = "web_asset_cache")]
{
use http_cache_ureq::{CACacheManager, CachedAgent};

static CACHED_AGENT: LazyLock<CachedAgent<CACacheManager>> = LazyLock::new(|| {
let cache_path = PathBuf::from(".web-asset-cache");
let manager = CACacheManager::new(cache_path, true);
CachedAgent::builder()
.cache_manager(manager)
.build()
.expect("failed to build http-cache ureq CachedAgent")
});

let uri = str_path.to_owned();
// The http-cache library already handles async execution internally
let result = CACHED_AGENT.get(&uri).call().await;

match result {
Ok(response) => {
let status = response.status();
if status == 404 {
return Err(AssetReaderError::NotFound(path));
}
if status >= 400 {
return Err(AssetReaderError::HttpError(status));
}

Ok(Box::new(VecReader::new(response.into_bytes())))
}
Err(err) => Err(AssetReaderError::Io(
io::Error::other(std::format!(
"unexpected error while loading asset {}: {}",
path.display(),
err
))
.into(),
)),
}
}
use ureq::tls::{RootCerts, TlsConfig};
use ureq::Agent;

static AGENT: LazyLock<Agent> = LazyLock::new(|| {
Agent::config_builder()
.tls_config(
TlsConfig::builder()
.root_certs(RootCerts::PlatformVerifier)
.build(),
)
.build()
.new_agent()
});

let uri = str_path.to_owned();
// Use [`unblock`] to run the http request on a separately spawned thread as to not block bevy's
// async executor.
let response = unblock(|| AGENT.get(uri).call()).await;

match response {
Ok(mut response) => {
let mut reader = BufReader::new(response.body_mut().with_config().reader());

let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;

#[cfg(all(not(target_arch = "wasm32"), feature = "web_asset_cache"))]
web_asset_cache::save_to_cache(str_path, &buffer).await?;

Ok(Box::new(VecReader::new(buffer)))
}
// ureq considers all >=400 status codes as errors
Err(ureq::Error::StatusCode(code)) => {
if code == 404 {
Err(AssetReaderError::NotFound(path))
} else {
Err(AssetReaderError::HttpError(code))
// Without "web_asset_cache", fall back to plain ureq.
#[cfg(not(feature = "web_asset_cache"))]
{
use alloc::vec::Vec;
use blocking::unblock;
use std::io::{BufReader, Read};
use ureq::tls::{RootCerts, TlsConfig};
use ureq::Agent;

static AGENT: LazyLock<Agent> = LazyLock::new(|| {
Agent::config_builder()
.tls_config(
TlsConfig::builder()
.root_certs(RootCerts::PlatformVerifier)
.build(),
)
.build()
.new_agent()
});

let uri = str_path.to_owned();
// Use [`unblock`] to run the http request on a separately spawned thread as to not block bevy's
// async executor.
let response = unblock(|| AGENT.get(uri).call()).await;

match response {
Ok(mut response) => {
let mut reader = BufReader::new(response.body_mut().with_config().reader());
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;

Ok(Box::new(VecReader::new(buffer)))
}
// ureq considers all >=400 status codes as errors
Err(ureq::Error::StatusCode(code)) => {
if code == 404 {
Err(AssetReaderError::NotFound(path))
} else {
Err(AssetReaderError::HttpError(code))
}
}
Err(err) => Err(AssetReaderError::Io(
io::Error::other(std::format!(
"unexpected error while loading asset {}: {}",
path.display(),
err
))
.into(),
)),
}
Err(err) => Err(AssetReaderError::Io(
io::Error::other(std::format!(
"unexpected error while loading asset {}: {}",
path.display(),
err
))
.into(),
)),
}
}

impl AssetReader for WebAssetReader {
fn read<'a>(
&'a self,
path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<Box<dyn Reader>, AssetReaderError>> {
) -> impl bevy_tasks::ConditionalSendFuture<Output = Result<Box<dyn Reader>, AssetReaderError>>
{
get(self.make_uri(path))
}

Expand All @@ -212,56 +251,6 @@ impl AssetReader for WebAssetReader {
}
}

/// A naive implementation of a cache for assets downloaded from the web that never invalidates.
/// `ureq` currently does not support caching, so this is a simple workaround.
/// It should eventually be replaced by `http-cache` or similar, see [tracking issue](https://github.com/06chaynes/http-cache/issues/91)
#[cfg(all(not(target_arch = "wasm32"), feature = "web_asset_cache"))]
mod web_asset_cache {
use alloc::string::String;
use alloc::vec::Vec;
use core::hash::{Hash, Hasher};
use futures_lite::AsyncWriteExt;
use std::collections::hash_map::DefaultHasher;
use std::io;
use std::path::PathBuf;

use crate::io::Reader;

const CACHE_DIR: &str = ".web-asset-cache";

fn url_to_hash(url: &str) -> String {
let mut hasher = DefaultHasher::new();
url.hash(&mut hasher);
std::format!("{:x}", hasher.finish())
}

pub async fn try_load_from_cache(url: &str) -> Result<Option<Vec<u8>>, io::Error> {
let filename = url_to_hash(url);
let cache_path = PathBuf::from(CACHE_DIR).join(&filename);

if cache_path.exists() {
let mut file = async_fs::File::open(&cache_path).await?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await?;
Ok(Some(buffer))
} else {
Ok(None)
}
}

pub async fn save_to_cache(url: &str, data: &[u8]) -> Result<(), io::Error> {
let filename = url_to_hash(url);
let cache_path = PathBuf::from(CACHE_DIR).join(&filename);

async_fs::create_dir_all(CACHE_DIR).await.ok();

let mut cache_file = async_fs::File::create(&cache_path).await?;
cache_file.write_all(data).await?;

Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ http = ["bevy_asset?/http"]
# Enables downloading assets from HTTPS sources
https = ["bevy_asset?/https"]

# Enable caching downloaded assets on the filesystem. NOTE: this cache currently never invalidates entries!
# Enable http caching for downloaded assets on the filesystem.
web_asset_cache = ["bevy_asset?/web_asset_cache"]

# Enables the built-in asset processor for processed assets.
Expand Down
2 changes: 1 addition & 1 deletion docs/cargo_features.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ This is the complete `bevy` cargo feature list, without "profiles" or "collectio
|wav|WAV audio format support|
|wayland|Wayland display server support|
|web|Enables use of browser APIs. Note this is currently only applicable on `wasm32` architectures.|
|web_asset_cache|Enable caching downloaded assets on the filesystem. NOTE: this cache currently never invalidates entries!|
|web_asset_cache|Enable http caching for downloaded assets on the filesystem.|
|webgl2|Enable some limitations to be able to use WebGL2. Please refer to the [WebGL2 and WebGPU](https://github.com/bevyengine/bevy/tree/latest/examples#webgl2-and-webgpu) section of the examples README for more information on how to run Wasm builds with WebGPU.|
|webgpu|Enable support for WebGPU in Wasm. When enabled, this feature will override the `webgl2` feature and you won't be able to run Wasm builds with WebGL2, only with WebGPU.|
|webp|WebP image format support|
Expand Down
3 changes: 1 addition & 2 deletions examples/asset/web_asset.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//! Example usage of the `https` asset source to load assets from the web.
//!
//! Run with the feature `https`, and optionally `web_asset_cache`
//! for a simple caching mechanism that never invalidates.
//! Run with the feature `https`, and optionally `web_asset_cache` for http caching.
//!
use bevy::{asset::io::web::WebAssetPlugin, prelude::*};

Expand Down
Loading