Skip to content

Commit c47df24

Browse files
committed
Published TempDirectory Resrouce
Allows configuration of the temp directory after startup, and the retrieval of the `Path` for 3rd party use.
1 parent 63a50dd commit c47df24

File tree

3 files changed

+255
-13
lines changed

3 files changed

+255
-13
lines changed

crates/bevy_asset/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ pub use path::*;
5151
pub use reflect::*;
5252
pub use server::*;
5353

54+
#[cfg(not(target_arch = "wasm32"))]
55+
pub use temp::TempDirectory;
56+
5457
/// Rusty Object Notation, a crate used to serialize and deserialize bevy assets.
5558
pub use ron;
5659

crates/bevy_asset/src/temp.rs

Lines changed: 233 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,55 @@ use std::{
44
};
55

66
use bevy_ecs::{system::Resource, world::World};
7+
use bevy_utils::Duration;
78

8-
use crate::io::AssetSourceBuilder;
9+
use crate::io::{
10+
AssetReader, AssetSource, AssetSourceBuilder, AssetSourceEvent, AssetWatcher, AssetWriter,
11+
ErasedAssetReader, ErasedAssetWriter,
12+
};
13+
14+
/// A [resource](`Resource`) providing access to the temporary directory used by the `temp://`
15+
/// [asset source](`AssetSource`).
16+
#[derive(Resource)]
17+
pub struct TempDirectory {
18+
directory: TempDirectoryKind,
19+
}
20+
21+
impl TempDirectory {
22+
/// Try to create a new [`TempDirectory`] resource, which uses a randomly created
23+
/// directory in the user's temporary directory. This can fail if the platform does not
24+
/// provide an appropriate temporary directory, or the directory itself could not be created.
25+
pub fn new_transient() -> std::io::Result<Self> {
26+
let directory = TempDirectoryKind::new_transient()?;
27+
28+
Ok(Self { directory })
29+
}
30+
31+
/// Create a new [`TempDirectory`] resource, which uses a provided directory to store temporary
32+
/// assets. It is assumed this directory already exists, and it will _not_ be deleted on exit.
33+
pub fn new_persistent(path: impl Into<PathBuf>) -> Self {
34+
let directory = TempDirectoryKind::new_persistent(path);
35+
36+
Self { directory }
37+
}
38+
39+
/// Get the [`Path`] to the directory used for temporary assets.
40+
pub fn path(&self) -> &Path {
41+
self.directory.path()
42+
}
43+
44+
/// Persist the current temporary asset directory after application exit.
45+
pub fn persist(&mut self) -> &mut Self {
46+
self.directory.persist();
47+
48+
self
49+
}
50+
}
951

1052
/// Private resource to store the temporary directory used by `temp://`.
1153
/// Kept private as it should only be removed on application exit.
12-
#[derive(Resource)]
13-
enum TempDirectory {
14-
/// Uses [`TempDir`](tempfile::TempDir)'s drop behaviour to delete the directory.
54+
enum TempDirectoryKind {
55+
/// Uses [`TempDir`](tempfile::TempDir)'s drop behavior to delete the directory.
1556
/// Note that this is not _guaranteed_ to succeed, so it is possible to leak files from this
1657
/// option until the underlying OS cleans temporary directories. For secure files, consider using
1758
/// [`tempfile`](tempfile::tempfile) directly.
@@ -21,13 +62,37 @@ enum TempDirectory {
2162
Persist(PathBuf),
2263
}
2364

24-
impl TempDirectory {
65+
impl TempDirectoryKind {
66+
fn new_transient() -> std::io::Result<Self> {
67+
let directory = tempfile::TempDir::with_prefix("bevy_")?;
68+
Ok(Self::Delete(directory))
69+
}
70+
71+
fn new_persistent(path: impl Into<PathBuf>) -> Self {
72+
Self::Persist(path.into())
73+
}
74+
2575
fn path(&self) -> &Path {
2676
match self {
27-
TempDirectory::Delete(x) => x.path(),
28-
TempDirectory::Persist(x) => x.as_ref(),
77+
Self::Delete(x) => x.as_ref(),
78+
Self::Persist(x) => x.as_ref(),
2979
}
3080
}
81+
82+
fn persist(&mut self) -> &mut Self {
83+
let mut swap = Self::Persist(PathBuf::new());
84+
85+
std::mem::swap(self, &mut swap);
86+
87+
let new = match swap {
88+
Self::Delete(x) => Self::Persist(x.into_path()),
89+
x @ Self::Persist(_) => x,
90+
};
91+
92+
*self = new;
93+
94+
self
95+
}
3196
}
3297

3398
pub(crate) fn get_temp_source(
@@ -37,20 +102,177 @@ pub(crate) fn get_temp_source(
37102
let temp_dir = match world.remove_resource::<TempDirectory>() {
38103
Some(resource) => resource,
39104
None => match temporary_file_path {
40-
Some(path) => TempDirectory::Persist(path.into()),
41-
None => TempDirectory::Delete(tempfile::TempDir::with_prefix("bevy")?),
105+
Some(path) => TempDirectory::new_persistent(path),
106+
None => TempDirectory::new_transient()?,
42107
},
43108
};
44109

45-
let path = temp_dir
110+
let path: &str = temp_dir
46111
.path()
47112
.as_os_str()
48113
.try_into()
49114
.map_err(|error| Error::new(ErrorKind::InvalidData, error))?;
50115

51-
let source = AssetSourceBuilder::platform_default(path, None);
116+
let path = path.to_owned();
117+
let debounce = Duration::from_millis(300);
118+
119+
let source = AssetSourceBuilder::default()
120+
.with_reader(TempAssetReader::get_default(path.clone()))
121+
.with_writer(TempAssetWriter::get_default(path.clone()))
122+
.with_watcher(TempAssetWatcher::get_default(path.clone(), debounce))
123+
.with_watch_warning(TempAssetWatcher::get_default_watch_warning());
52124

53125
world.insert_resource(temp_dir);
54126

55127
Ok(source)
56128
}
129+
130+
struct TempAssetReader {
131+
inner: Box<dyn ErasedAssetReader>,
132+
}
133+
134+
impl TempAssetReader {
135+
fn get_default(path: String) -> impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync {
136+
move || {
137+
let mut getter = AssetSource::get_default_reader(path.clone());
138+
let inner = getter();
139+
140+
Box::new(Self { inner })
141+
}
142+
}
143+
}
144+
145+
impl AssetReader for TempAssetReader {
146+
async fn read<'a>(
147+
&'a self,
148+
path: &'a Path,
149+
) -> Result<Box<crate::io::Reader<'a>>, crate::io::AssetReaderError> {
150+
self.inner.read(path).await
151+
}
152+
153+
async fn read_meta<'a>(
154+
&'a self,
155+
path: &'a Path,
156+
) -> Result<Box<crate::io::Reader<'a>>, crate::io::AssetReaderError> {
157+
self.inner.read_meta(path).await
158+
}
159+
160+
async fn read_directory<'a>(
161+
&'a self,
162+
path: &'a Path,
163+
) -> Result<Box<crate::io::PathStream>, crate::io::AssetReaderError> {
164+
self.inner.read_directory(path).await
165+
}
166+
167+
async fn is_directory<'a>(
168+
&'a self,
169+
path: &'a Path,
170+
) -> Result<bool, crate::io::AssetReaderError> {
171+
self.inner.is_directory(path).await
172+
}
173+
}
174+
175+
struct TempAssetWriter {
176+
inner: Box<dyn ErasedAssetWriter>,
177+
}
178+
179+
impl TempAssetWriter {
180+
fn get_default(
181+
path: String,
182+
) -> impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync {
183+
move |condition| {
184+
let mut getter = AssetSource::get_default_writer(path.clone());
185+
let inner = getter(condition)?;
186+
187+
Some(Box::new(Self { inner }))
188+
}
189+
}
190+
}
191+
192+
impl AssetWriter for TempAssetWriter {
193+
async fn write<'a>(
194+
&'a self,
195+
path: &'a Path,
196+
) -> Result<Box<crate::io::Writer>, crate::io::AssetWriterError> {
197+
self.inner.write(path).await
198+
}
199+
200+
async fn write_meta<'a>(
201+
&'a self,
202+
path: &'a Path,
203+
) -> Result<Box<crate::io::Writer>, crate::io::AssetWriterError> {
204+
self.inner.write_meta(path).await
205+
}
206+
207+
async fn remove<'a>(&'a self, path: &'a Path) -> Result<(), crate::io::AssetWriterError> {
208+
self.inner.remove(path).await
209+
}
210+
211+
async fn remove_meta<'a>(&'a self, path: &'a Path) -> Result<(), crate::io::AssetWriterError> {
212+
self.inner.remove_meta(path).await
213+
}
214+
215+
async fn rename<'a>(
216+
&'a self,
217+
old_path: &'a Path,
218+
new_path: &'a Path,
219+
) -> Result<(), crate::io::AssetWriterError> {
220+
self.inner.rename(old_path, new_path).await
221+
}
222+
223+
async fn rename_meta<'a>(
224+
&'a self,
225+
old_path: &'a Path,
226+
new_path: &'a Path,
227+
) -> Result<(), crate::io::AssetWriterError> {
228+
self.inner.rename_meta(old_path, new_path).await
229+
}
230+
231+
async fn remove_directory<'a>(
232+
&'a self,
233+
path: &'a Path,
234+
) -> Result<(), crate::io::AssetWriterError> {
235+
self.inner.remove_directory(path).await
236+
}
237+
238+
async fn remove_empty_directory<'a>(
239+
&'a self,
240+
path: &'a Path,
241+
) -> Result<(), crate::io::AssetWriterError> {
242+
self.inner.remove_empty_directory(path).await
243+
}
244+
245+
async fn remove_assets_in_directory<'a>(
246+
&'a self,
247+
path: &'a Path,
248+
) -> Result<(), crate::io::AssetWriterError> {
249+
self.inner.remove_assets_in_directory(path).await
250+
}
251+
}
252+
253+
struct TempAssetWatcher {
254+
_inner: Box<dyn AssetWatcher>,
255+
}
256+
257+
impl TempAssetWatcher {
258+
fn get_default(
259+
path: String,
260+
file_debounce_wait_time: Duration,
261+
) -> impl FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
262+
+ Send
263+
+ Sync {
264+
move |channel| {
265+
let mut getter =
266+
AssetSource::get_default_watcher(path.clone(), file_debounce_wait_time);
267+
let _inner = getter(channel)?;
268+
269+
Some(Box::new(Self { _inner }))
270+
}
271+
}
272+
273+
fn get_default_watch_warning() -> &'static str {
274+
AssetSource::get_default_watch_warning()
275+
}
276+
}
277+
278+
impl AssetWatcher for TempAssetWatcher {}

examples/asset/temp_asset.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use bevy::{
66
asset::{
77
saver::{AssetSaver, ErasedAssetSaver},
8-
AssetPath, ErasedLoadedAsset, LoadedAsset,
8+
AssetPath, ErasedLoadedAsset, LoadedAsset, TempDirectory,
99
},
1010
prelude::*,
1111
tasks::{block_on, IoTaskPool, Task},
@@ -25,7 +25,11 @@ fn main() {
2525
}
2626

2727
/// Attempt to save an asset to the temporary asset source.
28-
fn save_temp_asset(assets: Res<AssetServer>, mut commands: Commands) {
28+
fn save_temp_asset(
29+
assets: Res<AssetServer>,
30+
mut commands: Commands,
31+
temp_directory: Res<TempDirectory>,
32+
) {
2933
// This is the asset we will attempt to save.
3034
let my_text_asset = TextAsset("Hello World!".to_owned());
3135

@@ -34,6 +38,10 @@ fn save_temp_asset(assets: Res<AssetServer>, mut commands: Commands) {
3438
let path = AssetPath::from("temp://message.txt").into_owned();
3539
let server = assets.clone();
3640

41+
// We use Bevy's IoTaskPool to run the saving task asynchronously. This ensures
42+
// our application doesn't block during the (potentially lengthy!) saving process.
43+
// In this example, the asset is small so the blocking time will be short, but
44+
// that wont always be the case, especially for large assets.
3745
let task = IoTaskPool::get().spawn(async move {
3846
save_asset(my_text_asset, path, server, TextSaver)
3947
.await
@@ -43,6 +51,13 @@ fn save_temp_asset(assets: Res<AssetServer>, mut commands: Commands) {
4351
// To ensure the task completes before we try loading, we will manually poll this task
4452
// so we can react to its completion.
4553
commands.spawn(SavingTask(task));
54+
55+
// You can check the logged path to see the temporary directory yourself. Note
56+
// that the directory will be deleted once this example quits.
57+
info!(
58+
"Temporary Assets will be saved in {:?}",
59+
temp_directory.path()
60+
);
4661
}
4762

4863
/// Poll the save tasks until completion, and then start loading our temporary text asset.
@@ -52,7 +67,9 @@ fn wait_until_temp_saved(
5267
mut commands: Commands,
5368
) {
5469
for (entity, mut task) in tasks.iter_mut() {
70+
// Check our SavingTask to see if it's done...
5571
if let Some(()) = block_on(future::poll_once(&mut task.0)) {
72+
// ...and if so, load the temporary asset!
5673
commands.insert_resource(MyTempText {
5774
text: assets.load("temp://message.txt"),
5875
});

0 commit comments

Comments
 (0)