use e_log::preload::*;
use libc::mode_t;
pub use pavao::{SmbClient, SmbCredentials, SmbEncryptionLevel, SmbOptions, SmbShareMode};
use pavao::{SmbDirentType, SmbMode, SmbOpenOptions};
use remotefs::fs::{File, Metadata, ReadStream, UnixPex, Welcome, WriteStream};
use remotefs::{RemoteError, RemoteErrorType, RemoteFs, RemoteResult};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use crate::smb::utils::{path as path_utils, smb as smb_utils};
pub struct SmbFs {
client: SmbClient,
wrkdir: PathBuf,
}
impl SmbFs {
pub fn try_new(credentials: SmbCredentials, options: SmbOptions) -> RemoteResult<Self> {
Ok(Self {
client: SmbClient::new(credentials, options)
.map_err(|e| RemoteError::new_ex(RemoteErrorType::BadAddress, e))?,
wrkdir: PathBuf::from("/"),
})
}
pub fn client(&self) -> &SmbClient {
&self.client
}
pub fn client_mut(&mut self) -> &mut SmbClient {
&mut self.client
}
fn check_connection(&self) -> RemoteResult<()> {
trace!("checking connection...");
match self.client.get_user() {
Err(e) => {
error!("connection ERROR: {}", e);
Err(RemoteError::new_ex(RemoteErrorType::ConnectionError, e))
}
Ok(_) => {
trace!("connection OK");
Ok(())
}
}
}
fn get_uri<P: AsRef<Path>>(&self, p: P) -> String {
let p = path_utils::absolutize(self.wrkdir.as_path(), p.as_ref());
p.to_string_lossy().to_string()
}
}
impl RemoteFs for SmbFs {
fn connect(&mut self) -> RemoteResult<Welcome> {
self.check_connection()?;
Ok(Welcome::default())
}
fn disconnect(&mut self) -> RemoteResult<()> {
self.check_connection()
}
fn is_connected(&mut self) -> bool {
self.check_connection().is_ok()
}
fn pwd(&mut self) -> RemoteResult<PathBuf> {
self.check_connection().map(|_| self.wrkdir.clone())
}
fn change_dir(&mut self, dir: &Path) -> RemoteResult<PathBuf> {
self.check_connection()?;
let dir = path_utils::absolutize(self.wrkdir.as_path(), dir);
trace!("changing directory to {}", dir.display());
if self.stat(dir.as_path())?.is_dir() {
self.wrkdir = dir;
debug!("new working directory: {}", self.wrkdir.display());
Ok(self.wrkdir.clone())
} else {
error!("cannot enter directory {}. Not a directory", dir.display());
Err(RemoteError::new_ex(
RemoteErrorType::BadFile,
"not a directory",
))
}
}
fn list_dir(&mut self, path: &Path) -> RemoteResult<Vec<File>> {
self.check_connection()?;
let path = self.get_uri(path);
trace!("listing files at {}", path);
let dirents = self
.client
.list_dir(path.as_str())
.map_err(|e| RemoteError::new_ex(RemoteErrorType::StatFailed, e))?;
Ok(
dirents
.into_iter()
.filter_map(|d| {
if d.get_type() == SmbDirentType::File || d.get_type() == SmbDirentType::Dir {
let p = PathBuf::from(format!("{}/{}", path, d.name()));
Some(self.stat(&p))
} else {
None
}
})
.flatten()
.collect(),
)
}
fn stat(&mut self, path: &Path) -> RemoteResult<File> {
self.check_connection()?;
let path = self.get_uri(path);
trace!("get stat for {}", path);
self
.client
.stat(path.as_str())
.map_err(|e| RemoteError::new_ex(RemoteErrorType::StatFailed, e))
.map(|stat| smb_utils::smbstat_to_file(path, stat))
}
fn setstat(&mut self, _path: &Path, _metadata: Metadata) -> RemoteResult<()> {
Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
}
fn exists(&mut self, path: &Path) -> RemoteResult<bool> {
trace!("checking if {} exists...", path.display());
match self.stat(path) {
Ok(_) => Ok(true),
Err(RemoteError {
kind: RemoteErrorType::StatFailed,
..
}) => Ok(false),
Err(err) => Err(err),
}
}
fn remove_file(&mut self, path: &Path) -> RemoteResult<()> {
self.check_connection()?;
let path = self.get_uri(path);
trace!("removing file {}", path);
self
.client
.unlink(path)
.map_err(|e| RemoteError::new_ex(RemoteErrorType::CouldNotRemoveFile, e))
}
fn remove_dir(&mut self, path: &Path) -> RemoteResult<()> {
self.check_connection()?;
let path = self.get_uri(path);
trace!("removing directory at {}", path);
self
.client
.rmdir(path)
.map_err(|e| RemoteError::new_ex(RemoteErrorType::CouldNotRemoveFile, e))
}
fn create_dir(&mut self, path: &Path, mode: UnixPex) -> RemoteResult<()> {
self.check_connection()?;
if self.exists(path)? {
return Err(RemoteError::new(RemoteErrorType::DirectoryAlreadyExists));
}
let path = self.get_uri(path);
trace!("making directory at {}", path);
self
.client
.mkdir(path, SmbMode::from(u32::from(mode) as mode_t))
.map_err(|e| RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e))
}
fn symlink(&mut self, _path: &Path, _target: &Path) -> RemoteResult<()> {
Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
}
fn copy(&mut self, _src: &Path, _dest: &Path) -> RemoteResult<()> {
Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
}
fn mov(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
self.check_connection()?;
let src = self.get_uri(src);
let dest = self.get_uri(dest);
trace!("moving {} to {}", src, dest);
self
.client
.rename(src, dest)
.map_err(|e| RemoteError::new_ex(RemoteErrorType::ProtocolError, e))
}
fn exec(&mut self, _cmd: &str) -> RemoteResult<(u32, String)> {
Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
}
fn append_file(
&mut self,
path: &Path,
metadata: &Metadata,
mut reader: Box<dyn Read>,
) -> RemoteResult<u64> {
self.check_connection()?;
let path = self.get_uri(path);
trace!("opening file at {} for append", path);
let mut file = self
.client
.open_with(
path,
SmbOpenOptions::default()
.create(true)
.append(true)
.write(true)
.mode(u32::from(metadata.mode.unwrap_or_else(|| UnixPex::from(0o644))) as mode_t),
)
.map_err(|e| RemoteError::new_ex(RemoteErrorType::CouldNotOpenFile, e))?;
io::copy(&mut reader, &mut file).map_err(|e| RemoteError::new_ex(RemoteErrorType::IoError, e))
}
fn create_file(
&mut self,
path: &Path,
metadata: &Metadata,
mut reader: Box<dyn Read>,
) -> RemoteResult<u64> {
self.check_connection()?;
let path = self.get_uri(path);
trace!("creating file at {}", path);
let mut file = self
.client
.open_with(
path,
SmbOpenOptions::default()
.create(true)
.write(true)
.mode(u32::from(metadata.mode.unwrap_or_else(|| UnixPex::from(0o644))) as mode_t),
)
.map_err(|e| RemoteError::new_ex(RemoteErrorType::CouldNotOpenFile, e))?;
io::copy(&mut reader, &mut file).map_err(|e| RemoteError::new_ex(RemoteErrorType::IoError, e))
}
fn open_file(&mut self, path: &Path, mut dest: Box<dyn Write + Send>) -> RemoteResult<u64> {
self.check_connection()?;
let path = self.get_uri(path);
trace!("opening file at {} for read", path);
let mut file = self
.client
.open_with(path, SmbOpenOptions::default().read(true))
.map_err(|e| RemoteError::new_ex(RemoteErrorType::CouldNotOpenFile, e))?;
io::copy(&mut file, &mut dest).map_err(|e| RemoteError::new_ex(RemoteErrorType::IoError, e))
}
fn append(&mut self, _path: &Path, _metadata: &Metadata) -> RemoteResult<WriteStream> {
Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
}
fn create(&mut self, _path: &Path, _metadata: &Metadata) -> RemoteResult<WriteStream> {
Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
}
fn open(&mut self, _path: &Path) -> RemoteResult<ReadStream> {
Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
}
}
#[cfg(test)]
#[cfg(feature = "with-containers")]
mod test {
use std::io::Cursor;
use std::time::Duration;
use serial_test::serial;
use super::*;
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_append_to_file() {
crate::mock::logger();
let mut client = init_client();
let p = Path::new("/cargo-test/a.txt");
let file_data = "test data\n";
let reader = Cursor::new(file_data.as_bytes());
assert_eq!(
client
.create_file(p, &Metadata::default().size(10), Box::new(reader))
.ok()
.unwrap(),
10
);
assert_eq!(client.stat(p).ok().unwrap().metadata().size, 10);
let file_data = "Hello, world!\n";
let reader = Cursor::new(file_data.as_bytes());
assert_eq!(
client
.append_file(p, &Metadata::default().size(14), Box::new(reader))
.ok()
.unwrap(),
14
);
assert_eq!(client.stat(p).ok().unwrap().metadata().size, 24);
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_not_append_to_file() {
crate::mock::logger();
let mut client = init_client();
let p = Path::new("/tmp/aaaaaaa/hbbbbb/a.txt");
let file_data = "Hello, world!\n";
let reader = Cursor::new(file_data.as_bytes());
assert!(client
.append_file(p, &Metadata::default(), Box::new(reader))
.is_err());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_change_directory() {
crate::mock::logger();
let mut client = init_client();
let pwd = client.pwd().ok().unwrap();
assert!(client.change_dir(Path::new("/cargo-test")).is_ok());
assert!(client.change_dir(pwd.as_path()).is_ok());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_not_change_directory() {
crate::mock::logger();
let mut client = init_client();
assert!(client
.change_dir(Path::new("/tmp/sdfghjuireghiuergh/useghiyuwegh"))
.is_err());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_not_copy_file() {
crate::mock::logger();
let mut client = init_client();
let p = Path::new("a.txt");
let file_data = "test data\n";
let reader = Cursor::new(file_data.as_bytes());
assert!(client
.create_file(p, &Metadata::default(), Box::new(reader))
.is_ok());
assert!(client.copy(p, Path::new("aaa/bbbb/ccc/b.txt")).is_err());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_create_directory() {
crate::mock::logger();
let mut client = init_client();
assert!(client
.create_dir(Path::new("/cargo-test/mydir"), UnixPex::from(0o755))
.is_ok());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_not_create_directory_cause_already_exists() {
crate::mock::logger();
let mut client = init_client();
assert!(client
.create_dir(Path::new("/cargo-test/mydir"), UnixPex::from(0o755))
.is_ok());
assert_eq!(
client
.create_dir(Path::new("/cargo-test/mydir"), UnixPex::from(0o755))
.err()
.unwrap()
.kind,
RemoteErrorType::DirectoryAlreadyExists
);
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_not_create_directory() {
crate::mock::logger();
let mut client = init_client();
assert!(client
.create_dir(
Path::new("/tmp/werfgjwerughjwurih/iwerjghiwgui"),
UnixPex::from(0o755)
)
.is_err());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_create_file() {
crate::mock::logger();
let mut client = init_client();
let p = Path::new("/cargo-test/a.txt");
let file_data = "test data\n";
let reader = Cursor::new(file_data.as_bytes());
assert_eq!(
client
.create_file(p, &Metadata::default().size(10), Box::new(reader))
.ok()
.unwrap(),
10
);
assert_eq!(client.stat(p).ok().unwrap().metadata().size, 10);
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_not_create_file() {
crate::mock::logger();
let mut client = init_client();
let p = Path::new("/tmp/ahsufhauiefhuiashf/hfhfhfhf");
let file_data = "test data\n";
let reader = Cursor::new(file_data.as_bytes());
assert!(client
.create_file(p, &Metadata::default(), Box::new(reader))
.is_err());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_not_exec_command() {
crate::mock::logger();
let mut client = init_client();
assert!(client.exec("echo 5").is_err());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_tell_whether_file_exists() {
crate::mock::logger();
let mut client = init_client();
let p = Path::new("/cargo-test/a.txt");
let file_data = "test data\n";
let reader = Cursor::new(file_data.as_bytes());
assert!(client
.create_file(p, &Metadata::default(), Box::new(reader))
.is_ok());
assert_eq!(client.exists(p).ok().unwrap(), true);
assert_eq!(
client.exists(Path::new("/cargo-test/b.txt")).ok().unwrap(),
false
);
assert_eq!(
client.exists(Path::new("/tmp/ppppp/bhhrhu")).ok().unwrap(),
false
);
assert_eq!(client.exists(Path::new("/cargo-test/")).ok().unwrap(), true);
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_list_dir() {
crate::mock::logger();
let mut client = init_client();
let wrkdir = client.pwd().ok().unwrap();
let p = Path::new("/cargo-test/a.txt");
let file_data = "test data\n";
let reader = Cursor::new(file_data.as_bytes());
assert_eq!(
client
.append_file(p, &Metadata::default().size(10), Box::new(reader))
.unwrap(),
10
);
let file = client
.list_dir(Path::new("/cargo-test/"))
.ok()
.unwrap()
.get(0)
.unwrap()
.clone();
assert_eq!(file.name().as_str(), "a.txt");
let mut expected_path = wrkdir;
expected_path.push(p);
assert_eq!(file.path.as_path(), expected_path.as_path());
assert_eq!(file.extension().as_deref().unwrap(), "txt");
assert_eq!(file.metadata.size, 10);
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_not_list_dir() {
crate::mock::logger();
let mut client = init_client();
assert!(client.list_dir(Path::new("/tmp/auhhfh/hfhjfhf/")).is_err());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_not_move_file() {
crate::mock::logger();
let mut client = init_client();
let p = Path::new("a.txt");
let file_data = "test data\n";
let reader = Cursor::new(file_data.as_bytes());
assert!(client
.create_file(p, &Metadata::default(), Box::new(reader))
.is_ok());
let dest = Path::new("/tmp/wuefhiwuerfh/whjhh/b.txt");
assert!(client.mov(p, dest).is_err());
assert!(client
.mov(Path::new("/tmp/wuefhiwuerfh/whjhh/b.txt"), p)
.is_err());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_open_file() {
crate::mock::logger();
let mut client = init_client();
let p = Path::new("/cargo-test/a.txt");
let file_data = "test data\n";
let reader = Cursor::new(file_data.as_bytes());
assert!(client
.create_file(p, &Metadata::default().size(10), Box::new(reader))
.is_ok());
let buffer: Box<dyn std::io::Write + Send> = Box::new(Vec::with_capacity(512));
assert_eq!(client.open_file(p, buffer).ok().unwrap(), 10);
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_not_open_file() {
crate::mock::logger();
let mut client = init_client();
let buffer: Box<dyn std::io::Write + Send> = Box::new(Vec::with_capacity(512));
assert!(client
.open_file(Path::new("/tmp/aashafb/hhh"), buffer)
.is_err());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_print_working_directory() {
crate::mock::logger();
let mut client = init_client();
assert!(client.pwd().is_ok());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_remove_file() {
crate::mock::logger();
let mut client = init_client();
let p = Path::new("/cargo-test/a.txt");
let file_data = "test data\n";
let reader = Cursor::new(file_data.as_bytes());
assert!(client
.create_file(p, &Metadata::default(), Box::new(reader))
.is_ok());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_not_setstat_file() {
crate::mock::logger();
let mut client = init_client();
let p = Path::new("bbbbb/cccc/a.sh");
assert!(client
.setstat(
p,
Metadata {
accessed: None,
created: None,
file_type: remotefs::fs::FileType::File,
gid: Some(1),
mode: Some(UnixPex::from(0o755)),
modified: None,
size: 7,
symlink: None,
uid: Some(1),
}
)
.is_err());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_stat_file() {
crate::mock::logger();
let mut client = init_client();
let p = Path::new("/cargo-test/a.sh");
let file_data = "echo 5\n";
let reader = Cursor::new(file_data.as_bytes());
assert!(client
.create_file(p, &Metadata::default().size(7), Box::new(reader))
.is_ok());
let entry = client.stat(p).ok().unwrap();
assert_eq!(entry.name(), "a.sh");
let mut expected_path = client.pwd().ok().unwrap();
expected_path.push("/cargo-test/a.sh");
assert_eq!(entry.path(), expected_path.as_path());
let meta = entry.metadata();
assert_eq!(meta.size, 7);
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_not_stat_file() {
crate::mock::logger();
let mut client = init_client();
let p = Path::new("a.sh");
assert!(client.stat(p).is_err());
finalize_client(client);
}
#[test]
#[cfg(feature = "with-containers")]
#[serial]
fn should_not_make_symlink() {
crate::mock::logger();
let mut client = init_client();
let p = Path::new("/cargo-test/a.sh");
let symlink = Path::new("/cargo-test/b.sh");
assert!(client.symlink(symlink, p).is_err());
finalize_client(client);
}
fn init_client() -> SmbFs {
let _ = std::fs::remove_dir_all(Path::new("/tmp/cargo-test"));
let client = SmbFs::try_new(
SmbCredentials::default()
.server("smb://localhost:3445")
.share("/temp")
.username("test")
.password("test")
.workgroup("pavao"),
SmbOptions::default()
.case_sensitive(true)
.one_share_per_server(true),
)
.unwrap();
let _ = std::fs::create_dir(Path::new("/tmp/cargo-test"));
client
}
fn finalize_client(client: SmbFs) {
remove_dir_all("/cargo-test");
std::thread::sleep(Duration::from_secs(1));
drop(client);
}
fn remove_dir_all<S: AsRef<str>>(dir: S) {
let _ = std::fs::remove_dir_all(Path::new(dir.as_ref()));
}
}