Skip to content

Commit 6ead01d

Browse files
committed
Initial Implementation of temp:// Asset Source
1 parent 2aed777 commit 6ead01d

File tree

6 files changed

+276
-0
lines changed

6 files changed

+276
-0
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,17 @@ description = "How to configure the texture to repeat instead of the default cla
14131413
category = "Assets"
14141414
wasm = true
14151415

1416+
[[example]]
1417+
name = "temp_asset"
1418+
path = "examples/asset/temp_asset.rs"
1419+
doc-scrape-examples = true
1420+
1421+
[package.metadata.example.temp_asset]
1422+
name = "Temporary assets"
1423+
description = "How to use the temporary asset source"
1424+
category = "Assets"
1425+
wasm = false
1426+
14161427
# Async Tasks
14171428
[[example]]
14181429
name = "async_compute"

crates/bevy_asset/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ js-sys = "0.3"
5757

5858
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
5959
notify-debouncer-full = { version = "0.3.1", optional = true }
60+
tempfile = "3.10.1"
6061

6162
[dev-dependencies]
6263
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }

crates/bevy_asset/src/lib.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ mod path;
3131
mod reflect;
3232
mod server;
3333

34+
#[cfg(not(target_arch = "wasm32"))]
35+
mod temp;
36+
3437
pub use assets::*;
3538
pub use bevy_asset_macros::Asset;
3639
pub use direct_access_ext::DirectAssetAccessExt;
@@ -91,6 +94,9 @@ pub struct AssetPlugin {
9194
pub mode: AssetMode,
9295
/// How/If asset meta files should be checked.
9396
pub meta_check: AssetMetaCheck,
97+
/// The path to use for temporary assets (relative to the project root).
98+
/// If not provided, a platform specific folder will be created and deleted upon exit.
99+
pub temporary_file_path: Option<String>,
94100
}
95101

96102
#[derive(Debug)]
@@ -138,6 +144,7 @@ impl Default for AssetPlugin {
138144
processed_file_path: Self::DEFAULT_PROCESSED_FILE_PATH.to_string(),
139145
watch_for_changes_override: None,
140146
meta_check: AssetMetaCheck::default(),
147+
temporary_file_path: None,
141148
}
142149
}
143150
}
@@ -163,6 +170,23 @@ impl Plugin for AssetPlugin {
163170
);
164171
embedded.register_source(&mut sources);
165172
}
173+
174+
#[cfg(not(target_arch = "wasm32"))]
175+
{
176+
match temp::get_temp_source(app.world_mut(), self.temporary_file_path.clone()) {
177+
Ok(source) => {
178+
let mut sources = app
179+
.world_mut()
180+
.get_resource_or_insert_with::<AssetSourceBuilders>(Default::default);
181+
182+
sources.insert("temp", source);
183+
}
184+
Err(error) => {
185+
error!("Could not setup temp:// AssetSource due to an IO Error: {error}");
186+
}
187+
};
188+
}
189+
166190
{
167191
let mut watch = cfg!(feature = "watch");
168192
if let Some(watch_override) = self.watch_for_changes_override {

crates/bevy_asset/src/temp.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use std::{
2+
io::{Error, ErrorKind},
3+
path::{Path, PathBuf},
4+
};
5+
6+
use bevy_ecs::{system::Resource, world::World};
7+
8+
use crate::io::AssetSourceBuilder;
9+
10+
/// Private resource to store the temporary directory used by `temp://`.
11+
/// 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.
15+
/// Note that this is not _guaranteed_ to succeed, so it is possible to leak files from this
16+
/// option until the underlying OS cleans temporary directories. For secure files, consider using
17+
/// [`tempfile`](tempfile::tempfile) directly.
18+
Delete(tempfile::TempDir),
19+
/// Will not delete the temporary directory on exit, leaving cleanup the responsibility of
20+
/// the user or their system.
21+
Persist(PathBuf),
22+
}
23+
24+
impl TempDirectory {
25+
fn path(&self) -> &Path {
26+
match self {
27+
TempDirectory::Delete(x) => x.path(),
28+
TempDirectory::Persist(x) => x.as_ref(),
29+
}
30+
}
31+
}
32+
33+
pub(crate) fn get_temp_source(
34+
world: &mut World,
35+
temporary_file_path: Option<String>,
36+
) -> std::io::Result<AssetSourceBuilder> {
37+
let temp_dir = match world.remove_resource::<TempDirectory>() {
38+
Some(resource) => resource,
39+
None => match temporary_file_path {
40+
Some(path) => TempDirectory::Persist(path.into()),
41+
None => TempDirectory::Delete(tempfile::tempdir()?),
42+
},
43+
};
44+
45+
let path = temp_dir
46+
.path()
47+
.as_os_str()
48+
.try_into()
49+
.map_err(|error| Error::new(ErrorKind::InvalidData, error))?;
50+
51+
let source = AssetSourceBuilder::platform_default(path, None);
52+
53+
world.insert_resource(temp_dir);
54+
55+
Ok(source)
56+
}

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ Example | Description
215215
[Extra asset source](../examples/asset/extra_source.rs) | Load an asset from a non-standard asset source
216216
[Hot Reloading of Assets](../examples/asset/hot_asset_reloading.rs) | Demonstrates automatic reloading of assets when modified on disk
217217
[Repeated texture configuration](../examples/asset/repeated_texture.rs) | How to configure the texture to repeat instead of the default clamp to edges
218+
[Temporary assets](../examples/asset/temp_asset.rs) | How to use the temporary asset source
218219

219220
## Async Tasks
220221

examples/asset/temp_asset.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
//! This example shows how to use the temporary asset source, `temp://`.
2+
//! First, a [`TextAsset`] is created in-memory, then saved into the temporary asset source.
3+
//! Once the save operation is completed, we load the asset just like any other file, and display its contents!
4+
5+
use bevy::{
6+
asset::{
7+
saver::{AssetSaver, ErasedAssetSaver},
8+
AssetPath, ErasedLoadedAsset, LoadedAsset,
9+
},
10+
prelude::*,
11+
tasks::{block_on, IoTaskPool, Task},
12+
};
13+
14+
use futures_lite::future;
15+
use text_asset::{TextAsset, TextLoader, TextSaver};
16+
17+
fn main() {
18+
App::new()
19+
.add_plugins(DefaultPlugins)
20+
.init_asset::<TextAsset>()
21+
.register_asset_loader(TextLoader)
22+
.add_systems(Startup, (save_temp_asset, setup_ui))
23+
.add_systems(Update, (wait_until_temp_saved, display_text))
24+
.run();
25+
}
26+
27+
/// Attempt to save an asset to the temporary asset source.
28+
fn save_temp_asset(assets: Res<AssetServer>, mut commands: Commands) {
29+
// This is the asset we will attempt to save.
30+
let my_text_asset = TextAsset("Hello World!".to_owned());
31+
32+
// To ensure the `Task` can outlive this function, we must provide owned versions
33+
// of the `AssetServer` and our desired path.
34+
let path = AssetPath::from("temp://message.txt").into_owned();
35+
let server = assets.clone();
36+
37+
let task = IoTaskPool::get().spawn(async move {
38+
save_asset(my_text_asset, path, server, TextSaver)
39+
.await
40+
.unwrap();
41+
});
42+
43+
// To ensure the task completes before we try loading, we will manually poll this task
44+
// so we can react to its completion.
45+
commands.spawn(SavingTask(task));
46+
}
47+
48+
/// Poll the save tasks until completion, and then start loading our temporary text asset.
49+
fn wait_until_temp_saved(
50+
assets: Res<AssetServer>,
51+
mut tasks: Query<(Entity, &mut SavingTask)>,
52+
mut commands: Commands,
53+
) {
54+
for (entity, mut task) in tasks.iter_mut() {
55+
if let Some(()) = block_on(future::poll_once(&mut task.0)) {
56+
commands.insert_resource(MyTempText {
57+
text: assets.load("temp://message.txt"),
58+
});
59+
60+
commands.entity(entity).despawn_recursive();
61+
}
62+
}
63+
}
64+
65+
/// Setup a basic UI to display our [`TextAsset`] once it's loaded.
66+
fn setup_ui(mut commands: Commands) {
67+
commands.spawn(Camera2dBundle::default());
68+
69+
commands.spawn((TextBundle::from_section("Loading...", default())
70+
.with_text_justify(JustifyText::Center)
71+
.with_style(Style {
72+
position_type: PositionType::Absolute,
73+
bottom: Val::Percent(50.),
74+
right: Val::Percent(50.),
75+
..default()
76+
}),));
77+
}
78+
79+
/// Once the [`TextAsset`] is loaded, update our display text to its contents.
80+
fn display_text(
81+
mut query: Query<&mut Text>,
82+
my_text: Option<Res<MyTempText>>,
83+
texts: Res<Assets<TextAsset>>,
84+
) {
85+
let message = my_text
86+
.as_ref()
87+
.and_then(|resource| texts.get(&resource.text))
88+
.map(|text| text.0.as_str())
89+
.unwrap_or("Loading...");
90+
91+
for mut text in query.iter_mut() {
92+
*text = Text::from_section(message, default());
93+
}
94+
}
95+
96+
/// Save an [`Asset`] at the provided path. Returns [`None`] on failure.
97+
async fn save_asset<A: Asset>(
98+
asset: A,
99+
path: AssetPath<'_>,
100+
server: AssetServer,
101+
saver: impl AssetSaver<Asset = A> + ErasedAssetSaver,
102+
) -> Option<()> {
103+
let asset = ErasedLoadedAsset::from(LoadedAsset::from(asset));
104+
let source = server.get_source(path.source()).ok()?;
105+
let writer = source.writer().ok()?;
106+
107+
let mut writer = writer.write(path.path()).await.ok()?;
108+
ErasedAssetSaver::save(&saver, &mut writer, &asset, &())
109+
.await
110+
.ok()?;
111+
112+
Some(())
113+
}
114+
115+
#[derive(Component)]
116+
struct SavingTask(Task<()>);
117+
118+
#[derive(Resource)]
119+
struct MyTempText {
120+
text: Handle<TextAsset>,
121+
}
122+
123+
mod text_asset {
124+
//! Putting the implementation of an asset loader and writer for a text asset in this module to avoid clutter.
125+
//! While this is required for this example to function, it isn't the focus.
126+
127+
use bevy::{
128+
asset::{
129+
io::{Reader, Writer},
130+
saver::{AssetSaver, SavedAsset},
131+
AssetLoader, LoadContext,
132+
},
133+
prelude::*,
134+
};
135+
use futures_lite::{AsyncReadExt, AsyncWriteExt};
136+
137+
#[derive(Asset, TypePath, Debug)]
138+
pub struct TextAsset(pub String);
139+
140+
#[derive(Default)]
141+
pub struct TextLoader;
142+
143+
impl AssetLoader for TextLoader {
144+
type Asset = TextAsset;
145+
type Settings = ();
146+
type Error = std::io::Error;
147+
async fn load<'a>(
148+
&'a self,
149+
reader: &'a mut Reader<'_>,
150+
_settings: &'a Self::Settings,
151+
_load_context: &'a mut LoadContext<'_>,
152+
) -> Result<TextAsset, Self::Error> {
153+
let mut bytes = Vec::new();
154+
reader.read_to_end(&mut bytes).await?;
155+
let value = String::from_utf8(bytes).unwrap();
156+
Ok(TextAsset(value))
157+
}
158+
159+
fn extensions(&self) -> &[&str] {
160+
&["txt"]
161+
}
162+
}
163+
164+
#[derive(Default)]
165+
pub struct TextSaver;
166+
167+
impl AssetSaver for TextSaver {
168+
type Asset = TextAsset;
169+
type Settings = ();
170+
type OutputLoader = TextLoader;
171+
type Error = std::io::Error;
172+
173+
async fn save<'a>(
174+
&'a self,
175+
writer: &'a mut Writer,
176+
asset: SavedAsset<'a, Self::Asset>,
177+
_settings: &'a Self::Settings,
178+
) -> Result<(), Self::Error> {
179+
writer.write_all(asset.0.as_bytes()).await?;
180+
Ok(())
181+
}
182+
}
183+
}

0 commit comments

Comments
 (0)