bevy_http/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use bevy::{
4    asset::io::{AssetReader, AssetReaderError, AssetSource, AssetSourceId, PathStream, Reader},
5    prelude::*,
6    utils::BoxedFuture,
7};
8use std::path::{Path, PathBuf};
9
10use std::pin::Pin;
11use std::task::Poll;
12
13/// A custom asset reader implementation that wraps a given asset reader implementation
14pub struct HttpAssetReader {
15    client: surf::Client,
16}
17
18impl HttpAssetReader {
19    /// Creates a new `HttpAssetReader`. The path provided will be used to build URLs to query for assets.
20    pub fn new(base_url: &str) -> Self {
21        let base_url = surf::Url::parse(base_url).expect("invalid base url");
22
23        let client = surf::Config::new().set_timeout(Some(std::time::Duration::from_secs(5)));
24        let client = client.set_base_url(base_url);
25
26        let client = client.try_into().expect("could not create http client");
27
28        Self { client }
29    }
30
31    async fn fetch_bytes<'a>(&self, path: &str) -> Result<Box<Reader<'a>>, AssetReaderError> {
32        let resp = self.client.get(path).await;
33
34        trace!("fetched {resp:?} ... ");
35        let mut resp = resp.map_err(|e| {
36            AssetReaderError::Io(std::io::Error::new(
37                std::io::ErrorKind::Other,
38                format!("error fetching {path}: {e}"),
39            ))
40        })?;
41
42        let status = resp.status();
43
44        if !status.is_success() {
45            let err = match status {
46                surf::StatusCode::NotFound => AssetReaderError::NotFound(path.into()),
47                _ => AssetReaderError::Io(std::io::Error::new(
48                    std::io::ErrorKind::Other,
49                    format!("bad status code: {status}"),
50                )),
51            };
52            return Err(err);
53        };
54
55        let bytes = resp.body_bytes().await.map_err(|e| {
56            AssetReaderError::Io(std::io::Error::new(
57                std::io::ErrorKind::Other,
58                format!("error getting bytes for {path}: {e}"),
59            ))
60        })?;
61        let reader = bevy::asset::io::VecReader::new(bytes);
62        Ok(Box::new(reader))
63    }
64}
65
66struct EmptyPathStream;
67
68impl futures_core::Stream for EmptyPathStream {
69    type Item = PathBuf;
70
71    fn poll_next(
72        self: Pin<&mut Self>,
73        _cx: &mut std::task::Context<'_>,
74    ) -> Poll<Option<Self::Item>> {
75        Poll::Ready(None)
76    }
77}
78
79impl AssetReader for HttpAssetReader {
80    fn read<'a>(
81        &'a self,
82        path: &'a Path,
83    ) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
84        Box::pin(async move { self.fetch_bytes(&path.to_string_lossy()).await })
85    }
86
87    fn read_meta<'a>(
88        &'a self,
89        path: &'a Path,
90    ) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
91        Box::pin(async move {
92            let meta_path = path.to_string_lossy() + ".meta";
93            Ok(self.fetch_bytes(&meta_path).await?)
94        })
95    }
96
97    fn read_directory<'a>(
98        &'a self,
99        _path: &'a Path,
100    ) -> BoxedFuture<'a, Result<Box<PathStream>, AssetReaderError>> {
101        let stream: Box<PathStream> = Box::new(EmptyPathStream);
102        error!("Reading directories is not supported with the HttpAssetReader");
103        Box::pin(async move { Ok(stream) })
104    }
105
106    fn is_directory<'a>(
107        &'a self,
108        _path: &'a Path,
109    ) -> BoxedFuture<'a, std::result::Result<bool, AssetReaderError>> {
110        error!("Reading directories is not supported with the HttpAssetReader");
111        Box::pin(async move { Ok(false) })
112    }
113}
114
115/// A plugins that registers the `HttpAssetReader` as an asset source.
116pub struct HttpAssetReaderPlugin {
117    pub id: String,
118    pub base_url: String,
119}
120
121impl Plugin for HttpAssetReaderPlugin {
122    fn build(&self, app: &mut App) {
123        let id = self.id.clone();
124        let base_url = self.base_url.clone();
125        app.register_asset_source(
126            AssetSourceId::Name(id.into()),
127            AssetSource::build().with_reader(move || Box::new(HttpAssetReader::new(&base_url))),
128        );
129    }
130}