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

xuanwo pushed a commit to branch xuanwo/s3-crate-extract
in repository https://gitbox.apache.org/repos/asf/opendal.git

commit c0c740b1db76b8ef690c021ade4f7293d973066c
Author: Xuanwo <[email protected]>
AuthorDate: Thu Dec 4 23:27:12 2025 +0800

    refactor: Move services s3 out as a crate
    
    Signed-off-by: Xuanwo <[email protected]>
---
 core/Cargo.lock                                    | 31 +++++++++++--
 core/Cargo.toml                                    |  6 ++-
 core/core/Cargo.toml                               | 15 ------
 core/core/src/blocking/operator.rs                 |  6 +--
 core/core/src/docs/concepts.rs                     | 20 +++-----
 core/core/src/layers/http_client.rs                |  2 +-
 core/core/src/lib.rs                               |  8 ++--
 core/core/src/services/mod.rs                      |  5 --
 core/core/src/types/builder.rs                     |  5 +-
 core/core/src/types/operator/operator.rs           |  6 +--
 core/services/s3/Cargo.toml                        | 33 ++++++++++++++
 .../src/services/s3 => services/s3/src}/backend.rs | 53 +++++++++++++---------
 .../s3 => services/s3/src}/compatible_services.md  |  0
 .../src/services/s3 => services/s3/src}/config.rs  | 13 ++++--
 .../src/services/s3 => services/s3/src}/core.rs    |  4 +-
 .../src/services/s3 => services/s3/src}/deleter.rs | 12 ++---
 .../src/services/s3 => services/s3/src}/docs.md    | 10 ++--
 .../src/services/s3 => services/s3/src}/error.rs   |  4 +-
 .../services/s3/mod.rs => services/s3/src/lib.rs}  | 12 +++--
 .../src/services/s3 => services/s3/src}/lister.rs  | 37 ++++++++++-----
 .../src/services/s3 => services/s3/src}/mod.rs     |  2 +-
 .../src/services/s3 => services/s3/src}/writer.rs  | 27 ++++++-----
 core/src/lib.rs                                    |  9 +++-
 23 files changed, 195 insertions(+), 125 deletions(-)

diff --git a/core/Cargo.lock b/core/Cargo.lock
index 9c7b5927e..0dd01bee2 100644
--- a/core/Cargo.lock
+++ b/core/Cargo.lock
@@ -5406,6 +5406,7 @@ dependencies = [
  "libtest-mimic",
  "log",
  "opendal-core",
+ "opendal-service-s3",
  "opentelemetry",
  "opentelemetry-otlp",
  "opentelemetry_sdk",
@@ -5458,7 +5459,6 @@ dependencies = [
  "bytes",
  "cacache",
  "compio",
- "crc32c",
  "criterion",
  "ctor",
  "dashmap 6.1.0",
@@ -5512,10 +5512,6 @@ dependencies = [
  "redb",
  "redis",
  "reqsign",
- "reqsign-aws-v4",
- "reqsign-core",
- "reqsign-file-read-tokio",
- "reqsign-http-send-reqwest",
  "reqwest",
  "rocksdb",
  "rustls-native-certs 0.8.2",
@@ -5580,6 +5576,31 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "opendal-service-s3"
+version = "0.55.0"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "crc32c",
+ "ctor",
+ "http 1.3.1",
+ "log",
+ "md-5",
+ "opendal-core",
+ "pretty_assertions",
+ "quick-xml",
+ "reqsign-aws-v4",
+ "reqsign-core",
+ "reqsign-file-read-tokio",
+ "reqsign-http-send-reqwest",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tracing-subscriber",
+]
+
 [[package]]
 name = "openssh"
 version = "0.11.5"
diff --git a/core/Cargo.toml b/core/Cargo.toml
index f1c5aaf4a..5c8c363a9 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -17,7 +17,7 @@
 
 [workspace]
 default-members = [".", "core"]
-members = [".", "core", "examples/*", "fuzz", "edge/*", "benches/vs_*"]
+members = [".", "core", "examples/*", "fuzz", "edge/*", "benches/vs_*", 
"services/s3"]
 
 [workspace.package]
 edition = "2024"
@@ -116,7 +116,8 @@ services-redb = ["opendal-core/services-redb"]
 services-redis = ["opendal-core/services-redis"]
 services-redis-native-tls = ["opendal-core/services-redis-native-tls"]
 services-rocksdb = ["opendal-core/services-rocksdb"]
-services-s3 = ["opendal-core/services-s3"]
+services-s3 = ["dep:opendal-service-s3"]
+service-s3 = ["services-s3"]
 services-seafile = ["opendal-core/services-seafile"]
 services-sftp = ["opendal-core/services-sftp"]
 services-sled = ["opendal-core/services-sled"]
@@ -152,6 +153,7 @@ required-features = ["tests"]
 
 [dependencies]
 opendal-core = { path = "core", version = "0.55.0", default-features = false }
+opendal-service-s3 = { path = "services/s3", version = "0.55.0", optional = 
true, default-features = false }
 
 [dev-dependencies]
 anyhow = { version = "1.0.100", features = ["std"] }
diff --git a/core/core/Cargo.toml b/core/core/Cargo.toml
index 4082d9185..0f6edc6d5 100644
--- a/core/core/Cargo.toml
+++ b/core/core/Cargo.toml
@@ -62,7 +62,6 @@ tests = [
   "services-http",
   "services-memory",
   "internal-tokio-rt",
-  "services-s3",
 ]
 
 # Enable path cache.
@@ -191,13 +190,6 @@ services-redb = ["dep:redb", "internal-tokio-rt"]
 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-s3 = [
-  "dep:reqsign-core",
-  "dep:reqsign-aws-v4",
-  "dep:reqsign-file-read-tokio",
-  "dep:reqsign-http-send-reqwest",
-  "dep:crc32c",
-]
 services-seafile = []
 services-sftp = ["dep:openssh", "dep:openssh-sftp-client", "dep:fastpool"]
 services-sled = ["dep:sled", "internal-tokio-rt"]
@@ -262,11 +254,6 @@ sqlx = { version = "0.8.0", features = [
 
 # For http based services.
 reqsign = { version = "0.16.5", default-features = false, optional = true }
-# For S3 service migration to v1
-reqsign-aws-v4 = { version = "2.0.1", default-features = false, optional = 
true }
-reqsign-core = { version = "2.0.1", default-features = false, optional = true }
-reqsign-file-read-tokio = { version = "2.0.1", default-features = false, 
optional = true }
-reqsign-http-send-reqwest = { version = "2.0.1", default-features = false, 
optional = true }
 
 # for self-referencing structs
 ouroboros = { version = "0.18.4", optional = true }
@@ -339,8 +326,6 @@ compio = { version = "0.16.0", optional = true, features = [
   "polling",
   "dispatcher",
 ] }
-# for services-s3
-crc32c = { version = "0.6.6", optional = true }
 # for services-monoiofs
 flume = { version = "0.11", optional = true }
 monoio = { version = "0.2.4", optional = true, features = [
diff --git a/core/core/src/blocking/operator.rs 
b/core/core/src/blocking/operator.rs
index 27f0190be..0f6fa89cb 100644
--- a/core/core/src/blocking/operator.rs
+++ b/core/core/src/blocking/operator.rs
@@ -45,7 +45,7 @@ use crate::*;
 /// #[tokio::main]
 /// async fn main() -> Result<()> {
 ///     // Create fs backend builder.
-///     let mut builder = 
services::S3::default().bucket("test").region("us-east-1");
+///     let builder = services::Memory::default();
 ///     let op = Operator::new(builder)?.finish();
 ///
 ///     // Build an `blocking::Operator` with blocking layer to start 
operating the storage.
@@ -75,7 +75,7 @@ use crate::*;
 ///
 /// fn blocking_fn() -> Result<blocking::Operator> {
 ///     // Create fs backend builder.
-///     let mut builder = 
services::S3::default().bucket("test").region("us-east-1");
+///     let builder = services::Memory::default();
 ///     let op = Operator::new(builder)?.finish();
 ///
 ///     let handle = tokio::runtime::Handle::try_current().unwrap();
@@ -109,7 +109,7 @@ use crate::*;
 ///
 /// fn main() -> Result<()> {
 ///     // Create fs backend builder.
-///     let mut builder = 
services::S3::default().bucket("test").region("us-east-1");
+///     let builder = services::Memory::default();
 ///     let op = Operator::new(builder)?.finish();
 ///
 ///     // Fetch the `EnterGuard` from global runtime.
diff --git a/core/core/src/docs/concepts.rs b/core/core/src/docs/concepts.rs
index 9f8a0b179..d208b7a62 100644
--- a/core/core/src/docs/concepts.rs
+++ b/core/core/src/docs/concepts.rs
@@ -45,18 +45,16 @@
 //! └───────────┘                 └───────────┘
 //! ```
 //!
-//! All [`Builder`] provided by OpenDAL is under 
[`services`][crate::services], we can refer to them like 
`opendal::services::S3`.
+//! All [`Builder`] provided by OpenDAL is under 
[`services`][crate::services], we can refer to them like 
`opendal::services::Memory`.
 //! By right the builder will be named like `OneServiceBuilder`, but usually 
we will export it to public with renaming it as one
 //! general name. For example, we will rename `S3Builder` to `S3` and 
developer will use `S3` finally.
 //!
 //! For example:
 //!
 //! ```no_run
-//! use opendal_core::services::S3;
+//! use opendal_core::services::Memory;
 //!
-//! let mut builder = S3::default();
-//! builder.bucket("example");
-//! builder.root("/path/to/file");
+//! let builder = Memory::default();
 //! ```
 //!
 //! # Operator
@@ -79,13 +77,11 @@
 //!
 //! ```no_run
 //! # use opendal_core::Result;
-//! use opendal_core::services::S3;
+//! use opendal_core::services::Memory;
 //! use opendal_core::Operator;
 //!
 //! # fn test() -> Result<()> {
-//! let mut builder = S3::default();
-//! builder.bucket("example");
-//! builder.root("/path/to/file");
+//! let builder = Memory::default();
 //!
 //! let op = Operator::new(builder)?.finish();
 //! # Ok(())
@@ -117,13 +113,11 @@
 //!
 //! ```no_run
 //! # use opendal_core::Result;
-//! use opendal_core::services::S3;
+//! use opendal_core::services::Memory;
 //! use opendal_core::Operator;
 //!
 //! # async fn test() -> Result<()> {
-//! let mut builder = S3::default();
-//! builder.bucket("example");
-//! builder.root("/path/to/file");
+//! let builder = Memory::default();
 //!
 //! let op = Operator::new(builder)?.finish();
 //! let bs: Vec<u8> = op.read("abc").await?;
diff --git a/core/core/src/layers/http_client.rs 
b/core/core/src/layers/http_client.rs
index 92e57ac44..1b1eed4b0 100644
--- a/core/core/src/layers/http_client.rs
+++ b/core/core/src/layers/http_client.rs
@@ -38,7 +38,7 @@ use crate::*;
 /// // Create a custom HTTP client
 /// let custom_client = HttpClient::new()?;
 ///
-/// let op = Operator::new(services::S3::default())?
+/// let op = Operator::new(services::Memory::default())?
 ///     .layer(HttpClientLayer::new(custom_client))
 ///     .finish();
 /// # Ok(())
diff --git a/core/core/src/lib.rs b/core/core/src/lib.rs
index 1fb46bc53..fb7b08d05 100644
--- a/core/core/src/lib.rs
+++ b/core/core/src/lib.rs
@@ -38,7 +38,7 @@
 //! The first step is to pick a service and init it with a builder. All 
supported
 //! services could be found at [`services`].
 //!
-//! Let's take [`services::S3`] as an example:
+//! Let's take [`services::Memory`] as an example:
 //!
 //! ```no_run
 //! use opendal_core::services;
@@ -47,7 +47,7 @@
 //!
 //! fn main() -> Result<()> {
 //!     // Pick a builder and configure it.
-//!     let mut builder = services::S3::default().bucket("test");
+//!     let builder = services::Memory::default();
 //!
 //!     // Init an operator
 //!     let op = Operator::new(builder)?.finish();
@@ -72,7 +72,7 @@
 //! #[tokio::main]
 //! async fn main() -> Result<()> {
 //!     // Pick a builder and configure it.
-//!     let mut builder = services::S3::default().bucket("test");
+//!     let builder = services::Memory::default();
 //!
 //!     // Init an operator
 //!     let op = Operator::new(builder)?
@@ -111,7 +111,7 @@
 //! #[tokio::main]
 //! async fn main() -> Result<()> {
 //!     // Pick a builder and configure it.
-//!     let mut builder = services::S3::default().bucket("test");
+//!     let builder = services::Memory::default();
 //!
 //!     // Init an operator
 //!     let op = Operator::new(builder)?
diff --git a/core/core/src/services/mod.rs b/core/core/src/services/mod.rs
index 57cd81b4d..dd232fa2a 100644
--- a/core/core/src/services/mod.rs
+++ b/core/core/src/services/mod.rs
@@ -254,11 +254,6 @@ mod rocksdb;
 #[cfg(feature = "services-rocksdb")]
 pub use self::rocksdb::*;
 
-#[cfg(feature = "services-s3")]
-mod s3;
-#[cfg(feature = "services-s3")]
-pub use s3::*;
-
 #[cfg(feature = "services-seafile")]
 mod seafile;
 #[cfg(feature = "services-seafile")]
diff --git a/core/core/src/types/builder.rs b/core/core/src/types/builder.rs
index 99f84d70a..006704e12 100644
--- a/core/core/src/types/builder.rs
+++ b/core/core/src/types/builder.rs
@@ -102,14 +102,13 @@ impl Builder for () {
 /// use std::collections::HashMap;
 ///
 /// use opendal_core::raw::HttpClient;
-/// use opendal_core::services::S3Config;
+/// use opendal_core::services::MemoryConfig;
 /// use opendal_core::Configurator;
 /// use opendal_core::Operator;
 ///
 /// async fn test() -> Result<()> {
-///     let mut cfg = S3Config::default();
+///     let mut cfg = MemoryConfig::default();
 ///     cfg.root = Some("/".to_string());
-///     cfg.bucket = "test".to_string();
 ///
 ///     let builder = cfg.into_builder();
 ///     let builder = builder.http_client(HttpClient::new()?);
diff --git a/core/core/src/types/operator/operator.rs 
b/core/core/src/types/operator/operator.rs
index 5b657fc2a..d9cde1c67 100644
--- a/core/core/src/types/operator/operator.rs
+++ b/core/core/src/types/operator/operator.rs
@@ -39,8 +39,8 @@ use crate::*;
 ///
 /// Users can initialize an `Operator` through the following methods:
 ///
-/// - [`Operator::new`]: Creates an operator using a [`services`] builder, 
such as [`services::S3`].
-/// - [`Operator::from_config`]: Creates an operator using a [`services`] 
configuration, such as [`services::S3Config`].
+/// - [`Operator::new`]: Creates an operator using a [`services`] builder, 
such as [`services::Memory`].
+/// - [`Operator::from_config`]: Creates an operator using a [`services`] 
configuration, such as [`services::MemoryConfig`].
 /// - [`Operator::from_iter`]: Creates an operator from an iterator of 
configuration key-value pairs.
 ///
 /// ```
@@ -110,7 +110,7 @@ use crate::*;
 /// #[tokio::main]
 /// async fn main() -> Result<()> {
 ///     // Pick a builder and configure it.
-///     let mut builder = services::S3::default().bucket("test");
+///     let builder = services::Memory::default();
 ///
 ///     // Init an operator
 ///     let op = Operator::new(builder)?
diff --git a/core/services/s3/Cargo.toml b/core/services/s3/Cargo.toml
new file mode 100644
index 000000000..cb8655657
--- /dev/null
+++ b/core/services/s3/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "opendal-service-s3"
+version = "0.55.0"
+edition = "2024"
+license = "Apache-2.0"
+repository = "https://github.com/apache/opendal";
+description = "Apache OpenDAL S3 service implementation"
+
+[package.metadata.docs.rs]
+all-features = true
+
+[dependencies]
+opendal-core = { path = "../../core", version = "0.55.0", default-features = 
false }
+base64 = "0.22"
+bytes = "1.6"
+crc32c = "0.6.6"
+ctor = "0.6"
+http = "1.1"
+log = "0.4"
+md-5 = "0.10"
+quick-xml = { version = "0.38", features = ["serialize", "overlapped-lists"] }
+reqsign-aws-v4 = { version = "2.0.1", default-features = false }
+reqsign-core = { version = "2.0.1", default-features = false }
+reqsign-file-read-tokio = { version = "2.0.1", default-features = false }
+reqsign-http-send-reqwest = { version = "2.0.1", default-features = false }
+reqwest = { version = "0.12.24", features = ["stream"], default-features = 
false }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+tokio = { version = "1.48", features = ["macros", "rt-multi-thread", 
"io-util"] }
+
+[dev-dependencies]
+pretty_assertions = "1"
+tracing-subscriber = "0.3"
diff --git a/core/core/src/services/s3/backend.rs 
b/core/services/s3/src/backend.rs
similarity index 97%
rename from core/core/src/services/s3/backend.rs
rename to core/services/s3/src/backend.rs
index 3c619055a..dc3f0295d 100644
--- a/core/core/src/services/s3/backend.rs
+++ b/core/services/s3/src/backend.rs
@@ -45,19 +45,21 @@ use reqsign_file_read_tokio::TokioFileRead;
 use reqsign_http_send_reqwest::ReqwestHttpSend;
 use reqwest::Url;
 
-use super::S3_SCHEME;
-use super::config::S3Config;
-use super::core::*;
-use super::deleter::S3Deleter;
-use super::error::parse_error;
-use super::lister::S3ListerV1;
-use super::lister::S3ListerV2;
-use super::lister::S3Listers;
-use super::lister::S3ObjectVersionsLister;
-use super::writer::S3Writer;
-use super::writer::S3Writers;
-use crate::raw::*;
-use crate::*;
+use crate::S3_SCHEME;
+use crate::config::S3Config;
+use crate::core::*;
+use crate::deleter::S3Deleter;
+use crate::error::parse_error;
+use crate::lister::S3ListerV1;
+use crate::lister::S3ListerV2;
+use crate::lister::S3Listers;
+use crate::lister::S3ObjectVersionsLister;
+use crate::writer::S3Writer;
+use crate::writer::S3Writers;
+use opendal_core::raw::*;
+use opendal_core::*;
+
+static GLOBAL_REQWEST_CLIENT: LazyLock<reqwest::Client> = 
LazyLock::new(reqwest::Client::new);
 
 /// Allow constructing correct region endpoint if user gives a global endpoint.
 static ENDPOINT_TEMPLATES: LazyLock<HashMap<&'static str, &'static str>> = 
LazyLock::new(|| {
@@ -604,7 +606,7 @@ impl S3Builder {
     /// # Examples
     ///
     /// ```no_run
-    /// use opendal_core::services::S3;
+    /// use opendal_service_s3::S3;
     ///
     /// # async fn example() {
     /// let region: Option<String> = 
S3::detect_region("https://s3.amazonaws.com";, "example").await;
@@ -642,10 +644,11 @@ impl S3Builder {
         }
 
         // If this bucket is AWS, we can try to match the endpoint.
-        if let Some(v) = endpoint.strip_prefix("https://s3.";) {
-            if let Some(region) = v.strip_suffix(".amazonaws.com") {
-                return Some(region.to_string());
-            }
+        if let Some(region) = endpoint
+            .strip_prefix("https://s3.";)
+            .and_then(|v| v.strip_suffix(".amazonaws.com"))
+        {
+            return Some(region.to_string());
         }
 
         // If this bucket is OSS, we can try to match the endpoint.
@@ -679,10 +682,12 @@ impl S3Builder {
         );
 
         // Get region from response header no matter status code.
-        if let Some(header) = res.headers().get("x-amz-bucket-region") {
-            if let Ok(regin) = header.to_str() {
-                return Some(regin.to_string());
-            }
+        if let Some(region) = res
+            .headers()
+            .get("x-amz-bucket-region")
+            .and_then(|header| header.to_str().ok())
+        {
+            return Some(region.to_string());
         }
 
         // Status code is 403 or 200 means we already visit the correct
@@ -1119,6 +1124,10 @@ impl Access for S3Backend {
                 ErrorKind::Unsupported,
                 "operation is not supported",
             )),
+            _ => Err(Error::new(
+                ErrorKind::Unsupported,
+                "operation is not supported",
+            )),
         };
         let req = req?;
 
diff --git a/core/core/src/services/s3/compatible_services.md 
b/core/services/s3/src/compatible_services.md
similarity index 100%
rename from core/core/src/services/s3/compatible_services.md
rename to core/services/s3/src/compatible_services.md
diff --git a/core/core/src/services/s3/config.rs 
b/core/services/s3/src/config.rs
similarity index 98%
rename from core/core/src/services/s3/config.rs
rename to core/services/s3/src/config.rs
index e23c1bce2..9b2761e01 100644
--- a/core/core/src/services/s3/config.rs
+++ b/core/services/s3/src/config.rs
@@ -17,10 +17,13 @@
 
 use std::fmt::Debug;
 
+use opendal_core::Configurator;
+use opendal_core::OperatorUri;
+use opendal_core::Result;
 use serde::Deserialize;
 use serde::Serialize;
 
-use super::backend::S3Builder;
+use crate::backend::S3Builder;
 
 /// Config for Aws S3 and compatible services (including minio, digitalocean 
space,
 /// Tencent Cloud Object Storage(COS) and so on) support.
@@ -231,10 +234,10 @@ impl Debug for S3Config {
     }
 }
 
-impl crate::Configurator for S3Config {
+impl Configurator for S3Config {
     type Builder = S3Builder;
 
-    fn from_uri(uri: &crate::types::OperatorUri) -> crate::Result<Self> {
+    fn from_uri(uri: &OperatorUri) -> Result<Self> {
         let mut map = uri.options().clone();
 
         if let Some(name) = uri.name() {
@@ -263,8 +266,8 @@ mod tests {
     use std::iter;
 
     use super::*;
-    use crate::Configurator;
-    use crate::types::OperatorUri;
+    use opendal_core::Configurator;
+    use opendal_core::OperatorUri;
 
     #[test]
     fn test_s3_config_original_field_names() {
diff --git a/core/core/src/services/s3/core.rs b/core/services/s3/src/core.rs
similarity index 99%
rename from core/core/src/services/s3/core.rs
rename to core/services/s3/src/core.rs
index 6699dd2bf..03e14483a 100644
--- a/core/core/src/services/s3/core.rs
+++ b/core/services/s3/src/core.rs
@@ -43,8 +43,8 @@ use reqsign_core::Signer;
 use serde::Deserialize;
 use serde::Serialize;
 
-use crate::raw::*;
-use crate::*;
+use opendal_core::raw::*;
+use opendal_core::*;
 
 pub mod constants {
     pub const X_AMZ_COPY_SOURCE: &str = "x-amz-copy-source";
diff --git a/core/core/src/services/s3/deleter.rs 
b/core/services/s3/src/deleter.rs
similarity index 95%
rename from core/core/src/services/s3/deleter.rs
rename to core/services/s3/src/deleter.rs
index 9d8ae601d..feb9ff90b 100644
--- a/core/core/src/services/s3/deleter.rs
+++ b/core/services/s3/src/deleter.rs
@@ -20,12 +20,12 @@ use std::sync::Arc;
 use bytes::Buf;
 use http::StatusCode;
 
-use super::core::*;
-use super::error::parse_error;
-use super::error::parse_s3_error_code;
-use crate::raw::oio::BatchDeleteResult;
-use crate::raw::*;
-use crate::*;
+use crate::core::*;
+use crate::error::parse_error;
+use crate::error::parse_s3_error_code;
+use opendal_core::raw::oio::BatchDeleteResult;
+use opendal_core::raw::*;
+use opendal_core::*;
 
 pub struct S3Deleter {
     core: Arc<S3Core>,
diff --git a/core/core/src/services/s3/docs.md b/core/services/s3/src/docs.md
similarity index 97%
rename from core/core/src/services/s3/docs.md
rename to core/services/s3/src/docs.md
index 5a7ab9686..66c2b1d3f 100644
--- a/core/core/src/services/s3/docs.md
+++ b/core/services/s3/src/docs.md
@@ -81,7 +81,7 @@ Reference: [Protecting data using server-side 
encryption](https://docs.aws.amazo
 use std::sync::Arc;
 
 use anyhow::Result;
-use opendal_core::services::S3;
+use opendal_service_s3::S3;
 use opendal_core::Operator;
 
 #[tokio::main]
@@ -126,7 +126,7 @@ async fn main() -> Result<()> {
 ```rust,no_run
 use anyhow::Result;
 use log::info;
-use opendal_core::services::S3;
+use opendal_service_s3::S3;
 use opendal_core::Operator;
 
 #[tokio::main]
@@ -155,7 +155,7 @@ async fn main() -> Result<()> {
 ```rust,no_run
 use anyhow::Result;
 use log::info;
-use opendal_core::services::S3;
+use opendal_service_s3::S3;
 use opendal_core::Operator;
 
 #[tokio::main]
@@ -185,7 +185,7 @@ async fn main() -> Result<()> {
 ```rust,no_run
 use anyhow::Result;
 use log::info;
-use opendal_core::services::S3;
+use opendal_service_s3::S3;
 use opendal_core::Operator;
 
 #[tokio::main]
@@ -215,7 +215,7 @@ async fn main() -> Result<()> {
 ```rust,no_run
 use anyhow::Result;
 use log::info;
-use opendal_core::services::S3;
+use opendal_service_s3::S3;
 use opendal_core::Operator;
 
 #[tokio::main]
diff --git a/core/core/src/services/s3/error.rs b/core/services/s3/src/error.rs
similarity index 99%
rename from core/core/src/services/s3/error.rs
rename to core/services/s3/src/error.rs
index 3244abb44..da675c6de 100644
--- a/core/core/src/services/s3/error.rs
+++ b/core/services/s3/src/error.rs
@@ -21,8 +21,8 @@ use http::response::Parts;
 use quick_xml::de;
 use serde::Deserialize;
 
-use crate::raw::*;
-use crate::*;
+use opendal_core::raw::*;
+use opendal_core::*;
 
 /// S3Error is the error returned by s3 service.
 #[derive(Default, Debug, Deserialize, PartialEq, Eq)]
diff --git a/core/core/src/services/s3/mod.rs b/core/services/s3/src/lib.rs
similarity index 84%
copy from core/core/src/services/s3/mod.rs
copy to core/services/s3/src/lib.rs
index 432f8f45e..57b39c419 100644
--- a/core/core/src/services/s3/mod.rs
+++ b/core/services/s3/src/lib.rs
@@ -15,10 +15,9 @@
 // specific language governing permissions and limitations
 // under the License.
 
-/// Default scheme for s3 service.
-pub const S3_SCHEME: &str = "s3";
-
-use crate::types::DEFAULT_OPERATOR_REGISTRY;
+#![cfg_attr(docsrs, feature(doc_cfg))]
+//! Amazon S3 service implementation for Apache OpenDAL.
+#![deny(missing_docs)]
 
 mod backend;
 mod config;
@@ -31,7 +30,10 @@ mod writer;
 pub use backend::S3Builder as S3;
 pub use config::S3Config;
 
+/// Default scheme for s3 service.
+pub const S3_SCHEME: &str = "s3";
+
 #[ctor::ctor]
 fn register_s3_service() {
-    DEFAULT_OPERATOR_REGISTRY.register::<S3>(S3_SCHEME);
+    opendal_core::DEFAULT_OPERATOR_REGISTRY.register::<S3>(S3_SCHEME);
 }
diff --git a/core/core/src/services/s3/lister.rs 
b/core/services/s3/src/lister.rs
similarity index 94%
rename from core/core/src/services/s3/lister.rs
rename to core/services/s3/src/lister.rs
index ae15788ff..55a7c1821 100644
--- a/core/core/src/services/s3/lister.rs
+++ b/core/services/s3/src/lister.rs
@@ -20,14 +20,14 @@ use std::sync::Arc;
 use bytes::Buf;
 use quick_xml::de;
 
-use super::core::*;
-use super::error::parse_error;
-use crate::EntryMode;
-use crate::Error;
-use crate::Metadata;
-use crate::Result;
-use crate::raw::oio::PageContext;
-use crate::raw::*;
+use crate::core::*;
+use crate::error::parse_error;
+use opendal_core::EntryMode;
+use opendal_core::Error;
+use opendal_core::Metadata;
+use opendal_core::Result;
+use opendal_core::raw::oio::PageContext;
+use opendal_core::raw::*;
 
 pub type S3Listers = ThreeWays<
     oio::PageLister<S3ListerV1>,
@@ -138,7 +138,12 @@ impl oio::PageList for S3ListerV1 {
                 path = "/".to_string();
             }
 
-            let mut meta = Metadata::new(EntryMode::from_path(&path));
+            let mode = if path.ends_with('/') {
+                EntryMode::DIR
+            } else {
+                EntryMode::FILE
+            };
+            let mut meta = Metadata::new(mode);
             meta.set_is_current(true);
             if let Some(etag) = &object.etag {
                 meta.set_etag(etag);
@@ -248,7 +253,12 @@ impl oio::PageList for S3ListerV2 {
                 path = "/".to_string();
             }
 
-            let mut meta = Metadata::new(EntryMode::from_path(&path));
+            let mode = if path.ends_with('/') {
+                EntryMode::DIR
+            } else {
+                EntryMode::FILE
+            };
+            let mut meta = Metadata::new(mode);
             meta.set_is_current(true);
             if let Some(etag) = &object.etag {
                 meta.set_etag(etag);
@@ -364,7 +374,12 @@ impl oio::PageList for S3ObjectVersionsLister {
                 path = "/".to_owned();
             }
 
-            let mut meta = Metadata::new(EntryMode::from_path(&path));
+            let mode = if path.ends_with('/') {
+                EntryMode::DIR
+            } else {
+                EntryMode::FILE
+            };
+            let mut meta = Metadata::new(mode);
             meta.set_version(&version_object.version_id);
             meta.set_is_current(version_object.is_latest);
             meta.set_content_length(version_object.size);
diff --git a/core/core/src/services/s3/mod.rs b/core/services/s3/src/mod.rs
similarity index 96%
rename from core/core/src/services/s3/mod.rs
rename to core/services/s3/src/mod.rs
index 432f8f45e..5407b1653 100644
--- a/core/core/src/services/s3/mod.rs
+++ b/core/services/s3/src/mod.rs
@@ -18,7 +18,7 @@
 /// Default scheme for s3 service.
 pub const S3_SCHEME: &str = "s3";
 
-use crate::types::DEFAULT_OPERATOR_REGISTRY;
+use opendal_core::DEFAULT_OPERATOR_REGISTRY;
 
 mod backend;
 mod config;
diff --git a/core/core/src/services/s3/writer.rs 
b/core/services/s3/src/writer.rs
similarity index 93%
rename from core/core/src/services/s3/writer.rs
rename to core/services/s3/src/writer.rs
index bd73388fb..1732e0979 100644
--- a/core/core/src/services/s3/writer.rs
+++ b/core/services/s3/src/writer.rs
@@ -22,12 +22,12 @@ use constants::X_AMZ_OBJECT_SIZE;
 use constants::X_AMZ_VERSION_ID;
 use http::StatusCode;
 
-use super::core::*;
-use super::error::S3Error;
-use super::error::from_s3_error;
-use super::error::parse_error;
-use crate::raw::*;
-use crate::*;
+use crate::core::*;
+use crate::error::S3Error;
+use crate::error::from_s3_error;
+use crate::error::parse_error;
+use opendal_core::raw::*;
+use opendal_core::*;
 
 pub type S3Writers = TwoWays<oio::MultipartWriter<S3Writer>, 
oio::AppendWriter<S3Writer>>;
 
@@ -48,17 +48,22 @@ impl S3Writer {
     }
 
     fn parse_header_into_meta(path: &str, headers: &http::HeaderMap) -> 
Result<Metadata> {
-        let mut meta = Metadata::new(EntryMode::from_path(path));
+        let mode = if path.ends_with('/') {
+            EntryMode::DIR
+        } else {
+            EntryMode::FILE
+        };
+        let mut meta = Metadata::new(mode);
         if let Some(etag) = parse_etag(headers)? {
             meta.set_etag(etag);
         }
         if let Some(version) = parse_header_to_str(headers, X_AMZ_VERSION_ID)? 
{
             meta.set_version(version);
         }
-        if let Some(size) = parse_header_to_str(headers, X_AMZ_OBJECT_SIZE)? {
-            if let Ok(value) = size.parse() {
-                meta.set_content_length(value);
-            }
+        if let Some(value) =
+            parse_header_to_str(headers, X_AMZ_OBJECT_SIZE)?.and_then(|size| 
size.parse().ok())
+        {
+            meta.set_content_length(value);
         }
         Ok(meta)
     }
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 381539fb8..e02ba8c9c 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -19,7 +19,14 @@
     html_logo_url = 
"https://raw.githubusercontent.com/apache/opendal/main/website/static/img/logo.svg";
 )]
 #![cfg_attr(docsrs, feature(doc_cfg))]
-//! Facade crate that re-exports all public APIs from `opendal-core`.
+//! Facade crate that re-exports all public APIs from `opendal-core` and 
optional services/layers.
 #![deny(missing_docs)]
 
 pub use opendal_core::*;
+
+/// Re-export of service implementations.
+pub mod services {
+    pub use opendal_core::services::*;
+    #[cfg(feature = "services-s3")]
+    pub use opendal_service_s3::*;
+}

Reply via email to