Removed filetransfer module; migrated to remotefs crate

This commit is contained in:
veeso
2021-12-09 18:07:36 +01:00
committed by Christian Visintin
parent 25dd1b9b0a
commit df7a4381c4
60 changed files with 1185 additions and 6814 deletions

147
src/explorer/builder.rs Normal file
View File

@@ -0,0 +1,147 @@
//! ## Builder
//!
//! `builder` is the module which provides a builder for FileExplorer
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use super::formatter::Formatter;
use super::{ExplorerOpts, FileExplorer, FileSorting, GroupDirs};
// Ext
use std::collections::VecDeque;
/// ## FileExplorerBuilder
///
/// Struct used to create a `FileExplorer`
pub struct FileExplorerBuilder {
explorer: Option<FileExplorer>,
}
impl FileExplorerBuilder {
/// ### new
///
/// Build a new `FileExplorerBuilder`
pub fn new() -> Self {
FileExplorerBuilder {
explorer: Some(FileExplorer::default()),
}
}
/// ### build
///
/// Take FileExplorer out of builder
pub fn build(&mut self) -> FileExplorer {
self.explorer.take().unwrap()
}
/// ### with_hidden_files
///
/// Enable HIDDEN_FILES option
pub fn with_hidden_files(&mut self, val: bool) -> &mut FileExplorerBuilder {
if let Some(e) = self.explorer.as_mut() {
match val {
true => e.opts.insert(ExplorerOpts::SHOW_HIDDEN_FILES),
false => e.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES),
}
}
self
}
/// ### with_file_sorting
///
/// Set sorting method
pub fn with_file_sorting(&mut self, sorting: FileSorting) -> &mut FileExplorerBuilder {
if let Some(e) = self.explorer.as_mut() {
e.sort_by(sorting);
}
self
}
/// ### with_dirs_first
///
/// Enable DIRS_FIRST option
pub fn with_group_dirs(&mut self, group_dirs: Option<GroupDirs>) -> &mut FileExplorerBuilder {
if let Some(e) = self.explorer.as_mut() {
e.group_dirs_by(group_dirs);
}
self
}
/// ### with_stack_size
///
/// Set stack size for FileExplorer
pub fn with_stack_size(&mut self, sz: usize) -> &mut FileExplorerBuilder {
if let Some(e) = self.explorer.as_mut() {
e.stack_size = sz;
e.dirstack = VecDeque::with_capacity(sz);
}
self
}
/// ### with_formatter
///
/// Set formatter for FileExplorer
pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder {
if let Some(e) = self.explorer.as_mut() {
if let Some(fmt_str) = fmt_str {
e.fmt = Formatter::new(fmt_str);
}
}
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_fs_explorer_builder_new_default() {
let explorer: FileExplorer = FileExplorerBuilder::new().build();
// Verify
assert!(!explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES));
assert_eq!(explorer.file_sorting, FileSorting::Name); // Default
assert_eq!(explorer.group_dirs, None);
assert_eq!(explorer.stack_size, 16);
}
#[test]
fn test_fs_explorer_builder_new_all() {
let explorer: FileExplorer = FileExplorerBuilder::new()
.with_file_sorting(FileSorting::ModifyTime)
.with_group_dirs(Some(GroupDirs::First))
.with_hidden_files(true)
.with_stack_size(24)
.with_formatter(Some("{NAME}"))
.build();
// Verify
assert!(explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES));
assert_eq!(explorer.file_sorting, FileSorting::ModifyTime); // Default
assert_eq!(explorer.group_dirs, Some(GroupDirs::First));
assert_eq!(explorer.stack_size, 24);
}
}

968
src/explorer/formatter.rs Normal file
View File

@@ -0,0 +1,968 @@
//! ## Formatter
//!
//! `formatter` is the module which provides formatting utilities for `FileExplorer`
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
use crate::utils::path::diff_paths;
// Ext
use bytesize::ByteSize;
use regex::Regex;
use remotefs::Entry;
use std::path::PathBuf;
#[cfg(target_family = "unix")]
use users::{get_group_by_gid, get_user_by_uid};
// Types
// FmtCallback: Formatter, fsentry: &Entry, cur_str, prefix, length, extra
type FmtCallback = fn(&Formatter, &Entry, &str, &str, Option<&usize>, Option<&String>) -> String;
// Keys
const FMT_KEY_ATIME: &str = "ATIME";
const FMT_KEY_CTIME: &str = "CTIME";
const FMT_KEY_GROUP: &str = "GROUP";
const FMT_KEY_MTIME: &str = "MTIME";
const FMT_KEY_NAME: &str = "NAME";
const FMT_KEY_PATH: &str = "PATH";
const FMT_KEY_PEX: &str = "PEX";
const FMT_KEY_SIZE: &str = "SIZE";
const FMT_KEY_SYMLINK: &str = "SYMLINK";
const FMT_KEY_USER: &str = "USER";
// Default
const FMT_DEFAULT_STX: &str = "{NAME} {PEX} {USER} {SIZE} {MTIME}";
// Regex
lazy_static! {
/**
* Regex matches:
* - group 0: KEY NAME
* - group 1?: LENGTH
* - group 2?: EXTRA
*/
static ref FMT_KEY_REGEX: Regex = Regex::new(r"\{(.*?)\}").ok().unwrap();
static ref FMT_ATTR_REGEX: Regex = Regex::new(r"(?:([A-Z]+))(:?([0-9]+))?(:?(.+))?").ok().unwrap();
}
/// ## CallChainBlock
///
/// Call Chain block is a block in a chain of functions which are called in order to format the Entry.
/// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found
/// a chain of function is made using the Formatters method.
/// This method provides an extremely fast way to format fs entries
struct CallChainBlock {
/// The function to call to format current item
func: FmtCallback,
/// All the content which is between two `{KEY}` items
prefix: String,
/// The fmt len, specied for key as `{KEY:LEN}`
fmt_len: Option<usize>,
/// The extra argument for formatting, specified for key as `{KEY:LEN:EXTRA}`
fmt_extra: Option<String>,
/// The next block to format
next_block: Option<Box<CallChainBlock>>,
}
impl CallChainBlock {
/// ### new
///
/// Create a new `CallChainBlock`
pub fn new(
func: FmtCallback,
prefix: String,
fmt_len: Option<usize>,
fmt_extra: Option<String>,
) -> Self {
CallChainBlock {
func,
prefix,
fmt_len,
fmt_extra,
next_block: None,
}
}
/// ### next
///
/// Call next callback in the CallChain
pub fn next(&self, fmt: &Formatter, fsentry: &Entry, cur_str: &str) -> String {
// Call func
let new_str: String = (self.func)(
fmt,
fsentry,
cur_str,
self.prefix.as_str(),
self.fmt_len.as_ref(),
self.fmt_extra.as_ref(),
);
// If next is some, call next, otherwise (END OF CHAIN) return new_str
match &self.next_block {
Some(block) => block.next(fmt, fsentry, new_str.as_str()),
None => new_str,
}
}
/// ### push
///
/// Push func to the last element in the Call chain
pub fn push(
&mut self,
func: FmtCallback,
prefix: String,
fmt_len: Option<usize>,
fmt_extra: Option<String>,
) {
// Call recursively until an element with next_block equal to None is found
match &mut self.next_block {
None => {
self.next_block = Some(Box::new(CallChainBlock::new(
func, prefix, fmt_len, fmt_extra,
)))
}
Some(block) => block.push(func, prefix, fmt_len, fmt_extra),
}
}
}
/// ## Formatter
///
/// Formatter takes care of formatting FsEntries according to the provided keys.
/// Formatting is performed using the `CallChainBlock`, which composed makes a Call Chain. This method is extremely fast compared to match the format groups
/// at each fmt call.
pub struct Formatter {
call_chain: CallChainBlock,
}
impl Default for Formatter {
/// ### default
///
/// Instantiates a Formatter with the default fmt syntax
fn default() -> Self {
Formatter {
call_chain: Self::make_callchain(FMT_DEFAULT_STX),
}
}
}
impl Formatter {
/// ### new
///
/// Instantiates a new `Formatter` with the provided format string
pub fn new(fmt_str: &str) -> Self {
Formatter {
call_chain: Self::make_callchain(fmt_str),
}
}
/// ### fmt
///
/// Format fsentry
pub fn fmt(&self, fsentry: &Entry) -> String {
// Execute callchain blocks
self.call_chain.next(self, fsentry, "")
}
// Fmt methods
/// ### fmt_atime
///
/// Format last access time
fn fmt_atime(
&self,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
// Get date (use extra args as format or default "%b %d %Y %H:%M")
let datetime: String = fmt_time(
fsentry.metadata().atime,
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
},
);
// Add to cur str, prefix and the key value
format!(
"{}{}{:0width$}",
cur_str,
prefix,
datetime,
width = fmt_len.unwrap_or(&17)
)
}
/// ### fmt_ctime
///
/// Format creation time
fn fmt_ctime(
&self,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
// Get date
let datetime: String = fmt_time(
fsentry.metadata().ctime,
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
},
);
// Add to cur str, prefix and the key value
format!(
"{}{}{:0width$}",
cur_str,
prefix,
datetime,
width = fmt_len.unwrap_or(&17)
)
}
/// ### fmt_group
///
/// Format owner group
fn fmt_group(
&self,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Get username
#[cfg(target_family = "unix")]
let group: String = match fsentry.metadata().gid {
Some(gid) => match get_group_by_gid(gid) {
Some(user) => user.name().to_string_lossy().to_string(),
None => gid.to_string(),
},
None => 0.to_string(),
};
#[cfg(target_os = "windows")]
let group: String = match fsentry.metadata().gid {
Some(gid) => gid.to_string(),
None => 0.to_string(),
};
// Add to cur str, prefix and the key value
format!(
"{}{}{:0width$}",
cur_str,
prefix,
group,
width = fmt_len.unwrap_or(&12)
)
}
/// ### fmt_mtime
///
/// Format last change time
fn fmt_mtime(
&self,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
// Get date
let datetime: String = fmt_time(
fsentry.metadata().mtime,
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
},
);
// Add to cur str, prefix and the key value
format!(
"{}{}{:0width$}",
cur_str,
prefix,
datetime,
width = fmt_len.unwrap_or(&17)
)
}
/// ### fmt_name
///
/// Format file name
fn fmt_name(
&self,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Get file name (or elide if too long)
let file_len: usize = match fmt_len {
Some(l) => *l,
None => 24,
};
let name: &str = fsentry.name();
let last_idx: usize = match fsentry.is_dir() {
// NOTE: For directories is l - 2, since we push '/' to name
true => file_len - 2,
false => file_len - 1,
};
let mut name: String = match name.len() >= file_len {
false => name.to_string(),
true => format!("{}", &name[0..last_idx]),
};
if fsentry.is_dir() {
name.push('/');
}
// Add to cur str, prefix and the key value
format!("{}{}{:0width$}", cur_str, prefix, name, width = file_len)
}
/// ### fmt_path
///
/// Format path
fn fmt_path(
&self,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
let p = match fmt_extra {
None => fsentry.path().to_path_buf(),
Some(rel) => diff_paths(fsentry.path(), PathBuf::from(rel.as_str()).as_path())
.unwrap_or_else(|| fsentry.path().to_path_buf()),
};
format!(
"{}{}{}",
cur_str,
prefix,
match fmt_len {
None => p.display().to_string(),
Some(len) => fmt_path_elide(p.as_path(), *len),
}
)
}
/// ### fmt_pex
///
/// Format file permissions
fn fmt_pex(
&self,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Create mode string
let mut pex: String = String::with_capacity(10);
let file_type: char = match fsentry.metadata().symlink.is_some() {
true => 'l',
false => match fsentry.is_dir() {
true => 'd',
false => '-',
},
};
pex.push(file_type);
match fsentry.metadata().mode {
None => pex.push_str("?????????"),
Some(mode) => pex.push_str(
format!(
"{}{}{}",
fmt_pex(mode.user()),
fmt_pex(mode.group()),
fmt_pex(mode.others())
)
.as_str(),
),
}
// Add to cur str, prefix and the key value
format!("{}{}{:10}", cur_str, prefix, pex)
}
/// ### fmt_size
///
/// Format file size
fn fmt_size(
&self,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
if fsentry.is_file() {
// Get byte size
let size: ByteSize = ByteSize(fsentry.metadata().size);
// Add to cur str, prefix and the key value
format!("{}{}{:10}", cur_str, prefix, size.to_string())
} else {
// Add to cur str, prefix and the key value
format!("{}{} ", cur_str, prefix)
}
}
/// ### fmt_symlink
///
/// Format file symlink (if any)
fn fmt_symlink(
&self,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Get file name (or elide if too long)
let file_len: usize = match fmt_len {
Some(l) => *l,
None => 21,
};
// Replace `FMT_KEY_NAME` with name
match fsentry.metadata().symlink.as_deref() {
None => format!("{}{} ", cur_str, prefix),
Some(p) => format!(
"{}{}-> {:0width$}",
cur_str,
prefix,
fmt_path_elide(p, file_len - 1),
width = file_len
),
}
}
/// ### fmt_user
///
/// Format owner user
fn fmt_user(
&self,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Get username
#[cfg(target_family = "unix")]
let username: String = match fsentry.metadata().uid {
Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(),
None => uid.to_string(),
},
None => 0.to_string(),
};
#[cfg(target_os = "windows")]
let username: String = match fsentry.metadata().uid {
Some(uid) => uid.to_string(),
None => 0.to_string(),
};
// Add to cur str, prefix and the key value
format!("{}{}{:12}", cur_str, prefix, username)
}
/// ### fmt_fallback
///
/// Fallback function in case the format key is unknown
/// It does nothing, just returns cur_str
fn fmt_fallback(
&self,
_fsentry: &Entry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Add to cur str and prefix
format!("{}{}", cur_str, prefix)
}
// Static
/// ### make_callchain
///
/// Make a callchain starting from the fmt str
fn make_callchain(fmt_str: &str) -> CallChainBlock {
// Init chain block
let mut callchain: Option<CallChainBlock> = None;
// Track index of the last match found, to get the prefix for each token
let mut last_index: usize = 0;
// Match fmt str against regex
for regex_match in FMT_KEY_REGEX.captures_iter(fmt_str) {
// Get match index (unwrap is safe, since always exists)
let index: usize = fmt_str.find(&regex_match[0]).unwrap();
// Get prefix
let prefix: String = String::from(&fmt_str[last_index..index]);
// Increment last index (sum prefix lenght and the length of the key)
last_index += prefix.len() + regex_match[0].len();
// Match attributes
match FMT_ATTR_REGEX.captures(&regex_match[1]) {
Some(regex_match) => {
// Match group 0 (which is name)
let callback: FmtCallback = match &regex_match.get(1) {
Some(key) => match key.as_str() {
FMT_KEY_ATIME => Self::fmt_atime,
FMT_KEY_CTIME => Self::fmt_ctime,
FMT_KEY_GROUP => Self::fmt_group,
FMT_KEY_MTIME => Self::fmt_mtime,
FMT_KEY_NAME => Self::fmt_name,
FMT_KEY_PATH => Self::fmt_path,
FMT_KEY_PEX => Self::fmt_pex,
FMT_KEY_SIZE => Self::fmt_size,
FMT_KEY_SYMLINK => Self::fmt_symlink,
FMT_KEY_USER => Self::fmt_user,
_ => Self::fmt_fallback,
},
None => Self::fmt_fallback,
};
// Match format length: group 3
let fmt_len: Option<usize> = match &regex_match.get(3) {
Some(len) => match len.as_str().parse::<usize>() {
Ok(len) => Some(len),
Err(_) => None,
},
None => None,
};
// Match format extra: group 2 + 1
let fmt_extra: Option<String> = regex_match
.get(5)
.as_ref()
.map(|extra| extra.as_str().to_string());
// Create a callchain or push new element to its back
match callchain.as_mut() {
None => {
callchain =
Some(CallChainBlock::new(callback, prefix, fmt_len, fmt_extra))
}
Some(chain_block) => chain_block.push(callback, prefix, fmt_len, fmt_extra),
}
}
None => continue,
}
}
// Finalize and return
match callchain {
Some(callchain) => callchain,
None => CallChainBlock::new(Self::fmt_fallback, String::new(), None, None),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use remotefs::fs::{Directory, File, Metadata, UnixPex};
use std::path::PathBuf;
use std::time::SystemTime;
#[test]
fn test_fs_explorer_formatter_callchain() {
// Make a dummy formatter
let dummy_formatter: Formatter = Formatter::new("");
// Make a dummy entry
let t: SystemTime = SystemTime::now();
let dummy_entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
});
let prefix: String = String::from("h");
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None);
assert!(callchain.next_block.is_none());
assert_eq!(callchain.prefix, String::from("h"));
// Execute
assert_eq!(
callchain.next(&dummy_formatter, &dummy_entry, ""),
String::from("hA")
);
// Push 4 new blocks
callchain.push(dummy_fmt, String::from("h"), None, None);
callchain.push(dummy_fmt, String::from("h"), None, None);
callchain.push(dummy_fmt, String::from("h"), None, None);
callchain.push(dummy_fmt, String::from("h"), None, None);
// Verify
assert_eq!(
callchain.next(&dummy_formatter, &dummy_entry, ""),
String::from("hAhAhAhAhA")
);
}
#[test]
fn test_fs_explorer_formatter_format_files() {
// Make default
let formatter: Formatter = Formatter::default();
// Experiments :D
let t: SystemTime = SystemTime::now();
let entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
});
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -rw-r--r-- root 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -rw-r--r-- 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
// Elide name
let entry: Entry = Entry::File(File {
name: String::from("piroparoporoperoperupupu.txt"),
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
});
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
"piroparoporoperoperupup… -rw-r--r-- root 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"piroparoporoperoperupup… -rw-r--r-- 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
// No pex
let entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: None,
},
});
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? root 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
// No user
let entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: None,
uid: None,
gid: Some(0),
mode: None,
},
});
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
}
#[test]
fn test_fs_explorer_formatter_format_dirs() {
// Make default
let formatter: Formatter = Formatter::default();
// Experiments :D
let t: SystemTime = SystemTime::now();
let entry: Entry = Entry::Directory(Directory {
name: String::from("projects"),
path: PathBuf::from("/home/cvisintin/projects"),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 4096,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o755)),
},
});
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ drwxr-xr-x root {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ drwxr-xr-x 0 {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
// No pex, no user
let entry: Entry = Entry::Directory(Directory {
name: String::from("projects"),
path: PathBuf::from("/home/cvisintin/projects"),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 4096,
symlink: None,
uid: None,
gid: Some(0),
mode: None,
},
});
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ d????????? 0 {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ d????????? 0 {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
}
#[test]
fn test_fs_explorer_formatter_all_together_now() {
let formatter: Formatter =
Formatter::new("{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}");
// Directory (with symlink)
let t: SystemTime = SystemTime::now();
let entry: Entry = Entry::Directory(Directory {
name: String::from("projects"),
path: PathBuf::from("/home/cvisintin/project"),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 4096,
symlink: Some(PathBuf::from("project.info")),
uid: None,
gid: None,
mode: Some(UnixPex::from(0o755)),
},
});
assert_eq!(formatter.fmt(&entry), format!(
"projects/ -> project.info 0 0 lrwxr-xr-x {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
// Directory without symlink
let entry: Entry = Entry::Directory(Directory {
name: String::from("projects"),
path: PathBuf::from("/home/cvisintin/project"),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 4096,
symlink: None,
uid: None,
gid: None,
mode: Some(UnixPex::from(0o755)),
},
});
assert_eq!(formatter.fmt(&entry), format!(
"projects/ 0 0 drwxr-xr-x {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
// File with symlink
let entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: Some(PathBuf::from("project.info")),
uid: None,
gid: None,
mode: Some(UnixPex::from(0o644)),
},
});
assert_eq!(formatter.fmt(&entry), format!(
"bar.txt -> project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
// File without symlink
let entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: None,
uid: None,
gid: None,
mode: Some(UnixPex::from(0o644)),
},
});
assert_eq!(formatter.fmt(&entry), format!(
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
}
#[test]
#[cfg(target_family = "unix")]
fn should_fmt_path() {
let t: SystemTime = SystemTime::now();
let entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
path: PathBuf::from("/tmp/a/b/c/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: Some(PathBuf::from("project.info")),
uid: None,
gid: None,
mode: Some(UnixPex::from(0o644)),
},
});
let formatter: Formatter = Formatter::new("File path: {PATH}");
assert_eq!(
formatter.fmt(&entry).as_str(),
"File path: /tmp/a/b/c/bar.txt"
);
let formatter: Formatter = Formatter::new("File path: {PATH:8}");
assert_eq!(
formatter.fmt(&entry).as_str(),
"File path: /tmp/…/c/bar.txt"
);
let formatter: Formatter = Formatter::new("File path: {PATH:128:/tmp/a/b}");
assert_eq!(formatter.fmt(&entry).as_str(), "File path: c/bar.txt");
}
/// ### dummy_fmt
///
/// Dummy formatter, just yelds an 'A' at the end of the current string
fn dummy_fmt(
_fmt: &Formatter,
_entry: &Entry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
format!("{}{}A", cur_str, prefix)
}
}

707
src/explorer/mod.rs Normal file
View File

@@ -0,0 +1,707 @@
//! ## Explorer
//!
//! `explorer` is the module which provides an Helper in handling Directory status through
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Mods
pub(crate) mod builder;
mod formatter;
// Locals
use formatter::Formatter;
// Ext
use remotefs::fs::Entry;
use std::cmp::Reverse;
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;
bitflags! {
/// ## ExplorerOpts
///
/// ExplorerOpts are bit options which provides different behaviours to `FileExplorer`
pub(crate) struct ExplorerOpts: u32 {
const SHOW_HIDDEN_FILES = 0b00000001;
}
}
/// ## FileSorting
///
/// FileSorting defines the criteria for sorting files
#[derive(Copy, Clone, PartialEq, std::fmt::Debug)]
pub enum FileSorting {
Name,
ModifyTime,
CreationTime,
Size,
}
/// ## GroupDirs
///
/// GroupDirs defines how directories should be grouped in sorting files
#[derive(PartialEq, std::fmt::Debug)]
pub enum GroupDirs {
First,
Last,
}
/// ## FileExplorer
///
/// File explorer states
pub struct FileExplorer {
pub wrkdir: PathBuf, // Current directory
pub(crate) dirstack: VecDeque<PathBuf>, // Stack of visited directory (max 16)
pub(crate) stack_size: usize, // Directory stack size
pub(crate) file_sorting: FileSorting, // File sorting criteria
pub(crate) group_dirs: Option<GroupDirs>, // If Some, defines how to group directories
pub(crate) opts: ExplorerOpts, // Explorer options
pub(crate) fmt: Formatter, // Entry formatter
files: Vec<Entry>, // Files in directory
}
impl Default for FileExplorer {
fn default() -> Self {
FileExplorer {
wrkdir: PathBuf::from("/"),
dirstack: VecDeque::with_capacity(16),
stack_size: 16,
file_sorting: FileSorting::Name,
group_dirs: None,
opts: ExplorerOpts::empty(),
fmt: Formatter::default(),
files: Vec::new(),
}
}
}
impl FileExplorer {
/// ### pushd
///
/// push directory to stack
pub fn pushd(&mut self, dir: &Path) {
// Check if stack would overflow the size
while self.dirstack.len() >= self.stack_size {
self.dirstack.pop_front(); // Start cleaning events from back
}
// Eventually push front the new record
self.dirstack.push_back(PathBuf::from(dir));
}
/// ### popd
///
/// Pop directory from the stack and return the directory
pub fn popd(&mut self) -> Option<PathBuf> {
self.dirstack.pop_back()
}
/// ### set_files
///
/// Set Explorer files
/// This method will also sort entries based on current options
/// Once all sorting have been performed, index is moved to first valid entry.
pub fn set_files(&mut self, files: Vec<Entry>) {
self.files = files;
// Sort
self.sort();
}
/// ### del_entry
///
/// Delete file at provided index
pub fn del_entry(&mut self, idx: usize) {
if self.files.len() > idx {
self.files.remove(idx);
}
}
/*
/// ### count
///
/// Return amount of files
pub fn count(&self) -> usize {
self.files.len()
}
*/
/// ### iter_files
///
/// Iterate over files
/// Filters are applied based on current options (e.g. hidden files not returned)
pub fn iter_files(&self) -> impl Iterator<Item = &Entry> + '_ {
// Filter
let opts: ExplorerOpts = self.opts;
Box::new(self.files.iter().filter(move |x| {
// If true, element IS NOT filtered
let mut pass: bool = true;
// If hidden files SHOULDN'T be shown, AND pass with not hidden
if !opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) {
pass &= !x.is_hidden();
}
pass
}))
}
/// ### iter_files_all
///
/// Iterate all files; doesn't care about options
pub fn iter_files_all(&self) -> impl Iterator<Item = &Entry> + '_ {
Box::new(self.files.iter())
}
/// ### get
///
/// Get file at relative index
pub fn get(&self, idx: usize) -> Option<&Entry> {
let opts: ExplorerOpts = self.opts;
let filtered = self
.files
.iter()
.filter(move |x| {
// If true, element IS NOT filtered
let mut pass: bool = true;
// If hidden files SHOULDN'T be shown, AND pass with not hidden
if !opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) {
pass &= !x.is_hidden();
}
pass
})
.collect::<Vec<_>>();
filtered.get(idx).copied()
}
// Formatting
/// ### fmt_file
///
/// Format a file entry
pub fn fmt_file(&self, entry: &Entry) -> String {
self.fmt.fmt(entry)
}
// Sorting
/// ### sort_by
///
/// Choose sorting method; then sort files
pub fn sort_by(&mut self, sorting: FileSorting) {
// If method HAS ACTUALLY CHANGED, sort (performance!)
if self.file_sorting != sorting {
self.file_sorting = sorting;
self.sort();
}
}
/// ### get_file_sorting
///
/// Get current file sorting method
pub fn get_file_sorting(&self) -> FileSorting {
self.file_sorting
}
/// ### group_dirs_by
///
/// Choose group dirs method; then sort files
pub fn group_dirs_by(&mut self, group_dirs: Option<GroupDirs>) {
// If method HAS ACTUALLY CHANGED, sort (performance!)
if self.group_dirs != group_dirs {
self.group_dirs = group_dirs;
self.sort();
}
}
/// ### sort
///
/// Sort files based on Explorer options.
fn sort(&mut self) {
// Choose sorting method
match &self.file_sorting {
FileSorting::Name => self.sort_files_by_name(),
FileSorting::CreationTime => self.sort_files_by_creation_time(),
FileSorting::ModifyTime => self.sort_files_by_mtime(),
FileSorting::Size => self.sort_files_by_size(),
}
// Directories first (NOTE: MUST COME AFTER OTHER SORTING)
// Group directories if necessary
if let Some(group_dirs) = &self.group_dirs {
match group_dirs {
GroupDirs::First => self.sort_files_directories_first(),
GroupDirs::Last => self.sort_files_directories_last(),
}
}
}
/// ### sort_files_by_name
///
/// Sort explorer files by their name. All names are converted to lowercase
fn sort_files_by_name(&mut self) {
self.files.sort_by_key(|x: &Entry| x.name().to_lowercase());
}
/// ### sort_files_by_mtime
///
/// Sort files by mtime; the newest comes first
fn sort_files_by_mtime(&mut self) {
self.files
.sort_by(|a: &Entry, b: &Entry| b.metadata().mtime.cmp(&a.metadata().mtime));
}
/// ### sort_files_by_creation_time
///
/// Sort files by creation time; the newest comes first
fn sort_files_by_creation_time(&mut self) {
self.files
.sort_by_key(|b: &Entry| Reverse(b.metadata().ctime));
}
/// ### sort_files_by_size
///
/// Sort files by size
fn sort_files_by_size(&mut self) {
self.files
.sort_by_key(|b: &Entry| Reverse(b.metadata().size));
}
/// ### sort_files_directories_first
///
/// Sort files; directories come first
fn sort_files_directories_first(&mut self) {
self.files.sort_by_key(|x: &Entry| x.is_file());
}
/// ### sort_files_directories_last
///
/// Sort files; directories come last
fn sort_files_directories_last(&mut self) {
self.files.sort_by_key(|x: &Entry| x.is_dir());
}
/// ### toggle_hidden_files
///
/// Enable/disable hidden files
pub fn toggle_hidden_files(&mut self) {
self.opts.toggle(ExplorerOpts::SHOW_HIDDEN_FILES);
}
/// ### hidden_files_visible
///
/// Returns whether hidden files are visible
pub fn hidden_files_visible(&self) -> bool {
self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES)
}
}
// Traits
impl ToString for FileSorting {
fn to_string(&self) -> String {
String::from(match self {
FileSorting::CreationTime => "by_creation_time",
FileSorting::ModifyTime => "by_mtime",
FileSorting::Name => "by_name",
FileSorting::Size => "by_size",
})
}
}
impl FromStr for FileSorting {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"by_creation_time" => Ok(FileSorting::CreationTime),
"by_mtime" => Ok(FileSorting::ModifyTime),
"by_name" => Ok(FileSorting::Name),
"by_size" => Ok(FileSorting::Size),
_ => Err(()),
}
}
}
impl ToString for GroupDirs {
fn to_string(&self) -> String {
String::from(match self {
GroupDirs::First => "first",
GroupDirs::Last => "last",
})
}
}
impl FromStr for GroupDirs {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"first" => Ok(GroupDirs::First),
"last" => Ok(GroupDirs::Last),
_ => Err(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::fmt::fmt_time;
use pretty_assertions::assert_eq;
use remotefs::fs::{Directory, File, Metadata, UnixPex};
use std::thread::sleep;
use std::time::{Duration, SystemTime};
#[test]
fn test_fs_explorer_new() {
let explorer: FileExplorer = FileExplorer::default();
// Verify
assert_eq!(explorer.dirstack.len(), 0);
assert_eq!(explorer.files.len(), 0);
assert_eq!(explorer.opts, ExplorerOpts::empty());
assert_eq!(explorer.wrkdir, PathBuf::from("/"));
assert_eq!(explorer.stack_size, 16);
assert_eq!(explorer.group_dirs, None);
assert_eq!(explorer.file_sorting, FileSorting::Name);
assert_eq!(explorer.get_file_sorting(), FileSorting::Name);
}
#[test]
fn test_fs_explorer_stack() {
let mut explorer: FileExplorer = FileExplorer::default();
explorer.stack_size = 2;
explorer.dirstack = VecDeque::with_capacity(2);
// Push dir
explorer.pushd(&Path::new("/tmp"));
explorer.pushd(&Path::new("/home/omar"));
// Pop
assert_eq!(explorer.popd().unwrap(), PathBuf::from("/home/omar"));
assert_eq!(explorer.dirstack.len(), 1);
assert_eq!(explorer.popd().unwrap(), PathBuf::from("/tmp"));
assert_eq!(explorer.dirstack.len(), 0);
// Dirstack is empty now
assert!(explorer.popd().is_none());
// Exceed limit
explorer.pushd(&Path::new("/tmp"));
explorer.pushd(&Path::new("/home/omar"));
explorer.pushd(&Path::new("/dev"));
assert_eq!(explorer.dirstack.len(), 2);
assert_eq!(*explorer.dirstack.get(1).unwrap(), PathBuf::from("/dev"));
assert_eq!(
*explorer.dirstack.get(0).unwrap(),
PathBuf::from("/home/omar")
);
}
#[test]
fn test_fs_explorer_files() {
let mut explorer: FileExplorer = FileExplorer::default();
// Don't show hidden files
explorer.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES);
assert_eq!(explorer.hidden_files_visible(), false);
// Create files
explorer.set_files(vec![
make_fs_entry("README.md", false),
make_fs_entry("src/", true),
make_fs_entry(".git/", true),
make_fs_entry("CONTRIBUTING.md", false),
make_fs_entry("codecov.yml", false),
make_fs_entry(".gitignore", false),
]);
assert!(explorer.get(0).is_some());
assert!(explorer.get(100).is_none());
//assert_eq!(explorer.count(), 6);
// Verify (files are sorted by name)
assert_eq!(explorer.files.get(0).unwrap().name(), ".git/");
// Iter files (all)
assert_eq!(explorer.iter_files_all().count(), 6);
// Iter files (hidden excluded) (.git, .gitignore are hidden)
assert_eq!(explorer.iter_files().count(), 4);
// Toggle hidden
explorer.toggle_hidden_files();
assert_eq!(explorer.hidden_files_visible(), true);
assert_eq!(explorer.iter_files().count(), 6); // All files are returned now
}
#[test]
fn test_fs_explorer_sort_by_name() {
let mut explorer: FileExplorer = FileExplorer::default();
// Create files (files are then sorted by name)
explorer.set_files(vec![
make_fs_entry("README.md", false),
make_fs_entry("src/", true),
make_fs_entry("CONTRIBUTING.md", false),
make_fs_entry("CODE_OF_CONDUCT.md", false),
make_fs_entry("CHANGELOG.md", false),
make_fs_entry("LICENSE", false),
make_fs_entry("Cargo.toml", false),
make_fs_entry("Cargo.lock", false),
make_fs_entry("codecov.yml", false),
]);
explorer.sort_by(FileSorting::Name);
// First entry should be "Cargo.lock"
assert_eq!(explorer.files.get(0).unwrap().name(), "Cargo.lock");
// Last should be "src/"
assert_eq!(explorer.files.get(8).unwrap().name(), "src/");
}
#[test]
fn test_fs_explorer_sort_by_mtime() {
let mut explorer: FileExplorer = FileExplorer::default();
let entry1: Entry = make_fs_entry("README.md", false);
// Wait 1 sec
sleep(Duration::from_secs(1));
let entry2: Entry = make_fs_entry("CODE_OF_CONDUCT.md", false);
// Create files (files are then sorted by name)
explorer.set_files(vec![entry1, entry2]);
explorer.sort_by(FileSorting::ModifyTime);
// First entry should be "CODE_OF_CONDUCT.md"
assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md");
// Last should be "src/"
assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
}
#[test]
fn test_fs_explorer_sort_by_creation_time() {
let mut explorer: FileExplorer = FileExplorer::default();
let entry1: Entry = make_fs_entry("README.md", false);
// Wait 1 sec
sleep(Duration::from_secs(1));
let entry2: Entry = make_fs_entry("CODE_OF_CONDUCT.md", false);
// Create files (files are then sorted by name)
explorer.set_files(vec![entry1, entry2]);
explorer.sort_by(FileSorting::CreationTime);
// First entry should be "CODE_OF_CONDUCT.md"
assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md");
// Last should be "src/"
assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
}
#[test]
fn test_fs_explorer_sort_by_size() {
let mut explorer: FileExplorer = FileExplorer::default();
// Create files (files are then sorted by name)
explorer.set_files(vec![
make_fs_entry_with_size("README.md", false, 1024),
make_fs_entry_with_size("src/", true, 4096),
make_fs_entry_with_size("CONTRIBUTING.md", false, 256),
]);
explorer.sort_by(FileSorting::Size);
// Directory has size 4096
assert_eq!(explorer.files.get(0).unwrap().name(), "src/");
assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
assert_eq!(explorer.files.get(2).unwrap().name(), "CONTRIBUTING.md");
}
#[test]
fn test_fs_explorer_sort_by_name_and_dirs_first() {
let mut explorer: FileExplorer = FileExplorer::default();
// Create files (files are then sorted by name)
explorer.set_files(vec![
make_fs_entry("README.md", false),
make_fs_entry("src/", true),
make_fs_entry("docs/", true),
make_fs_entry("CONTRIBUTING.md", false),
make_fs_entry("CODE_OF_CONDUCT.md", false),
make_fs_entry("CHANGELOG.md", false),
make_fs_entry("LICENSE", false),
make_fs_entry("Cargo.toml", false),
make_fs_entry("Cargo.lock", false),
make_fs_entry("codecov.yml", false),
]);
explorer.sort_by(FileSorting::Name);
explorer.group_dirs_by(Some(GroupDirs::First));
// First entry should be "docs"
assert_eq!(explorer.files.get(0).unwrap().name(), "docs/");
assert_eq!(explorer.files.get(1).unwrap().name(), "src/");
// 3rd is file first for alphabetical order
assert_eq!(explorer.files.get(2).unwrap().name(), "Cargo.lock");
// Last should be "README.md" (last file for alphabetical ordening)
assert_eq!(explorer.files.get(9).unwrap().name(), "README.md");
}
#[test]
fn test_fs_explorer_sort_by_name_and_dirs_last() {
let mut explorer: FileExplorer = FileExplorer::default();
// Create files (files are then sorted by name)
explorer.set_files(vec![
make_fs_entry("README.md", false),
make_fs_entry("src/", true),
make_fs_entry("docs/", true),
make_fs_entry("CONTRIBUTING.md", false),
make_fs_entry("CODE_OF_CONDUCT.md", false),
make_fs_entry("CHANGELOG.md", false),
make_fs_entry("LICENSE", false),
make_fs_entry("Cargo.toml", false),
make_fs_entry("Cargo.lock", false),
make_fs_entry("codecov.yml", false),
]);
explorer.sort_by(FileSorting::Name);
explorer.group_dirs_by(Some(GroupDirs::Last));
// Last entry should be "src"
assert_eq!(explorer.files.get(8).unwrap().name(), "docs/");
assert_eq!(explorer.files.get(9).unwrap().name(), "src/");
// first is file for alphabetical order
assert_eq!(explorer.files.get(0).unwrap().name(), "Cargo.lock");
// Last in files should be "README.md" (last file for alphabetical ordening)
assert_eq!(explorer.files.get(7).unwrap().name(), "README.md");
}
#[test]
fn test_fs_explorer_fmt() {
let explorer: FileExplorer = FileExplorer::default();
// Create fs entry
let t: SystemTime = SystemTime::now();
let entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
size: 8192,
mtime: t,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
});
#[cfg(target_family = "unix")]
assert_eq!(
explorer.fmt_file(&entry),
format!(
"bar.txt -rw-r--r-- root 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
explorer.fmt_file(&entry),
format!(
"bar.txt -rw-r--r-- 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
}
#[test]
fn test_fs_explorer_to_string_from_str_traits() {
// File Sorting
assert_eq!(FileSorting::CreationTime.to_string(), "by_creation_time");
assert_eq!(FileSorting::ModifyTime.to_string(), "by_mtime");
assert_eq!(FileSorting::Name.to_string(), "by_name");
assert_eq!(FileSorting::Size.to_string(), "by_size");
assert_eq!(
FileSorting::from_str("by_creation_time").ok().unwrap(),
FileSorting::CreationTime
);
assert_eq!(
FileSorting::from_str("by_mtime").ok().unwrap(),
FileSorting::ModifyTime
);
assert_eq!(
FileSorting::from_str("by_name").ok().unwrap(),
FileSorting::Name
);
assert_eq!(
FileSorting::from_str("by_size").ok().unwrap(),
FileSorting::Size
);
assert!(FileSorting::from_str("omar").is_err());
// Group dirs
assert_eq!(GroupDirs::First.to_string(), "first");
assert_eq!(GroupDirs::Last.to_string(), "last");
assert_eq!(GroupDirs::from_str("first").ok().unwrap(), GroupDirs::First);
assert_eq!(GroupDirs::from_str("last").ok().unwrap(), GroupDirs::Last);
assert!(GroupDirs::from_str("omar").is_err());
}
#[test]
fn test_fs_explorer_del_entry() {
let mut explorer: FileExplorer = FileExplorer::default();
// Create files (files are then sorted by name)
explorer.set_files(vec![
make_fs_entry("CONTRIBUTING.md", false),
make_fs_entry("docs/", true),
make_fs_entry("src/", true),
make_fs_entry("README.md", false),
]);
explorer.del_entry(0);
assert_eq!(explorer.files.len(), 3);
assert_eq!(explorer.files[0].name(), "docs/");
explorer.del_entry(5);
assert_eq!(explorer.files.len(), 3);
}
fn make_fs_entry(name: &str, is_dir: bool) -> Entry {
let t: SystemTime = SystemTime::now();
let metadata = Metadata {
atime: t,
ctime: t,
mtime: t,
symlink: None,
gid: Some(0),
uid: Some(0),
mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })),
size: 64,
};
match is_dir {
false => Entry::File(File {
name: name.to_string(),
path: PathBuf::from(name),
extension: None,
metadata,
}),
true => Entry::Directory(Directory {
name: name.to_string(),
path: PathBuf::from(name),
metadata,
}),
}
}
fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> Entry {
let t: SystemTime = SystemTime::now();
let metadata = Metadata {
atime: t,
ctime: t,
mtime: t,
symlink: None,
gid: Some(0),
uid: Some(0),
mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })),
size: size as u64,
};
match is_dir {
false => Entry::File(File {
name: name.to_string(),
path: PathBuf::from(name),
extension: None,
metadata,
}),
true => Entry::Directory(Directory {
name: name.to_string(),
path: PathBuf::from(name),
metadata,
}),
}
}
}