This is an automated email from the ASF dual-hosted git repository.

koushiro pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 1ce6e0614 refactor: split sftp service into a separate crate (#7060)
1ce6e0614 is described below

commit 1ce6e0614258f55dc51114df62a69e23ac5ad5ee
Author: Sahil Shadwal <[email protected]>
AuthorDate: Sat Dec 20 13:10:42 2025 +0530

    refactor: split sftp service into a separate crate (#7060)
    
    * splitting sftp
    
    * rust format and licence fix
    
    * behavioral fix
    
    * trigger ci
    
    * behavioral fix
    
    * merge conflict
    
    * f
    
    * Merge branch 'main' into sftp
    
    * f
    
    * merge conflict
    
    * f
    
    * Update core/services/sftp/src/core.rs
    
    Co-authored-by: Qinxuan Chen <[email protected]>
    
    * fix: update sftp service documentation import and add development 
dependencies
    
    * style: format sftp Cargo.toml with taplo
    
    ---------
    
    Co-authored-by: Qinxuan Chen <[email protected]>
---
 core/Cargo.lock                                    | 20 ++++++-
 core/Cargo.toml                                    |  3 +-
 core/core/Cargo.toml                               |  7 ---
 core/core/src/services/mod.rs                      |  5 --
 core/core/src/services/sftp/utils.rs               | 54 -------------------
 core/services/sftp/Cargo.toml                      | 48 +++++++++++++++++
 .../services/sftp => services/sftp/src}/backend.rs |  7 +--
 .../services/sftp => services/sftp/src}/config.rs  |  8 +--
 .../services/sftp => services/sftp/src}/core.rs    |  4 +-
 .../services/sftp => services/sftp/src}/deleter.rs |  4 +-
 .../services/sftp => services/sftp/src}/docs.md    |  2 +-
 .../services/sftp => services/sftp/src}/error.rs   |  4 +-
 .../sftp/mod.rs => services/sftp/src/lib.rs}       | 11 ++--
 .../services/sftp => services/sftp/src}/lister.rs  | 11 ++--
 .../services/sftp => services/sftp/src}/reader.rs  |  4 +-
 .../sftp/deleter.rs => services/sftp/src/utils.rs} | 62 ++++++++++------------
 .../services/sftp => services/sftp/src}/writer.rs  |  4 +-
 core/src/lib.rs                                    |  2 +
 18 files changed, 131 insertions(+), 129 deletions(-)

diff --git a/core/Cargo.lock b/core/Cargo.lock
index d0bf60233..0c1dcd2db 100644
--- a/core/Cargo.lock
+++ b/core/Cargo.lock
@@ -5599,6 +5599,7 @@ dependencies = [
  "opendal-service-redb",
  "opendal-service-s3",
  "opendal-service-seafile",
+ "opendal-service-sftp",
  "opendal-service-sled",
  "opendal-service-sqlite",
  "opendal-service-surrealdb",
@@ -5671,8 +5672,6 @@ dependencies = [
  "moka",
  "mongodb",
  "mongodb-internal-macros",
- "openssh",
- "openssh-sftp-client",
  "percent-encoding",
  "pretty_assertions",
  "probe",
@@ -6584,6 +6583,23 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "opendal-service-sftp"
+version = "0.55.0"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "ctor",
+ "fastpool",
+ "futures",
+ "log",
+ "opendal-core",
+ "openssh",
+ "openssh-sftp-client",
+ "serde",
+ "tokio",
+]
+
 [[package]]
 name = "opendal-service-sled"
 version = "0.55.0"
diff --git a/core/Cargo.toml b/core/Cargo.toml
index a5c86bbfe..337ba6b05 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -162,7 +162,7 @@ services-redis-native-tls = 
["opendal-core/services-redis-native-tls"]
 services-rocksdb = ["opendal-core/services-rocksdb"]
 services-s3 = ["dep:opendal-service-s3"]
 services-seafile = ["dep:opendal-service-seafile"]
-services-sftp = ["opendal-core/services-sftp"]
+services-sftp = ["dep:opendal-service-sftp"]
 services-sled = ["dep:opendal-service-sled"]
 services-sqlite = ["dep:opendal-service-sqlite"]
 services-surrealdb = ["dep:opendal-service-surrealdb"]
@@ -260,6 +260,7 @@ opendal-service-postgresql = { path = 
"services/postgresql", version = "0.55.0",
 opendal-service-redb = { path = "services/redb", version = "0.55.0", optional 
= true, default-features = false }
 opendal-service-s3 = { path = "services/s3", version = "0.55.0", optional = 
true, default-features = false }
 opendal-service-seafile = { path = "services/seafile", version = "0.55.0", 
optional = true, default-features = false }
+opendal-service-sftp = { path = "services/sftp", version = "0.55.0", optional 
= true, default-features = false }
 opendal-service-sled = { path = "services/sled", version = "0.55.0", optional 
= true, default-features = false }
 opendal-service-sqlite = { path = "services/sqlite", version = "0.55.0", 
optional = true, default-features = false }
 opendal-service-surrealdb = { path = "services/surrealdb", version = "0.55.0", 
optional = true, default-features = false }
diff --git a/core/core/Cargo.toml b/core/core/Cargo.toml
index 3311d59d0..8de41c8fc 100644
--- a/core/core/Cargo.toml
+++ b/core/core/Cargo.toml
@@ -65,7 +65,6 @@ services-mongodb = ["dep:mongodb", 
"dep:mongodb-internal-macros"]
 services-redis = ["dep:redis", "dep:fastpool", "redis?/tokio-rustls-comp"]
 services-redis-native-tls = ["services-redis", "redis?/tokio-native-tls-comp"]
 services-rocksdb = ["dep:rocksdb", "internal-tokio-rt"]
-services-sftp = ["dep:openssh", "dep:openssh-sftp-client", "dep:fastpool"]
 services-vercel-artifacts = []
 services-webhdfs = []
 
@@ -112,12 +111,6 @@ moka = { version = "0.12", optional = true, features = 
["future", "sync"] }
 # for services-mongodb
 mongodb = { version = "3.3.0", optional = true }
 mongodb-internal-macros = { version = "3.2.4", optional = true }
-# for services-sftp
-openssh = { version = "0.11.0", optional = true }
-openssh-sftp-client = { version = "0.15.3", optional = true, features = [
-  "openssh",
-  "tracing",
-] }
 # for services-redis
 redis = { version = "1.0", features = [
   "cluster-async",
diff --git a/core/core/src/services/mod.rs b/core/core/src/services/mod.rs
index 7a4adc065..62ee7588e 100644
--- a/core/core/src/services/mod.rs
+++ b/core/core/src/services/mod.rs
@@ -43,8 +43,3 @@ pub use self::redis::*;
 mod rocksdb;
 #[cfg(feature = "services-rocksdb")]
 pub use self::rocksdb::*;
-
-#[cfg(feature = "services-sftp")]
-mod sftp;
-#[cfg(feature = "services-sftp")]
-pub use sftp::*;
diff --git a/core/core/src/services/sftp/utils.rs 
b/core/core/src/services/sftp/utils.rs
deleted file mode 100644
index 98e15bfbf..000000000
--- a/core/core/src/services/sftp/utils.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use openssh_sftp_client::metadata::MetaData as SftpMeta;
-
-use crate::EntryMode;
-use crate::Metadata;
-use crate::raw::Timestamp;
-
-/// REMOVE ME: we should not implement `From<SftpMeta> for Metadata`.
-impl From<SftpMeta> for Metadata {
-    fn from(meta: SftpMeta) -> Self {
-        let mode = meta
-            .file_type()
-            .map(|filetype| {
-                if filetype.is_file() {
-                    EntryMode::FILE
-                } else if filetype.is_dir() {
-                    EntryMode::DIR
-                } else {
-                    EntryMode::Unknown
-                }
-            })
-            .unwrap_or(EntryMode::Unknown);
-
-        let mut metadata = Metadata::new(mode);
-
-        if let Some(size) = meta.len() {
-            metadata.set_content_length(size);
-        }
-
-        if let Some(modified) = meta.modified() {
-            if let Ok(m) = Timestamp::try_from(modified.as_system_time()) {
-                metadata.set_last_modified(m);
-            }
-        }
-
-        metadata
-    }
-}
diff --git a/core/services/sftp/Cargo.toml b/core/services/sftp/Cargo.toml
new file mode 100644
index 000000000..5186394ef
--- /dev/null
+++ b/core/services/sftp/Cargo.toml
@@ -0,0 +1,48 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[package]
+description = "Apache OpenDAL SFTP service implementation"
+name = "opendal-service-sftp"
+
+authors = { workspace = true }
+edition = { workspace = true }
+homepage = { workspace = true }
+license = { workspace = true }
+repository = { workspace = true }
+rust-version = { workspace = true }
+version = { workspace = true }
+
+[package.metadata.docs.rs]
+all-features = true
+
+[dependencies]
+opendal-core = { path = "../../core", version = "0.55.0", default-features = 
false }
+
+bytes = { workspace = true }
+ctor = { workspace = true }
+fastpool = "1.0.2"
+futures = { workspace = true }
+log = { workspace = true }
+openssh = "0.11.0"
+openssh-sftp-client = { version = "0.15.3", features = ["openssh", "tracing"] }
+serde = { workspace = true, features = ["derive"] }
+tokio = { workspace = true }
+
+[dev-dependencies]
+anyhow = "1.0"
+tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
diff --git a/core/core/src/services/sftp/backend.rs 
b/core/services/sftp/src/backend.rs
similarity index 98%
rename from core/core/src/services/sftp/backend.rs
rename to core/services/sftp/src/backend.rs
index 384c0b3e4..35e9e1eb5 100644
--- a/core/core/src/services/sftp/backend.rs
+++ b/core/services/sftp/src/backend.rs
@@ -33,9 +33,10 @@ use super::error::is_sftp_protocol_error;
 use super::error::parse_sftp_error;
 use super::lister::SftpLister;
 use super::reader::SftpReader;
+use super::utils::to_metadata;
 use super::writer::SftpWriter;
-use crate::raw::*;
-use crate::*;
+use opendal_core::raw::*;
+use opendal_core::*;
 
 /// SFTP services support. (only works on unix)
 ///
@@ -244,7 +245,7 @@ impl Access for SftpBackend {
         let mut fs = client.fs();
         fs.set_cwd(&self.core.root);
 
-        let meta: Metadata = 
fs.metadata(path).await.map_err(parse_sftp_error)?.into();
+        let meta: Metadata = 
to_metadata(fs.metadata(path).await.map_err(parse_sftp_error)?);
 
         Ok(RpStat::new(meta))
     }
diff --git a/core/core/src/services/sftp/config.rs 
b/core/services/sftp/src/config.rs
similarity index 94%
rename from core/core/src/services/sftp/config.rs
rename to core/services/sftp/src/config.rs
index 033268e7d..48445770a 100644
--- a/core/core/src/services/sftp/config.rs
+++ b/core/services/sftp/src/config.rs
@@ -50,10 +50,10 @@ impl Debug for SftpConfig {
     }
 }
 
-impl crate::Configurator for SftpConfig {
+impl opendal_core::Configurator for SftpConfig {
     type Builder = SftpBuilder;
 
-    fn from_uri(uri: &crate::types::OperatorUri) -> crate::Result<Self> {
+    fn from_uri(uri: &opendal_core::OperatorUri) -> opendal_core::Result<Self> 
{
         let mut map = uri.options().clone();
         if let Some(authority) = uri.authority() {
             map.insert("endpoint".to_string(), authority.to_string());
@@ -74,8 +74,8 @@ impl crate::Configurator for SftpConfig {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::Configurator;
-    use crate::types::OperatorUri;
+    use opendal_core::Configurator;
+    use opendal_core::OperatorUri;
 
     #[test]
     fn from_uri_sets_endpoint_and_root() {
diff --git a/core/core/src/services/sftp/core.rs 
b/core/services/sftp/src/core.rs
similarity index 98%
rename from core/core/src/services/sftp/core.rs
rename to core/services/sftp/src/core.rs
index 83ff99258..bb8e24bb3 100644
--- a/core/core/src/services/sftp/core.rs
+++ b/core/services/sftp/src/core.rs
@@ -18,10 +18,10 @@
 use super::error::is_sftp_protocol_error;
 use super::error::parse_sftp_error;
 use super::error::parse_ssh_error;
-use crate::raw::*;
-use crate::*;
 use fastpool::{ManageObject, ObjectStatus, bounded};
 use log::debug;
+use opendal_core::raw::*;
+use opendal_core::*;
 use openssh::KnownHosts;
 use openssh::SessionBuilder;
 use openssh_sftp_client::Sftp;
diff --git a/core/core/src/services/sftp/deleter.rs 
b/core/services/sftp/src/deleter.rs
similarity index 97%
copy from core/core/src/services/sftp/deleter.rs
copy to core/services/sftp/src/deleter.rs
index 9376b1f4b..43cb11faa 100644
--- a/core/core/src/services/sftp/deleter.rs
+++ b/core/services/sftp/src/deleter.rs
@@ -20,8 +20,8 @@ use std::sync::Arc;
 use super::core::SftpCore;
 use super::error::is_not_found;
 use super::error::parse_sftp_error;
-use crate::raw::*;
-use crate::*;
+use opendal_core::raw::*;
+use opendal_core::*;
 
 pub struct SftpDeleter {
     core: Arc<SftpCore>,
diff --git a/core/core/src/services/sftp/docs.md 
b/core/services/sftp/src/docs.md
similarity index 97%
rename from core/core/src/services/sftp/docs.md
rename to core/services/sftp/src/docs.md
index 88e33953a..123a167f7 100644
--- a/core/core/src/services/sftp/docs.md
+++ b/core/services/sftp/src/docs.md
@@ -31,7 +31,7 @@ You can refer to [`SftpBuilder`]'s docs for more information
 
 ```rust,no_run
 use anyhow::Result;
-use opendal_core::services::Sftp;
+use opendal_service_sftp::Sftp;
 use opendal_core::Operator;
 
 #[tokio::main]
diff --git a/core/core/src/services/sftp/error.rs 
b/core/services/sftp/src/error.rs
similarity index 97%
rename from core/core/src/services/sftp/error.rs
rename to core/services/sftp/src/error.rs
index 5cc071d5c..3fdb749f3 100644
--- a/core/core/src/services/sftp/error.rs
+++ b/core/services/sftp/src/error.rs
@@ -19,8 +19,8 @@ use openssh::Error as SshError;
 use openssh_sftp_client::Error as SftpClientError;
 use openssh_sftp_client::error::SftpErrorKind;
 
-use crate::Error;
-use crate::ErrorKind;
+use opendal_core::Error;
+use opendal_core::ErrorKind;
 
 pub fn parse_sftp_error(e: SftpClientError) -> Error {
     let kind = match &e {
diff --git a/core/core/src/services/sftp/mod.rs b/core/services/sftp/src/lib.rs
similarity index 85%
rename from core/core/src/services/sftp/mod.rs
rename to core/services/sftp/src/lib.rs
index 5712c4bdc..878be1956 100644
--- a/core/core/src/services/sftp/mod.rs
+++ b/core/services/sftp/src/lib.rs
@@ -15,10 +15,10 @@
 // specific language governing permissions and limitations
 // under the License.
 
-/// Default scheme for sftp service.
-pub const SFTP_SCHEME: &str = "sftp";
+//! SFTP service implementation for Apache OpenDAL.
 
-use crate::types::DEFAULT_OPERATOR_REGISTRY;
+#![cfg_attr(docsrs, feature(doc_cfg))]
+#![deny(missing_docs)]
 
 mod backend;
 mod config;
@@ -33,7 +33,10 @@ mod writer;
 pub use backend::SftpBuilder as Sftp;
 pub use config::SftpConfig;
 
+/// Default scheme for sftp service.
+pub const SFTP_SCHEME: &str = "sftp";
+
 #[ctor::ctor]
 fn register_sftp_service() {
-    DEFAULT_OPERATOR_REGISTRY.register::<Sftp>(SFTP_SCHEME);
+    opendal_core::DEFAULT_OPERATOR_REGISTRY.register::<Sftp>(SFTP_SCHEME);
 }
diff --git a/core/core/src/services/sftp/lister.rs 
b/core/services/sftp/src/lister.rs
similarity index 90%
rename from core/core/src/services/sftp/lister.rs
rename to core/services/sftp/src/lister.rs
index c36fb2f42..6c8dc759d 100644
--- a/core/core/src/services/sftp/lister.rs
+++ b/core/services/sftp/src/lister.rs
@@ -22,9 +22,10 @@ use openssh_sftp_client::fs::DirEntry;
 use openssh_sftp_client::fs::ReadDir;
 
 use super::error::parse_sftp_error;
-use crate::Result;
-use crate::raw::oio;
-use crate::raw::oio::Entry;
+use super::utils::to_metadata;
+use opendal_core::Result;
+use opendal_core::raw::oio;
+use opendal_core::raw::oio::Entry;
 
 pub struct SftpLister {
     dir: Pin<Box<ReadDir>>,
@@ -61,7 +62,7 @@ impl oio::List for SftpLister {
                         if self.prefix.is_empty() {
                             path = "/";
                         }
-                        return Ok(Some(Entry::new(path, e.metadata().into())));
+                        return Ok(Some(Entry::new(path, 
to_metadata(e.metadata()))));
                     } else {
                         return Ok(Some(map_entry(self.prefix.as_str(), e)));
                     }
@@ -84,5 +85,5 @@ fn map_entry(prefix: &str, value: DirEntry) -> Entry {
         }
     );
 
-    Entry::new(path.as_str(), value.metadata().into())
+    Entry::new(path.as_str(), to_metadata(value.metadata()))
 }
diff --git a/core/core/src/services/sftp/reader.rs 
b/core/services/sftp/src/reader.rs
similarity index 97%
rename from core/core/src/services/sftp/reader.rs
rename to core/services/sftp/src/reader.rs
index 676389123..f345d2208 100644
--- a/core/core/src/services/sftp/reader.rs
+++ b/core/services/sftp/src/reader.rs
@@ -21,8 +21,8 @@ use openssh_sftp_client::file::File;
 
 use super::core::Manager;
 use super::error::parse_sftp_error;
-use crate::raw::*;
-use crate::*;
+use opendal_core::raw::*;
+use opendal_core::*;
 
 pub struct SftpReader {
     /// Keep the connection alive while data stream is alive.
diff --git a/core/core/src/services/sftp/deleter.rs 
b/core/services/sftp/src/utils.rs
similarity index 50%
rename from core/core/src/services/sftp/deleter.rs
rename to core/services/sftp/src/utils.rs
index 9376b1f4b..070fef47c 100644
--- a/core/core/src/services/sftp/deleter.rs
+++ b/core/services/sftp/src/utils.rs
@@ -15,41 +15,37 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use std::sync::Arc;
-
-use super::core::SftpCore;
-use super::error::is_not_found;
-use super::error::parse_sftp_error;
-use crate::raw::*;
-use crate::*;
-
-pub struct SftpDeleter {
-    core: Arc<SftpCore>,
-}
-
-impl SftpDeleter {
-    pub fn new(core: Arc<SftpCore>) -> Self {
-        Self { core }
+use openssh_sftp_client::metadata::MetaData as SftpMeta;
+
+use opendal_core::EntryMode;
+use opendal_core::Metadata;
+use opendal_core::raw::Timestamp;
+
+pub fn to_metadata(meta: SftpMeta) -> Metadata {
+    let mode = meta
+        .file_type()
+        .map(|filetype| {
+            if filetype.is_file() {
+                EntryMode::FILE
+            } else if filetype.is_dir() {
+                EntryMode::DIR
+            } else {
+                EntryMode::Unknown
+            }
+        })
+        .unwrap_or(EntryMode::Unknown);
+
+    let mut metadata = Metadata::new(mode);
+
+    if let Some(size) = meta.len() {
+        metadata.set_content_length(size);
     }
-}
 
-impl oio::OneShotDelete for SftpDeleter {
-    async fn delete_once(&self, path: String, _: OpDelete) -> Result<()> {
-        let client = self.core.connect().await?;
-
-        let mut fs = client.fs();
-        fs.set_cwd(&self.core.root);
-
-        let res = if path.ends_with('/') {
-            fs.remove_dir(path).await
-        } else {
-            fs.remove_file(path).await
-        };
-
-        match res {
-            Ok(()) => Ok(()),
-            Err(e) if is_not_found(&e) => Ok(()),
-            Err(e) => Err(parse_sftp_error(e)),
+    if let Some(modified) = meta.modified() {
+        if let Ok(m) = Timestamp::try_from(modified.as_system_time()) {
+            metadata.set_last_modified(m);
         }
     }
+
+    metadata
 }
diff --git a/core/core/src/services/sftp/writer.rs 
b/core/services/sftp/src/writer.rs
similarity index 97%
rename from core/core/src/services/sftp/writer.rs
rename to core/services/sftp/src/writer.rs
index 7667069c9..542db18a9 100644
--- a/core/core/src/services/sftp/writer.rs
+++ b/core/services/sftp/src/writer.rs
@@ -22,8 +22,8 @@ use openssh_sftp_client::file::File;
 use openssh_sftp_client::file::TokioCompatFile;
 use tokio::io::AsyncWriteExt;
 
-use crate::raw::*;
-use crate::*;
+use opendal_core::raw::*;
+use opendal_core::*;
 
 pub struct SftpWriter {
     /// TODO: maybe we can use `File` directly?
diff --git a/core/src/lib.rs b/core/src/lib.rs
index c6b596cbd..956d5b33b 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -120,6 +120,8 @@ pub mod services {
     pub use opendal_service_s3::*;
     #[cfg(feature = "services-seafile")]
     pub use opendal_service_seafile::*;
+    #[cfg(feature = "services-sftp")]
+    pub use opendal_service_sftp::*;
     #[cfg(feature = "services-sled")]
     pub use opendal_service_sled::*;
     #[cfg(feature = "services-sqlite")]

Reply via email to