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 field for setting the user agent #24

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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 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
Loading