use async_trait::async_trait;
use std::io::{Error as IoError, ErrorKind};
use std::path::Path;
use std::time::SystemTime;
use super::limits::{FsLimits, FsUsage};
use crate::error::Result;
#[allow(dead_code)]
pub mod fs_errors {
use super::*;
#[inline]
pub fn is_a_directory() -> crate::Error {
IoError::other("is a directory").into()
}
#[inline]
pub fn already_exists(msg: &str) -> crate::Error {
IoError::new(ErrorKind::AlreadyExists, msg).into()
}
#[inline]
pub fn parent_not_found() -> crate::Error {
IoError::new(ErrorKind::NotFound, "parent directory not found").into()
}
#[inline]
pub fn not_found(msg: &str) -> crate::Error {
IoError::new(ErrorKind::NotFound, msg).into()
}
#[inline]
pub fn not_a_directory() -> crate::Error {
IoError::other("not a directory").into()
}
#[inline]
pub fn directory_not_empty() -> crate::Error {
IoError::other("directory not empty").into()
}
}
#[async_trait]
pub trait FileSystemExt: Send + Sync {
fn usage(&self) -> FsUsage {
FsUsage::default()
}
async fn mkfifo(&self, _path: &Path, _mode: u32) -> Result<()> {
Err(std::io::Error::other("mkfifo not supported").into())
}
fn limits(&self) -> FsLimits {
FsLimits::unlimited()
}
fn vfs_snapshot(&self) -> Option<super::VfsSnapshot> {
None
}
fn vfs_restore(&self, _snapshot: &super::VfsSnapshot) -> bool {
false
}
}
#[async_trait]
pub trait FileSystem: FileSystemExt {
async fn read_file(&self, path: &Path) -> Result<Vec<u8>>;
async fn write_file(&self, path: &Path, content: &[u8]) -> Result<()>;
async fn append_file(&self, path: &Path, content: &[u8]) -> Result<()>;
async fn mkdir(&self, path: &Path, recursive: bool) -> Result<()>;
async fn remove(&self, path: &Path, recursive: bool) -> Result<()>;
async fn stat(&self, path: &Path) -> Result<Metadata>;
async fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>>;
async fn exists(&self, path: &Path) -> Result<bool>;
async fn rename(&self, from: &Path, to: &Path) -> Result<()>;
async fn copy(&self, from: &Path, to: &Path) -> Result<()>;
async fn symlink(&self, target: &Path, link: &Path) -> Result<()>;
async fn read_link(&self, path: &Path) -> Result<std::path::PathBuf>;
async fn chmod(&self, path: &Path, mode: u32) -> Result<()>;
fn as_search_capable(&self) -> Option<&dyn super::SearchCapable> {
None
}
}
#[derive(Debug, Clone)]
pub struct Metadata {
pub file_type: FileType,
pub size: u64,
pub mode: u32,
pub modified: SystemTime,
pub created: SystemTime,
}
impl Default for Metadata {
fn default() -> Self {
Self {
file_type: FileType::File,
size: 0,
mode: 0o644,
modified: SystemTime::now(),
created: SystemTime::now(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FileType {
File,
Directory,
Symlink,
Fifo,
}
impl FileType {
pub fn is_file(&self) -> bool {
matches!(self, FileType::File)
}
pub fn is_dir(&self) -> bool {
matches!(self, FileType::Directory)
}
pub fn is_symlink(&self) -> bool {
matches!(self, FileType::Symlink)
}
pub fn is_fifo(&self) -> bool {
matches!(self, FileType::Fifo)
}
}
#[derive(Debug, Clone)]
pub struct DirEntry {
pub name: String,
pub metadata: Metadata,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fs_error_is_a_directory_message() {
let err = fs_errors::is_a_directory();
let msg = format!("{err}");
assert!(msg.contains("is a directory"), "got: {msg}");
}
#[test]
fn fs_error_already_exists_message() {
let err = fs_errors::already_exists("path /tmp exists");
let msg = format!("{err}");
assert!(msg.contains("path /tmp exists"), "got: {msg}");
}
#[test]
fn fs_error_parent_not_found_message() {
let err = fs_errors::parent_not_found();
let msg = format!("{err}");
assert!(msg.contains("parent directory not found"), "got: {msg}");
}
#[test]
fn fs_error_not_found_message() {
let err = fs_errors::not_found("no such file");
let msg = format!("{err}");
assert!(msg.contains("no such file"), "got: {msg}");
}
#[test]
fn fs_error_not_a_directory_message() {
let err = fs_errors::not_a_directory();
let msg = format!("{err}");
assert!(msg.contains("not a directory"), "got: {msg}");
}
#[test]
fn fs_error_directory_not_empty_message() {
let err = fs_errors::directory_not_empty();
let msg = format!("{err}");
assert!(msg.contains("directory not empty"), "got: {msg}");
}
#[test]
fn file_type_is_file() {
assert!(FileType::File.is_file());
assert!(!FileType::Directory.is_file());
assert!(!FileType::Symlink.is_file());
}
#[test]
fn file_type_is_dir() {
assert!(FileType::Directory.is_dir());
assert!(!FileType::File.is_dir());
assert!(!FileType::Symlink.is_dir());
}
#[test]
fn file_type_is_symlink() {
assert!(FileType::Symlink.is_symlink());
assert!(!FileType::File.is_symlink());
assert!(!FileType::Directory.is_symlink());
}
#[test]
fn file_type_equality() {
assert_eq!(FileType::File, FileType::File);
assert_eq!(FileType::Directory, FileType::Directory);
assert_eq!(FileType::Symlink, FileType::Symlink);
assert_ne!(FileType::File, FileType::Directory);
assert_ne!(FileType::File, FileType::Symlink);
assert_ne!(FileType::Directory, FileType::Symlink);
}
#[test]
fn file_type_debug() {
let dbg = format!("{:?}", FileType::File);
assert_eq!(dbg, "File");
}
#[test]
fn metadata_default_is_file() {
let m = Metadata::default();
assert!(m.file_type.is_file());
assert_eq!(m.size, 0);
assert_eq!(m.mode, 0o644);
}
#[test]
fn metadata_custom_fields() {
let now = SystemTime::now();
let m = Metadata {
file_type: FileType::Directory,
size: 4096,
mode: 0o755,
modified: now,
created: now,
};
assert!(m.file_type.is_dir());
assert_eq!(m.size, 4096);
assert_eq!(m.mode, 0o755);
}
#[test]
fn metadata_clone() {
let m = Metadata::default();
let cloned = m.clone();
assert_eq!(cloned.size, m.size);
assert_eq!(cloned.mode, m.mode);
assert!(cloned.file_type.is_file());
}
#[test]
fn dir_entry_construction() {
let entry = DirEntry {
name: "test.txt".into(),
metadata: Metadata::default(),
};
assert_eq!(entry.name, "test.txt");
assert!(entry.metadata.file_type.is_file());
}
#[test]
fn dir_entry_with_directory_type() {
let now = SystemTime::now();
let entry = DirEntry {
name: "subdir".into(),
metadata: Metadata {
file_type: FileType::Directory,
size: 0,
mode: 0o755,
modified: now,
created: now,
},
};
assert_eq!(entry.name, "subdir");
assert!(entry.metadata.file_type.is_dir());
}
#[test]
fn dir_entry_debug() {
let entry = DirEntry {
name: "f".into(),
metadata: Metadata::default(),
};
let dbg = format!("{:?}", entry);
assert!(dbg.contains("DirEntry"));
assert!(dbg.contains("\"f\""));
}
#[test]
fn filesystem_default_usage_returns_zeros() {
struct Dummy;
#[async_trait]
impl FileSystemExt for Dummy {}
#[async_trait]
impl FileSystem for Dummy {
async fn read_file(&self, _: &Path) -> crate::error::Result<Vec<u8>> {
unimplemented!()
}
async fn write_file(&self, _: &Path, _: &[u8]) -> crate::error::Result<()> {
unimplemented!()
}
async fn append_file(&self, _: &Path, _: &[u8]) -> crate::error::Result<()> {
unimplemented!()
}
async fn mkdir(&self, _: &Path, _: bool) -> crate::error::Result<()> {
unimplemented!()
}
async fn remove(&self, _: &Path, _: bool) -> crate::error::Result<()> {
unimplemented!()
}
async fn stat(&self, _: &Path) -> crate::error::Result<Metadata> {
unimplemented!()
}
async fn read_dir(&self, _: &Path) -> crate::error::Result<Vec<DirEntry>> {
unimplemented!()
}
async fn exists(&self, _: &Path) -> crate::error::Result<bool> {
unimplemented!()
}
async fn rename(&self, _: &Path, _: &Path) -> crate::error::Result<()> {
unimplemented!()
}
async fn copy(&self, _: &Path, _: &Path) -> crate::error::Result<()> {
unimplemented!()
}
async fn symlink(&self, _: &Path, _: &Path) -> crate::error::Result<()> {
unimplemented!()
}
async fn read_link(&self, _: &Path) -> crate::error::Result<std::path::PathBuf> {
unimplemented!()
}
async fn chmod(&self, _: &Path, _: u32) -> crate::error::Result<()> {
unimplemented!()
}
}
let d = Dummy;
let usage = d.usage();
assert_eq!(usage.total_bytes, 0);
assert_eq!(usage.file_count, 0);
assert_eq!(usage.dir_count, 0);
let limits = d.limits();
assert_eq!(limits.max_total_bytes, u64::MAX);
}
}