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

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


The following commit(s) were added to refs/heads/main by this push:
     new e518d5b9e feat(services): add seafile support (#3771)
e518d5b9e is described below

commit e518d5b9eb3f0340b738501326d36f0c90217f73
Author: hoslo <[email protected]>
AuthorDate: Mon Dec 18 23:18:55 2023 +0800

    feat(services): add seafile support (#3771)
---
 .env.example                                |   6 +
 .github/services/seafile/seafile/action.yml |  45 +++
 .github/workflows/ci.yml                    |   1 +
 bindings/java/Cargo.toml                    |   2 +
 bindings/nodejs/Cargo.toml                  |   2 +
 bindings/python/Cargo.toml                  |   2 +
 core/Cargo.toml                             |   1 +
 core/src/raw/chrono_util.rs                 |   9 +
 core/src/raw/http_util/multipart.rs         |  40 ++-
 core/src/services/mod.rs                    |   7 +
 core/src/services/seafile/backend.rs        | 341 +++++++++++++++++++++++
 core/src/services/seafile/core.rs           | 417 ++++++++++++++++++++++++++++
 core/src/services/seafile/docs.md           |  56 ++++
 core/src/services/seafile/error.rs          |  94 +++++++
 core/src/services/seafile/lister.rs         | 119 ++++++++
 core/src/services/seafile/mod.rs            |  25 ++
 core/src/services/seafile/writer.rs         |  89 ++++++
 core/src/types/operator/builder.rs          |   2 +
 core/src/types/scheme.rs                    |   6 +
 fixtures/seafile/docker-compose-seafile.yml |  62 +++++
 20 files changed, 1312 insertions(+), 14 deletions(-)

diff --git a/.env.example b/.env.example
index 6277e05ae..f95a10298 100644
--- a/.env.example
+++ b/.env.example
@@ -180,3 +180,9 @@ OPENDAL_HUGGINGFACE_REPO_TYPE=dataset
 OPENDAL_HUGGINGFACE_REPO_ID=opendal/huggingface-testdata
 OPENDAL_HUGGINGFACE_REVISION=main
 OPENDAL_HUGGINGFACE_ROOT=/testdata/
+# seafile
+OPENDAL_SEAFILE_ROOT=/path/to/dir
+OPENDAL_SEAFILE_ENDPOINT=<endpoint>
+OPENDAL_SEAFILE_USERNAME=<usernmae>
+OPENDAL_SEAFILE_PASSWORD=<password>
+OPENDAL_SEAFILE_REPO_NAME=<repo_name>
\ No newline at end of file
diff --git a/.github/services/seafile/seafile/action.yml 
b/.github/services/seafile/seafile/action.yml
new file mode 100644
index 000000000..c504bc5b1
--- /dev/null
+++ b/.github/services/seafile/seafile/action.yml
@@ -0,0 +1,45 @@
+# 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.
+
+name: seafile
+description: "Behavior test for Seafile"
+
+runs:
+  using: "composite"
+  steps:
+    - name: Setup Seafile service
+      shell: bash
+      working-directory: fixtures/seafile
+      run: |
+        docker compose -f docker-compose-seafile.yml up -d --wait
+
+    - name: Create test token and setup test library
+      shell: bash
+      run: |
+        token=$(curl --location --request POST -d 
"[email protected]&password=asecret" 
http://127.0.0.1:80/api2/auth-token/ | awk -F '"' '/token/{print $4}')
+        curl --location --request POST -d 'name=test' -H "Authorization: Token 
$token" http://127.0.0.1:80/api2/repos/
+
+    - name: Set environment variables
+      shell: bash
+      run: |
+        cat << EOF >> $GITHUB_ENV
+        OPENDAL_SEAFILE_ENDPOINT=http://127.0.0.1:80
+        [email protected]
+        OPENDAL_SEAFILE_PASSWORD=asecret
+        OPENDAL_SEAFILE_REPO_NAME=test
+        OPENDAL_SEAFILE_ROOT=/
+        EOF
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 73b90e981..3d6d2c24c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -279,6 +279,7 @@ jobs:
             # TODO: we need to find ways to using pre-install rocksdb library
             # services-rocksdb
             services-s3
+            services-seafile
             # TODO: sftp is known to not work on windows, waiting for 
https://github.com/apache/incubator-opendal/issues/2963
             # services-sftp
             services-sled
diff --git a/bindings/java/Cargo.toml b/bindings/java/Cargo.toml
index b5e986235..de42caf06 100644
--- a/bindings/java/Cargo.toml
+++ b/bindings/java/Cargo.toml
@@ -90,6 +90,7 @@ services-all = [
   "services-swift",
   "services-alluxio",
   "services-b2",
+  "services-seafile",
 ]
 
 # Default services provided by opendal.
@@ -137,6 +138,7 @@ services-redis = ["opendal/services-redis"]
 services-rocksdb = ["opendal/services-rocksdb"]
 services-sftp = ["opendal/services-sftp"]
 services-sled = ["opendal/services-sled"]
+services-seafile = ["opendal/services-seafile"]
 services-sqlite = ["opendal/services-sqlite"]
 services-supabase = ["opendal/services-supabase"]
 services-swift = ["opendal/services-swift"]
diff --git a/bindings/nodejs/Cargo.toml b/bindings/nodejs/Cargo.toml
index 4a1a1f638..a812d69ec 100644
--- a/bindings/nodejs/Cargo.toml
+++ b/bindings/nodejs/Cargo.toml
@@ -85,6 +85,7 @@ services-all = [
   "services-libsql",
   "services-alluxio",
   "services-b2",
+  "services-seafile",
 ]
 
 # Default services provided by opendal.
@@ -133,6 +134,7 @@ services-rocksdb = ["opendal/services-rocksdb"]
 services-sftp = ["opendal/services-sftp"]
 services-sled = ["opendal/services-sled"]
 services-sqlite = ["opendal/services-sqlite"]
+services-seafile = ["opendal/services-seafile"]
 services-supabase = ["opendal/services-supabase"]
 services-swift = ["opendal/services-swift"]
 services-tikv = ["opendal/services-tikv"]
diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml
index beea4e11a..5de87a081 100644
--- a/bindings/python/Cargo.toml
+++ b/bindings/python/Cargo.toml
@@ -84,6 +84,7 @@ services-all = [
   "services-libsql",
   "services-alluxio",
   "services-b2",
+  "services-seafile",
 ]
 
 # Default services provided by opendal.
@@ -132,6 +133,7 @@ services-rocksdb = ["opendal/services-rocksdb"]
 services-sftp = ["opendal/services-sftp"]
 services-sled = ["opendal/services-sled"]
 services-sqlite = ["opendal/services-sqlite"]
+services-seafile = ["opendal/services-seafile"]
 services-supabase = ["opendal/services-supabase"]
 services-swift = ["opendal/services-swift"]
 services-tikv = ["opendal/services-tikv"]
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 301dc99e7..dac151d42 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -126,6 +126,7 @@ services-azdls = [
 ]
 services-azfile = []
 services-b2 = []
+services-seafile = []
 services-cacache = ["dep:cacache"]
 services-cloudflare-kv = []
 services-cos = [
diff --git a/core/src/raw/chrono_util.rs b/core/src/raw/chrono_util.rs
index 2039ee19e..b15a3f935 100644
--- a/core/src/raw/chrono_util.rs
+++ b/core/src/raw/chrono_util.rs
@@ -53,3 +53,12 @@ pub fn parse_datetime_from_from_timestamp_millis(s: i64) -> 
Result<DateTime<Utc>
 
     Ok(st.into())
 }
+
+/// parse datetime from given timestamp
+pub fn parse_datetime_from_from_timestamp(s: i64) -> Result<DateTime<Utc>> {
+    let st = UNIX_EPOCH
+        .checked_add(Duration::from_secs(s as u64))
+        .ok_or_else(|| Error::new(ErrorKind::Unexpected, "input timestamp 
overflow"))?;
+
+    Ok(st.into())
+}
diff --git a/core/src/raw/http_util/multipart.rs 
b/core/src/raw/http_util/multipart.rs
index a6c6f7652..55c502b9a 100644
--- a/core/src/raw/http_util/multipart.rs
+++ b/core/src/raw/http_util/multipart.rs
@@ -291,7 +291,19 @@ impl Part for FormDataPart {
 
         // Building pre-content.
         for (k, v) in self.headers.iter() {
-            bs.extend_from_slice(k.as_str().as_bytes());
+            // Trick!
+            //
+            // Seafile could not recognize header names like 
`content-disposition`
+            // and requires to use `Content-Disposition`. So we hardcode the 
part
+            // headers name here.
+            match k.as_str() {
+                "content-disposition" => {
+                    bs.extend_from_slice("Content-Disposition".as_bytes());
+                }
+                _ => {
+                    bs.extend_from_slice(k.as_str().as_bytes());
+                }
+            }
             bs.extend_from_slice(b": ");
             bs.extend_from_slice(v.as_bytes());
             bs.extend_from_slice(b"\r\n");
@@ -728,11 +740,11 @@ mod tests {
         assert_eq!(size, bs.len() as u64);
 
         let expected = "--lalala\r\n\
-             content-disposition: form-data; name=\"foo\"\r\n\
+             Content-Disposition: form-data; name=\"foo\"\r\n\
              \r\n\
              bar\r\n\
              --lalala\r\n\
-             content-disposition: form-data; name=\"hello\"\r\n\
+             Content-Disposition: form-data; name=\"hello\"\r\n\
              \r\n\
              world\r\n\
              --lalala--\r\n";
@@ -764,48 +776,48 @@ mod tests {
         assert_eq!(size, bs.len() as u64);
 
         let expected = r#"--9431149156168
-content-disposition: form-data; name="key"
+Content-Disposition: form-data; name="key"
 
 user/eric/MyPicture.jpg
 --9431149156168
-content-disposition: form-data; name="acl"
+Content-Disposition: form-data; name="acl"
 
 public-read
 --9431149156168
-content-disposition: form-data; name="success_action_redirect"
+Content-Disposition: form-data; name="success_action_redirect"
 
 https://awsexamplebucket1.s3.us-west-1.amazonaws.com/successful_upload.html
 --9431149156168
-content-disposition: form-data; name="content-type"
+Content-Disposition: form-data; name="content-type"
 
 image/jpeg
 --9431149156168
-content-disposition: form-data; name="x-amz-meta-uuid"
+Content-Disposition: form-data; name="x-amz-meta-uuid"
 
 14365123651274
 --9431149156168
-content-disposition: form-data; name="x-amz-meta-tag"
+Content-Disposition: form-data; name="x-amz-meta-tag"
 
 Some,Tag,For,Picture
 --9431149156168
-content-disposition: form-data; name="AWSAccessKeyId"
+Content-Disposition: form-data; name="AWSAccessKeyId"
 
 AKIAIOSFODNN7EXAMPLE
 --9431149156168
-content-disposition: form-data; name="Policy"
+Content-Disposition: form-data; name="Policy"
 
 
eyAiZXhwaXJhdGlvbiI6ICIyMDA3LTEyLTAxVDEyOjAwOjAwLjAwMFoiLAogICJjb25kaXRpb25zIjogWwogICAgeyJidWNrZXQiOiAiam9obnNtaXRoIn0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci9lcmljLyJdLAogICAgeyJhY2wiOiAicHVibGljLXJlYWQifSwKICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL2pvaG5zbWl0aC5zMy5hbWF6b25hd3MuY29tL3N1Y2Nlc3NmdWxfdXBsb2FkLmh0bWwifSwKICAgIFsic3RhcnRzLXdpdGgiLCAiJENvbnRlbnQtVHlwZSIsICJpbWFnZS8iXSwKICAgIHsieC1hbXotbWV0YS11dWlkIjogIjE0MzY1MTIzNjUxMjc0In0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiR4
 [...]
 --9431149156168
-content-disposition: form-data; name="Signature"
+Content-Disposition: form-data; name="Signature"
 
 0RavWzkygo6QX9caELEqKi9kDbU=
 --9431149156168
-content-disposition: form-data; name="file"
+Content-Disposition: form-data; name="file"
 content-type: image/jpeg
 
 ...file content...
 --9431149156168
-content-disposition: form-data; name="submit"
+Content-Disposition: form-data; name="submit"
 
 Upload to Amazon S3
 --9431149156168--
diff --git a/core/src/services/mod.rs b/core/src/services/mod.rs
index 0ae125537..086db17cd 100644
--- a/core/src/services/mod.rs
+++ b/core/src/services/mod.rs
@@ -305,3 +305,10 @@ mod b2;
 pub use b2::B2Config;
 #[cfg(feature = "services-b2")]
 pub use b2::B2;
+
+#[cfg(feature = "services-seafile")]
+mod seafile;
+#[cfg(feature = "services-seafile")]
+pub use seafile::Seafile;
+#[cfg(feature = "services-seafile")]
+pub use seafile::SeafileConfig;
diff --git a/core/src/services/seafile/backend.rs 
b/core/src/services/seafile/backend.rs
new file mode 100644
index 000000000..63f2f7122
--- /dev/null
+++ b/core/src/services/seafile/backend.rs
@@ -0,0 +1,341 @@
+// 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 async_trait::async_trait;
+use http::StatusCode;
+use log::debug;
+use serde::Deserialize;
+use std::collections::HashMap;
+use std::fmt::Debug;
+use std::fmt::Formatter;
+use std::sync::Arc;
+use tokio::sync::RwLock;
+
+use super::core::parse_dir_detail;
+use super::core::parse_file_detail;
+use super::core::SeafileCore;
+use super::error::parse_error;
+use super::lister::SeafileLister;
+use super::writer::SeafileWriter;
+use super::writer::SeafileWriters;
+use crate::raw::*;
+use crate::services::seafile::core::SeafileSigner;
+use crate::*;
+
+/// Config for backblaze seafile services support.
+#[derive(Default, Deserialize)]
+#[serde(default)]
+#[non_exhaustive]
+pub struct SeafileConfig {
+    /// root of this backend.
+    ///
+    /// All operations will happen under this root.
+    pub root: Option<String>,
+    /// endpoint address of this backend.
+    pub endpoint: Option<String>,
+    /// username of this backend.
+    pub username: Option<String>,
+    /// password of this backend.
+    pub password: Option<String>,
+    /// repo_name of this backend.
+    ///
+    /// required.
+    pub repo_name: String,
+}
+
+impl Debug for SeafileConfig {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        let mut d = f.debug_struct("SeafileConfig");
+
+        d.field("root", &self.root)
+            .field("endpoint", &self.endpoint)
+            .field("username", &self.username)
+            .field("repo_name", &self.repo_name);
+
+        d.finish_non_exhaustive()
+    }
+}
+
+/// [seafile](https://www.seafile.com) services support.
+#[doc = include_str!("docs.md")]
+#[derive(Default)]
+pub struct SeafileBuilder {
+    config: SeafileConfig,
+
+    http_client: Option<HttpClient>,
+}
+
+impl Debug for SeafileBuilder {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        let mut d = f.debug_struct("SeafileBuilder");
+
+        d.field("config", &self.config);
+        d.finish_non_exhaustive()
+    }
+}
+
+impl SeafileBuilder {
+    /// Set root of this backend.
+    ///
+    /// All operations will happen under this root.
+    pub fn root(&mut self, root: &str) -> &mut Self {
+        self.config.root = if root.is_empty() {
+            None
+        } else {
+            Some(root.to_string())
+        };
+
+        self
+    }
+
+    /// endpoint of this backend.
+    ///
+    /// It is required. e.g. `http://127.0.0.1:80`
+    pub fn endpoint(&mut self, endpoint: &str) -> &mut Self {
+        self.config.endpoint = if endpoint.is_empty() {
+            None
+        } else {
+            Some(endpoint.to_string())
+        };
+
+        self
+    }
+
+    /// username of this backend.
+    ///
+    /// It is required. e.g. `[email protected]`
+    pub fn username(&mut self, username: &str) -> &mut Self {
+        self.config.username = if username.is_empty() {
+            None
+        } else {
+            Some(username.to_string())
+        };
+
+        self
+    }
+
+    /// password of this backend.
+    ///
+    /// It is required. e.g. `asecret`
+    pub fn password(&mut self, password: &str) -> &mut Self {
+        self.config.password = if password.is_empty() {
+            None
+        } else {
+            Some(password.to_string())
+        };
+
+        self
+    }
+
+    /// Set repo name of this backend.
+    ///
+    /// It is required. e.g. `myrepo`
+    pub fn repo_name(&mut self, repo_name: &str) -> &mut Self {
+        self.config.repo_name = repo_name.to_string();
+
+        self
+    }
+
+    /// Specify the http client that used by this service.
+    ///
+    /// # Notes
+    ///
+    /// This API is part of OpenDAL's Raw API. `HttpClient` could be changed
+    /// during minor updates.
+    pub fn http_client(&mut self, client: HttpClient) -> &mut Self {
+        self.http_client = Some(client);
+        self
+    }
+}
+
+impl Builder for SeafileBuilder {
+    const SCHEME: Scheme = Scheme::Seafile;
+    type Accessor = SeafileBackend;
+
+    /// Converts a HashMap into an SeafileBuilder instance.
+    ///
+    /// # Arguments
+    ///
+    /// * `map` - A HashMap containing the configuration values.
+    ///
+    /// # Returns
+    ///
+    /// Returns an instance of SeafileBuilder.
+    fn from_map(map: HashMap<String, String>) -> Self {
+        // Deserialize the configuration from the HashMap.
+        let config = SeafileConfig::deserialize(ConfigDeserializer::new(map))
+            .expect("config deserialize must succeed");
+
+        // Create an SeafileBuilder instance with the deserialized config.
+        SeafileBuilder {
+            config,
+            http_client: None,
+        }
+    }
+
+    /// Builds the backend and returns the result of SeafileBackend.
+    fn build(&mut self) -> Result<Self::Accessor> {
+        debug!("backend build started: {:?}", &self);
+
+        let root = 
normalize_root(&self.config.root.clone().unwrap_or_default());
+        debug!("backend use root {}", &root);
+
+        // Handle bucket.
+        if self.config.repo_name.is_empty() {
+            return Err(Error::new(ErrorKind::ConfigInvalid, "repo_name is 
empty")
+                .with_operation("Builder::build")
+                .with_context("service", Scheme::Seafile));
+        }
+
+        debug!("backend use repo_name {}", &self.config.repo_name);
+
+        let endpoint = match &self.config.endpoint {
+            Some(endpoint) => Ok(endpoint.clone()),
+            None => Err(Error::new(ErrorKind::ConfigInvalid, "endpoint is 
empty")
+                .with_operation("Builder::build")
+                .with_context("service", Scheme::Seafile)),
+        }?;
+
+        let username = match &self.config.username {
+            Some(username) => Ok(username.clone()),
+            None => Err(Error::new(ErrorKind::ConfigInvalid, "username is 
empty")
+                .with_operation("Builder::build")
+                .with_context("service", Scheme::Seafile)),
+        }?;
+
+        let password = match &self.config.password {
+            Some(password) => Ok(password.clone()),
+            None => Err(Error::new(ErrorKind::ConfigInvalid, "password is 
empty")
+                .with_operation("Builder::build")
+                .with_context("service", Scheme::Seafile)),
+        }?;
+
+        let client = if let Some(client) = self.http_client.take() {
+            client
+        } else {
+            HttpClient::new().map_err(|err| {
+                err.with_operation("Builder::build")
+                    .with_context("service", Scheme::Seafile)
+            })?
+        };
+
+        Ok(SeafileBackend {
+            core: Arc::new(SeafileCore {
+                root,
+                endpoint,
+                username,
+                password,
+                repo_name: self.config.repo_name.clone(),
+                signer: Arc::new(RwLock::new(SeafileSigner::default())),
+                client,
+            }),
+        })
+    }
+}
+
+/// Backend for seafile services.
+#[derive(Debug, Clone)]
+pub struct SeafileBackend {
+    core: Arc<SeafileCore>,
+}
+
+#[async_trait]
+impl Accessor for SeafileBackend {
+    type Reader = IncomingAsyncBody;
+
+    type BlockingReader = ();
+
+    type Writer = SeafileWriters;
+
+    type BlockingWriter = ();
+
+    type Lister = oio::PageLister<SeafileLister>;
+
+    type BlockingLister = ();
+
+    fn info(&self) -> AccessorInfo {
+        let mut am = AccessorInfo::default();
+        am.set_scheme(Scheme::Seafile)
+            .set_root(&self.core.root)
+            .set_native_capability(Capability {
+                stat: true,
+
+                read: true,
+                read_can_next: true,
+
+                write: true,
+                write_can_empty: true,
+
+                delete: true,
+
+                list: true,
+
+                ..Default::default()
+            });
+
+        am
+    }
+
+    async fn read(&self, path: &str, _args: OpRead) -> Result<(RpRead, 
Self::Reader)> {
+        let resp = self.core.download_file(path).await?;
+
+        let status = resp.status();
+
+        match status {
+            StatusCode::OK => {
+                let size = parse_content_length(resp.headers())?;
+                Ok((RpRead::new().with_size(size), resp.into_body()))
+            }
+            _ => Err(parse_error(resp).await?),
+        }
+    }
+
+    async fn stat(&self, path: &str, _args: OpStat) -> Result<RpStat> {
+        if path == "/" {
+            return Ok(RpStat::new(Metadata::new(EntryMode::DIR)));
+        }
+
+        let metadata = if path.ends_with('/') {
+            let dir_detail = self.core.dir_detail(path).await?;
+            parse_dir_detail(dir_detail)
+        } else {
+            let file_detail = self.core.file_detail(path).await?;
+
+            parse_file_detail(file_detail)
+        };
+
+        metadata.map(RpStat::new)
+    }
+
+    async fn write(&self, path: &str, args: OpWrite) -> Result<(RpWrite, 
Self::Writer)> {
+        let w = SeafileWriter::new(self.core.clone(), args, path.to_string());
+        let w = oio::OneShotWriter::new(w);
+
+        Ok((RpWrite::default(), w))
+    }
+
+    async fn delete(&self, path: &str, _args: OpDelete) -> Result<RpDelete> {
+        let _ = self.core.delete(path).await?;
+
+        Ok(RpDelete::default())
+    }
+
+    async fn list(&self, path: &str, _args: OpList) -> Result<(RpList, 
Self::Lister)> {
+        let l = SeafileLister::new(self.core.clone(), path);
+        Ok((RpList::default(), oio::PageLister::new(l)))
+    }
+}
diff --git a/core/src/services/seafile/core.rs 
b/core/src/services/seafile/core.rs
new file mode 100644
index 000000000..a7cb8af7f
--- /dev/null
+++ b/core/src/services/seafile/core.rs
@@ -0,0 +1,417 @@
+// 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 bytes::Bytes;
+use http::header;
+use http::Request;
+use http::Response;
+use http::StatusCode;
+use serde::Deserialize;
+use std::sync::Arc;
+use tokio::sync::RwLock;
+
+use std::fmt::Debug;
+use std::fmt::Formatter;
+
+use crate::raw::*;
+use crate::*;
+
+use super::error::parse_error;
+
+/// Core of [seafile](https://www.seafile.com) services support.
+#[derive(Clone)]
+pub struct SeafileCore {
+    /// The root of this core.
+    pub root: String,
+    /// The endpoint of this backend.
+    pub endpoint: String,
+    /// The username of this backend.
+    pub username: String,
+    /// The password id of this backend.
+    pub password: String,
+    /// The repo name of this backend.
+    pub repo_name: String,
+
+    /// signer of this backend.
+    pub signer: Arc<RwLock<SeafileSigner>>,
+
+    pub client: HttpClient,
+}
+
+impl Debug for SeafileCore {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Backend")
+            .field("root", &self.root)
+            .field("endpoint", &self.endpoint)
+            .field("username", &self.username)
+            .field("repo_name", &self.repo_name)
+            .finish_non_exhaustive()
+    }
+}
+
+impl SeafileCore {
+    #[inline]
+    pub async fn send(&self, req: Request<AsyncBody>) -> 
Result<Response<IncomingAsyncBody>> {
+        self.client.send(req).await
+    }
+
+    /// get auth info
+    pub async fn get_auth_info(&self) -> Result<AuthInfo> {
+        {
+            let signer = self.signer.read().await;
+
+            if !signer.auth_info.token.is_empty() {
+                let auth_info = signer.auth_info.clone();
+                return Ok(auth_info.clone());
+            }
+        }
+
+        {
+            let mut signer = self.signer.write().await;
+            let body = format!(
+                "username={}&password={}",
+                percent_encode_path(&self.username),
+                percent_encode_path(&self.password)
+            );
+            let req = Request::post(format!("{}/api2/auth-token/", 
self.endpoint))
+                .header(header::CONTENT_TYPE, 
"application/x-www-form-urlencoded")
+                .body(AsyncBody::Bytes(Bytes::from(body)))
+                .map_err(new_request_build_error)?;
+
+            let resp = self.client.send(req).await?;
+            let status = resp.status();
+
+            match status {
+                StatusCode::OK => {
+                    let resp_body = &resp.into_body().bytes().await?;
+                    let auth_response = 
serde_json::from_slice::<AuthTokenResponse>(resp_body)
+                        .map_err(new_json_deserialize_error)?;
+                    signer.auth_info = AuthInfo {
+                        token: auth_response.token,
+                        repo_id: "".to_string(),
+                    };
+                }
+                _ => {
+                    return Err(parse_error(resp).await?);
+                }
+            }
+
+            let url = format!("{}/api2/repos", self.endpoint);
+
+            let req = Request::get(url)
+                .header(
+                    header::AUTHORIZATION,
+                    format!("Token {}", signer.auth_info.token),
+                )
+                .body(AsyncBody::Empty)
+                .map_err(new_request_build_error)?;
+
+            let resp = self.client.send(req).await?;
+
+            let status = resp.status();
+
+            match status {
+                StatusCode::OK => {
+                    let resp_body = &resp.into_body().bytes().await?;
+                    let list_library_response =
+                        
serde_json::from_slice::<Vec<ListLibraryResponse>>(resp_body)
+                            .map_err(new_json_deserialize_error)?;
+
+                    for library in list_library_response {
+                        if library.name == self.repo_name {
+                            signer.auth_info.repo_id = library.id;
+                            break;
+                        }
+                    }
+
+                    // repo not found
+                    if signer.auth_info.repo_id.is_empty() {
+                        return Err(Error::new(
+                            ErrorKind::NotFound,
+                            &format!("repo {} not found", self.repo_name),
+                        ));
+                    }
+                }
+                _ => {
+                    return Err(parse_error(resp).await?);
+                }
+            }
+            Ok(signer.auth_info.clone())
+        }
+    }
+}
+
+impl SeafileCore {
+    /// get upload url
+    pub async fn get_upload_url(&self) -> Result<String> {
+        let auth_info = self.get_auth_info().await?;
+
+        let req = Request::get(format!(
+            "{}/api2/repos/{}/upload-link/",
+            self.endpoint, auth_info.repo_id
+        ));
+
+        let req = req
+            .header(header::AUTHORIZATION, format!("Token {}", 
auth_info.token))
+            .body(AsyncBody::Empty)
+            .map_err(new_request_build_error)?;
+
+        let resp = self.send(req).await?;
+        let status = resp.status();
+
+        match status {
+            StatusCode::OK => {
+                let resp_body = &resp.into_body().bytes().await?;
+                let upload_url = serde_json::from_slice::<String>(resp_body)
+                    .map_err(new_json_deserialize_error)?;
+                Ok(upload_url)
+            }
+            _ => Err(parse_error(resp).await?),
+        }
+    }
+
+    /// get download
+    pub async fn get_download_url(&self, path: &str) -> Result<String> {
+        let path = build_abs_path(&self.root, path);
+        let path = percent_encode_path(&path);
+
+        let auth_info = self.get_auth_info().await?;
+
+        let req = Request::get(format!(
+            "{}/api2/repos/{}/file/?p={}",
+            self.endpoint, auth_info.repo_id, path
+        ));
+
+        let req = req
+            .header(header::AUTHORIZATION, format!("Token {}", 
auth_info.token))
+            .body(AsyncBody::Empty)
+            .map_err(new_request_build_error)?;
+
+        let resp = self.send(req).await?;
+        let status = resp.status();
+
+        match status {
+            StatusCode::OK => {
+                let resp_body = &resp.into_body().bytes().await?;
+                let download_url = serde_json::from_slice::<String>(resp_body)
+                    .map_err(new_json_deserialize_error)?;
+
+                Ok(download_url)
+            }
+            _ => Err(parse_error(resp).await?),
+        }
+    }
+
+    /// download file
+    pub async fn download_file(&self, path: &str) -> 
Result<Response<IncomingAsyncBody>> {
+        let download_url = self.get_download_url(path).await?;
+
+        let req = Request::get(download_url);
+
+        let req = req
+            .body(AsyncBody::Empty)
+            .map_err(new_request_build_error)?;
+
+        let resp = self.send(req).await?;
+        let status = resp.status();
+
+        match status {
+            StatusCode::OK => Ok(resp),
+            _ => Err(parse_error(resp).await?),
+        }
+    }
+
+    /// file detail
+    pub async fn file_detail(&self, path: &str) -> Result<FileDetail> {
+        let path = build_abs_path(&self.root, path);
+        let path = percent_encode_path(&path);
+
+        let auth_info = self.get_auth_info().await?;
+
+        let req = Request::get(format!(
+            "{}/api2/repos/{}/file/detail/?p={}",
+            self.endpoint, auth_info.repo_id, path
+        ));
+
+        let req = req
+            .header(header::AUTHORIZATION, format!("Token {}", 
auth_info.token))
+            .body(AsyncBody::Empty)
+            .map_err(new_request_build_error)?;
+
+        let resp = self.send(req).await?;
+        let status = resp.status();
+
+        match status {
+            StatusCode::OK => {
+                let resp_body = &resp.into_body().bytes().await?;
+                let file_detail = 
serde_json::from_slice::<FileDetail>(resp_body)
+                    .map_err(new_json_deserialize_error)?;
+                Ok(file_detail)
+            }
+            _ => Err(parse_error(resp).await?),
+        }
+    }
+
+    /// dir detail
+    pub async fn dir_detail(&self, path: &str) -> Result<DirDetail> {
+        let path = build_abs_path(&self.root, path);
+        let path = percent_encode_path(&path);
+
+        let auth_info = self.get_auth_info().await?;
+
+        let req = Request::get(format!(
+            "{}/api/v2.1/repos/{}/dir/detail/?path={}",
+            self.endpoint, auth_info.repo_id, path
+        ));
+
+        let req = req
+            .header(header::AUTHORIZATION, format!("Token {}", 
auth_info.token))
+            .body(AsyncBody::Empty)
+            .map_err(new_request_build_error)?;
+
+        let resp = self.send(req).await?;
+        let status = resp.status();
+
+        match status {
+            StatusCode::OK => {
+                let resp_body = &resp.into_body().bytes().await?;
+                let dir_detail = serde_json::from_slice::<DirDetail>(resp_body)
+                    .map_err(new_json_deserialize_error)?;
+                Ok(dir_detail)
+            }
+            _ => Err(parse_error(resp).await?),
+        }
+    }
+
+    /// create dir
+    pub async fn create_dir(&self, path: &str) -> Result<()> {
+        let path = build_abs_path(&self.root, path);
+        let path = format!("/{}", &path[..path.len() - 1]);
+        let path = percent_encode_path(&path);
+
+        let auth_info = self.get_auth_info().await?;
+
+        let req = Request::post(format!(
+            "{}/api2/repos/{}/dir/?p={}",
+            self.endpoint, auth_info.repo_id, path,
+        ));
+
+        let body = "operation=mkdir".to_string();
+
+        let req = req
+            .header(header::AUTHORIZATION, format!("Token {}", 
auth_info.token))
+            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
+            .body(AsyncBody::Bytes(Bytes::from(body)))
+            .map_err(new_request_build_error)?;
+
+        let resp = self.send(req).await?;
+        let status = resp.status();
+
+        match status {
+            StatusCode::CREATED => Ok(()),
+            _ => Err(parse_error(resp).await?),
+        }
+    }
+
+    /// delete file or dir
+    pub async fn delete(&self, path: &str) -> Result<()> {
+        let path = build_abs_path(&self.root, path);
+        let path = percent_encode_path(&path);
+
+        let auth_info = self.get_auth_info().await?;
+
+        let url = if path.ends_with('/') {
+            format!(
+                "{}/api2/repos/{}/dir/?p={}",
+                self.endpoint, auth_info.repo_id, path
+            )
+        } else {
+            format!(
+                "{}/api2/repos/{}/file/?p={}",
+                self.endpoint, auth_info.repo_id, path
+            )
+        };
+
+        let req = Request::delete(url);
+
+        let req = req
+            .header(header::AUTHORIZATION, format!("Token {}", 
auth_info.token))
+            .body(AsyncBody::Empty)
+            .map_err(new_request_build_error)?;
+
+        let resp = self.send(req).await?;
+
+        let status = resp.status();
+
+        match status {
+            StatusCode::OK => Ok(()),
+            _ => Err(parse_error(resp).await?),
+        }
+    }
+}
+
+#[derive(Deserialize)]
+pub struct AuthTokenResponse {
+    pub token: String,
+}
+
+#[derive(Deserialize)]
+pub struct FileDetail {
+    pub last_modified: String,
+    pub size: u64,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct DirDetail {
+    mtime: String,
+}
+
+pub fn parse_dir_detail(dir_detail: DirDetail) -> Result<Metadata> {
+    let mut md = Metadata::new(EntryMode::DIR);
+
+    md.set_last_modified(parse_datetime_from_rfc3339(&dir_detail.mtime)?);
+
+    Ok(md)
+}
+
+pub fn parse_file_detail(file_detail: FileDetail) -> Result<Metadata> {
+    let mut md = Metadata::new(EntryMode::FILE);
+
+    md.set_content_length(file_detail.size);
+    
md.set_last_modified(parse_datetime_from_rfc3339(&file_detail.last_modified)?);
+
+    Ok(md)
+}
+
+#[derive(Clone, Default)]
+pub struct SeafileSigner {
+    pub auth_info: AuthInfo,
+}
+
+#[derive(Clone, Default)]
+pub struct AuthInfo {
+    /// The repo id of this auth info.
+    pub repo_id: String,
+    /// The token of this auth info,
+    pub token: String,
+}
+
+#[derive(Deserialize)]
+pub struct ListLibraryResponse {
+    pub name: String,
+    pub id: String,
+}
diff --git a/core/src/services/seafile/docs.md 
b/core/src/services/seafile/docs.md
new file mode 100644
index 000000000..469040535
--- /dev/null
+++ b/core/src/services/seafile/docs.md
@@ -0,0 +1,56 @@
+## Capabilities
+
+This service can be used to:
+
+- [x] stat
+- [x] read
+- [x] write
+- [x] create_dir
+- [x] delete
+- [ ] copy
+- [ ] rename
+- [x] list
+- [x] scan
+- [ ] presign
+- [ ] blocking
+
+## Configuration
+
+- `root`: Set the work directory for backend
+- `endpoint`: Seafile endpoint address
+- `username` Seafile username
+- `password` Seafile password
+- `repo_name` Seafile repo name
+
+You can refer to [`SeafileBuilder`]'s docs for more information
+
+## Example
+
+### Via Builder
+
+```rust
+use anyhow::Result;
+use opendal::services::Seafile;
+use opendal::Operator;
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    // create backend builder
+    let mut builder = Seafile::default();
+
+    // set the storage bucket for OpenDAL
+    builder.root("/");
+    // set the endpoint for OpenDAL
+    builder.endpoint("http://127.0.0.1:80";);
+    // set the username for OpenDAL
+    builder.username("xxxxxxxxxx");
+    // set the password name for OpenDAL
+    builder.password("opendal");
+    // set the repo_name for OpenDAL
+    builder.repo_name("xxxxxxxxxxxxx");
+
+    let op: Operator = Operator::new(builder)?.finish();
+
+    Ok(())
+}
+```
diff --git a/core/src/services/seafile/error.rs 
b/core/src/services/seafile/error.rs
new file mode 100644
index 000000000..05a0fed99
--- /dev/null
+++ b/core/src/services/seafile/error.rs
@@ -0,0 +1,94 @@
+// 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 bytes::Buf;
+use http::Response;
+use serde::Deserialize;
+
+use crate::raw::*;
+use crate::Error;
+use crate::ErrorKind;
+use crate::Result;
+
+/// the error response of seafile
+#[derive(Default, Debug, Deserialize)]
+#[allow(dead_code)]
+struct SeafileError {
+    error_msg: String,
+}
+
+/// Parse error response into Error.
+pub async fn parse_error(resp: Response<IncomingAsyncBody>) -> Result<Error> {
+    let (parts, body) = resp.into_parts();
+    let bs = body.bytes().await?;
+
+    let (kind, _retryable) = match parts.status.as_u16() {
+        400 => (ErrorKind::InvalidInput, false),
+        403 => (ErrorKind::PermissionDenied, false),
+        404 => (ErrorKind::NotFound, false),
+        520 => (ErrorKind::Unexpected, false),
+        _ => (ErrorKind::Unexpected, false),
+    };
+
+    let (message, _seafile_err) = serde_json::from_reader::<_, 
SeafileError>(bs.clone().reader())
+        .map(|seafile_err| (format!("{seafile_err:?}"), Some(seafile_err)))
+        .unwrap_or_else(|_| (String::from_utf8_lossy(&bs).into_owned(), None));
+
+    let mut err = Error::new(kind, &message);
+
+    err = with_error_response_context(err, parts);
+
+    Ok(err)
+}
+
+#[cfg(test)]
+mod test {
+    use futures::stream;
+    use http::StatusCode;
+
+    use super::*;
+
+    #[tokio::test]
+    async fn test_parse_error() {
+        let err_res = vec![
+            (
+                r#"{"error_msg": "Permission denied"}"#,
+                ErrorKind::PermissionDenied,
+                StatusCode::FORBIDDEN,
+            ),
+            (
+                r#"{"error_msg": "Folder 
/e982e75a-fead-487c-9f41-63094d9bf0de/a9d867b9-778d-4612-b674-47e674c14c28/ not 
found."}"#,
+                ErrorKind::NotFound,
+                StatusCode::NOT_FOUND,
+            ),
+        ];
+
+        for res in err_res {
+            let bs = bytes::Bytes::from(res.0);
+            let body = IncomingAsyncBody::new(
+                Box::new(oio::into_stream(stream::iter(vec![Ok(bs.clone())]))),
+                None,
+            );
+            let resp = Response::builder().status(res.2).body(body).unwrap();
+
+            let err = parse_error(resp).await;
+
+            assert!(err.is_ok());
+            assert_eq!(err.unwrap().kind(), res.1);
+        }
+    }
+}
diff --git a/core/src/services/seafile/lister.rs 
b/core/src/services/seafile/lister.rs
new file mode 100644
index 000000000..c1ee6b947
--- /dev/null
+++ b/core/src/services/seafile/lister.rs
@@ -0,0 +1,119 @@
+// 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 std::sync::Arc;
+
+use async_trait::async_trait;
+use http::{header, Request, StatusCode};
+use serde::Deserialize;
+
+use super::core::SeafileCore;
+use super::error::parse_error;
+
+use crate::raw::oio::Entry;
+use crate::raw::*;
+use crate::*;
+
+pub struct SeafileLister {
+    core: Arc<SeafileCore>,
+
+    path: String,
+}
+
+impl SeafileLister {
+    pub(super) fn new(core: Arc<SeafileCore>, path: &str) -> Self {
+        SeafileLister {
+            core,
+            path: path.to_string(),
+        }
+    }
+}
+
+#[async_trait]
+impl oio::PageList for SeafileLister {
+    async fn next_page(&self, ctx: &mut oio::PageContext) -> Result<()> {
+        let path = build_rooted_abs_path(&self.core.root, &self.path);
+
+        let auth_info = self.core.get_auth_info().await?;
+
+        let url = format!(
+            "{}/api2/repos/{}/dir/?p={}",
+            self.core.endpoint,
+            auth_info.repo_id,
+            percent_encode_path(&path)
+        );
+
+        let req = Request::get(url);
+
+        let req = req
+            .header(header::AUTHORIZATION, format!("Token {}", 
auth_info.token))
+            .body(AsyncBody::Empty)
+            .map_err(new_request_build_error)?;
+
+        let resp = self.core.send(req).await?;
+
+        let status = resp.status();
+
+        match status {
+            StatusCode::OK => {
+                let resp_body = &resp.into_body().bytes().await?;
+                let infos = serde_json::from_slice::<Vec<Info>>(resp_body)
+                    .map_err(new_json_deserialize_error)?;
+
+                for info in infos {
+                    if !info.name.is_empty() {
+                        let rel_path =
+                            build_rel_path(&self.core.root, &format!("{}{}", 
path, info.name));
+
+                        let entry = if info.type_field == "file" {
+                            let meta = Metadata::new(EntryMode::FILE)
+                                
.with_last_modified(parse_datetime_from_from_timestamp(info.mtime)?)
+                                .with_content_length(info.size.unwrap_or(0));
+                            Entry::new(&rel_path, meta)
+                        } else {
+                            let path = format!("{}/", rel_path);
+                            Entry::new(&path, Metadata::new(EntryMode::DIR))
+                        };
+
+                        ctx.entries.push_back(entry);
+                    }
+                }
+
+                ctx.done = true;
+
+                Ok(())
+            }
+            // return nothing when not exist
+            StatusCode::NOT_FOUND => {
+                ctx.done = true;
+                Ok(())
+            }
+            _ => {
+                return Err(parse_error(resp).await?);
+            }
+        }
+    }
+}
+
+#[derive(Debug, Deserialize)]
+struct Info {
+    #[serde(rename = "type")]
+    pub type_field: String,
+    pub mtime: i64,
+    pub size: Option<u64>,
+    pub name: String,
+}
diff --git a/core/src/services/seafile/mod.rs b/core/src/services/seafile/mod.rs
new file mode 100644
index 000000000..dc9e1ccce
--- /dev/null
+++ b/core/src/services/seafile/mod.rs
@@ -0,0 +1,25 @@
+// 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.
+
+mod backend;
+pub use backend::SeafileBuilder as Seafile;
+pub use backend::SeafileConfig;
+
+mod core;
+mod error;
+mod lister;
+mod writer;
diff --git a/core/src/services/seafile/writer.rs 
b/core/src/services/seafile/writer.rs
new file mode 100644
index 000000000..8c7c0ce9d
--- /dev/null
+++ b/core/src/services/seafile/writer.rs
@@ -0,0 +1,89 @@
+// 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 std::sync::Arc;
+
+use async_trait::async_trait;
+use http::{header, Request, StatusCode};
+
+use super::core::SeafileCore;
+use super::error::parse_error;
+use crate::raw::*;
+use crate::*;
+
+pub type SeafileWriters = oio::OneShotWriter<SeafileWriter>;
+
+pub struct SeafileWriter {
+    core: Arc<SeafileCore>,
+    _op: OpWrite,
+    path: String,
+}
+
+impl SeafileWriter {
+    pub fn new(core: Arc<SeafileCore>, op: OpWrite, path: String) -> Self {
+        SeafileWriter {
+            core,
+            _op: op,
+            path,
+        }
+    }
+}
+
+#[async_trait]
+impl oio::OneShotWrite for SeafileWriter {
+    async fn write_once(&self, bs: &dyn oio::WriteBuf) -> Result<()> {
+        let path = build_abs_path(&self.core.root, &self.path);
+        let bs = 
oio::ChunkedBytes::from_vec(bs.vectored_bytes(bs.remaining()));
+
+        let upload_url = self.core.get_upload_url().await?;
+
+        let req = Request::post(upload_url);
+
+        let paths = path.split('/').collect::<Vec<&str>>();
+        let filename = paths[paths.len() - 1];
+        let relative_path = path.replace(filename, "");
+
+        let file_part = FormDataPart::new("file")
+            .header(
+                header::CONTENT_DISPOSITION,
+                format!("form-data; name=\"file\"; filename=\"{filename}\"")
+                    .parse()
+                    .unwrap(),
+            )
+            .stream(bs.len() as u64, Box::new(bs));
+
+        let multipart = Multipart::new()
+            .part(file_part)
+            .part(FormDataPart::new("parent_dir").content("/"))
+            
.part(FormDataPart::new("relative_path").content(relative_path.clone()))
+            .part(FormDataPart::new("replace").content("1"));
+
+        let req = multipart.apply(req)?;
+
+        let resp = self.core.send(req).await?;
+
+        let status = resp.status();
+
+        match status {
+            StatusCode::OK => {
+                resp.into_body().consume().await?;
+                Ok(())
+            }
+            _ => Err(parse_error(resp).await?),
+        }
+    }
+}
diff --git a/core/src/types/operator/builder.rs 
b/core/src/types/operator/builder.rs
index 785b86229..c5f1f27b2 100644
--- a/core/src/types/operator/builder.rs
+++ b/core/src/types/operator/builder.rs
@@ -229,6 +229,8 @@ impl Operator {
             Scheme::Rocksdb => 
Self::from_map::<services::Rocksdb>(map)?.finish(),
             #[cfg(feature = "services-s3")]
             Scheme::S3 => Self::from_map::<services::S3>(map)?.finish(),
+            #[cfg(feature = "services-seafile")]
+            Scheme::Seafile => 
Self::from_map::<services::Seafile>(map)?.finish(),
             #[cfg(feature = "services-sftp")]
             Scheme::Sftp => Self::from_map::<services::Sftp>(map)?.finish(),
             #[cfg(feature = "services-sled")]
diff --git a/core/src/types/scheme.rs b/core/src/types/scheme.rs
index bdc8630de..295de6a96 100644
--- a/core/src/types/scheme.rs
+++ b/core/src/types/scheme.rs
@@ -40,6 +40,8 @@ pub enum Scheme {
     Azdls,
     /// [B2][crate::services::B2]: Backblaze B2 Services.
     B2,
+    /// [Seafile][crate::services::Seafile]: Seafile Services.
+    Seafile,
     /// [cacache][crate::services::Cacache]: cacache backend support.
     Cacache,
     /// [cloudflare-kv][crate::services::CloudflareKv]: Cloudflare KV services.
@@ -239,6 +241,8 @@ impl Scheme {
             Scheme::Rocksdb,
             #[cfg(feature = "services-s3")]
             Scheme::S3,
+            #[cfg(feature = "services-seafile")]
+            Scheme::Seafile,
             #[cfg(feature = "services-sftp")]
             Scheme::Sftp,
             #[cfg(feature = "services-sled")]
@@ -326,6 +330,7 @@ impl FromStr for Scheme {
             "redis" => Ok(Scheme::Redis),
             "rocksdb" => Ok(Scheme::Rocksdb),
             "s3" => Ok(Scheme::S3),
+            "seafile" => Ok(Scheme::Seafile),
             "sftp" => Ok(Scheme::Sftp),
             "sled" => Ok(Scheme::Sled),
             "supabase" => Ok(Scheme::Supabase),
@@ -382,6 +387,7 @@ impl From<Scheme> for &'static str {
             Scheme::Redis => "redis",
             Scheme::Rocksdb => "rocksdb",
             Scheme::S3 => "s3",
+            Scheme::Seafile => "seafile",
             Scheme::Sftp => "sftp",
             Scheme::Sled => "sled",
             Scheme::Supabase => "supabase",
diff --git a/fixtures/seafile/docker-compose-seafile.yml 
b/fixtures/seafile/docker-compose-seafile.yml
new file mode 100644
index 000000000..dc883f6a9
--- /dev/null
+++ b/fixtures/seafile/docker-compose-seafile.yml
@@ -0,0 +1,62 @@
+# 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.
+
+version: "3.8"
+services:
+  db:
+    image: mariadb:10.11
+    container_name: seafile-mysql
+    environment:
+      - MYSQL_ROOT_PASSWORD=db_dev  # Requested, set the root's password of 
MySQL service.
+      - MYSQL_LOG_CONSOLE=true
+    networks:
+      - seafile-net
+
+  memcached:
+    image: memcached:1.6.18
+    container_name: seafile-memcached
+    entrypoint: memcached -m 256
+    networks:
+      - seafile-net
+          
+  seafile:
+    image: seafileltd/seafile-mc:latest
+    container_name: seafile
+    ports:
+      - "80:80"
+    healthcheck:
+      test: curl --fail http://127.0.0.1:80/ || exit 1
+      interval: 10s
+      timeout: 30s
+      retries: 5
+      start_period: 60s
+    environment:
+      - DB_HOST=db
+      - DB_ROOT_PASSWD=db_dev  # Requested, the value should be root's 
password of MySQL service.
+      - TIME_ZONE=Asia/Shanghai  # Optional, default is UTC. Should be 
uncomment and set to your local time zone.
+      - [email protected] # Specifies Seafile admin user, 
default is '[email protected]'.
+      - SEAFILE_ADMIN_PASSWORD=asecret     # Specifies Seafile admin password, 
default is 'asecret'.
+      - SEAFILE_SERVER_LETSENCRYPT=false   # Whether to use https or not.
+      - SEAFILE_SERVER_HOSTNAME=127.0.0.1:80 # Specifies your host name if 
https is enabled.
+    depends_on:
+      - db
+      - memcached
+    networks:
+      - seafile-net
+
+networks:
+  seafile-net:

Reply via email to