From ed99646cfae6f32cfe962386769957bce85b6668 Mon Sep 17 00:00:00 2001 From: ArberSephirotheca Date: Fri, 2 Sep 2022 13:50:20 -0700 Subject: [PATCH] Done: "Cargo.toml": Hide FTP support under new feature flag. "operator.rs", "scheme.rs, src/services/mod.rs": Added conditional compilation for ftp services. "src/services/ftp/backend.rs": Removed any unnecessary helper functions that only use once. Removed username and password store to avoid data breach. Combine port and endpoint into one. Built a FTP command stream inside Builder's build() function and persist this stream inside Backend. Changed tls to enable_secure. Developer can use enable_secure() function to enable tls connection. Modified read() function to avoid putting all stream content into memory. Fixed stat() function, when path argument is empty, stat() function will return the stat of root directory. "src/services/ftp/dir_stream.rs" Added more metadata in DirEntry. "src/services/ftp/err.rs": Removed any unnecessary error functions. "src/services/ftp/mod.rs": Remove empty line. "tests/behavior/behavior.rs": Added feature for ftp. Resolve conflict. --- .env.example | 4 +- Cargo.toml | 4 +- examples/ftp.rs | 17 +- src/operator.rs | 2 +- src/scheme.rs | 1 + src/services/ftp/backend.rs | 429 ++++++++++++++------------------- src/services/ftp/dir_stream.rs | 8 +- src/services/ftp/err.rs | 86 ------- src/services/ftp/mod.rs | 1 - src/services/mod.rs | 1 + 10 files changed, 205 insertions(+), 348 deletions(-) diff --git a/.env.example b/.env.example index 9ae98f66d58..acb61a4db12 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,4 @@ OPENDAL_FTP_TEST=false OPENDAL_FTP_ENDPOINT= OPENDAL_FTP_ROOT=/path/to/dir OPENDAL_FTP_USER= -OPENDAL_FTP_PASSWORD= - - +OPENDAL_FTP_PASSWORD= \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index ef19b9f8969..1aff88773a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,8 @@ layers-tracing = ["tracing"] services-hdfs = ["hdrs"] # Enable services http support services-http = ["radix_trie"] +# Enable services ftp support +services-ftp = ["suppaftp"] [lib] bench = false @@ -89,7 +91,7 @@ thiserror = "1.0" time = "0.3" tokio = { version = "1.20", features = ["fs"] } tracing = { version = "0.1", optional = true } -suppaftp = {version = "^4.4.0", features = ["secure"]} +suppaftp = {version = "4.4", features = ["secure"], optional = true} [dev-dependencies] cfg-if = "1.0" diff --git a/examples/ftp.rs b/examples/ftp.rs index c3553cae62c..73413b0f1c3 100644 --- a/examples/ftp.rs +++ b/examples/ftp.rs @@ -33,11 +33,9 @@ async fn main() -> Result<()> { Available Environment Values: - OPENDAL_FTP_ENDPOINT=endpoint # required - - OPENDAL_FTP_Port=port # default with 21 - - OPENDAL_FTP_ROOT=/path/to/dir/ # if not set, will be seen as "/ftp" + - OPENDAL_FTP_ROOT=/path/to/dir/ # if not set, will be seen as "/" - OPENDAL_FTP_USER=user # default with empty string "" - - OPENDAL_FTP_PWD=password # default with empty string "" - - OPENDAL_FTP_TLS=bool # default with false + - OPENDAL_FTP_PASSWORD=password # default with empty string "" "# ); @@ -47,11 +45,10 @@ Available Environment Values: // Set the root for ftp, all operations will happen under this root. // NOTE: the root must be absolute path. - builder.endpoint(&env::var("OPENDAL_FTP_ENDPOINT").unwrap_or_else(|_| "127.0.0.1".to_string())); + builder + .endpoint(&env::var("OPENDAL_FTP_ENDPOINT").unwrap_or_else(|_| "127.0.0.1:21".to_string())); builder.user(&env::var("OPENDAL_FTP_USER").unwrap_or_else(|_| "".to_string())); - builder.password(&env::var("OPENDAL_FTP_PWD").unwrap_or_else(|_| "".to_string())); - builder.port(&env::var("OPENDAL_FTP_PORT").unwrap_or_else(|_| "21".to_string())); - builder.tls(&env::var("OPENDAL_FTP_TLS").unwrap_or_else(|_| "false".to_string())); + builder.password(&env::var("OPENDAL_FTP_PASSWORD").unwrap_or_else(|_| "".to_string())); // Use `Operator` normally. let op: Operator = Operator::new(builder.build()?); @@ -78,8 +75,8 @@ Available Environment Values: op.object(&path).write("write test").await?; info!("write to file successful!",); - info!("try to read file: {}", &path); - let content = op.object(&path).read().await?; + info!("try to read file content between 5-10: {}", &path); + let content = op.object(&path).range_read(5..10).await?; info!( "read file successful, content: {}", String::from_utf8_lossy(&content) diff --git a/src/operator.rs b/src/operator.rs index c2a7c3aebde..49ae53e1eaf 100644 --- a/src/operator.rs +++ b/src/operator.rs @@ -131,9 +131,9 @@ impl Operator { Scheme::Hdfs => services::hdfs::Backend::from_iter(it)?.into(), #[cfg(feature = "services-http")] Scheme::Http => services::http::Backend::from_iter(it)?.into(), - Scheme::Ipfs => services::ipfs::Backend::from_iter(it)?.into(), #[cfg(feature = "services-ftp")] Scheme::Ftp => services::ftp::Backend::from_iter(it)?.into(), + Scheme::Ipfs => services::ipfs::Backend::from_iter(it)?.into(), Scheme::Memory => services::memory::Builder::default().build()?.into(), Scheme::Gcs => services::gcs::Backend::from_iter(it)?.into(), Scheme::S3 => services::s3::Backend::from_iter(it)?.into(), diff --git a/src/scheme.rs b/src/scheme.rs index ad1841468f9..b095d654f5d 100644 --- a/src/scheme.rs +++ b/src/scheme.rs @@ -109,6 +109,7 @@ impl FromStr for Scheme { #[cfg(feature = "services-ftp")] "ftp" => Ok(Scheme::Ftp), "ipfs" => Ok(Scheme::Ipfs), + "gcs" => Ok(Scheme::Gcs), "memory" => Ok(Scheme::Memory), "obs" => Ok(Scheme::Obs), "s3" => Ok(Scheme::S3), diff --git a/src/services/ftp/backend.rs b/src/services/ftp/backend.rs index 993c261a287..83b0a70cde2 100644 --- a/src/services/ftp/backend.rs +++ b/src/services/ftp/backend.rs @@ -12,24 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. +use futures::io::Cursor; +use futures::lock::Mutex; use std::collections::HashMap; use std::fmt::Debug; use std::fmt::Formatter; -use std::io::Cursor; +use std::io::copy; +use std::io::sink; use std::io::Error; use std::io::ErrorKind; +use std::io::Read; use std::io::Result; -use std::io::SeekFrom; use std::str; use std::str::FromStr; use std::sync::Arc; use anyhow::anyhow; -use async_compat::Compat; use async_trait::async_trait; -use futures::AsyncRead; use futures::AsyncReadExt; -use futures::AsyncSeekExt; use log::info; use suppaftp::list::File; use suppaftp::native_tls::TlsConnector; @@ -38,21 +38,9 @@ use time::OffsetDateTime; use super::dir_stream::DirStream; use super::dir_stream::ReadDir; -use super::err::new_request_append_error; -use super::err::new_request_connection_error; -use super::err::new_request_list_error; -use super::err::new_request_mkdir_error; -use super::err::new_request_put_error; -use super::err::new_request_quit_error; -use super::err::new_request_remove_error; -use super::err::new_request_retr_error; -use super::err::new_request_root_eror; -use super::err::new_request_secure_error; -use super::err::new_request_sign_error; -use super::err::parse_io_error; use crate::error::other; use crate::error::BackendError; -use crate::io_util::unshared_reader; +use crate::error::ObjectError; use crate::ops::OpCreate; use crate::ops::OpDelete; use crate::ops::OpList; @@ -69,11 +57,10 @@ use crate::ObjectMode; #[derive(Default)] pub struct Builder { endpoint: Option, - port: Option, root: Option, user: Option, password: Option, - tls: Option, + enable_secure: bool, } impl Debug for Builder { @@ -97,17 +84,6 @@ impl Builder { self } - /// set port for ftp backend. - pub fn port(&mut self, port: &str) -> &mut Self { - self.port = if port.is_empty() { - None - } else { - Some(port.to_string()) - }; - - self - } - /// set root path for ftp backend. pub fn root(&mut self, root: &str) -> &mut Self { self.root = if root.is_empty() { @@ -142,12 +118,8 @@ impl Builder { } /// set tls for ftp backend. - pub fn tls(&mut self, tls: &str) -> &mut Self { - self.tls = if tls.is_empty() { - Some("false".to_string()) - } else { - Some(tls.to_string()) - }; + pub fn enable_secure(&mut self) -> &mut Self { + self.enable_secure = true; self } @@ -165,14 +137,9 @@ impl Builder { Some(v) => v, }; - let port = match &self.port { - None => "21".to_string(), - Some(v) => v.clone(), - }; - let root = match &self.root { - // set default path to '/ftp' - None => "/ftp".to_string(), + // set default path to '/' + None => "/".to_string(), Some(v) => { debug_assert!(!v.is_empty()); let mut v = v.clone(); @@ -201,24 +168,59 @@ impl Builder { let credential = (user, password); - let tls = match &self.tls { - None => false, - Some(v) => { - if v == "true" { - true - } else { - false - } - } - }; + let enable_secure = self.enable_secure; + + let mut ftp_stream = FtpStream::connect(&endpoint).map_err(|e| { + other(ObjectError::new( + "connection", + &endpoint, + anyhow!("connection request: {e:?}"), + )) + })?; + + // switch to secure mode if ssl/tls is on. + if self.enable_secure { + ftp_stream = ftp_stream + .into_secure(TlsConnector::new().unwrap(), &endpoint) + .map_err(|e| { + other(ObjectError::new( + "connection", + &endpoint, + anyhow!("switching to secure mode request: {e:?}"), + )) + })?; + } + + // login if needed + if !credential.0.is_empty() { + ftp_stream + .login(&credential.0, &credential.1) + .map_err(|e| { + other(ObjectError::new( + "connection", + &endpoint, + anyhow!("signing request: {e:?}"), + )) + })?; + } + + // change to the root path + ftp_stream.cwd(&root).map_err(|e| { + other(ObjectError::new( + "connection", + &endpoint, + anyhow!("change root request: {e:?}"), + )) + })?; + + let client = ftp_stream; info!("ftp backend finished: {:?}", &self); Ok(Backend { endpoint: endpoint.to_string(), - port, root, - credential, - tls, + client: Arc::new(Mutex::new(client)), + enable_secure, }) } } @@ -227,10 +229,9 @@ impl Builder { #[derive(Clone)] pub struct Backend { endpoint: String, - port: String, root: String, - credential: (String, String), - tls: bool, + client: Arc>, + enable_secure: bool, } impl Debug for Backend { @@ -238,8 +239,7 @@ impl Debug for Backend { f.debug_struct("Backend") .field("endpoint", &self.endpoint) .field("root", &self.root) - .field("credential", &self.credential) - //.field("stream", &self.stream) + .field("tls", &self.enable_secure) .finish() } } @@ -257,39 +257,36 @@ impl Backend { } builder.build() } - /* - pub(crate) fn get_rel_path(&self, path: &str) -> String { - match path.strip_prefix(&self.root) { - Some(v) => v.to_string(), - None => unreachable!( - "invalid path{} that does not start with backend root {}", - &path, &self.root - ), - } - } - - pub(crate) fn get_abs_path(&self, path: &str) -> String { - if path == "/" { - return self.root.to_string(); - } - format!("{}{}", self.root, path) - } - */ } #[async_trait] impl Accessor for Backend { async fn create(&self, args: &OpCreate) -> Result<()> { let path = args.path(); - if args.mode() == ObjectMode::FILE { - self.ftp_put(path).await?; - + //let mut ftp_stream = self.ftp_connect()?; + self.client + .try_lock() + .unwrap() + .put_file(&path, &mut "".as_bytes()) + .map_err(|e| { + other(ObjectError::new( + "create", + path, + anyhow!("put request: {e:?}"), + )) + })?; return Ok(()); } if args.mode() == ObjectMode::DIR { - self.ftp_mkdir(path).await?; + self.client.try_lock().unwrap().mkdir(&path).map_err(|e| { + other(ObjectError::new( + "create", + path, + anyhow!("mkdir request: {e:?}"), + )) + })?; return Ok(()); } @@ -299,30 +296,65 @@ impl Accessor for Backend { async fn read(&self, args: &OpRead) -> Result { let path = args.path(); + let mut guard = self.client.lock().await; - let reader = self.ftp_get(path).await?; - - let mut reader = Compat::new(reader); + let mut stream = guard.retr_as_stream(path).map_err(|e| { + other(ObjectError::new( + "read", + path, + anyhow!("retrieve request: {e:?}"), + )) + })?; if let Some(offset) = args.offset() { - reader - .seek(SeekFrom::Start(offset)) - .await - .map_err(|e| parse_io_error(e, "read", args.path()))?; + copy(&mut stream.by_ref().take(offset), &mut sink())?; } - let r: BytesReader = match args.size() { - Some(size) => Box::new(reader.take(size)), - None => Box::new(reader), + let mut buf = Vec::new(); + let r = match args.size() { + None => { + stream.by_ref().read_to_end(&mut buf)?; + Box::new(Cursor::new(buf)) + } + Some(size) => { + stream.by_ref().take(size).read_to_end(&mut buf)?; + Box::new(Cursor::new(buf)) + } }; + guard.finalize_retr_stream(stream).map_err(|e| { + other(ObjectError::new( + "read", + path, + anyhow!("finalizing stream request: {e:?}"), + )) + })?; + + drop(guard); + Ok(r) } async fn write(&self, args: &OpWrite, r: BytesReader) -> Result { let path = args.path(); - let n = self.ftp_append(path, unshared_reader(r)).await?; + let mut guard = self.client.lock().await; + + let mut reader = Box::pin(r); + + let mut buf = Vec::new(); + + let _byte = reader.read_to_end(&mut buf).await?; + + let n = guard.append_file(path, &mut buf.as_slice()).map_err(|e| { + other(ObjectError::new( + "write", + path, + anyhow!("append request: {e:?}"), + )) + })?; + + drop(guard); Ok(n) } @@ -330,183 +362,92 @@ impl Accessor for Backend { async fn stat(&self, args: &OpStat) -> Result { let path = args.path(); - if path == self.root { + if path.is_empty() { let mut meta = ObjectMetadata::default(); meta.set_mode(ObjectMode::DIR); return Ok(meta); } - let resp = self.ftp_stat(path).await?; + let mut guard = self.client.lock().await; - let mut meta = ObjectMetadata::default(); + let mut resp = guard.list(Some(path)).map_err(|e| { + other(ObjectError::new( + "stat", + path, + anyhow!("list request: {e:?}"), + )) + })?; - if resp.is_file() { - meta.set_mode(ObjectMode::FILE); - } else if resp.is_directory() { - meta.set_mode(ObjectMode::DIR); - } else { - meta.set_mode(ObjectMode::Unknown); - } + drop(guard); + // As result is not empty, we can safely use swap_remove without panic + if !resp.is_empty() { + let mut meta = ObjectMetadata::default(); - meta.set_content_length(resp.size() as u64); + let f = File::from_str(&resp.swap_remove(0)) + .map_err(|e| Error::new(ErrorKind::InvalidData, e))?; + if f.is_file() { + meta.set_mode(ObjectMode::FILE); + } else if f.is_directory() { + meta.set_mode(ObjectMode::DIR); + } else { + meta.set_mode(ObjectMode::Unknown); + } - meta.set_last_modified(OffsetDateTime::from(resp.modified())); + meta.set_content_length(f.size() as u64); - Ok(meta) - } + meta.set_last_modified(OffsetDateTime::from(f.modified())); - async fn delete(&self, args: &OpDelete) -> Result<()> { - let path = args.path(); - if args.path().ends_with('/') { - self.ftp_rmdir(path).await?; + Ok(meta) } else { - self.ftp_rm(path).await?; + Err(Error::new(ErrorKind::NotFound, "file not found")) } - - Ok(()) } - async fn list(&self, args: &OpList) -> Result { + async fn delete(&self, args: &OpDelete) -> Result<()> { let path = args.path(); - let rd = self.ftp_list(path).await?; - - Ok(Box::new(DirStream::new(Arc::new(self.clone()), path, rd))) - } -} - -impl Backend { - pub(crate) fn ftp_connect(&self) -> Result { - // connecting to remote address - let u = format! {"{}:{}", self.endpoint, self.port}; - - let mut ftp_stream = FtpStream::connect(&u) - .map_err(|e| new_request_connection_error("connection", &u, e))?; - - // switch to secure mode if ssl/tls is on. - if self.tls { - ftp_stream = ftp_stream - .into_secure(TlsConnector::new().unwrap(), &u) - .map_err(|e| new_request_secure_error("connection", &u, e))?; - } + let mut guard = self.client.lock().await; - // login if needed - if !self.credential.0.is_empty() { - ftp_stream - .login(&self.credential.0, &self.credential.1) - .map_err(|e| new_request_sign_error("connection", &self.endpoint, e))?; + if args.path().ends_with('/') { + guard.rmdir(&path).map_err(|e| { + other(ObjectError::new( + "delete", + path, + anyhow!("remove directory request: {e:?}"), + )) + })?; + } else { + guard.rm(&path).map_err(|e| { + other(ObjectError::new( + "delete", + path, + anyhow!("remove file request: {e:?}"), + )) + })?; } - // change to the root path - ftp_stream - .cwd(&self.root) - .map_err(|e| new_request_root_eror("connection", &self.root, e))?; - - Ok(ftp_stream) - } - - pub(crate) async fn ftp_get(&self, path: &str) -> Result>> { - let mut ftp_stream = self.ftp_connect()?; - - let reader = ftp_stream - .retr_as_buffer(path) - .map_err(|e| new_request_retr_error("get", path, e))?; - - ftp_stream - .quit() - .map_err(|e| new_request_quit_error("get", path, e))?; - - Ok(reader) - } - - pub(crate) async fn ftp_put(&self, path: &str) -> Result { - let mut ftp_stream = self.ftp_connect()?; - - let n = ftp_stream - .put_file(&path, &mut "".as_bytes()) - .map_err(|e| new_request_put_error("put", path, e))?; - Ok(n) - } - - pub(crate) async fn ftp_mkdir(&self, path: &str) -> Result<()> { - let mut ftp_stream = self.ftp_connect()?; - ftp_stream - .mkdir(&path) - .map_err(|e| new_request_mkdir_error("mkdir", path, e))?; + drop(guard); Ok(()) } - pub(crate) async fn ftp_rm(&self, path: &str) -> Result<()> { - let mut ftp_stream = self.ftp_connect()?; - - ftp_stream - .rm(&path) - .map_err(|e| new_request_remove_error("rm", path, e))?; - - Ok(()) - } - - pub(crate) async fn ftp_rmdir(&self, path: &str) -> Result<()> { - let mut ftp_stream = self.ftp_connect()?; - - ftp_stream - .rmdir(&path) - .map_err(|e| new_request_remove_error("rmdir", path, e))?; - - Ok(()) - } - - pub(crate) async fn ftp_append(&self, path: &str, r: R) -> Result - where - R: AsyncRead + Send + Sync + 'static, - { - let mut ftp_stream = self.ftp_connect()?; - - let mut reader = Box::pin(r); - - let mut buf = Vec::new(); - - let _byte = reader.read_to_end(&mut buf).await?; - - let n = ftp_stream - .append_file(path, &mut buf.as_slice()) - .map_err(|e| new_request_append_error("append", path, e))?; - - Ok(n) - } - - pub(crate) async fn ftp_stat(&self, path: &str) -> Result { - let mut ftp_stream = self.ftp_connect()?; - - let mut result = ftp_stream - .list(Some(path)) - .map_err(|e| new_request_list_error("stat", path, e))?; - - ftp_stream - .quit() - .map_err(|e| new_request_quit_error("stat", path, e))?; - - // As result is not empty, we can safely use swap_remove without panic - if !result.is_empty() { - let f = File::from_str(&result.swap_remove(0)) - .map_err(|e| Error::new(ErrorKind::InvalidData, e))?; + async fn list(&self, args: &OpList) -> Result { + let path = args.path(); - Ok(f) - } else { - Err(Error::new(ErrorKind::NotFound, "file not found")) - } - } + let mut guard = self.client.lock().await; - pub(crate) async fn ftp_list(&self, path: &str) -> Result { - let mut ftp_stream = self.ftp_connect()?; + let files = guard.list(Some(path)).map_err(|e| { + other(ObjectError::new( + "list", + path, + anyhow!("list request: {e:?}"), + )) + })?; - let files = ftp_stream - .list(Some(path)) - .map_err(|e| new_request_list_error("list", path, e))?; + drop(guard); - let dir = ReadDir::new(files); + let rd = ReadDir::new(files); - Ok(dir) + Ok(Box::new(DirStream::new(Arc::new(self.clone()), path, rd))) } } diff --git a/src/services/ftp/dir_stream.rs b/src/services/ftp/dir_stream.rs index dffcf56788e..d842e28e437 100644 --- a/src/services/ftp/dir_stream.rs +++ b/src/services/ftp/dir_stream.rs @@ -98,10 +98,14 @@ impl futures::Stream for DirStream { }; debug!( - "dir object {} got entry, mode: {}, path: {}", + "dir object {} got entry, mode: {}, path: {}, content length: {:?}, last modified: {:?}, content_md5: {:?}, etag: {:?}", &self.path, d.mode(), - d.path() + d.path(), + d.content_length(), + d.last_modified(), + d.content_md5(), + d.etag(), ); Poll::Ready(Some(Ok(d))) } diff --git a/src/services/ftp/err.rs b/src/services/ftp/err.rs index 640b46585df..4f03deaffd5 100644 --- a/src/services/ftp/err.rs +++ b/src/services/ftp/err.rs @@ -26,92 +26,6 @@ use crate::error::ObjectError; /// In the future, we may have our own error struct. /// -// Create error happened during connection to ftp server. -pub fn new_request_connection_error(op: &'static str, path: &str, err: FtpError) -> Error { - other(ObjectError::new( - op, - path, - anyhow!("connection request: {err:?}"), - )) -} - -// Create error happened during swtiching to secure mode. -pub fn new_request_secure_error(op: &'static str, path: &str, err: FtpError) -> Error { - other(ObjectError::new( - op, - path, - anyhow!("switching to secure mode request: {err:?}"), - )) -} - -// Create error happened during signing to ftp server. -pub fn new_request_sign_error(op: &'static str, path: &str, err: FtpError) -> Error { - other(ObjectError::new( - op, - path, - anyhow!("signing request: {err:?}"), - )) -} - -// Create error happened during quitting ftp server. -pub fn new_request_quit_error(op: &'static str, path: &str, err: FtpError) -> Error { - other(ObjectError::new(op, path, anyhow!("quit request: {err:?}"))) -} - -// Create error happened during retrieving from ftp server. -pub fn new_request_retr_error(op: &'static str, path: &str, err: FtpError) -> Error { - other(ObjectError::new( - op, - path, - anyhow!("retrieve request: {err:?}"), - )) -} - -// Create error happened during putting to ftp server. -pub fn new_request_put_error(op: &'static str, path: &str, err: FtpError) -> Error { - other(ObjectError::new(op, path, anyhow!("put request: {err:?}"))) -} - -// Create error happended uring appending to ftp server. -pub fn new_request_append_error(op: &'static str, path: &str, err: FtpError) -> Error { - other(ObjectError::new( - op, - path, - anyhow!("append request: {err:?}"), - )) -} - -// Create error happened during removeing from ftp server. -pub fn new_request_remove_error(op: &'static str, path: &str, err: FtpError) -> Error { - other(ObjectError::new( - op, - path, - anyhow!("remove request: {err:?}"), - )) -} - -// Create error happened during listing from ftp server. -pub fn new_request_list_error(op: &'static str, path: &str, err: FtpError) -> Error { - other(ObjectError::new(op, path, anyhow!("list request: {err:?}"))) -} - -// Create error happened during making directory to ftp server. -pub fn new_request_mkdir_error(op: &'static str, path: &str, err: FtpError) -> Error { - other(ObjectError::new( - op, - path, - anyhow!("mkdir request: {err:?}"), - )) -} - -pub fn new_request_root_eror(op: &'static str, path: &str, err: FtpError) -> Error { - other(ObjectError::new( - op, - path, - anyhow!("change root request: {err:?}"), - )) -} - pub fn parse_io_error(err: Error, op: &'static str, path: &str) -> Error { Error::new(err.kind(), ObjectError::new(op, path, err)) } diff --git a/src/services/ftp/mod.rs b/src/services/ftp/mod.rs index 4b5a6be875c..a2b081ba476 100644 --- a/src/services/ftp/mod.rs +++ b/src/services/ftp/mod.rs @@ -86,6 +86,5 @@ mod backend; pub use backend::Backend; pub use backend::Builder; - mod dir_stream; mod err; diff --git a/src/services/mod.rs b/src/services/mod.rs index 1f9dcd03c9b..ee54d677910 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -21,6 +21,7 @@ pub mod azblob; pub mod fs; +#[cfg(feature = "services-ftp")] pub mod ftp; pub mod gcs; #[cfg(feature = "services-hdfs")]