Skip to content

Commit

Permalink
Add field for setting the user agent
Browse files Browse the repository at this point in the history
  • Loading branch information
oli-obk committed Feb 24, 2024
1 parent 27f94fc commit c425418
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 117 deletions.
2 changes: 1 addition & 1 deletion examples/web_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ fn main() {
.add_plugins((
// The web asset plugin must be inserted before the `AssetPlugin` so
// that the AssetPlugin recognizes the new sources.
WebAssetPlugin,
WebAssetPlugin::default(),
DefaultPlugins,
))
.add_systems(Startup, setup)
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ mod web_asset_plugin;
mod web_asset_source;

pub use web_asset_plugin::WebAssetPlugin;
pub use web_asset_source::WebAssetReader;
pub use web_asset_source::{WebAssetReader, WebAssetReaderKind};
29 changes: 20 additions & 9 deletions src/web_asset_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,28 @@ use bevy::asset::io::AssetSource;
/// ));
/// ```
#[derive(Default)]
pub struct WebAssetPlugin;
pub struct WebAssetPlugin {
/// User agent to tell the server about. Some servers require this to be set.
/// Note: This flag is entirely ignored on wasm, as only firefox reliably supports this.
pub user_agent: Option<String>,
}

impl Plugin for WebAssetPlugin {
fn build(&self, app: &mut App) {
app.register_asset_source(
"http",
AssetSource::build().with_reader(|| Box::new(WebAssetReader::Http)),
);
app.register_asset_source(
"https",
AssetSource::build().with_reader(|| Box::new(WebAssetReader::Https)),
);
for (id, kind) in [
("http", WebAssetReaderKind::Http),
("https", WebAssetReaderKind::Https),
] {
let user_agent = self.user_agent.clone();
app.register_asset_source(
id,
AssetSource::build().with_reader(move || {
Box::new(WebAssetReader {
user_agent: user_agent.clone(),
kind,
})
}),
);
}
}
}
240 changes: 134 additions & 106 deletions src/web_asset_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,40 @@ use std::path::{Path, PathBuf};
use bevy::asset::io::{AssetReader, AssetReaderError, Reader};

/// Treats paths as urls to load assets from.
pub enum WebAssetReader {
pub struct WebAssetReader {
/// Whether to use https or http
pub kind: WebAssetReaderKind,
/// Optional user agent, some servers (e.g. openstreetmap) will reject connections without user agents
pub user_agent: Option<String>,
}

#[derive(Copy, Clone)]
/// Whether to use https or http
pub enum WebAssetReaderKind {
/// Unencrypted connections.
Http,
/// Use TLS for setting up connections.
Https,
}

impl WebAssetReader {
#[allow(non_upper_case_globals)]
/// Unencrypted connections.
pub const Http: Self = Self {
kind: WebAssetReaderKind::Http,
user_agent: None,
};
#[allow(non_upper_case_globals)]
/// Use TLS for setting up connections.
pub const Https: Self = Self {
kind: WebAssetReaderKind::Https,
user_agent: None,
};

fn make_uri(&self, path: &Path) -> PathBuf {
PathBuf::from(match self {
Self::Http => "http://",
Self::Https => "https://",
PathBuf::from(match self.kind {
WebAssetReaderKind::Http => "http://",
WebAssetReaderKind::Https => "https://",
})
.join(path)
}
Expand All @@ -32,124 +54,130 @@ impl WebAssetReader {
uri.set_extension(extension);
uri
}
}

#[cfg(target_arch = "wasm32")]
async fn get<'a>(path: PathBuf) -> Result<Box<Reader<'a>>, AssetReaderError> {
use bevy::asset::io::VecReader;
use js_sys::Uint8Array;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::Response;

fn js_value_to_err<'a>(
context: &'a str,
) -> impl FnOnce(wasm_bindgen::JsValue) -> std::io::Error + 'a {
move |value| {
let message = match js_sys::JSON::stringify(&value) {
Ok(js_str) => format!("Failed to {context}: {js_str}"),
Err(_) => {
format!(
#[cfg(target_arch = "wasm32")]
async fn get(&self, path: PathBuf) -> Result<Box<Reader<'_>>, AssetReaderError> {
use bevy::asset::io::VecReader;
use js_sys::Uint8Array;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::Response;

fn js_value_to_err<'a>(
context: &'a str,
) -> impl FnOnce(wasm_bindgen::JsValue) -> std::io::Error + 'a {
move |value| {
let message = match js_sys::JSON::stringify(&value) {
Ok(js_str) => format!("Failed to {context}: {js_str}"),
Err(_) => {
format!(
"Failed to {context} and also failed to stringify the JSValue of the error"
)
}
};
}
};

std::io::Error::new(std::io::ErrorKind::Other, message)
std::io::Error::new(std::io::ErrorKind::Other, message)
}
}
}

let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_str(path.to_str().unwrap()))
.await
.map_err(js_value_to_err("fetch path"))?;
let resp = resp_value
.dyn_into::<Response>()
.map_err(js_value_to_err("convert fetch to Response"))?;
match resp.status() {
200 => {
let data = JsFuture::from(resp.array_buffer().unwrap()).await.unwrap();
let bytes = Uint8Array::new(&data).to_vec();
let reader: Box<Reader> = Box::new(VecReader::new(bytes));
Ok(reader)
let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_str(path.to_str().unwrap()))
.await
.map_err(js_value_to_err("fetch path"))?;
let resp = resp_value
.dyn_into::<Response>()
.map_err(js_value_to_err("convert fetch to Response"))?;
match resp.status() {
200 => {
let data = JsFuture::from(resp.array_buffer().unwrap()).await.unwrap();
let bytes = Uint8Array::new(&data).to_vec();
let reader: Box<Reader> = Box::new(VecReader::new(bytes));
Ok(reader)
}
404 => Err(AssetReaderError::NotFound(path)),
status => Err(AssetReaderError::Io(
std::io::Error::new(
std::io::ErrorKind::Other,
format!("Encountered unexpected HTTP status {status}"),
)
.into(),
)),
}
404 => Err(AssetReaderError::NotFound(path)),
status => Err(AssetReaderError::Io(
std::io::Error::new(
std::io::ErrorKind::Other,
format!("Encountered unexpected HTTP status {status}"),
)
.into(),
)),
}
}

#[cfg(not(target_arch = "wasm32"))]
async fn get<'a>(path: PathBuf) -> Result<Box<Reader<'a>>, AssetReaderError> {
use std::future::Future;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
#[cfg(not(target_arch = "wasm32"))]
async fn get(&self, path: PathBuf) -> Result<Box<Reader<'_>>, AssetReaderError> {
use std::future::Future;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};

use bevy::asset::io::VecReader;
use surf::StatusCode;
use bevy::asset::io::VecReader;
use surf::http::headers::USER_AGENT;
use surf::{Client, Config, StatusCode};

#[pin_project::pin_project]
struct ContinuousPoll<T>(#[pin] T);
#[pin_project::pin_project]
struct ContinuousPoll<T>(#[pin] T);

impl<T: Future> Future for ContinuousPoll<T> {
type Output = T::Output;
impl<T: Future> Future for ContinuousPoll<T> {
type Output = T::Output;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// Always wake - blocks on single threaded executor.
cx.waker().wake_by_ref();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// Always wake - blocks on single threaded executor.
cx.waker().wake_by_ref();

self.project().0.poll(cx)
self.project().0.poll(cx)
}
}
}

let str_path = path.to_str().ok_or_else(|| {
AssetReaderError::Io(
io::Error::new(
io::ErrorKind::Other,
format!("non-utf8 path: {}", path.display()),
let str_path = path.to_str().ok_or_else(|| {
AssetReaderError::Io(
io::Error::new(
io::ErrorKind::Other,
format!("non-utf8 path: {}", path.display()),
)
.into(),
)
.into(),
)
})?;
let mut response = ContinuousPoll(surf::get(str_path)).await.map_err(|err| {
AssetReaderError::Io(
io::Error::new(
io::ErrorKind::Other,
format!(
"unexpected status code {} while loading {}: {}",
err.status(),
path.display(),
err.into_inner(),
),
)
.into(),
)
})?;

match response.status() {
StatusCode::Ok => Ok(Box::new(VecReader::new(
ContinuousPoll(response.body_bytes())
.await
.map_err(|_| AssetReaderError::NotFound(path.to_path_buf()))?,
)) as _),
StatusCode::NotFound => Err(AssetReaderError::NotFound(path)),
code => Err(AssetReaderError::Io(
io::Error::new(
io::ErrorKind::Other,
format!(
"unexpected status code {} while loading {}",
code,
path.display()
),
})?;
let mut config = Config::new();
if let Some(user_agent) = &self.user_agent {
config = config.add_header(USER_AGENT, user_agent).unwrap();
}
let client: Client = config.try_into().unwrap();
let mut response = ContinuousPoll(client.get(str_path)).await.map_err(|err| {
AssetReaderError::Io(
io::Error::new(
io::ErrorKind::Other,
format!(
"unexpected status code {} while loading {}: {}",
err.status(),
path.display(),
err.into_inner(),
),
)
.into(),
)
.into(),
)),
})?;

match response.status() {
StatusCode::Ok => Ok(Box::new(VecReader::new(
ContinuousPoll(response.body_bytes())
.await
.map_err(|_| AssetReaderError::NotFound(path.to_path_buf()))?,
)) as _),
StatusCode::NotFound => Err(AssetReaderError::NotFound(path)),
code => Err(AssetReaderError::Io(
io::Error::new(
io::ErrorKind::Other,
format!(
"unexpected status code {} while loading {}",
code,
path.display()
),
)
.into(),
)),
}
}
}

Expand All @@ -158,14 +186,14 @@ impl AssetReader for WebAssetReader {
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
Box::pin(get(self.make_uri(path)))
Box::pin(self.get(self.make_uri(path)))
}

fn read_meta<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
Box::pin(get(self.make_meta_uri(path)))
Box::pin(self.get(self.make_meta_uri(path)))
}

fn is_directory<'a>(
Expand Down

0 comments on commit c425418

Please sign in to comment.