2024-03-21 16:48:54 +01:00
|
|
|
//! Virtual file system for creating and manipulating files and folders in memory.
|
|
|
|
|
2024-03-21 17:05:48 +01:00
|
|
|
use std::{
|
|
|
|
collections::HashMap,
|
|
|
|
fs,
|
|
|
|
io::{self, Write},
|
|
|
|
path::Path,
|
|
|
|
};
|
|
|
|
|
|
|
|
use zip::ZipWriter;
|
2024-03-21 16:26:13 +01:00
|
|
|
|
2024-03-21 16:48:54 +01:00
|
|
|
/// Folder representation in virtual file system
|
2024-03-21 16:26:13 +01:00
|
|
|
#[derive(Debug, Default, Clone)]
|
2024-03-21 18:42:34 +01:00
|
|
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
2024-03-21 16:26:13 +01:00
|
|
|
pub struct VFolder {
|
|
|
|
folders: HashMap<String, VFolder>,
|
|
|
|
files: HashMap<String, VFile>,
|
|
|
|
}
|
|
|
|
impl VFolder {
|
2024-03-21 16:48:54 +01:00
|
|
|
/// Create a new, empty virtual folder.
|
2024-03-21 16:26:13 +01:00
|
|
|
pub fn new() -> VFolder {
|
|
|
|
VFolder {
|
|
|
|
folders: HashMap::new(),
|
|
|
|
files: HashMap::new(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-21 16:48:54 +01:00
|
|
|
/// Get all direct subfolders in the folder.
|
2024-03-21 16:26:13 +01:00
|
|
|
pub fn get_folders(&self) -> &HashMap<String, VFolder> {
|
|
|
|
&self.folders
|
|
|
|
}
|
2024-03-21 16:48:54 +01:00
|
|
|
/// Get all direct files in the folder.
|
2024-03-21 16:26:13 +01:00
|
|
|
pub fn get_files(&self) -> &HashMap<String, VFile> {
|
|
|
|
&self.files
|
|
|
|
}
|
|
|
|
|
2024-03-21 16:48:54 +01:00
|
|
|
/// Recursively add a new folder to the folder.
|
|
|
|
pub fn add_folder(&mut self, path: &str) {
|
|
|
|
self.add_existing_folder(path, VFolder::new());
|
2024-03-21 16:26:13 +01:00
|
|
|
}
|
2024-03-21 16:48:54 +01:00
|
|
|
/// Recursively add an existing folder to the folder.
|
|
|
|
pub fn add_existing_folder(&mut self, path: &str, folder: VFolder) {
|
|
|
|
let (head, tail) = path
|
2024-03-21 16:26:13 +01:00
|
|
|
.split_once('/')
|
|
|
|
.map(|(h, t)| (h, (!t.is_empty()).then_some(t)))
|
2024-03-21 16:48:54 +01:00
|
|
|
.unwrap_or((path, None));
|
2024-03-21 16:26:13 +01:00
|
|
|
if let Some(tail) = tail {
|
|
|
|
if let Some(subfolder) = self.get_folder_mut(head) {
|
|
|
|
subfolder.add_folder(tail);
|
|
|
|
} else {
|
|
|
|
let mut new_folder = VFolder::new();
|
|
|
|
new_folder.add_folder(tail);
|
|
|
|
self.add_existing_folder(head, new_folder);
|
|
|
|
}
|
|
|
|
} else {
|
2024-03-21 16:48:54 +01:00
|
|
|
self.folders.insert(path.to_string(), folder);
|
2024-03-21 16:26:13 +01:00
|
|
|
}
|
|
|
|
}
|
2024-03-21 16:48:54 +01:00
|
|
|
/// Recursively add a new file to the folder.
|
|
|
|
pub fn add_file(&mut self, path: &str, file: VFile) {
|
|
|
|
let (head, tail) = path
|
2024-03-21 16:26:13 +01:00
|
|
|
.split_once('/')
|
|
|
|
.map(|(h, t)| (h, (!t.is_empty()).then_some(t)))
|
2024-03-21 16:48:54 +01:00
|
|
|
.unwrap_or((path, None));
|
2024-03-21 16:26:13 +01:00
|
|
|
if let Some(tail) = tail {
|
|
|
|
if let Some(subfolder) = self.get_folder_mut(head) {
|
|
|
|
subfolder.add_file(tail, file);
|
|
|
|
} else {
|
|
|
|
let mut new_folder = VFolder::new();
|
|
|
|
new_folder.add_file(tail, file);
|
|
|
|
self.add_existing_folder(head, new_folder);
|
|
|
|
}
|
|
|
|
} else {
|
2024-03-21 16:48:54 +01:00
|
|
|
self.files.insert(path.to_string(), file);
|
2024-03-21 16:26:13 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-21 16:48:54 +01:00
|
|
|
/// Recursively get a subfolder by path.
|
|
|
|
pub fn get_folder(&self, path: &str) -> Option<&VFolder> {
|
|
|
|
let (head, tail) = path
|
2024-03-21 16:26:13 +01:00
|
|
|
.split_once('/')
|
|
|
|
.map(|(h, t)| (h, (!t.is_empty()).then_some(t)))
|
2024-03-21 16:48:54 +01:00
|
|
|
.unwrap_or((path, None));
|
2024-03-21 16:26:13 +01:00
|
|
|
if let Some(tail) = tail {
|
|
|
|
self.folders.get(head)?.get_folder(tail)
|
|
|
|
} else {
|
2024-03-21 16:48:54 +01:00
|
|
|
self.folders.get(path)
|
2024-03-21 16:26:13 +01:00
|
|
|
}
|
|
|
|
}
|
2024-03-21 16:48:54 +01:00
|
|
|
/// Recursively get a mutable subfolder by path.
|
|
|
|
pub fn get_folder_mut(&mut self, path: &str) -> Option<&mut VFolder> {
|
|
|
|
let (head, tail) = path
|
2024-03-21 16:26:13 +01:00
|
|
|
.split_once('/')
|
|
|
|
.map(|(h, t)| (h, (!t.is_empty()).then_some(t)))
|
2024-03-21 16:48:54 +01:00
|
|
|
.unwrap_or((path, None));
|
2024-03-21 16:26:13 +01:00
|
|
|
if let Some(tail) = tail {
|
|
|
|
self.folders.get_mut(head)?.get_folder_mut(tail)
|
|
|
|
} else {
|
2024-03-21 16:48:54 +01:00
|
|
|
self.folders.get_mut(path)
|
2024-03-21 16:26:13 +01:00
|
|
|
}
|
|
|
|
}
|
2024-03-21 16:48:54 +01:00
|
|
|
/// Recursively get a file by path.
|
|
|
|
pub fn get_file(&self, path: &str) -> Option<&VFile> {
|
|
|
|
let (head, tail) = path
|
2024-03-21 16:26:13 +01:00
|
|
|
.split_once('/')
|
|
|
|
.map(|(h, t)| (h, (!t.is_empty()).then_some(t)))
|
2024-03-21 16:48:54 +01:00
|
|
|
.unwrap_or((path, None));
|
2024-03-21 16:26:13 +01:00
|
|
|
if let Some(tail) = tail {
|
|
|
|
self.folders.get(head)?.get_file(tail)
|
|
|
|
} else {
|
2024-03-21 16:48:54 +01:00
|
|
|
self.files.get(path)
|
2024-03-21 16:26:13 +01:00
|
|
|
}
|
|
|
|
}
|
2024-03-21 16:48:54 +01:00
|
|
|
/// Recursively get a mutable file by path.
|
|
|
|
pub fn get_file_mut(&mut self, path: &str) -> Option<&mut VFile> {
|
|
|
|
let (head, tail) = path
|
2024-03-21 16:26:13 +01:00
|
|
|
.split_once('/')
|
|
|
|
.map(|(h, t)| (h, (!t.is_empty()).then_some(t)))
|
2024-03-21 16:48:54 +01:00
|
|
|
.unwrap_or((path, None));
|
2024-03-21 16:26:13 +01:00
|
|
|
if let Some(tail) = tail {
|
|
|
|
self.folders.get_mut(head)?.get_file_mut(tail)
|
|
|
|
} else {
|
2024-03-21 16:48:54 +01:00
|
|
|
self.files.get_mut(path)
|
2024-03-21 16:26:13 +01:00
|
|
|
}
|
|
|
|
}
|
2024-03-21 16:34:27 +01:00
|
|
|
|
2024-03-21 16:48:54 +01:00
|
|
|
/// Place the folder and its contents on the file system.
|
2024-03-21 16:34:27 +01:00
|
|
|
pub fn place(&self, path: &Path) -> io::Result<()> {
|
|
|
|
fs::create_dir_all(path)?;
|
|
|
|
for (name, folder) in &self.folders {
|
|
|
|
folder.place(&path.join(name))?;
|
|
|
|
}
|
|
|
|
for (name, file) in &self.files {
|
|
|
|
match file {
|
|
|
|
VFile::Text(text) => {
|
|
|
|
fs::write(path.join(name), text)?;
|
|
|
|
}
|
|
|
|
VFile::Binary(data) => {
|
|
|
|
fs::write(path.join(name), data)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
2024-03-21 17:05:48 +01:00
|
|
|
|
|
|
|
/// Zip the folder and its contents into a zip archive.
|
|
|
|
pub fn zip(&self, path: &Path) -> io::Result<()> {
|
|
|
|
let file = fs::File::create(path)?;
|
|
|
|
let mut writer = ZipWriter::new(file);
|
|
|
|
let virtual_files = self.flatten();
|
|
|
|
|
|
|
|
for (path, file) in virtual_files {
|
|
|
|
writer.start_file(path, Default::default())?;
|
|
|
|
match file {
|
|
|
|
VFile::Text(text) => {
|
|
|
|
writer.write_all(text.as_bytes())?;
|
|
|
|
}
|
|
|
|
VFile::Binary(data) => {
|
|
|
|
writer.write_all(data)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
writer.finish()?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Flatten the folder and its contents into a list of files with full paths.
|
|
|
|
fn flatten(&self) -> Vec<(String, &VFile)> {
|
|
|
|
let mut files = self
|
|
|
|
.files
|
|
|
|
.iter()
|
|
|
|
.map(|(k, v)| (k.to_owned(), v))
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
for (name, folder) in &self.folders {
|
|
|
|
let sub_files = folder
|
|
|
|
.flatten()
|
|
|
|
.into_iter()
|
|
|
|
.map(|(path, file)| (format!("{}/{}", name, path), file))
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
files.extend(sub_files);
|
|
|
|
}
|
|
|
|
|
|
|
|
files
|
|
|
|
}
|
2024-03-21 16:26:13 +01:00
|
|
|
}
|
|
|
|
|
2024-03-21 19:50:00 +01:00
|
|
|
impl TryFrom<&Path> for VFolder {
|
|
|
|
type Error = io::Error;
|
|
|
|
|
|
|
|
fn try_from(value: &Path) -> Result<Self, Self::Error> {
|
|
|
|
let mut root_vfolder = VFolder::new();
|
|
|
|
let root_folder = fs::read_dir(value)?;
|
|
|
|
for dir_entry in root_folder {
|
|
|
|
let dir_entry = dir_entry?;
|
|
|
|
let path = dir_entry.path();
|
|
|
|
let name = dir_entry
|
|
|
|
.file_name()
|
|
|
|
.into_string()
|
|
|
|
.map(Some)
|
|
|
|
.unwrap_or_default();
|
|
|
|
if let Some(name) = name {
|
|
|
|
if path.is_dir() {
|
|
|
|
root_vfolder.add_existing_folder(&name, VFolder::try_from(path.as_path())?);
|
|
|
|
} else if path.is_file() {
|
|
|
|
let data = fs::read(path)?;
|
|
|
|
root_vfolder.add_file(&name, VFile::Binary(data));
|
|
|
|
} else {
|
|
|
|
unreachable!("Path is neither file nor directory");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return Err(io::Error::new(
|
|
|
|
io::ErrorKind::InvalidInput,
|
|
|
|
"Invalid file name",
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(root_vfolder)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-21 16:48:54 +01:00
|
|
|
/// File representation in virtual file system
|
2024-03-21 16:26:13 +01:00
|
|
|
#[derive(Debug, Clone)]
|
2024-03-21 18:42:34 +01:00
|
|
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
2024-03-21 16:26:13 +01:00
|
|
|
pub enum VFile {
|
2024-03-21 16:48:54 +01:00
|
|
|
/// Text file
|
2024-03-21 16:26:13 +01:00
|
|
|
Text(String),
|
2024-03-21 16:48:54 +01:00
|
|
|
/// Binary file
|
2024-03-21 16:26:13 +01:00
|
|
|
Binary(Vec<u8>),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<String> for VFile {
|
|
|
|
fn from(value: String) -> Self {
|
|
|
|
VFile::Text(value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
impl From<&str> for VFile {
|
|
|
|
fn from(value: &str) -> Self {
|
|
|
|
VFile::Text(value.to_string())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
impl Default for VFile {
|
|
|
|
fn default() -> Self {
|
|
|
|
VFile::Text(String::new())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_vfolder() {
|
|
|
|
let mut v_folder = VFolder::new();
|
|
|
|
let v_file_1 = VFile::from("foo");
|
|
|
|
v_folder.add_file("foo.txt", v_file_1);
|
|
|
|
|
|
|
|
assert_eq!(v_folder.get_files().len(), 1);
|
|
|
|
assert_eq!(v_folder.get_folders().len(), 0);
|
|
|
|
|
|
|
|
let v_file_2 = VFile::from("baz");
|
|
|
|
v_folder.add_file("bar/baz.txt", v_file_2);
|
|
|
|
|
|
|
|
assert_eq!(v_folder.get_files().len(), 1);
|
|
|
|
assert_eq!(v_folder.get_folders().len(), 1);
|
|
|
|
assert!(v_folder.get_file("bar/baz.txt").is_some());
|
|
|
|
assert!(v_folder
|
|
|
|
.get_folder("bar")
|
|
|
|
.expect("folder not found")
|
|
|
|
.get_file("baz.txt")
|
|
|
|
.is_some());
|
|
|
|
}
|
|
|
|
}
|