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
13pub struct HttpAssetReader {
15 client: surf::Client,
16}
17
18impl HttpAssetReader {
19 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
115pub 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}