Skip to main content

OverlayFs

Struct OverlayFs 

Source
pub struct OverlayFs { /* private fields */ }
Expand description

Copy-on-write overlay filesystem.

OverlayFs layers a writable upper filesystem on top of a read-only base (lower) filesystem, similar to Docker’s overlay storage driver or Linux overlayfs.

§Behavior

  • Reads: Check upper layer first, fall back to lower layer
  • Writes: Always go to the upper layer (copy-on-write)
  • Deletes: Tracked via whiteouts - deleted files are hidden but the lower layer is unchanged

§Use Cases

  • Template systems: Start from a read-only template, allow modifications
  • Immutable infrastructure: Keep base images unchanged while allowing runtime modifications
  • Testing: Run tests against a base state without modifying it
  • Undo support: Discard the upper layer to “reset” to the base state

§Example

use bashkit::{Bash, FileSystem, InMemoryFs, OverlayFs};
use std::path::Path;
use std::sync::Arc;

// Create a base filesystem with template files
let base = Arc::new(InMemoryFs::new());
base.mkdir(Path::new("/config"), false).await?;
base.write_file(Path::new("/config/app.conf"), b"debug=false").await?;

// Create overlay - base is read-only, changes go to overlay
let overlay = Arc::new(OverlayFs::new(base.clone()));

// Use with Bash
let mut bash = Bash::builder().fs(overlay.clone()).build();

// Read from base layer
let result = bash.exec("cat /config/app.conf").await?;
assert_eq!(result.stdout, "debug=false");

// Modify - changes go to overlay only
bash.exec("echo 'debug=true' > /config/app.conf").await?;

// Overlay shows modified content
let result = bash.exec("cat /config/app.conf").await?;
assert_eq!(result.stdout, "debug=true\n");

// Base is unchanged!
let original = base.read_file(Path::new("/config/app.conf")).await?;
assert_eq!(original, b"debug=false");

§Whiteouts (Deletion Handling)

When you delete a file that exists in the base layer, OverlayFs creates a “whiteout” marker that hides the file without modifying the base:

use bashkit::{Bash, FileSystem, InMemoryFs, OverlayFs};
use std::path::Path;
use std::sync::Arc;

let base = Arc::new(InMemoryFs::new());
base.write_file(Path::new("/tmp/secret.txt"), b"sensitive").await?;

let overlay = Arc::new(OverlayFs::new(base.clone()));
let mut bash = Bash::builder().fs(overlay.clone()).build();

// File exists initially
assert!(overlay.exists(Path::new("/tmp/secret.txt")).await?);

// Delete it
bash.exec("rm /tmp/secret.txt").await?;

// Gone from overlay's view
assert!(!overlay.exists(Path::new("/tmp/secret.txt")).await?);

// But base is unchanged
assert!(base.exists(Path::new("/tmp/secret.txt")).await?);

§Directory Listing

When listing directories, entries from both layers are merged, with the upper layer taking precedence for files that exist in both:

use bashkit::{FileSystem, InMemoryFs, OverlayFs};
use std::path::Path;
use std::sync::Arc;

let base = Arc::new(InMemoryFs::new());
base.write_file(Path::new("/tmp/base.txt"), b"from base").await?;

let overlay = OverlayFs::new(base);
overlay.write_file(Path::new("/tmp/upper.txt"), b"from upper").await?;

// Both files visible
let entries = overlay.read_dir(Path::new("/tmp")).await?;
let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"base.txt"));
assert!(names.contains(&"upper.txt"));

Implementations§

Source§

impl OverlayFs

Source

pub fn new(lower: Arc<dyn FileSystem>) -> Self

Create a new overlay filesystem with the given base layer and default limits.

The lower filesystem is treated as read-only - all reads will first check the upper layer, then fall back to the lower layer. All writes go to a new InMemoryFs upper layer.

§Arguments
  • lower - The base (read-only) filesystem
§Example
use bashkit::{FileSystem, InMemoryFs, OverlayFs};
use std::path::Path;
use std::sync::Arc;

// Create base with some files
let base = Arc::new(InMemoryFs::new());
base.mkdir(Path::new("/data"), false).await?;
base.write_file(Path::new("/data/readme.txt"), b"Read me!").await?;

// Create overlay
let overlay = OverlayFs::new(base);

// Can read from base
let content = overlay.read_file(Path::new("/data/readme.txt")).await?;
assert_eq!(content, b"Read me!");

// Writes go to upper layer
overlay.write_file(Path::new("/data/new.txt"), b"New file").await?;
Source

pub fn with_limits(lower: Arc<dyn FileSystem>, limits: FsLimits) -> Self

Create a new overlay filesystem with custom limits.

Limits apply to the combined view (upper layer writes + lower layer content).

§Example
use bashkit::{FileSystem, InMemoryFs, OverlayFs, FsLimits};
use std::path::Path;
use std::sync::Arc;

let base = Arc::new(InMemoryFs::new());
let limits = FsLimits::new().max_total_bytes(10_000_000); // 10MB

let overlay = OverlayFs::with_limits(base, limits);
Source

pub fn upper(&self) -> &InMemoryFs

Access the upper (writable) filesystem layer.

This provides direct access to the InMemoryFs that stores all writes. Useful for pre-populating files during construction.

§Example
use bashkit::{InMemoryFs, OverlayFs};
use std::sync::Arc;

let base = Arc::new(InMemoryFs::new());
let overlay = OverlayFs::new(base);

// Add files directly to upper layer
overlay.upper().add_file("/config/app.conf", "debug=true\n", 0o644);

Trait Implementations§

Source§

impl FileSystem for OverlayFs

Source§

fn read_file<'life0, 'life1, 'async_trait>( &'life0 self, path: &'life1 Path, ) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Read a file’s contents as bytes. Read more
Source§

fn write_file<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, path: &'life1 Path, content: &'life2 [u8], ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Write contents to a file, creating it if necessary. Read more
Source§

fn append_file<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, path: &'life1 Path, content: &'life2 [u8], ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Append contents to a file, creating it if necessary. Read more
Source§

fn mkdir<'life0, 'life1, 'async_trait>( &'life0 self, path: &'life1 Path, recursive: bool, ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Create a directory. Read more
Source§

fn remove<'life0, 'life1, 'async_trait>( &'life0 self, path: &'life1 Path, recursive: bool, ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Remove a file or directory. Read more
Source§

fn stat<'life0, 'life1, 'async_trait>( &'life0 self, path: &'life1 Path, ) -> Pin<Box<dyn Future<Output = Result<Metadata>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Get file or directory metadata. Read more
Source§

fn read_dir<'life0, 'life1, 'async_trait>( &'life0 self, path: &'life1 Path, ) -> Pin<Box<dyn Future<Output = Result<Vec<DirEntry>>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

List directory contents. Read more
Source§

fn exists<'life0, 'life1, 'async_trait>( &'life0 self, path: &'life1 Path, ) -> Pin<Box<dyn Future<Output = Result<bool>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Check if a path exists. Read more
Source§

fn rename<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, from: &'life1 Path, to: &'life2 Path, ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Rename or move a file or directory. Read more
Source§

fn copy<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, from: &'life1 Path, to: &'life2 Path, ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Copy a file. Read more
Create a symbolic link. Read more
Read a symbolic link’s target. Read more
Source§

fn chmod<'life0, 'life1, 'async_trait>( &'life0 self, path: &'life1 Path, mode: u32, ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Change file permissions. Read more
Source§

fn usage(&self) -> FsUsage

Get current filesystem usage statistics. Read more
Source§

fn limits(&self) -> FsLimits

Get filesystem limits. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.