Skip to main content

khora_io/asset/
file.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! File-based asset loader for editor/development mode.
16//!
17//! Reads assets directly from individual files on disk under a root directory.
18
19use anyhow::{bail, Context, Result};
20use khora_core::asset::AssetSource;
21use std::path::{Path, PathBuf};
22
23use super::{AssetIo, AssetWriter};
24
25/// File-based asset loader for editor/development mode.
26///
27/// Reads assets directly from individual files on disk. The `root` path is
28/// typically `<project>/assets/`.
29pub struct FileLoader {
30    root: PathBuf,
31}
32
33impl FileLoader {
34    /// Creates a new `FileLoader` with the given root directory.
35    pub fn new(root: impl Into<PathBuf>) -> Self {
36        Self { root: root.into() }
37    }
38
39    /// Returns the root directory the loader reads/writes from.
40    pub fn root(&self) -> &Path {
41        &self.root
42    }
43}
44
45impl AssetIo for FileLoader {
46    fn load_bytes(&mut self, source: &AssetSource) -> Result<Vec<u8>> {
47        match source {
48            AssetSource::Path(rel) => {
49                let full_path = self.root.join(rel);
50                std::fs::read(&full_path)
51                    .with_context(|| format!("Failed to read asset: {:?}", full_path))
52            }
53            AssetSource::Packed { .. } => {
54                bail!("FileLoader does not support Packed sources")
55            }
56        }
57    }
58}
59
60impl AssetWriter for FileLoader {
61    fn write_bytes(&self, rel_path: &Path, bytes: &[u8]) -> Result<()> {
62        let full_path = self.root.join(rel_path);
63        if let Some(parent) = full_path.parent() {
64            std::fs::create_dir_all(parent).with_context(|| {
65                format!("Failed to create parent directory for {:?}", full_path)
66            })?;
67        }
68        std::fs::write(&full_path, bytes)
69            .with_context(|| format!("Failed to write asset: {:?}", full_path))
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use tempfile::tempdir;
77
78    #[test]
79    fn write_bytes_creates_parent_dirs() {
80        let dir = tempdir().unwrap();
81        let loader = FileLoader::new(dir.path());
82        loader
83            .write_bytes(Path::new("scenes/default.kscene"), b"SCN")
84            .unwrap();
85        let written = std::fs::read(dir.path().join("scenes").join("default.kscene")).unwrap();
86        assert_eq!(written, b"SCN");
87    }
88
89    #[test]
90    fn round_trip_via_load_bytes() {
91        let dir = tempdir().unwrap();
92        let loader = FileLoader::new(dir.path());
93        loader
94            .write_bytes(Path::new("textures/foo.png"), b"PNG")
95            .unwrap();
96
97        let mut reader = FileLoader::new(dir.path());
98        let bytes = reader
99            .load_bytes(&AssetSource::Path(PathBuf::from("textures/foo.png")))
100            .unwrap();
101        assert_eq!(bytes, b"PNG");
102    }
103}