use crate::error::VfsErrorKind;
use crate::{FileSystem, SeekAndRead, SeekAndWrite, VfsMetadata, VfsPath, VfsResult};
use std::collections::HashSet;
use std::time::SystemTime;
#[derive(Debug, Clone)]
pub struct OverlayFS {
layers: Vec<VfsPath>,
}
impl OverlayFS {
pub fn new(layers: &[VfsPath]) -> Self {
if layers.is_empty() {
panic!("OverlayFS needs at least one layer")
}
OverlayFS {
layers: layers.to_vec(),
}
}
fn write_layer(&self) -> &VfsPath {
&self.layers[0]
}
fn read_path(&self, path: &str) -> VfsResult<VfsPath> {
if path.is_empty() {
return Ok(self.layers[0].clone());
}
if self.whiteout_path(path)?.exists()? {
return Err(VfsErrorKind::FileNotFound.into());
}
for layer in &self.layers {
let layer_path = layer.join(&path[1..])?;
if layer_path.exists()? {
return Ok(layer_path);
}
}
let read_path = self.write_layer().join(&path[1..])?;
if !read_path.exists()? {
return Err(VfsErrorKind::FileNotFound.into());
}
Ok(read_path)
}
fn write_path(&self, path: &str) -> VfsResult<VfsPath> {
if path.is_empty() {
return Ok(self.layers[0].clone());
}
self.write_layer().join(&path[1..])
}
fn whiteout_path(&self, path: &str) -> VfsResult<VfsPath> {
if path.is_empty() {
return self.write_layer().join(".whiteout/_wo");
}
self.write_layer()
.join(format!(".whiteout/{}_wo", &path[1..]))
}
fn ensure_has_parent(&self, path: &str) -> VfsResult<()> {
let separator = path.rfind('/');
if let Some(index) = separator {
let parent_path = &path[..index];
if self.exists(parent_path)? {
self.write_path(parent_path)?.create_dir_all()?;
return Ok(());
}
}
Err(VfsErrorKind::Other("Parent path does not exist".into()).into())
}
}
impl FileSystem for OverlayFS {
fn read_dir(&self, path: &str) -> VfsResult<Box<dyn Iterator<Item = String> + Send>> {
let actual_path = if !path.is_empty() { &path[1..] } else { path };
if !self.read_path(path)?.exists()? {
return Err(VfsErrorKind::FileNotFound.into());
}
let mut entries = HashSet::<String>::new();
for layer in &self.layers {
let layer_path = layer.join(actual_path)?;
if layer_path.exists()? {
for path in layer_path.read_dir()? {
entries.insert(path.filename());
}
}
}
let whiteout_path = self.write_layer().join(format!(".whiteout{}", path))?;
if whiteout_path.exists()? {
for path in whiteout_path.read_dir()? {
let filename = path.filename();
if filename.ends_with("_wo") {
entries.remove(&filename[..filename.len() - 3]);
}
}
}
Ok(Box::new(entries.into_iter()))
}
fn create_dir(&self, path: &str) -> VfsResult<()> {
self.ensure_has_parent(path)?;
self.write_path(path)?.create_dir()?;
let whiteout_path = self.whiteout_path(path)?;
if whiteout_path.exists()? {
whiteout_path.remove_file()?;
}
Ok(())
}
fn open_file(&self, path: &str) -> VfsResult<Box<dyn SeekAndRead + Send>> {
self.read_path(path)?.open_file()
}
fn create_file(&self, path: &str) -> VfsResult<Box<dyn SeekAndWrite + Send>> {
self.ensure_has_parent(path)?;
let result = self.write_path(path)?.create_file()?;
let whiteout_path = self.whiteout_path(path)?;
if whiteout_path.exists()? {
whiteout_path.remove_file()?;
}
Ok(result)
}
fn append_file(&self, path: &str) -> VfsResult<Box<dyn SeekAndWrite + Send>> {
let write_path = self.write_path(path)?;
if !write_path.exists()? {
self.ensure_has_parent(path)?;
self.read_path(path)?.copy_file(&write_path)?;
}
write_path.append_file()
}
fn metadata(&self, path: &str) -> VfsResult<VfsMetadata> {
self.read_path(path)?.metadata()
}
fn set_creation_time(&self, path: &str, time: SystemTime) -> VfsResult<()> {
self.write_path(path)?.set_creation_time(time)
}
fn set_modification_time(&self, path: &str, time: SystemTime) -> VfsResult<()> {
self.write_path(path)?.set_modification_time(time)
}
fn set_access_time(&self, path: &str, time: SystemTime) -> VfsResult<()> {
self.write_path(path)?.set_access_time(time)
}
fn exists(&self, path: &str) -> VfsResult<bool> {
if self
.whiteout_path(path)
.map_err(|err| err.with_context(|| "whiteout_path"))?
.exists()?
{
return Ok(false);
}
self.read_path(path)
.map(|path| path.exists())
.unwrap_or(Ok(false))
}
fn remove_file(&self, path: &str) -> VfsResult<()> {
self.read_path(path)?;
let write_path = self.write_path(path)?;
if write_path.exists()? {
write_path.remove_file()?;
}
let whiteout_path = self.whiteout_path(path)?;
whiteout_path.parent().create_dir_all()?;
whiteout_path.create_file()?;
Ok(())
}
fn remove_dir(&self, path: &str) -> VfsResult<()> {
self.read_path(path)?;
let write_path = self.write_path(path)?;
if write_path.exists()? {
write_path.remove_dir()?;
}
let whiteout_path = self.whiteout_path(path)?;
whiteout_path.parent().create_dir_all()?;
whiteout_path.create_file()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MemoryFS;
test_vfs!({
let upper_root: VfsPath = MemoryFS::new().into();
let lower_root: VfsPath = MemoryFS::new().into();
OverlayFS::new(&[upper_root, lower_root])
});
fn create_roots() -> (VfsPath, VfsPath, VfsPath) {
let lower_root: VfsPath = MemoryFS::new().into();
let upper_root: VfsPath = MemoryFS::new().into();
let overlay_root: VfsPath =
OverlayFS::new(&[upper_root.clone(), lower_root.clone()]).into();
(lower_root, upper_root, overlay_root)
}
#[test]
fn read() -> VfsResult<()> {
let (lower_root, upper_root, overlay_root) = create_roots();
let lower_path = lower_root.join("foo.txt")?;
let upper_path = upper_root.join("foo.txt")?;
let overlay_path = overlay_root.join("foo.txt")?;
lower_path.create_file()?.write_all(b"Hello Lower")?;
assert_eq!(&overlay_path.read_to_string()?, "Hello Lower");
upper_path.create_file()?.write_all(b"Hello Upper")?;
assert_eq!(&overlay_path.read_to_string()?, "Hello Upper");
lower_path.remove_file()?;
assert_eq!(&overlay_path.read_to_string()?, "Hello Upper");
upper_path.remove_file()?;
assert!(!overlay_path.exists()?, "File should not exist anymore");
Ok(())
}
#[test]
fn read_dir() -> VfsResult<()> {
let (lower_root, upper_root, overlay_root) = create_roots();
upper_root.join("foo/upper")?.create_dir_all()?;
upper_root.join("foo/common")?.create_dir_all()?;
lower_root.join("foo/common")?.create_dir_all()?;
lower_root.join("foo/lower")?.create_dir_all()?;
let entries: Vec<_> = overlay_root.join("foo")?.read_dir()?.collect();
let mut paths: Vec<_> = entries.iter().map(|path| path.as_str()).collect();
paths.sort();
assert_eq!(paths, vec!["/foo/common", "/foo/lower", "/foo/upper"]);
Ok(())
}
#[test]
fn read_dir_root() -> VfsResult<()> {
let (lower_root, upper_root, overlay_root) = create_roots();
upper_root.join("upper")?.create_dir_all()?;
upper_root.join("common")?.create_dir_all()?;
lower_root.join("common")?.create_dir_all()?;
lower_root.join("lower")?.create_dir_all()?;
let entries: Vec<_> = overlay_root.read_dir()?.collect();
let mut paths: Vec<_> = entries.iter().map(|path| path.as_str()).collect();
paths.sort();
assert_eq!(paths, vec!["/common", "/lower", "/upper"]);
Ok(())
}
#[test]
fn create_dir() -> VfsResult<()> {
let (lower_root, _upper_root, overlay_root) = create_roots();
lower_root.join("foo")?.create_dir_all()?;
assert!(overlay_root.join("foo")?.exists()?, "dir should exist");
overlay_root.join("foo/bar")?.create_dir()?;
assert!(overlay_root.join("foo/bar")?.exists()?, "dir should exist");
Ok(())
}
#[test]
fn create_file() -> VfsResult<()> {
let (lower_root, _upper_root, overlay_root) = create_roots();
lower_root.join("foo")?.create_dir_all()?;
assert!(overlay_root.join("foo")?.exists()?, "dir should exist");
overlay_root.join("foo/bar")?.create_file()?;
assert!(overlay_root.join("foo/bar")?.exists()?, "file should exist");
Ok(())
}
#[test]
fn append_file() -> VfsResult<()> {
let (lower_root, _upper_root, overlay_root) = create_roots();
lower_root.join("foo")?.create_dir_all()?;
lower_root
.join("foo/bar.txt")?
.create_file()?
.write_all(b"Hello Lower\n")?;
overlay_root
.join("foo/bar.txt")?
.append_file()?
.write_all(b"Hello Overlay\n")?;
assert_eq!(
&overlay_root.join("foo/bar.txt")?.read_to_string()?,
"Hello Lower\nHello Overlay\n"
);
Ok(())
}
#[test]
fn remove_file() -> VfsResult<()> {
let (lower_root, _upper_root, overlay_root) = create_roots();
lower_root.join("foo")?.create_dir_all()?;
lower_root
.join("foo/bar.txt")?
.create_file()?
.write_all(b"Hello Lower\n")?;
assert!(
overlay_root.join("foo/bar.txt")?.exists()?,
"file should exist"
);
overlay_root.join("foo/bar.txt")?.remove_file()?;
assert!(
!overlay_root.join("foo/bar.txt")?.exists()?,
"file should not exist anymore"
);
overlay_root
.join("foo/bar.txt")?
.create_file()?
.write_all(b"Hello Overlay\n")?;
assert!(
overlay_root.join("foo/bar.txt")?.exists()?,
"file should exist"
);
assert_eq!(
&overlay_root.join("foo/bar.txt")?.read_to_string()?,
"Hello Overlay\n"
);
Ok(())
}
#[test]
fn remove_dir() -> VfsResult<()> {
let (lower_root, _upper_root, overlay_root) = create_roots();
lower_root.join("foo")?.create_dir_all()?;
lower_root.join("foo/bar")?.create_dir_all()?;
assert!(overlay_root.join("foo/bar")?.exists()?, "dir should exist");
overlay_root.join("foo/bar")?.remove_dir()?;
assert!(
!overlay_root.join("foo/bar")?.exists()?,
"dir should not exist anymore"
);
overlay_root.join("foo/bar")?.create_dir()?;
assert!(overlay_root.join("foo/bar")?.exists()?, "dir should exist");
Ok(())
}
#[test]
fn read_dir_removed_entries() -> VfsResult<()> {
let (lower_root, _upper_root, overlay_root) = create_roots();
lower_root.join("foo")?.create_dir_all()?;
lower_root.join("foo/bar")?.create_dir_all()?;
lower_root.join("foo/bar.txt")?.create_dir_all()?;
let entries: Vec<_> = overlay_root.join("foo")?.read_dir()?.collect();
let mut paths: Vec<_> = entries.iter().map(|path| path.as_str()).collect();
paths.sort();
assert_eq!(paths, vec!["/foo/bar", "/foo/bar.txt"]);
overlay_root.join("foo/bar")?.remove_dir()?;
overlay_root.join("foo/bar.txt")?.remove_file()?;
let entries: Vec<_> = overlay_root.join("foo")?.read_dir()?.collect();
let mut paths: Vec<_> = entries.iter().map(|path| path.as_str()).collect();
paths.sort();
assert_eq!(paths, vec![] as Vec<&str>);
Ok(())
}
}
#[cfg(test)]
mod tests_physical {
use super::*;
use crate::PhysicalFS;
test_vfs!({
let temp_dir = std::env::temp_dir();
let dir = temp_dir.join(uuid::Uuid::new_v4().to_string());
let lower_path = dir.join("lower");
std::fs::create_dir_all(&lower_path).unwrap();
let upper_path = dir.join("upper");
std::fs::create_dir_all(&upper_path).unwrap();
let upper_root: VfsPath = PhysicalFS::new(upper_path).into();
let lower_root: VfsPath = PhysicalFS::new(lower_path).into();
OverlayFS::new(&[upper_root, lower_root])
});
}