Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add http & https asset sources #16366

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0eeb08c
feat: http_source initial commit
mrchantey Nov 13, 2024
c0d3b3c
docs: update README
mrchantey Nov 13, 2024
f4cdc63
feat: http_cache_surf
mrchantey Nov 13, 2024
66046b9
docs: add backticks
mrchantey Nov 13, 2024
9160a1d
patch
mrchantey Nov 13, 2024
35b943c
fix: wasm build
mrchantey Nov 13, 2024
4c2702d
fix: wasm build
mrchantey Nov 13, 2024
f73e61f
Merge branch 'bevyengine:main' into upstream-bevy-web-asset
mrchantey Nov 17, 2024
91385f1
replace `surf` with `ureq`
mrchantey Nov 18, 2024
71d8fb2
cargo format
mrchantey Nov 18, 2024
7e9611f
rename http_source example
mrchantey Nov 18, 2024
7ca96f2
fix: asset source metadata
mrchantey Dec 6, 2024
9aa844e
fix: map ureq 404 to AssetReadError::NotFound
mrchantey Dec 6, 2024
ad69a6d
rename: WebAsseetReader > HttpSourceAssetReader
mrchantey Dec 6, 2024
a031226
build templated page
mrchantey Dec 6, 2024
777aaa9
format
mrchantey Dec 6, 2024
a82a954
refactor: reuse HttpWasmAssetReader::fetch_bytes
mrchantey Dec 6, 2024
adbfc5e
format
mrchantey Dec 6, 2024
4225cf1
patch
mrchantey Dec 6, 2024
fd8e6b3
fix: box reader
mrchantey Dec 6, 2024
3d51c1e
docs: fix references
mrchantey Dec 6, 2024
b46a410
feat: simple native asset cache
mrchantey Dec 6, 2024
dca682a
fix: clippy
mrchantey Dec 6, 2024
9c42d34
use AssertReaderError::HttpError
mrchantey Dec 7, 2024
6abb4e9
use hash instead of filename
mrchantey Dec 7, 2024
d9bd718
Merge branch 'main' into upstream-bevy-web-asset
mrchantey Dec 7, 2024
d67677f
prefer core to std
mrchantey Dec 6, 2024
1a9fae2
feat: top level `http_source` & `http_source_cache` features
mrchantey Dec 11, 2024
df8e119
fix: http_source features
mrchantey Dec 11, 2024
eb71d70
docs: update cargo features
mrchantey Dec 11, 2024
e1fc632
Add required feature for http_source example
mrchantey Dec 12, 2024
d4e72b0
Merge branch 'main' into upstream-bevy-web-asset
mockersf Dec 14, 2024
27f69f9
use example.com url
mrchantey Dec 14, 2024
5d87bb1
format
mrchantey Dec 14, 2024
0d58f97
ureq default-features=false
mrchantey Dec 18, 2024
20b6cca
allow "Unicode-3.0" & "MPL-2.0" licenses
mrchantey Dec 24, 2024
b423a92
ureq disable default features
mrchantey Dec 24, 2024
c675287
remove ureq tls feature
mrchantey Dec 24, 2024
88d04f6
docs: add license issue context
mrchantey Dec 24, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dxil.dll
assets/**/*.meta
crates/bevy_asset/imported_assets
imported_assets
.http-asset-cache

# Bevy Examples
example_showcase_config.ron
Expand Down
13 changes: 12 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1637,12 +1637,23 @@ path = "examples/asset/extra_source.rs"
doc-scrape-examples = true

[package.metadata.example.extra_asset_source]
name = "Extra asset source"
name = "Extra Asset Source"
description = "Load an asset from a non-standard asset source"
category = "Assets"
# Uses non-standard asset path
wasm = false

[[example]]
name = "http_source"
path = "examples/asset/http_source.rs"
doc-scrape-examples = true

[package.metadata.example.http_source]
name = "HTTP Asset Source"
description = "Load an asset from a http source"
category = "Assets"
wasm = true
mrchantey marked this conversation as resolved.
Show resolved Hide resolved

[[example]]
name = "hot_asset_reloading"
path = "examples/asset/hot_asset_reloading.rs"
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_asset/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ keywords = ["bevy"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[features]
default = ["http_source"]
mrchantey marked this conversation as resolved.
Show resolved Hide resolved
file_watcher = ["notify-debouncer-full", "watch"]
embedded_watcher = ["file_watcher"]
multi_threaded = ["bevy_tasks/multi_threaded"]
http_source = ["ureq"]
asset_processor = []
watch = []
trace = []
Expand Down Expand Up @@ -63,6 +65,7 @@ js-sys = "0.3"

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
notify-debouncer-full = { version = "0.4.0", optional = true }
ureq = { version = "2.10", optional = true }
mrchantey marked this conversation as resolved.
Show resolved Hide resolved

[dev-dependencies]
bevy_core = { path = "../bevy_core", version = "0.15.0-dev" }
Expand Down
242 changes: 242 additions & 0 deletions crates/bevy_asset/src/http_source.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
use crate::io::{AssetReader, AssetReaderError, Reader};
use crate::io::{AssetSource, PathStream};
use crate::AssetApp;
use bevy_app::App;
use bevy_utils::ConditionalSendFuture;
use std::path::{Path, PathBuf};

/// Adds the `http` and `https` asset sources to the app.
/// Any asset path that begins with `http` or `https` will be loaded from the web
/// via `fetch`(wasm) or `surf`(native).
mrchantey marked this conversation as resolved.
Show resolved Hide resolved
pub fn http_source_plugin(app: &mut App) {
app.register_asset_source(
"http",
AssetSource::build().with_reader(|| Box::new(HttpSourceAssetReader::Http)),
);
app.register_asset_source(
"https",
AssetSource::build().with_reader(|| Box::new(HttpSourceAssetReader::Https)),
);
}

/// Asset reader that treats paths as urls to load assets from.
pub enum HttpSourceAssetReader {
/// Unencrypted connections.
Http,
/// Use TLS for setting up connections.
Https,
}

impl HttpSourceAssetReader {
fn make_uri(&self, path: &Path) -> PathBuf {
PathBuf::from(match self {
Self::Http => "http://",
Self::Https => "https://",
})
.join(path)
}

/// See [`crate::io::get_meta_path`]
fn make_meta_uri(&self, path: &Path) -> Option<PathBuf> {
let mut uri = self.make_uri(path);
let mut extension = path.extension()?.to_os_string();
extension.push(".meta");
uri.set_extension(extension);
Some(uri)
}
}

#[cfg(target_arch = "wasm32")]
async fn get<'a>(path: PathBuf) -> Result<Box<dyn Reader>, AssetReaderError> {
use crate::io::wasm::HttpWasmAssetReader;

HttpWasmAssetReader::new("")
.fetch_bytes(path)
.await
.map(|r| Box::new(r) as Box<dyn Reader>)
}

#[cfg(not(target_arch = "wasm32"))]
async fn get<'a>(path: PathBuf) -> Result<Box<dyn Reader>, AssetReaderError> {
use crate::io::VecReader;
use std::io;

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

if let Some(data) = http_asset_cache::try_load_from_cache(str_path)? {
return Ok(Box::new(VecReader::new(data)));
}

match ureq::get(str_path).call() {
Ok(response) => {
let mut reader = response.into_reader();
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;

http_asset_cache::save_to_cache(str_path, &buffer)?;

Ok(Box::new(VecReader::new(buffer)))
}
// ureq considers all >=400 status codes as errors
Err(ureq::Error::Status(code, _response)) => {
if code == 404 {
mrchantey marked this conversation as resolved.
Show resolved Hide resolved
Err(AssetReaderError::NotFound(path))
} else {
Err(AssetReaderError::HttpError(code))
mrchantey marked this conversation as resolved.
Show resolved Hide resolved
}
}
Err(ureq::Error::Transport(err)) => Err(AssetReaderError::Io(
io::Error::new(
io::ErrorKind::Other,
format!(
"unexpected error while loading asset {}: {}",
path.display(),
err
),
)
.into(),
)),
}
}

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

async fn read_meta<'a>(&'a self, path: &'a Path) -> Result<Box<dyn Reader>, AssetReaderError> {
match self.make_meta_uri(path) {
Some(uri) => get(uri).await,
None => Err(AssetReaderError::NotFound(
"source path has no extension".into(),
)),
}
}

async fn is_directory<'a>(&'a self, _path: &'a Path) -> Result<bool, AssetReaderError> {
Ok(false)
}

async fn read_directory<'a>(
&'a self,
path: &'a Path,
) -> Result<Box<PathStream>, AssetReaderError> {
Err(AssetReaderError::NotFound(self.make_uri(path)))
}
}

/// A naive implementation of an HTTP asset cache 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)
mod http_asset_cache {
mrchantey marked this conversation as resolved.
Show resolved Hide resolved
use core::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;
use std::fs::{self, File};
use std::io::{self, Read, Write};
use std::path::PathBuf;

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

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

pub 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 = File::open(&cache_path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
Ok(Some(buffer))
} else {
Ok(None)
}
}

pub 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);

fs::create_dir_all(CACHE_DIR).ok();

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

Ok(())
}
}

#[cfg(test)]
mrchantey marked this conversation as resolved.
Show resolved Hide resolved
mod tests {
use super::*;

#[test]
fn make_http_uri() {
assert_eq!(
HttpSourceAssetReader::Http
.make_uri(Path::new("s3.johanhelsing.studio/dump/favicon.png"))
.to_str()
.unwrap(),
"http://s3.johanhelsing.studio/dump/favicon.png"
);
}

#[test]
fn make_https_uri() {
assert_eq!(
HttpSourceAssetReader::Https
.make_uri(Path::new("s3.johanhelsing.studio/dump/favicon.png"))
.to_str()
.unwrap(),
"https://s3.johanhelsing.studio/dump/favicon.png"
);
}

#[test]
fn make_http_meta_uri() {
assert_eq!(
HttpSourceAssetReader::Http
.make_meta_uri(Path::new("s3.johanhelsing.studio/dump/favicon.png"))
.expect("cannot create meta uri")
.to_str()
.unwrap(),
"http://s3.johanhelsing.studio/dump/favicon.png.meta"
);
}

#[test]
fn make_https_meta_uri() {
assert_eq!(
HttpSourceAssetReader::Https
.make_meta_uri(Path::new("s3.johanhelsing.studio/dump/favicon.png"))
.expect("cannot create meta uri")
.to_str()
.unwrap(),
"https://s3.johanhelsing.studio/dump/favicon.png.meta"
);
}

#[test]
fn make_https_without_extension_meta_uri() {
assert_eq!(
HttpSourceAssetReader::Https
.make_meta_uri(Path::new("s3.johanhelsing.studio/dump/favicon")),
None
);
}
}
3 changes: 2 additions & 1 deletion crates/bevy_asset/src/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ pub enum AssetReaderError {
Io(Arc<std::io::Error>),

/// The HTTP request completed but returned an unhandled [HTTP response status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status).
/// If the request fails before getting a status code (e.g. request timeout, interrupted connection, etc), expect [`AssetReaderError::Io`].
/// - If the request returns a 404 error, expect [`AssetReaderError::NotFound`].
/// - If the request fails before getting a status code (e.g. request timeout, interrupted connection, etc), expect [`AssetReaderError::Io`].
#[error("Encountered HTTP status {0:?} when loading asset")]
HttpError(u16),
}
Expand Down
6 changes: 5 additions & 1 deletion crates/bevy_asset/src/io/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ fn js_value_to_err(context: &str) -> impl FnOnce(JsValue) -> std::io::Error + '_
}

impl HttpWasmAssetReader {
async fn fetch_bytes<'a>(&self, path: PathBuf) -> Result<impl Reader, AssetReaderError> {
// Also used by HttpSourceAssetReader
pub(crate) async fn fetch_bytes<'a>(
&self,
path: PathBuf,
) -> Result<impl Reader, AssetReaderError> {
// The JS global scope includes a self-reference via a specializing name, which can be used to determine the type of global context available.
let global: Global = js_sys::global().unchecked_into();
let promise = if !global.window().is_undefined() {
Expand Down
6 changes: 6 additions & 0 deletions crates/bevy_asset/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ mod reflect;
mod render_asset;
mod server;

#[cfg(feature = "http_source")]
mod http_source;

pub use assets::*;
pub use bevy_asset_macros::Asset;
pub use direct_access_ext::DirectAssetAccessExt;
Expand Down Expand Up @@ -310,6 +313,9 @@ impl AssetPlugin {

impl Plugin for AssetPlugin {
fn build(&self, app: &mut App) {
#[cfg(feature = "http_source")]
app.add_plugins(http_source::http_source_plugin);

let embedded = EmbeddedAssetRegistry::default();
{
let mut sources = app
Expand Down
3 changes: 2 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,8 @@ Example | Description
[Custom Asset](../examples/asset/custom_asset.rs) | Implements a custom asset loader
[Custom Asset IO](../examples/asset/custom_asset_reader.rs) | Implements a custom AssetReader
[Embedded Asset](../examples/asset/embedded_asset.rs) | Embed an asset in the application binary and load it
[Extra asset source](../examples/asset/extra_source.rs) | Load an asset from a non-standard asset source
[Extra Asset Source](../examples/asset/extra_source.rs) | Load an asset from a non-standard asset source
[HTTP Asset Source](../examples/asset/http_source.rs) | Load an asset from a http source
[Hot Reloading of Assets](../examples/asset/hot_asset_reloading.rs) | Demonstrates automatic reloading of assets when modified on disk
[Mult-asset synchronization](../examples/asset/multi_asset_sync.rs) | Demonstrates how to wait for multiple assets to be loaded.
[Repeated texture configuration](../examples/asset/repeated_texture.rs) | How to configure the texture to repeat instead of the default clamp to edges
Expand Down
18 changes: 18 additions & 0 deletions examples/asset/http_source.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//! Example usage of the `http` asset source to load assets from the web.
use bevy::prelude::*;

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.run();
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2d);

commands.spawn(
// Simply use a url where you would normally use an asset folder relative path
Sprite::from_image(asset_server.load("https://raw.githubusercontent.com/bevyengine/bevy/refs/heads/main/assets/branding/bevy_bird_dark.png"))
);
}
Loading