mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Added LIST command parser for Windows server (DOS-like syntax)
This commit is contained in:
@@ -36,6 +36,8 @@ FIXME: Released on
|
|||||||
- Windows: `C:\Users\Alice\AppData\Roaming\termscp\.ssh\`
|
- Windows: `C:\Users\Alice\AppData\Roaming\termscp\.ssh\`
|
||||||
- Enhancements:
|
- Enhancements:
|
||||||
- Replaced `sha256` sum with last modification time check, to verify if a file has been changed in the text editor
|
- Replaced `sha256` sum with last modification time check, to verify if a file has been changed in the text editor
|
||||||
|
- **FTP**
|
||||||
|
- Added `LIST` command parser for Windows server (DOS-like syntax)
|
||||||
- Default protocol changed to default protocol in configuration when providing address as CLI argument
|
- Default protocol changed to default protocol in configuration when providing address as CLI argument
|
||||||
- Explorers:
|
- Explorers:
|
||||||
- Hidden files are now not shown by default; use `A` to show hidden files.
|
- Hidden files are now not shown by default; use `A` to show hidden files.
|
||||||
@@ -44,6 +46,9 @@ FIXME: Released on
|
|||||||
- `A`: Toggle hidden files
|
- `A`: Toggle hidden files
|
||||||
- `B`: Sort files by (name, size, creation time, modify time)
|
- `B`: Sort files by (name, size, creation time, modify time)
|
||||||
- `N`: New file
|
- `N`: New file
|
||||||
|
- Bugfix:
|
||||||
|
- SCP client didn't show file types for files
|
||||||
|
- FTP client didn't show file types for files
|
||||||
- Dependencies:
|
- Dependencies:
|
||||||
- added `bitflags 1.2.1`
|
- added `bitflags 1.2.1`
|
||||||
- removed `data-encoding`
|
- removed `data-encoding`
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ extern crate regex;
|
|||||||
|
|
||||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||||
use crate::utils::parser::parse_lstime;
|
use crate::utils::parser::{parse_datetime, parse_lstime};
|
||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
use ftp4::native_tls::TlsConnector;
|
use ftp4::native_tls::TlsConnector;
|
||||||
@@ -60,6 +60,25 @@ impl FtpFileTransfer {
|
|||||||
///
|
///
|
||||||
/// Parse a line of LIST command output and instantiates an FsEntry from it
|
/// Parse a line of LIST command output and instantiates an FsEntry from it
|
||||||
fn parse_list_line(&self, path: &Path, line: &str) -> Result<FsEntry, ()> {
|
fn parse_list_line(&self, path: &Path, line: &str) -> Result<FsEntry, ()> {
|
||||||
|
// Try to parse using UNIX syntax
|
||||||
|
match self.parse_unix_list_line(path, line) {
|
||||||
|
Ok(entry) => Ok(entry),
|
||||||
|
Err(_) => match self.parse_dos_list_line(path, line) {
|
||||||
|
// If UNIX parsing fails, try DOS
|
||||||
|
Ok(entry) => Ok(entry),
|
||||||
|
Err(_) => Err(()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### parse_unix_list_line
|
||||||
|
///
|
||||||
|
/// Try to parse a "LIST" output command line in UNIX format.
|
||||||
|
/// Returns error if syntax is not UNIX compliant.
|
||||||
|
/// UNIX syntax has the following syntax:
|
||||||
|
/// {FILE_TYPE}{UNIX_PEX} {HARD_LINKS} {USER} {GROUP} {SIZE} {DATE} {FILENAME}
|
||||||
|
/// -rw-r--r-- 1 cvisintin staff 4968 27 Dic 10:46 CHANGELOG.md
|
||||||
|
fn parse_unix_list_line(&self, path: &Path, line: &str) -> Result<FsEntry, ()> {
|
||||||
// Prepare list regex
|
// Prepare list regex
|
||||||
// NOTE: about this damn regex <https://stackoverflow.com/questions/32480890/is-there-a-regex-to-parse-the-values-from-an-ftp-directory-listing>
|
// NOTE: about this damn regex <https://stackoverflow.com/questions/32480890/is-there-a-regex-to-parse-the-values-from-an-ftp-directory-listing>
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
@@ -210,6 +229,96 @@ impl FtpFileTransfer {
|
|||||||
None => Err(()),
|
None => Err(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### parse_dos_list_line
|
||||||
|
///
|
||||||
|
/// Try to parse a "LIST" output command line in DOS format.
|
||||||
|
/// Returns error if syntax is not DOS compliant.
|
||||||
|
/// DOS syntax has the following syntax:
|
||||||
|
/// {DATE} {TIME} {<DIR> | SIZE} {FILENAME}
|
||||||
|
/// 10-19-20 03:19PM <DIR> pub
|
||||||
|
/// 04-08-14 03:09PM 403 readme.txt
|
||||||
|
fn parse_dos_list_line(&self, path: &Path, line: &str) -> Result<FsEntry, ()> {
|
||||||
|
// Prepare list regex
|
||||||
|
// NOTE: you won't find this regex on the internet. It seems I'm the only person in the world who needs this
|
||||||
|
lazy_static! {
|
||||||
|
static ref DOS_RE: Regex = Regex::new(
|
||||||
|
r#"^(\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}\s*[AP]M)\s+(<DIR>)?([\d,]*)\s+(.+)$"#
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
// Apply regex to result
|
||||||
|
match DOS_RE.captures(line) {
|
||||||
|
// String matches regex
|
||||||
|
Some(metadata) => {
|
||||||
|
// NOTE: metadata fmt: (regex, date_time, is_dir?, file_size?, file_name)
|
||||||
|
// Expected 4 + 1 (5) values: + 1 cause regex is repeated at 0
|
||||||
|
if metadata.len() < 5 {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
// Parse date time
|
||||||
|
let time: SystemTime =
|
||||||
|
match parse_datetime(metadata.get(1).unwrap().as_str(), "%d-%m-%y %I:%M%p") {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => SystemTime::UNIX_EPOCH, // Don't return error
|
||||||
|
};
|
||||||
|
// Get if is a directory
|
||||||
|
let is_dir: bool = metadata.get(2).is_some();
|
||||||
|
// Get file size
|
||||||
|
let file_size: usize = match is_dir {
|
||||||
|
true => 0, // If is directory, filesize is 0
|
||||||
|
false => match metadata.get(3) {
|
||||||
|
// If is file, parse arg 3
|
||||||
|
Some(val) => match val.as_str().parse::<usize>() {
|
||||||
|
Ok(sz) => sz,
|
||||||
|
Err(_) => 0,
|
||||||
|
},
|
||||||
|
None => 0, // Should not happen
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Get file name
|
||||||
|
let file_name: String = String::from(metadata.get(4).unwrap().as_str());
|
||||||
|
// Get absolute path
|
||||||
|
let mut abs_path: PathBuf = PathBuf::from(path);
|
||||||
|
abs_path.push(file_name.as_str());
|
||||||
|
// Get extension
|
||||||
|
let extension: Option<String> = match abs_path.as_path().extension() {
|
||||||
|
None => None,
|
||||||
|
Some(s) => Some(String::from(s.to_string_lossy())),
|
||||||
|
};
|
||||||
|
// Return entry
|
||||||
|
Ok(match is_dir {
|
||||||
|
true => FsEntry::Directory(FsDirectory {
|
||||||
|
name: file_name,
|
||||||
|
abs_path,
|
||||||
|
last_change_time: time,
|
||||||
|
last_access_time: time,
|
||||||
|
creation_time: time,
|
||||||
|
readonly: false,
|
||||||
|
symlink: None,
|
||||||
|
user: None,
|
||||||
|
group: None,
|
||||||
|
unix_pex: None,
|
||||||
|
}),
|
||||||
|
false => FsEntry::File(FsFile {
|
||||||
|
name: file_name,
|
||||||
|
abs_path,
|
||||||
|
last_change_time: time,
|
||||||
|
last_access_time: time,
|
||||||
|
creation_time: time,
|
||||||
|
size: file_size,
|
||||||
|
ftype: extension,
|
||||||
|
readonly: false,
|
||||||
|
symlink: None,
|
||||||
|
user: None,
|
||||||
|
group: None,
|
||||||
|
unix_pex: None,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => Err(()), // Invalid syntax
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileTransfer for FtpFileTransfer {
|
impl FileTransfer for FtpFileTransfer {
|
||||||
@@ -835,9 +944,32 @@ mod tests {
|
|||||||
.is_err());
|
.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
/* NOTE: they don't work
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_filetransfer_ftp_list_dir() {
|
fn test_filetransfer_ftp_list_dir_dos_syntax() {
|
||||||
|
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||||
|
// Connect
|
||||||
|
assert!(ftp
|
||||||
|
.connect(
|
||||||
|
String::from("test.rebex.net"),
|
||||||
|
21,
|
||||||
|
Some(String::from("demo")),
|
||||||
|
Some(String::from("password"))
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
|
// Pwd
|
||||||
|
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
|
||||||
|
// List dir
|
||||||
|
println!("{:?}", ftp.list_dir(PathBuf::from("/").as_path()));
|
||||||
|
let files: Vec<FsEntry> = ftp.list_dir(PathBuf::from("/").as_path()).ok().unwrap();
|
||||||
|
// There should be at least 1 file
|
||||||
|
assert!(files.len() > 0);
|
||||||
|
// Disconnect
|
||||||
|
assert!(ftp.disconnect().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_filetransfer_ftp_list_dir_unix_syntax() {
|
||||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||||
// Connect
|
// Connect
|
||||||
assert!(ftp.connect(String::from("speedtest.tele2.net"), 21, None, None).is_ok());
|
assert!(ftp.connect(String::from("speedtest.tele2.net"), 21, None, None).is_ok());
|
||||||
@@ -846,36 +978,13 @@ mod tests {
|
|||||||
// List dir
|
// List dir
|
||||||
println!("{:?}", ftp.list_dir(PathBuf::from("/").as_path()));
|
println!("{:?}", ftp.list_dir(PathBuf::from("/").as_path()));
|
||||||
let files: Vec<FsEntry> = ftp.list_dir(PathBuf::from("/").as_path()).ok().unwrap();
|
let files: Vec<FsEntry> = ftp.list_dir(PathBuf::from("/").as_path()).ok().unwrap();
|
||||||
// There should be 19 files
|
// There should be at least 1 file
|
||||||
assert_eq!(files.len(), 19);
|
assert!(files.len() > 0);
|
||||||
// Verify first entry (1000GB.zip)
|
|
||||||
let first: &FsEntry = files.get(0).unwrap();
|
|
||||||
if let FsEntry::File(f) = first {
|
|
||||||
assert_eq!(f.name, String::from("1000GB.zip"));
|
|
||||||
assert_eq!(f.abs_path, PathBuf::from("/1000GB.zip"));
|
|
||||||
assert_eq!(f.size, 1073741824000);
|
|
||||||
assert_eq!(*f.ftype.as_ref().unwrap(), String::from("zip"));
|
|
||||||
assert_eq!(f.unix_pex.unwrap(), (6, 4, 4));
|
|
||||||
assert_eq!(f.creation_time.duration_since(SystemTime::UNIX_EPOCH).unwrap(), Duration::from_secs(1455840000));
|
|
||||||
assert_eq!(f.last_access_time.duration_since(SystemTime::UNIX_EPOCH).unwrap(), Duration::from_secs(1455840000));
|
|
||||||
assert_eq!(f.last_change_time.duration_since(SystemTime::UNIX_EPOCH).unwrap(), Duration::from_secs(1455840000));
|
|
||||||
} else {
|
|
||||||
panic!("First should be a file, but it a directory");
|
|
||||||
}
|
|
||||||
// Verify last entry (directory upload)
|
|
||||||
let last: &FsEntry = files.get(18).unwrap();
|
|
||||||
if let FsEntry::Directory(d) = last {
|
|
||||||
assert_eq!(d.name, String::from("upload"));
|
|
||||||
assert_eq!(d.abs_path, PathBuf::from("/upload"));
|
|
||||||
assert_eq!(d.readonly, false);
|
|
||||||
assert_eq!(d.unix_pex.unwrap(), (7, 5, 5));
|
|
||||||
} else {
|
|
||||||
panic!("Last should be a directory, but is a file");
|
|
||||||
}
|
|
||||||
// Disconnect
|
// Disconnect
|
||||||
assert!(ftp.disconnect().is_ok());
|
assert!(ftp.disconnect().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* NOTE: they don't work
|
||||||
#[test]
|
#[test]
|
||||||
fn test_filetransfer_ftp_recv() {
|
fn test_filetransfer_ftp_recv() {
|
||||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user