#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileKind {
File,
Directory,
Symlink,
}
#[derive(Debug, Clone)]
pub struct FileMetadata {
pub path: String,
pub is_dir: bool,
pub size: u64,
pub permissions: FilePermissions,
pub is_symlink: bool,
pub symlink_target: Option<String>,
pub modified: i64,
pub hash: Option<String>,
pub kind: FileKind,
}
impl FileMetadata {
pub fn is_readable(&self) -> bool {
self.permissions.owner_read
}
pub fn is_writable(&self) -> bool {
self.permissions.owner_write
}
pub fn is_executable(&self) -> bool {
self.permissions.owner_exec
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FilePermissions {
pub owner_read: bool,
pub owner_write: bool,
pub owner_exec: bool,
pub group_read: bool,
pub group_write: bool,
pub group_exec: bool,
pub other_read: bool,
pub other_write: bool,
pub other_exec: bool,
}
impl FilePermissions {
pub fn file() -> Self {
Self {
owner_read: true,
owner_write: true,
owner_exec: false,
group_read: true,
group_write: false,
group_exec: false,
other_read: true,
other_write: false,
other_exec: false,
}
}
pub fn executable() -> Self {
Self {
owner_read: true,
owner_write: true,
owner_exec: true,
group_read: true,
group_write: false,
group_exec: true,
other_read: true,
other_write: false,
other_exec: true,
}
}
pub fn readonly() -> Self {
Self {
owner_read: true,
owner_write: false,
owner_exec: false,
group_read: true,
group_write: false,
group_exec: false,
other_read: true,
other_write: false,
other_exec: false,
}
}
pub fn from_octal(mode: u32) -> Self {
let owner = (mode >> 6) & 7;
let group = (mode >> 3) & 7;
let other = mode & 7;
Self {
owner_read: owner & 4 != 0,
owner_write: owner & 2 != 0,
owner_exec: owner & 1 != 0,
group_read: group & 4 != 0,
group_write: group & 2 != 0,
group_exec: group & 1 != 0,
other_read: other & 4 != 0,
other_write: other & 2 != 0,
other_exec: other & 1 != 0,
}
}
pub fn to_octal(&self) -> u32 {
let mut mode = 0u32;
if self.owner_read {
mode |= 4 << 6;
}
if self.owner_write {
mode |= 2 << 6;
}
if self.owner_exec {
mode |= 1 << 6;
}
if self.group_read {
mode |= 4 << 3;
}
if self.group_write {
mode |= 2 << 3;
}
if self.group_exec {
mode |= 1 << 3;
}
if self.other_read {
mode |= 4;
}
if self.other_write {
mode |= 2;
}
if self.other_exec {
mode |= 1;
}
mode
}
pub fn to_string(&self) -> String {
let mut s = String::with_capacity(9);
s.push(if self.owner_read { 'r' } else { '-' });
s.push(if self.owner_write { 'w' } else { '-' });
s.push(if self.owner_exec { 'x' } else { '-' });
s.push(if self.group_read { 'r' } else { '-' });
s.push(if self.group_write { 'w' } else { '-' });
s.push(if self.group_exec { 'x' } else { '-' });
s.push(if self.other_read { 'r' } else { '-' });
s.push(if self.other_write { 'w' } else { '-' });
s.push(if self.other_exec { 'x' } else { '-' });
s
}
}
impl Default for FilePermissions {
fn default() -> Self {
Self::file()
}
}
impl From<u32> for FilePermissions {
fn from(mode: u32) -> Self {
Self::from_octal(mode)
}
}
impl From<FilePermissions> for u32 {
fn from(perms: FilePermissions) -> Self {
perms.to_octal()
}
}
#[derive(Debug, Clone)]
pub enum FsOperation {
WriteFile { path: String, content: Vec<u8> },
CopyFile { src: String, dst: String },
CopyDir { src: String, dst: String },
MoveFile { src: String, dst: String },
MoveDir { src: String, dst: String },
DeleteFile { path: String },
DeleteDir { path: String },
Chmod {
path: String,
permissions: FilePermissions,
recursive: bool,
},
MakeExecutable { path: String },
Symlink {
link_path: String,
target_path: String,
},
}
impl FsOperation {
pub fn describe(&self) -> String {
match self {
FsOperation::WriteFile { path, .. } => format!("Write file: {}", path),
FsOperation::CopyFile { src, dst } => format!("Copy file: {} -> {}", src, dst),
FsOperation::CopyDir { src, dst } => format!("Copy directory: {} -> {}", src, dst),
FsOperation::MoveFile { src, dst } => format!("Move file: {} -> {}", src, dst),
FsOperation::MoveDir { src, dst } => format!("Move directory: {} -> {}", src, dst),
FsOperation::DeleteFile { path } => format!("Delete file: {}", path),
FsOperation::DeleteDir { path } => format!("Delete directory: {}", path),
FsOperation::Chmod {
path,
permissions,
recursive,
} => {
if *recursive {
format!("Chmod {} (recursive): {}", path, permissions.to_string())
} else {
format!("Chmod {}: {}", path, permissions.to_string())
}
}
FsOperation::MakeExecutable { path } => format!("Make executable: {}", path),
FsOperation::Symlink {
link_path,
target_path,
} => {
format!("Create symlink: {} -> {}", link_path, target_path)
}
}
}
}
#[derive(Debug, Clone)]
pub struct FindResults {
pub files: Vec<String>,
pub count: usize,
pub dirs_traversed: usize,
}
impl FindResults {
pub fn new() -> Self {
Self {
files: Vec::new(),
count: 0,
dirs_traversed: 0,
}
}
pub fn dirs_only(self) -> Vec<String> {
self.files
.into_iter()
.filter(|p| p.ends_with('/'))
.collect()
}
pub fn files_only(self) -> Vec<String> {
self.files
.into_iter()
.filter(|p| !p.ends_with('/'))
.collect()
}
}
impl Default for FindResults {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct DirectoryEntry {
pub name: String,
pub is_dir: bool,
pub size: u64,
pub permissions: FilePermissions,
pub modified: i64,
}
#[derive(Debug, Clone)]
pub struct OperationSummary {
pub operations: Vec<FsOperation>,
pub files_affected: usize,
pub dirs_affected: usize,
pub bytes_changed: u64,
}
impl OperationSummary {
pub fn new() -> Self {
Self {
operations: Vec::new(),
files_affected: 0,
dirs_affected: 0,
bytes_changed: 0,
}
}
pub fn add_operation(&mut self, op: FsOperation, bytes: u64) {
match &op {
FsOperation::WriteFile { .. } | FsOperation::CopyFile { .. } => {
self.files_affected += 1;
}
FsOperation::CopyDir { .. } => {
self.dirs_affected += 1;
}
FsOperation::DeleteFile { .. } | FsOperation::DeleteDir { .. } => {
self.files_affected += 1;
}
_ => {}
}
self.bytes_changed += bytes;
self.operations.push(op);
}
}
impl Default for OperationSummary {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permissions_octal() {
let perms = FilePermissions::from_octal(0o755);
assert!(perms.owner_read);
assert!(perms.owner_write);
assert!(perms.owner_exec);
assert_eq!(perms.to_octal(), 0o755);
}
#[test]
fn test_permissions_string() {
let perms = FilePermissions::executable();
assert_eq!(perms.to_string(), "rwxr-xr-x");
}
#[test]
fn test_operation_describe() {
let op = FsOperation::WriteFile {
path: "/tmp/test.txt".to_string(),
content: vec![],
};
assert_eq!(op.describe(), "Write file: /tmp/test.txt");
}
#[test]
fn test_find_results() {
let results1 = FindResults {
files: vec!["file.txt".to_string(), "dir/".to_string()],
count: 2,
dirs_traversed: 1,
};
let results2 = FindResults {
files: vec!["file.txt".to_string(), "dir/".to_string()],
count: 2,
dirs_traversed: 1,
};
assert_eq!(results1.files_only().len(), 1);
assert_eq!(results2.dirs_only().len(), 1);
}
}