Initial commit

This commit is contained in:
Ben Bridle 2022-08-25 21:27:39 +12:00
commit 8f410d1ead
10 changed files with 341 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

9
Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "vagabond"
version = "1.0.0"
authors = ["Ben Bridle <bridle.benjamin@gmail.com>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

86
src/entry.rs Normal file
View File

@ -0,0 +1,86 @@
use crate::EntryReadError;
use std::path::{Path, PathBuf};
#[derive(PartialEq)]
pub enum EntryType {
File,
Directory,
}
pub struct Entry {
pub entry_type: EntryType,
pub is_symlink: bool,
pub name: String,
pub extension: String,
pub path: PathBuf,
pub original_path: PathBuf,
}
impl Entry {
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, EntryReadError> {
let path = path.as_ref();
let metadata = path.metadata()?;
let canonical_path = std::fs::canonicalize(path)?;
let canonical_metadata = canonical_path.metadata()?;
let entry_type = if canonical_metadata.is_file() {
EntryType::File
} else if canonical_metadata.is_dir() {
EntryType::Directory
} else {
return Err(EntryReadError::NotFound);
};
let name = match path.file_name() {
Some(osstr) => osstr.to_string_lossy().to_string(),
None => unreachable!(),
};
let extension = match path.extension() {
Some(extension) => extension.to_string_lossy().into(),
None => String::default(),
};
Ok(Entry {
entry_type,
name,
extension,
path: canonical_path,
original_path: path.to_path_buf(),
is_symlink: metadata.is_symlink(),
})
}
/// Splits the filename on the last period, ignoring any period at the
/// start of the filename. If no extension is found, the extension is empty.
pub fn split_name(&self) -> (String, String) {
match self.name.rsplit_once(".") {
Some(("", _)) | None => (self.name.to_string(), String::new()),
Some((prefix, extension)) => (prefix.to_string(), extension.to_string()),
}
}
pub fn is_file(&self) -> bool {
match self.entry_type {
EntryType::File => true,
_ => false,
}
}
pub fn is_directory(&self) -> bool {
match self.entry_type {
EntryType::Directory => true,
_ => false,
}
}
pub fn read_as_bytes(&self) -> Result<Vec<u8>, EntryReadError> {
return Ok(std::fs::read(&self.path)?);
}
pub fn read_as_utf8_string(&self) -> Result<String, EntryReadError> {
return Ok(String::from_utf8_lossy(&self.read_as_bytes()?).to_string());
}
}
impl AsRef<Path> for Entry {
fn as_ref(&self) -> &Path {
&self.path
}
}

48
src/error.rs Normal file
View File

@ -0,0 +1,48 @@
use std::io::Error as IoError;
use std::io::ErrorKind;
#[derive(Debug)]
pub enum EntryReadError {
NotFound,
PermissionDenied,
}
impl From<IoError> for EntryReadError {
fn from(io_error: IoError) -> Self {
match io_error.kind() {
ErrorKind::NotFound => Self::NotFound,
// An intermediate path component was a plain file, not a directory
ErrorKind::NotADirectory => Self::NotFound,
// A cyclic symbolic link chain was included in the provided path
ErrorKind::FilesystemLoop => Self::NotFound,
ErrorKind::PermissionDenied => Self::PermissionDenied,
err => panic!("Unexpected IoError encountered: {:?}", err),
}
}
}
#[derive(Debug)]
pub enum EntryWriteError {
NotFound,
PermissionDenied,
}
impl From<EntryReadError> for EntryWriteError {
fn from(error: EntryReadError) -> Self {
match error {
EntryReadError::NotFound => EntryWriteError::NotFound,
EntryReadError::PermissionDenied => EntryWriteError::PermissionDenied,
}
}
}
impl From<IoError> for EntryWriteError {
fn from(io_error: IoError) -> Self {
match io_error.kind() {
ErrorKind::NotFound => Self::NotFound,
// An intermediate path component was a plain file, not a directory
ErrorKind::NotADirectory => Self::NotFound,
// A cyclic symbolic link chain was included in the provided path
ErrorKind::FilesystemLoop => Self::NotFound,
ErrorKind::PermissionDenied => Self::PermissionDenied,
err => panic!("Unexpected IoError encountered: {:?}", err),
}
}
}

10
src/lib.rs Normal file
View File

@ -0,0 +1,10 @@
#![feature(io_error_more)]
mod error;
pub use error::*;
mod operations;
pub use operations::*;
mod entry;
pub use entry::{Entry, EntryType};

30
src/operations.rs Normal file
View File

@ -0,0 +1,30 @@
use crate::*;
use std::path::Path;
mod ls;
pub use ls::*;
mod cp;
pub use cp::*;
mod rm;
pub use rm::*;
mod mkdir;
pub use mkdir::*;
pub fn append_to_file<P>(_path: P, _content: &str) -> Result<(), EntryWriteError>
where
P: AsRef<Path>,
{
unimplemented!()
}
pub fn write_to_file<P>(path: P, content: &str) -> Result<(), EntryWriteError>
where
P: AsRef<Path>,
{
make_parent_directory(&path)?;
std::fs::write(&path, content)?;
Ok(())
}

62
src/operations/cp.rs Normal file
View File

@ -0,0 +1,62 @@
use crate::make_parent_directory;
use crate::{get_entry, get_optional_entry, list_directory};
use crate::{EntryType, EntryWriteError};
use std::path::Path;
pub fn copy<P, Q>(source_path: P, target_path: Q) -> Result<(), EntryWriteError>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let source = get_entry(&source_path)?;
let target = get_optional_entry(&target_path)?;
let target_type = target.and_then(|e| Some(e.entry_type));
match (source.entry_type, target_type) {
(EntryType::File, Some(EntryType::File)) => {
copy_file(source_path, target_path)?;
}
(EntryType::File, Some(EntryType::Directory)) => {
let target_path = target_path.as_ref().join(source.name);
copy_file(source_path, target_path)?;
}
(EntryType::File, None) => {
make_parent_directory(&target_path)?;
copy_file(source_path, target_path)?;
}
(EntryType::Directory, Some(EntryType::File)) => {
std::fs::remove_file(&target_path)?;
copy_directory(&source_path, &target_path)?;
}
(EntryType::Directory, Some(EntryType::Directory)) => {
let target_path = target_path.as_ref().join(source.name);
copy_directory(source_path, target_path)?;
}
(EntryType::Directory, None) => {
make_parent_directory(&target_path)?;
copy_directory(source_path, target_path)?;
}
}
Ok(())
}
fn copy_file<P, Q>(source_path: P, target_path: Q) -> Result<(), EntryWriteError>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
std::fs::copy(source_path, target_path)?;
Ok(())
}
fn copy_directory<P, Q>(source_path: P, target_path: Q) -> Result<(), EntryWriteError>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
for entry in list_directory(&source_path)? {
let target_path = target_path.as_ref().join(entry.name);
copy(entry.path, &target_path)?;
}
Ok(())
}

51
src/operations/ls.rs Normal file
View File

@ -0,0 +1,51 @@
use crate::{Entry, EntryReadError, EntryType};
use std::path::Path;
pub fn get_entry<P>(path: P) -> Result<Entry, EntryReadError>
where
P: AsRef<Path>,
{
Entry::from_path(path)
}
pub fn get_optional_entry<P>(path: P) -> Result<Option<Entry>, EntryReadError>
where
P: AsRef<Path>,
{
match get_entry(path) {
Ok(e) => Ok(Some(e)),
Err(EntryReadError::NotFound) => Ok(None),
Err(other) => Err(other),
}
}
pub fn list_directory<P>(path: P) -> Result<Vec<Entry>, EntryReadError>
where
P: AsRef<Path>,
{
let mut entries = Vec::new();
for dir_entry in std::fs::read_dir(path)? {
let entry = match Entry::from_path(&dir_entry?.path()) {
Ok(v) => v,
Err(_) => continue,
};
entries.push(entry);
}
return Ok(entries);
}
/// Recursively descend into a directory and all sub-directories,
/// returning an [`Entry`](struct.Entry.html) for each discovered file.
pub fn traverse_directory<P>(path: P) -> Result<Vec<Entry>, EntryReadError>
where
P: AsRef<Path>,
{
let mut file_entries = Vec::new();
for entry in list_directory(path)? {
match entry.entry_type {
EntryType::File => file_entries.push(entry),
EntryType::Directory => file_entries.extend(traverse_directory(&entry.path)?),
}
}
return Ok(file_entries);
}

20
src/operations/mkdir.rs Normal file
View File

@ -0,0 +1,20 @@
use crate::EntryWriteError;
use std::path::Path;
pub fn make_directory<P>(path: P) -> Result<(), EntryWriteError>
where
P: AsRef<Path>,
{
std::fs::DirBuilder::new().recursive(true).create(path)?;
Ok(())
}
pub fn make_parent_directory<P>(path: P) -> Result<(), EntryWriteError>
where
P: AsRef<Path>,
{
match path.as_ref().parent() {
Some(parent) => make_directory(parent),
None => Ok(()),
}
}

23
src/operations/rm.rs Normal file
View File

@ -0,0 +1,23 @@
use crate::EntryWriteError;
use crate::{get_entry, EntryType};
use std::path::Path;
pub fn remove<P>(path: P) -> Result<(), EntryWriteError>
where
P: AsRef<Path>,
{
let entry = get_entry(&path)?;
match entry.entry_type {
EntryType::File => std::fs::remove_file(&path)?,
EntryType::Directory => std::fs::remove_dir_all(&path)?,
}
Ok(())
}
pub fn remove_file<P>(path: P) -> Result<(), EntryWriteError>
where
P: AsRef<Path>,
{
std::fs::remove_file(path)?;
Ok(())
}