Initial commit
This commit is contained in:
commit
8f410d1ead
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
|
@ -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]
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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};
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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(()),
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(())
|
||||||
|
}
|
Loading…
Reference in New Issue