├── integrations ├── fuse3 │ ├── .gitignore │ ├── Cargo.toml │ ├── src │ │ ├── file.rs │ │ ├── lib.rs │ │ └── file_system.rs │ ├── README.md │ └── DEPENDENCIES.rust.tsv └── cloud_filter │ ├── .gitignore │ ├── tests │ └── behavior │ │ ├── README.md │ │ ├── fetch_placeholder.rs │ │ ├── fetch_data.rs │ │ ├── utils.rs │ │ └── main.rs │ ├── src │ ├── file.rs │ └── lib.rs │ ├── Cargo.toml │ ├── examples │ └── readonly.rs │ ├── README.md │ └── DEPENDENCIES.rust.tsv ├── DEPENDENCIES.md ├── .gitignore ├── NOTICE ├── licenserc.toml ├── rust-toolchain.toml ├── rustfmt.toml ├── .github ├── pull_request_template.md ├── ISSUE_TEMPLATE │ ├── 3-new-release.md │ ├── config.yml │ ├── 2-feature-request.yml │ └── 1-bug-report.yml ├── workflows │ └── ci.yml └── actions │ └── setup │ └── action.yml ├── .asf.yaml ├── Cargo.toml ├── README.md ├── tests ├── path.rs ├── file.rs └── common │ └── mod.rs ├── src └── main.rs ├── DEPENDENCIES.rust.tsv └── LICENSE /integrations/fuse3/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | -------------------------------------------------------------------------------- /integrations/cloud_filter/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | -------------------------------------------------------------------------------- /DEPENDENCIES.md: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | 3 | Refer to [DEPENDENCIES.rust.tsv](DEPENDENCIES.rust.tsv) for full list. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | /target 3 | 4 | # Editor or OS clutter 5 | .DS_Store 6 | 7 | # Backup files created by rustfmt or editors 8 | **/*.rs.bk 9 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Apache OpenDAL ofs 2 | Copyright 2025 The Apache Software Foundation 3 | 4 | This product includes software developed at 5 | The Apache Software Foundation (http://www.apache.org/). 6 | -------------------------------------------------------------------------------- /integrations/cloud_filter/tests/behavior/README.md: -------------------------------------------------------------------------------- 1 | # Behavior tests for OpenDAL™ Cloud Filter Integration 2 | 3 | Behavior tests are used to make sure every service works correctly. 4 | 5 | `cloud_filter_opendal` is readonly currently, so we assume `fixtures/data` is the root of the test data. 6 | 7 | ## Run 8 | 9 | ```pwsh 10 | cd .\integrations\cloud_filter 11 | $env:OPENDAL_TEST='fs'; $env:OPENDAL_FS_ROOT='../../fixtures/data'; $env:OPENDAL_DISABLE_RANDOM_ROOT='true'; cargo test --test behavior 12 | ``` 13 | -------------------------------------------------------------------------------- /licenserc.toml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | headerPath = "Apache-2.0-ASF.txt" 19 | 20 | includes = ['**/*.rs', '**/*.yml', '**/*.yaml', '**/*.toml'] 21 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | [toolchain] 19 | channel = "stable" 20 | components = ["cargo", "rustfmt", "clippy", "rust-analyzer"] 21 | -------------------------------------------------------------------------------- /integrations/cloud_filter/src/file.rs: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The ASF licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | use serde::{Deserialize, Serialize}; 19 | 20 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 21 | pub struct FileBlob { 22 | pub etag: Option, 23 | pub md5: Option, 24 | } 25 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | edition = "2024" 19 | reorder_imports = true 20 | 21 | # format_code_in_doc_comments = true 22 | # group_imports = "StdExternalCrate" 23 | # imports_granularity = "Item" 24 | # overflow_delimited_expr = true 25 | # trailing_comma = "Vertical" 26 | # where_single_line = true 27 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Which issue does this PR close? 2 | 3 | 6 | 7 | Closes #. 8 | 9 | # Rationale for this change 10 | 11 | 15 | 16 | # What changes are included in this PR? 17 | 18 | 21 | 22 | # Are there any user-facing changes? 23 | 24 | 25 | 28 | 29 | 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-new-release.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Release 3 | about: Use this template for start making a new release 4 | title: "Tracking issues of Apache OpenDAL ofs (opendal-ofs) ${opendal_ofs_version} Release" 5 | --- 6 | 7 | This issue is used to track tasks of the Apache OpenDAL ofs (opendal-ofs) ${opendal_ofs_version} release. 8 | 9 | ## Tasks 10 | 11 | ### Blockers 12 | 13 | 14 | 15 | ### Build Release 16 | 17 | #### GitHub Side 18 | 19 | - [ ] Bump version in project 20 | - [ ] Update docs 21 | - [ ] Generate dependencies list 22 | - [ ] Push release candidate tag to GitHub 23 | 24 | #### ASF Side 25 | 26 | - [ ] Create an ASF Release 27 | - [ ] Upload artifacts to the SVN dist repo 28 | - [ ] Close the Nexus staging repo 29 | 30 | ### Voting 31 | 32 | - [ ] Start VOTE at the Apache OpenDAL community 33 | 34 | ### Official Release 35 | 36 | - [ ] Push the release git tag 37 | - [ ] Publish artifacts to SVN RELEASE branch 38 | - [ ] Release Maven artifacts 39 | - [ ] Send the announcement 40 | 41 | For details of each step, please refer to: https://opendal.apache.org/community/release/ 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | blank_issues_enabled: true 19 | contact_links: 20 | - name: Question 21 | url: https://github.com/apache/opendal-ofs/discussions/new?category=q-a 22 | about: Ask questions about Apache OpenDAL ofs (opendal-ofs) usage and development. 23 | - name: Discord 24 | url: https://opendal.apache.org/discord 25 | about: Join the Apache OpenDAL Discord for real-time discussions about opendal-ofs. 26 | -------------------------------------------------------------------------------- /integrations/cloud_filter/tests/behavior/fetch_placeholder.rs: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The ASF licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | use libtest_mimic::Failed; 19 | 20 | use crate::{ROOT_PATH, utils::list}; 21 | 22 | pub fn test_fetch_placeholder() -> Result<(), Failed> { 23 | let files = ["normal_file.txt", "special_file !@#$%^&()_+-=;',.txt"]; 24 | let dirs = ["normal_dir", "special_dir !@#$%^&()_+-=;',"]; 25 | 26 | assert_eq!( 27 | list(ROOT_PATH, "File").expect("list files"), 28 | files, 29 | "list files" 30 | ); 31 | assert_eq!( 32 | list(ROOT_PATH, "Directory").expect("list dirs"), 33 | dirs, 34 | "list dirs" 35 | ); 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /integrations/fuse3/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | [package] 19 | description = "fuse3 integration for Apache OpenDAL" 20 | name = "fuse3_opendal" 21 | 22 | authors = ["Apache OpenDAL "] 23 | edition = "2024" 24 | homepage = "https://opendal.apache.org/" 25 | license = "Apache-2.0" 26 | repository = "https://github.com/apache/opendal" 27 | rust-version = "1.85" 28 | version = "0.0.19" 29 | 30 | [dependencies] 31 | bytes = "1.6.0" 32 | fuse3 = { version = "0.8.1", "features" = ["tokio-runtime", "unprivileged"] } 33 | futures-util = "0.3.30" 34 | libc = "0.2.155" 35 | log = "0.4.21" 36 | opendal = { version = "0.54.0" } 37 | sharded-slab = "0.1.7" 38 | tokio = "1.38.0" 39 | 40 | [dev-dependencies] 41 | -------------------------------------------------------------------------------- /integrations/cloud_filter/tests/behavior/fetch_data.rs: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The ASF licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | use libtest_mimic::Failed; 19 | 20 | use crate::{ 21 | ROOT_PATH, 22 | utils::{file_content, file_length}, 23 | }; 24 | 25 | pub fn test_fetch_data() -> Result<(), Failed> { 26 | let files = [ 27 | ( 28 | "normal_file.txt", 29 | include_str!("..\\..\\..\\..\\fixtures/data/normal_file.txt"), 30 | ), 31 | ( 32 | "special_file !@#$%^&()_+-=;',.txt", 33 | include_str!("..\\..\\..\\..\\fixtures/data/special_file !@#$%^&()_+-=;',.txt"), 34 | ), 35 | ]; 36 | for (file, expected_content) in files { 37 | let path = format!("{ROOT_PATH}\\{file}"); 38 | assert_eq!( 39 | expected_content.len(), 40 | file_length(&path).expect("file length"), 41 | "file length", 42 | ); 43 | 44 | assert_eq!( 45 | expected_content, 46 | file_content(&path).expect("file content"), 47 | "file content", 48 | ) 49 | } 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | name: Ofs CI 19 | 20 | on: 21 | push: 22 | branches: 23 | - main 24 | pull_request: 25 | branches: 26 | - main 27 | paths: 28 | - "src/**" 29 | - ".github/workflows/ci.yml" 30 | 31 | concurrency: 32 | group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} 33 | cancel-in-progress: true 34 | 35 | jobs: 36 | check_clippy_and_test: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v5 40 | 41 | - name: Setup Rust toolchain 42 | uses: ./.github/actions/setup 43 | with: 44 | need-rocksdb: true 45 | need-protoc: true 46 | github-token: ${{ secrets.GITHUB_TOKEN }} 47 | - name: Run sccache-cache 48 | uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad 49 | - name: Cargo clippy && test 50 | env: 51 | SCCACHE_GHA_ENABLED: "true" 52 | RUSTC_WRAPPER: "sccache" 53 | run: | 54 | cargo clippy --all-targets --all-features -- -D warnings 55 | cargo test --all-targets --all-features 56 | -------------------------------------------------------------------------------- /integrations/cloud_filter/tests/behavior/utils.rs: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The ASF licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | use std::fmt::Display; 19 | 20 | use anyhow::Context; 21 | 22 | pub fn file_length(path: impl Display) -> anyhow::Result { 23 | let len = powershell_script::run(&format!("(Get-Item \"{path}\" -Force).Length")) 24 | .context("run powershell")? 25 | .stdout() 26 | .unwrap_or_default() 27 | .trim() 28 | .parse() 29 | .context("parse length")?; 30 | 31 | Ok(len) 32 | } 33 | 34 | pub fn file_content(path: impl Display) -> anyhow::Result { 35 | let content = powershell_script::run(&format!("Get-Content \"{path}\"")) 36 | .context("run powershell")? 37 | .stdout() 38 | .unwrap_or_default(); 39 | Ok(content) 40 | } 41 | 42 | pub fn list(path: impl Display, option: impl Display) -> anyhow::Result> { 43 | let entries = powershell_script::run(&format!("(Get-ChildItem \"{path}\" -{option}).Name")) 44 | .context("run powershell")? 45 | .stdout() 46 | .unwrap_or_default() 47 | .lines() 48 | .map(Into::into) 49 | .collect(); 50 | Ok(entries) 51 | } 52 | -------------------------------------------------------------------------------- /.asf.yaml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | # All configurations could be found here: https://github.com/apache/infrastructure-asfyaml/ 19 | 20 | github: 21 | description: "A userspace filesystem backing by Apache OpenDAL." 22 | homepage: https://opendal.apache.org 23 | labels: 24 | - rust 25 | - storage 26 | - s3 27 | - fs 28 | - cli 29 | features: 30 | issues: true 31 | discussions: true 32 | enabled_merge_buttons: 33 | squash: true 34 | merge: false 35 | rebase: false 36 | protected_branches: 37 | main: 38 | required_pull_request_reviews: 39 | required_approving_review_count: 1 40 | custom_subjects: 41 | new_discussion: "{title}" 42 | edit_discussion: "Re: {title}" 43 | close_discussion: "Re: {title}" 44 | close_discussion_with_comment: "Re: {title}" 45 | reopen_discussion: "Re: {title}" 46 | new_comment_discussion: "Re: {title}" 47 | edit_comment_discussion: "Re: {title}" 48 | delete_comment_discussion: "Re: {title}" 49 | 50 | notifications: 51 | commits: commits@opendal.apache.org 52 | issues: commits@opendal.apache.org 53 | pullrequests: commits@opendal.apache.org 54 | discussions: dev@opendal.apache.org 55 | 56 | -------------------------------------------------------------------------------- /integrations/cloud_filter/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | [package] 19 | authors = ["Apache OpenDAL "] 20 | description = "Cloud Filter Integration for Apache OpenDAL" 21 | edition = "2024" 22 | homepage = "https://opendal.apache.org/" 23 | license = "Apache-2.0" 24 | name = "cloud_filter_opendal" 25 | repository = "https://github.com/apache/opendal" 26 | rust-version = "1.85" 27 | version = "0.0.12" 28 | 29 | [package.metadata.docs.rs] 30 | default-target = "x86_64-pc-windows-msvc" 31 | 32 | [dependencies] 33 | anyhow = "1.0.86" 34 | bincode = "1.3.3" 35 | cloud-filter = "0.0.6" 36 | futures = "0.3.30" 37 | log = "0.4.17" 38 | opendal = { version = "0.54.0" } 39 | serde = { version = "1.0.203", features = ["derive"] } 40 | 41 | [dev-dependencies] 42 | libtest-mimic = { version = "0.8.1" } 43 | logforth = { version = "0.23.1", default-features = false } 44 | opendal = { version = "0.54.0", features = [ 45 | "services-fs", 46 | "tests", 47 | ] } 48 | powershell_script = "1.1.0" 49 | tokio = { version = "1.38.0", features = [ 50 | "macros", 51 | "rt-multi-thread", 52 | "signal", 53 | ] } 54 | 55 | [[test]] 56 | harness = false 57 | name = "behavior" 58 | path = "tests/behavior/main.rs" 59 | -------------------------------------------------------------------------------- /integrations/fuse3/src/file.rs: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The ASF licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | use std::ffi::OsString; 19 | use std::sync::Arc; 20 | 21 | use fuse3::Errno; 22 | use opendal::Writer; 23 | use tokio::sync::Mutex; 24 | 25 | /// Opened file represents file that opened in memory. 26 | /// 27 | /// # FIXME 28 | /// 29 | /// We should remove the `pub` filed to avoid unexpected changes. 30 | pub struct OpenedFile { 31 | pub path: OsString, 32 | pub is_read: bool, 33 | pub inner_writer: Option>>, 34 | } 35 | 36 | /// # FIXME 37 | /// 38 | /// We need better naming and API for this struct. 39 | pub struct InnerWriter { 40 | pub writer: Writer, 41 | pub written: u64, 42 | } 43 | 44 | /// File key is the key of opened file. 45 | /// 46 | /// # FIXME 47 | /// 48 | /// We should remove the `pub` filed to avoid unexpected changes. 49 | #[derive(Debug, Clone, Copy)] 50 | pub struct FileKey(pub usize); 51 | 52 | impl TryFrom for FileKey { 53 | type Error = Errno; 54 | 55 | fn try_from(value: u64) -> std::result::Result { 56 | match value { 57 | 0 => Err(Errno::from(libc::EBADF)), 58 | _ => Ok(FileKey(value as usize - 1)), 59 | } 60 | } 61 | } 62 | 63 | impl FileKey { 64 | pub fn to_fh(self) -> u64 { 65 | self.0 as u64 + 1 // ensure fh is not 0 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /integrations/fuse3/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The ASF licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | //! `fuse3_opendal` is an [`fuse3`](https://github.com/Sherlock-Holo/fuse3) implementation using opendal. 19 | //! 20 | //! This crate can help you to access ANY storage services by mounting locally by [`FUSE`](https://www.kernel.org/doc/html/next/filesystems/fuse.html). 21 | //! 22 | //! ``` 23 | //! use fuse3::path::Session; 24 | //! use fuse3::MountOptions; 25 | //! use fuse3::Result; 26 | //! use fuse3_opendal::Filesystem; 27 | //! use opendal::services::Memory; 28 | //! use opendal::Operator; 29 | //! 30 | //! #[tokio::test] 31 | //! async fn test() -> Result<()> { 32 | //! // Build opendal Operator. 33 | //! let op = Operator::new(Memory::default())?.finish(); 34 | //! 35 | //! // Build fuse3 file system. 36 | //! let fs = Filesystem::new(op, 1000, 1000); 37 | //! 38 | //! // Configure mount options. 39 | //! let mount_options = MountOptions::default(); 40 | //! 41 | //! // Start a fuse3 session and mount it. 42 | //! let mut mount_handle = Session::new(mount_options) 43 | //! .mount_with_unprivileged(fs, "/tmp/mount_test") 44 | //! .await?; 45 | //! let handle = &mut mount_handle; 46 | //! 47 | //! tokio::select! { 48 | //! res = handle => res?, 49 | //! _ = tokio::signal::ctrl_c() => { 50 | //! mount_handle.unmount().await? 51 | //! } 52 | //! } 53 | //! 54 | //! Ok(()) 55 | //! } 56 | //! ``` 57 | 58 | mod file; 59 | mod file_system; 60 | pub use file_system::Filesystem; 61 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | [package] 19 | categories = ["filesystem"] 20 | description = "OpenDAL File System" 21 | keywords = ["storage", "data", "s3", "fs", "azblob"] 22 | name = "ofs" 23 | version = "0.0.24" 24 | 25 | authors = ["Apache OpenDAL "] 26 | edition = "2024" 27 | homepage = "https://opendal.apache.org/" 28 | license = "Apache-2.0" 29 | repository = "https://github.com/apache/opendal" 30 | rust-version = "1.85" 31 | 32 | [dependencies] 33 | anyhow = { version = "1" } 34 | clap = { version = "4.5.40", features = ["derive", "env"] } 35 | log = { version = "0.4.22" } 36 | logforth = { version = "0.28.1", features = ["starter-log"] } 37 | opendal = { version = "0.54.0" } 38 | tokio = { version = "1.47.0", features = [ 39 | "fs", 40 | "macros", 41 | "rt-multi-thread", 42 | "io-std", 43 | "signal", 44 | ] } 45 | url = { version = "2.5.4" } 46 | 47 | [target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies] 48 | fuse3 = { version = "0.8.1", "features" = ["tokio-runtime", "unprivileged"] } 49 | fuse3_opendal = { version = "0.0.19", path = "integrations/fuse3" } 50 | libc = "0.2.154" 51 | nix = { version = "0.30.1", features = ["user"] } 52 | 53 | [target.'cfg(target_os = "windows")'.dependencies] 54 | cloud-filter = { version = "0.0.6" } 55 | cloud_filter_opendal = { version = "0.0.12", path = "integrations/cloud_filter" } 56 | 57 | [features] 58 | default = ["services-fs", "services-s3"] 59 | services-fs = ["opendal/services-fs"] 60 | services-s3 = ["opendal/services-s3"] 61 | 62 | [dev-dependencies] 63 | opendal = { version = "0.54.0", features = ["tests"] } 64 | tempfile = "3.23.0" 65 | test-context = "0.4.1" 66 | walkdir = "2.5.0" 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apache OpenDAL™ ofs 2 | 3 | [![Build Status]][actions] [![Latest Version]][crates.io] [![Crate Downloads]][crates.io] [![chat]][discord] 4 | 5 | [build status]: https://img.shields.io/github/actions/workflow/status/apache/opendal-ofs/ci.yml?branch=main 6 | [actions]: https://github.com/apache/opendal-ofs/actions?query=branch%3Amain 7 | [latest version]: https://img.shields.io/crates/v/ofs.svg 8 | [crates.io]: https://crates.io/crates/ofs 9 | [crate downloads]: https://img.shields.io/crates/d/ofs.svg 10 | [chat]: https://img.shields.io/discord/1081052318650339399 11 | [discord]: https://opendal.apache.org/discord 12 | 13 | `ofs` is a userspace filesystem backing by OpenDAL. 14 | 15 | ## Status 16 | 17 | `ofs` is a work in progress. we only support `fs` and `s3` as backend on `Linux` currently. 18 | 19 | ## How to use `ofs` 20 | 21 | ### Install `FUSE` on Linux 22 | 23 | ```shell 24 | sudo pacman -S fuse3 --noconfirm # archlinux 25 | sudo apt-get -y install fuse3 # debian/ubuntu 26 | ``` 27 | 28 | ### Load `FUSE` kernel module on FreeBSD 29 | 30 | ```shell 31 | kldload fuse 32 | ``` 33 | 34 | ### Install `ofs` 35 | 36 | `ofs` could be installed by `cargo`: 37 | 38 | ```shell 39 | cargo install ofs 40 | ``` 41 | 42 | > `cargo` is the Rust package manager. `cargo` could be installed by following the [Installation](https://www.rust-lang.org/tools/install) from Rust official website. 43 | 44 | ### Mount directory 45 | 46 | ```shell 47 | ofs 'fs://?root=' 48 | ``` 49 | 50 | ### Mount S3 bucket 51 | 52 | ```shell 53 | ofs 's3://?root=&bucket=&endpoint=®ion=&access_key_id=&secret_access_key=' 54 | ``` 55 | 56 | ## Branding 57 | 58 | The first and most prominent mentions must use the full form: **Apache OpenDAL™** of the name for any individual usage (webpage, handout, slides, etc.) Depending on the context and writing style, you should use the full form of the name sufficiently often to ensure that readers clearly understand the association of both the OpenDAL project and the OpenDAL software product to the ASF as the parent organization. 59 | 60 | For more details, see the [Apache Product Name Usage Guide](https://www.apache.org/foundation/marks/guide). 61 | 62 | ## License and Trademarks 63 | 64 | Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 65 | 66 | Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or trademarks of the Apache Software Foundation. 67 | -------------------------------------------------------------------------------- /tests/path.rs: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The ASF licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | #![cfg(any(target_os = "linux", target_os = "freebsd"))] 19 | 20 | mod common; 21 | 22 | use std::fs; 23 | 24 | use common::OfsTestContext; 25 | use test_context::test_context; 26 | use walkdir::WalkDir; 27 | 28 | #[test_context(OfsTestContext)] 29 | #[test] 30 | fn test_path(ctx: &mut OfsTestContext) { 31 | let actual_entries = [ 32 | ("dir1", false), 33 | ("dir2", false), 34 | ("dir3", false), 35 | ("dir3/dir4", false), 36 | ("dir3/dir5", false), 37 | ("dir3/file3", true), 38 | ("dir3/file4", true), 39 | ("file1", true), 40 | ("file2", true), 41 | ] 42 | .into_iter() 43 | .map(|(x, y)| (x.to_string(), y)) 44 | .collect::>(); 45 | 46 | for (path, is_file) in actual_entries.iter() { 47 | let path = ctx.mount_point.path().join(path); 48 | match is_file { 49 | true => fs::write(path, "hello").unwrap(), 50 | false => fs::create_dir(path).unwrap(), 51 | } 52 | } 53 | 54 | assert_eq!( 55 | actual_entries, 56 | WalkDir::new(ctx.mount_point.path()) 57 | .min_depth(1) 58 | .max_depth(2) 59 | .sort_by_file_name() 60 | .into_iter() 61 | .map(|x| { 62 | let x = x.unwrap(); 63 | ( 64 | x.path() 65 | .strip_prefix(&ctx.mount_point) 66 | .unwrap() 67 | .to_str() 68 | .unwrap() 69 | .to_string(), 70 | x.file_type().is_file(), 71 | ) 72 | }) 73 | .collect::>() 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | name: Feature Request 19 | description: Suggest an idea for this project 20 | title: "new feature: " 21 | labels: ["enhancement"] 22 | body: 23 | - type: markdown 24 | attributes: 25 | value: "Thank you for suggesting a new feature. Please fill out the details below to help us understand your idea better." 26 | 27 | - type: textarea 28 | id: feature-description 29 | attributes: 30 | label: Feature Description 31 | description: "A detailed description of the feature you would like to see." 32 | placeholder: "Describe the feature you'd like..." 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | id: problem-solution 38 | attributes: 39 | label: Problem and Solution 40 | description: "Describe the problem that this feature would solve. Explain how you envision it working." 41 | placeholder: "What problem does this feature solve? How do you envision it working?" 42 | validations: 43 | required: true 44 | 45 | - type: textarea 46 | id: additional-context 47 | attributes: 48 | label: Additional Context 49 | description: "Add any other context or screenshots about the feature request here." 50 | placeholder: "Add any other context or screenshots about the feature request here." 51 | validations: 52 | required: false 53 | 54 | - type: checkboxes 55 | id: willing-to-contribute 56 | attributes: 57 | label: "Are you willing to contribute to the development of this feature?" 58 | description: "Let us know if you are willing to help by contributing code or other resources." 59 | options: 60 | - label: "Yes, I am willing to contribute to the development of this feature." 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug-report.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | name: Bug Report 19 | description: Create a report to help us improve 20 | title: "bug: " 21 | labels: ["bug"] 22 | body: 23 | - type: markdown 24 | attributes: 25 | value: "Thank you for taking the time to report a bug. Please provide as much information as possible to help us understand and resolve the issue." 26 | 27 | - type: textarea 28 | id: describe-bug 29 | attributes: 30 | label: Describe the bug 31 | description: "A clear and concise description of what the bug is." 32 | placeholder: "Describe the bug..." 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | id: steps-to-reproduce 38 | attributes: 39 | label: Steps to Reproduce 40 | description: "Steps to reproduce the behavior:" 41 | validations: 42 | required: true 43 | 44 | - type: textarea 45 | id: expected-behavior 46 | attributes: 47 | label: Expected Behavior 48 | description: "A clear and concise description of what you expected to happen." 49 | placeholder: "Explain what you expected to happen..." 50 | validations: 51 | required: true 52 | 53 | - type: textarea 54 | id: additional-context 55 | attributes: 56 | label: Additional Context 57 | description: "Add any other context about the problem here." 58 | placeholder: "Additional details..." 59 | validations: 60 | required: false 61 | 62 | - type: markdown 63 | attributes: 64 | value: "Please make sure to include any relevant information such as screenshots, logs, or code snippets that may help in diagnosing the issue." 65 | 66 | - type: checkboxes 67 | id: willing-to-submit-pr 68 | attributes: 69 | label: "Are you willing to submit a PR to fix this bug?" 70 | description: "Let us know if you are willing to contribute a fix by submitting a Pull Request." 71 | options: 72 | - label: "Yes, I would like to submit a PR." 73 | -------------------------------------------------------------------------------- /integrations/fuse3/README.md: -------------------------------------------------------------------------------- 1 | # Apache OpenDAL™ fuse3 integration 2 | 3 | [![Build Status]][actions] [![Latest Version]][crates.io] [![Crate Downloads]][crates.io] [![chat]][discord] 4 | 5 | [build status]: https://img.shields.io/github/actions/workflow/status/apache/opendal/ci_integration_fuse3.yml?branch=main 6 | [actions]: https://github.com/apache/opendal/actions?query=branch%3Amain 7 | [latest version]: https://img.shields.io/crates/v/fuse3_opendal.svg 8 | [crates.io]: https://crates.io/crates/fuse3_opendal 9 | [crate downloads]: https://img.shields.io/crates/d/fuse3_opendal.svg 10 | [chat]: https://img.shields.io/discord/1081052318650339399 11 | [discord]: https://opendal.apache.org/discord 12 | 13 | `fuse3_opendal` is an [`fuse3`](https://github.com/Sherlock-Holo/fuse3) implementation using opendal. 14 | 15 | This crate can help you to access ANY storage services by mounting locally by [`FUSE`](https://www.kernel.org/doc/html/next/filesystems/fuse.html). 16 | 17 | ## Useful Links 18 | 19 | - Documentation: [release](https://docs.rs/fuse3_opendal/) | [dev](https://opendal.apache.org/docs/fuse3-opendal/fuse3_opendal/) 20 | 21 | ## Examples 22 | 23 | ```rust 24 | use fuse3::path::Session; 25 | use fuse3::MountOptions; 26 | use fuse3::Result; 27 | use fuse3_opendal::Filesystem; 28 | use opendal::services::Memory; 29 | use opendal::Operator; 30 | 31 | #[tokio::test] 32 | async fn test() -> Result<()> { 33 | // Build opendal Operator. 34 | let op = Operator::new(Memory::default())?.finish(); 35 | 36 | // Build fuse3 file system. 37 | let fs = Filesystem::new(op, 1000, 1000); 38 | 39 | // Configure mount options. 40 | let mount_options = MountOptions::default(); 41 | 42 | // Start a fuse3 session and mount it. 43 | let mut mount_handle = Session::new(mount_options) 44 | .mount_with_unprivileged(fs, "/tmp/mount_test") 45 | .await?; 46 | let handle = &mut mount_handle; 47 | 48 | tokio::select! { 49 | res = handle => res?, 50 | _ = tokio::signal::ctrl_c() => { 51 | mount_handle.unmount().await? 52 | } 53 | } 54 | 55 | Ok(()) 56 | } 57 | ``` 58 | 59 | ## Branding 60 | 61 | The first and most prominent mentions must use the full form: **Apache OpenDAL™** of the name for any individual usage (webpage, handout, slides, etc.) Depending on the context and writing style, you should use the full form of the name sufficiently often to ensure that readers clearly understand the association of both the OpenDAL project and the OpenDAL software product to the ASF as the parent organization. 62 | 63 | For more details, see the [Apache Product Name Usage Guide](https://www.apache.org/foundation/marks/guide). 64 | 65 | ## License and Trademarks 66 | 67 | Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 68 | 69 | Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or trademarks of the Apache Software Foundation. 70 | -------------------------------------------------------------------------------- /integrations/cloud_filter/examples/readonly.rs: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The ASF licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | use std::env; 19 | 20 | use cloud_filter::root::{ 21 | HydrationType, PopulationType, SecurityId, Session, SyncRootIdBuilder, SyncRootInfo, 22 | }; 23 | use opendal::{Operator, services}; 24 | use tokio::{runtime::Handle, signal}; 25 | 26 | const PROVIDER_NAME: &str = "ro-cloud_filter"; 27 | const DISPLAY_NAME: &str = "Read Only Cloud Filter"; 28 | 29 | #[tokio::main] 30 | async fn main() { 31 | logforth::stderr().apply(); 32 | 33 | let root = env::var("ROOT").expect("$ROOT is set"); 34 | let client_path = env::var("CLIENT_PATH").expect("$CLIENT_PATH is set"); 35 | 36 | let fs = services::Fs::default().root(&root); 37 | 38 | let op = Operator::new(fs).expect("build operator").finish(); 39 | 40 | let sync_root_id = SyncRootIdBuilder::new(PROVIDER_NAME) 41 | .user_security_id(SecurityId::current_user().unwrap()) 42 | .build(); 43 | 44 | if !sync_root_id.is_registered().unwrap() { 45 | sync_root_id 46 | .register( 47 | SyncRootInfo::default() 48 | .with_display_name(DISPLAY_NAME) 49 | .with_hydration_type(HydrationType::Full) 50 | .with_population_type(PopulationType::Full) 51 | .with_icon("%SystemRoot%\\system32\\charmap.exe,0") 52 | .with_version("1.0.0") 53 | .with_recycle_bin_uri("http://cloudmirror.example.com/recyclebin") 54 | .unwrap() 55 | .with_path(&client_path) 56 | .unwrap(), 57 | ) 58 | .unwrap(); 59 | } 60 | 61 | let handle = Handle::current(); 62 | let connection = Session::new() 63 | .connect_async( 64 | &client_path, 65 | cloud_filter_opendal::CloudFilter::new(op, client_path.clone().into()), 66 | move |f| handle.block_on(f), 67 | ) 68 | .expect("create session"); 69 | 70 | signal::ctrl_c().await.unwrap(); 71 | 72 | drop(connection); 73 | sync_root_id.unregister().unwrap(); 74 | } 75 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | name: Setup Rust Builder 19 | description: "Prepare Rust Build Environment" 20 | inputs: 21 | need-rocksdb: 22 | description: "This setup needs rocksdb or not" 23 | need-protoc: 24 | description: "This setup needs protoc or not" 25 | github-token: 26 | description: "Github Token" 27 | default: "" 28 | 29 | runs: 30 | using: "composite" 31 | steps: 32 | - name: Setup rust related environment variables 33 | shell: bash 34 | run: | 35 | # Disable full debug symbol generation to speed up CI build and keep memory down 36 | # "1" means line tables only, which is useful for panic tracebacks. 37 | # About `force-frame-pointers`, here's the discussion history: https://github.com/apache/opendal/issues/3756 38 | echo "RUSTFLAGS=-C force-frame-pointers=yes -C debuginfo=1" >> $GITHUB_ENV 39 | # Enable backtraces 40 | echo "RUST_BACKTRACE=1" >> $GITHUB_ENV 41 | # Enable logging 42 | echo "RUST_LOG=opendal=trace" >> $GITHUB_ENV 43 | # Enable sparse index 44 | echo "CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse" >> $GITHUB_ENV 45 | # Make sure rust has been setup 46 | cargo version 47 | 48 | # Make sure all required lib has been installed. 49 | - name: Setup Linux 50 | if: runner.os == 'Linux' 51 | shell: bash 52 | run: sudo apt-get install libgflags-dev libsnappy-dev zlib1g-dev libbz2-dev liblz4-dev libzstd-dev 53 | 54 | - name: Setup Protoc 55 | if: inputs.need-protoc == 'true' 56 | uses: arduino/setup-protoc@v3 57 | with: 58 | version: "23.4" 59 | repo-token: ${{ inputs.github-token }} 60 | 61 | - name: Setup rocksdb on linux 62 | if: runner.os == 'Linux' && inputs.need-rocksdb == 'true' 63 | shell: bash 64 | run: | 65 | # Set rocksdb lib path 66 | echo "ROCKSDB_LIB_DIR=/tmp/rocksdb/lib" >> $GITHUB_ENV 67 | 68 | - name: Cache rocksdb 69 | id: cache-rocksdb 70 | uses: actions/cache@v4 71 | if: runner.os == 'Linux' && inputs.need-rocksdb == 'true' 72 | with: 73 | path: /tmp/rocksdb 74 | key: r2-rocksdb-8.1.1 75 | 76 | - name: Build rocksdb if not cached 77 | if: steps.cache-rocksdb.outputs.cache-hit != 'true' && runner.os == 'Linux' && inputs.need-rocksdb == 'true' 78 | shell: bash 79 | run: | 80 | set -e 81 | 82 | cd /tmp 83 | curl https://github.com/facebook/rocksdb/archive/refs/tags/v8.1.1.tar.gz -L -o rocksdb.tar.gz 84 | tar -xzf rocksdb.tar.gz 85 | cd rocksdb-8.1.1 86 | 87 | mkdir /tmp/rocksdb 88 | cmake -DCMAKE_INSTALL_PREFIX=/tmp/rocksdb -DPORTABLE=1 89 | make -j$(nproc) 90 | make install 91 | 92 | cd .. 93 | rm -rf /tmp/rocksdb-8.1.1 94 | -------------------------------------------------------------------------------- /integrations/cloud_filter/README.md: -------------------------------------------------------------------------------- 1 | # Apache OpenDAL™ Cloud Filter Integration 2 | 3 | [![Build Status]][actions] [![Latest Version]][crates.io] [![Crate Downloads]][crates.io] [![chat]][discord] 4 | 5 | [build status]: https://img.shields.io/github/actions/workflow/status/apache/opendal/test_behavior_integration_cloud_filter.yml?branch=main 6 | [actions]: https://github.com/apache/opendal/actions?query=branch%3Amain 7 | [latest version]: https://img.shields.io/crates/v/cloud_filter_opendal.svg 8 | [crates.io]: https://crates.io/crates/cloud_filter_opendal 9 | [crate downloads]: https://img.shields.io/crates/d/cloud_filter_opendal.svg 10 | [chat]: https://img.shields.io/discord/1081052318650339399 11 | [discord]: https://opendal.apache.org/discord 12 | 13 | `cloud_filter_opendal` integrates OpenDAL with [cloud sync engines](https://learn.microsoft.com/en-us/windows/win32/cfapi/build-a-cloud-file-sync-engine). It provides a way to access various cloud storage on Windows. 14 | 15 | Note that `cloud_filter_opendal` is a read-only service, and it is not recommended to use it in production. 16 | 17 | ## Example 18 | 19 | ```rust 20 | use anyhow::Result; 21 | use cloud_filter::root::PopulationType; 22 | use cloud_filter::root::SecurityId; 23 | use cloud_filter::root::Session; 24 | use cloud_filter::root::SyncRootIdBuilder; 25 | use cloud_filter::root::SyncRootInfo; 26 | use opendal::services; 27 | use opendal::Operator; 28 | use tokio::runtime::Handle; 29 | use tokio::signal; 30 | 31 | #[tokio::main] 32 | async fn main() -> Result<()> { 33 | // Create any service desired 34 | let op = Operator::from_iter::([ 35 | ("bucket".to_string(), "my_bucket".to_string()), 36 | ("access_key".to_string(), "my_access_key".to_string()), 37 | ("secret_key".to_string(), "my_secret_key".to_string()), 38 | ("endpoint".to_string(), "my_endpoint".to_string()), 39 | ("region".to_string(), "my_region".to_string()), 40 | ])? 41 | .finish(); 42 | 43 | let client_path = std::env::var("CLIENT_PATH").expect("$CLIENT_PATH is set"); 44 | 45 | // Create a sync root id 46 | let sync_root_id = SyncRootIdBuilder::new("cloud_filter_opendal") 47 | .user_security_id(SecurityId::current_user()?) 48 | .build(); 49 | 50 | // Register the sync root if not exists 51 | if !sync_root_id.is_registered()? { 52 | sync_root_id.register( 53 | SyncRootInfo::default() 54 | .with_display_name("OpenDAL Cloud Filter") 55 | .with_population_type(PopulationType::Full) 56 | .with_icon("shell32.dll,3") 57 | .with_version("1.0.0") 58 | .with_recycle_bin_uri("http://cloudmirror.example.com/recyclebin")? 59 | .with_path(&client_path)?, 60 | )?; 61 | } 62 | 63 | let handle = Handle::current(); 64 | let connection = Session::new().connect_async( 65 | &client_path, 66 | cloud_filter_opendal::CloudFilter::new(op, client_path.clone().into()), 67 | move |f| handle.block_on(f), 68 | )?; 69 | 70 | signal::ctrl_c().await?; 71 | 72 | // Drop the connection before unregister the sync root 73 | drop(connection); 74 | sync_root_id.unregister()?; 75 | 76 | Ok(()) 77 | } 78 | ``` 79 | 80 | ## Branding 81 | 82 | The first and most prominent mentions must use the full form: **Apache OpenDAL™** of the name for any individual usage (webpage, handout, slides, etc.) Depending on the context and writing style, you should use the full form of the name sufficiently often to ensure that readers clearly understand the association of both the OpenDAL project and the OpenDAL software product to the ASF as the parent organization. 83 | 84 | For more details, see the [Apache Product Name Usage Guide](https://www.apache.org/foundation/marks/guide). 85 | 86 | ## License and Trademarks 87 | 88 | Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 89 | 90 | Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or trademarks of the Apache Software Foundation. 91 | -------------------------------------------------------------------------------- /integrations/cloud_filter/tests/behavior/main.rs: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The ASF licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | mod fetch_data; 19 | mod fetch_placeholder; 20 | mod utils; 21 | 22 | use std::{ 23 | fs, 24 | future::Future, 25 | path::{Path, PathBuf}, 26 | pin::Pin, 27 | process::ExitCode, 28 | }; 29 | 30 | use cloud_filter::{ 31 | filter::AsyncBridge, 32 | root::{ 33 | Connection, HydrationType, PopulationType, SecurityId, Session, SyncRootId, 34 | SyncRootIdBuilder, SyncRootInfo, 35 | }, 36 | }; 37 | use cloud_filter_opendal::CloudFilter; 38 | use libtest_mimic::{Arguments, Trial}; 39 | use opendal::{Operator, raw::tests}; 40 | use tokio::runtime::Handle; 41 | 42 | const PROVIDER_NAME: &str = "ro-cloud_filter"; 43 | const DISPLAY_NAME: &str = "Test Cloud Filter"; 44 | const ROOT_PATH: &str = "C:\\sync_root"; 45 | 46 | type Callback = Pin>>; 47 | 48 | #[tokio::main] 49 | async fn main() -> ExitCode { 50 | let args = Arguments::from_args(); 51 | 52 | logforth::stderr().apply(); 53 | 54 | let Ok(Some(op)) = tests::init_test_service() else { 55 | return ExitCode::SUCCESS; 56 | }; 57 | 58 | if !Path::new(ROOT_PATH).try_exists().expect("try exists") { 59 | fs::create_dir(ROOT_PATH).expect("create root dir"); 60 | } 61 | 62 | let (sync_root_id, connection) = init(op); 63 | 64 | let tests = vec![ 65 | Trial::test("fetch_data", fetch_data::test_fetch_data), 66 | Trial::test( 67 | "fetch_placeholder", 68 | fetch_placeholder::test_fetch_placeholder, 69 | ), 70 | ]; 71 | 72 | let conclusion = libtest_mimic::run(&args, tests); 73 | 74 | drop(connection); 75 | sync_root_id.unregister().unwrap(); 76 | fs::remove_dir_all(ROOT_PATH).expect("remove root dir"); 77 | 78 | conclusion.exit_code() 79 | } 80 | 81 | fn init( 82 | op: Operator, 83 | ) -> ( 84 | SyncRootId, 85 | Connection>, 86 | ) { 87 | let sync_root_id = SyncRootIdBuilder::new(PROVIDER_NAME) 88 | .user_security_id(SecurityId::current_user().unwrap()) 89 | .build(); 90 | 91 | if !sync_root_id.is_registered().unwrap() { 92 | sync_root_id 93 | .register( 94 | SyncRootInfo::default() 95 | .with_display_name(DISPLAY_NAME) 96 | .with_hydration_type(HydrationType::Full) 97 | .with_population_type(PopulationType::Full) 98 | .with_icon("%SystemRoot%\\system32\\charmap.exe,0") 99 | .with_version("1.0.0") 100 | .with_recycle_bin_uri("http://cloudmirror.example.com/recyclebin") 101 | .unwrap() 102 | .with_path(ROOT_PATH) 103 | .unwrap(), 104 | ) 105 | .unwrap(); 106 | } 107 | 108 | let handle = Handle::current(); 109 | let connection = Session::new() 110 | .connect_async( 111 | ROOT_PATH, 112 | CloudFilter::new(op, PathBuf::from(&ROOT_PATH)), 113 | move |f| handle.clone().block_on(f), 114 | ) 115 | .expect("create session"); 116 | 117 | (sync_root_id, connection) 118 | } 119 | -------------------------------------------------------------------------------- /tests/file.rs: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The ASF licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | #![cfg(any(target_os = "linux", target_os = "freebsd"))] 19 | 20 | mod common; 21 | 22 | use std::fs::File; 23 | use std::fs::OpenOptions; 24 | use std::fs::{self}; 25 | use std::io::Read; 26 | use std::io::Seek; 27 | use std::io::SeekFrom; 28 | use std::io::Write; 29 | use std::thread; 30 | use std::time::Duration; 31 | 32 | use common::OfsTestContext; 33 | use test_context::test_context; 34 | 35 | static TEST_TEXT: &str = include_str!("../Cargo.toml"); 36 | 37 | #[test_context(OfsTestContext)] 38 | #[test] 39 | fn test_file(ctx: &mut OfsTestContext) { 40 | let path = ctx.mount_point.path().join("test_file.txt"); 41 | let mut file = File::create(&path).unwrap(); 42 | 43 | file.write_all(TEST_TEXT.as_bytes()).unwrap(); 44 | drop(file); 45 | 46 | let mut file = File::open(&path).unwrap(); 47 | let mut buf = String::new(); 48 | file.read_to_string(&mut buf).unwrap(); 49 | assert_eq!(buf, TEST_TEXT); 50 | drop(file); 51 | 52 | fs::remove_file(path).unwrap(); 53 | } 54 | 55 | #[test_context(OfsTestContext)] 56 | #[test] 57 | fn test_file_append(ctx: &mut OfsTestContext) { 58 | if !ctx.capability.write_can_append { 59 | // wait for ofs to be ready 60 | thread::sleep(Duration::from_secs(1)); 61 | return; 62 | } 63 | 64 | let path = ctx.mount_point.path().join("test_file_append.txt"); 65 | let mut file = File::create(&path).unwrap(); 66 | 67 | file.write_all(TEST_TEXT.as_bytes()).unwrap(); 68 | drop(file); 69 | 70 | let mut file = File::options().append(true).open(&path).unwrap(); 71 | file.write_all(b"test").unwrap(); 72 | drop(file); 73 | 74 | let mut file = File::open(&path).unwrap(); 75 | let mut buf = String::new(); 76 | file.read_to_string(&mut buf).unwrap(); 77 | assert_eq!(buf, TEST_TEXT.to_owned() + "test"); 78 | drop(file); 79 | 80 | fs::remove_file(path).unwrap(); 81 | } 82 | 83 | #[test_context(OfsTestContext)] 84 | #[test] 85 | fn test_file_seek(ctx: &mut OfsTestContext) { 86 | let path = ctx.mount_point.path().join("test_file_seek.txt"); 87 | let mut file = File::create(&path).unwrap(); 88 | 89 | file.write_all(TEST_TEXT.as_bytes()).unwrap(); 90 | drop(file); 91 | 92 | let mut file = File::open(&path).unwrap(); 93 | file.seek(SeekFrom::Start(TEST_TEXT.len() as u64 / 2)) 94 | .unwrap(); 95 | let mut buf = String::new(); 96 | file.read_to_string(&mut buf).unwrap(); 97 | assert_eq!(buf, TEST_TEXT[TEST_TEXT.len() / 2..]); 98 | drop(file); 99 | 100 | fs::remove_file(path).unwrap(); 101 | } 102 | 103 | #[test_context(OfsTestContext)] 104 | #[test] 105 | fn test_file_truncate(ctx: &mut OfsTestContext) { 106 | let path = ctx.mount_point.path().join("test_file_truncate.txt"); 107 | let mut file = File::create(&path).unwrap(); 108 | file.write_all(TEST_TEXT.as_bytes()).unwrap(); 109 | drop(file); 110 | 111 | let mut file = OpenOptions::new() 112 | .write(true) 113 | .truncate(true) 114 | .open(&path) 115 | .unwrap(); 116 | file.write_all(&TEST_TEXT.as_bytes()[..TEST_TEXT.len() / 2]) 117 | .unwrap(); 118 | drop(file); 119 | 120 | assert_eq!( 121 | fs::read_to_string(&path).unwrap(), 122 | TEST_TEXT[..TEST_TEXT.len() / 2] 123 | ); 124 | 125 | fs::remove_file(path).unwrap(); 126 | } 127 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The ASF licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | use std::sync::OnceLock; 19 | 20 | use opendal::raw::tests; 21 | use opendal::services; 22 | use opendal::Capability; 23 | use opendal::Operator; 24 | use tempfile::TempDir; 25 | use test_context::TestContext; 26 | use tokio::runtime::Runtime; 27 | use tokio::runtime::{self}; 28 | 29 | static INIT_LOGGER: OnceLock<()> = OnceLock::new(); 30 | static RUNTIME: OnceLock = OnceLock::new(); 31 | 32 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 33 | pub struct OfsTestContext { 34 | pub mount_point: TempDir, 35 | // Keep backend root alive for the fs-based fallback backend. 36 | #[allow(dead_code)] 37 | backend_root: Option, 38 | // This is a false positive, the field is used in the test. 39 | #[allow(dead_code)] 40 | pub capability: Capability, 41 | mount_handle: fuse3::raw::MountHandle, 42 | } 43 | 44 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 45 | impl TestContext for OfsTestContext { 46 | fn setup() -> Self { 47 | let mut backend_root = None; 48 | let backend = tests::init_test_service() 49 | .expect("init test services failed") 50 | .unwrap_or_else(|| { 51 | let tmp_root = tempfile::tempdir().expect("create temporary backend root"); 52 | let root_path = tmp_root.path().to_string_lossy().to_string(); 53 | let fs = services::Fs::default().root(&root_path); 54 | let backend = Operator::new(fs) 55 | .expect("build fallback fs operator") 56 | .finish(); 57 | backend_root = Some(tmp_root); 58 | backend 59 | }); 60 | let capability = backend.info().full_capability(); 61 | 62 | INIT_LOGGER.get_or_init(|| logforth::starter_log::stderr().apply()); 63 | 64 | let mount_point = tempfile::tempdir().unwrap(); 65 | let mount_point_str = mount_point.path().to_string_lossy().to_string(); 66 | let mount_handle = RUNTIME 67 | .get_or_init(|| { 68 | runtime::Builder::new_multi_thread() 69 | .enable_all() 70 | .build() 71 | .expect("build runtime") 72 | }) 73 | .block_on( 74 | #[allow(clippy::async_yields_async)] 75 | async move { 76 | let mut mount_options = fuse3::MountOptions::default(); 77 | let gid = nix::unistd::getgid().into(); 78 | mount_options.gid(gid); 79 | let uid = nix::unistd::getuid().into(); 80 | mount_options.uid(uid); 81 | 82 | let fs = fuse3_opendal::Filesystem::new(backend, uid, gid); 83 | fuse3::path::Session::new(mount_options) 84 | .mount_with_unprivileged(fs, mount_point_str) 85 | .await 86 | .unwrap() 87 | }, 88 | ); 89 | 90 | OfsTestContext { 91 | mount_point, 92 | backend_root, 93 | capability, 94 | mount_handle, 95 | } 96 | } 97 | 98 | // We don't care if the unmount fails, so we ignore the result. 99 | fn teardown(self) { 100 | let _ = RUNTIME 101 | .get() 102 | .expect("runtime") 103 | .block_on(async move { self.mount_handle.unmount().await }); 104 | let _ = self.mount_point.close(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The ASF licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | use anyhow::Result; 19 | use anyhow::anyhow; 20 | use clap::Parser; 21 | use url::Url; 22 | 23 | #[derive(Parser, Debug)] 24 | #[command(version, about)] 25 | struct Config { 26 | /// fuse mount path 27 | #[arg(env = "OFS_MOUNT_PATH", index = 1)] 28 | mount_path: String, 29 | 30 | /// location of opendal service 31 | /// format: ://?=&= 32 | /// example: fs://?root=/tmp 33 | #[arg(env = "OFS_BACKEND", index = 2)] 34 | backend: Url, 35 | } 36 | 37 | #[tokio::main(flavor = "multi_thread")] 38 | async fn main() -> Result<()> { 39 | let cfg = Config::parse(); 40 | 41 | logforth::starter_log::stderr().apply(); 42 | execute(cfg).await 43 | } 44 | 45 | #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))] 46 | async fn execute(cfg: Config) -> Result<()> { 47 | use std::env; 48 | use std::str::FromStr; 49 | 50 | use fuse3::MountOptions; 51 | use fuse3::path::Session; 52 | use opendal::Operator; 53 | use opendal::Scheme; 54 | 55 | if cfg.backend.has_host() { 56 | log::warn!("backend host will be ignored"); 57 | } 58 | 59 | let scheme_str = cfg.backend.scheme(); 60 | let op_args = cfg.backend.query_pairs().into_owned(); 61 | 62 | let scheme = match Scheme::from_str(scheme_str) { 63 | Ok(Scheme::Custom(_)) | Err(_) => Err(anyhow!("invalid scheme: {}", scheme_str)), 64 | Ok(s) => Ok(s), 65 | }?; 66 | let backend = Operator::via_iter(scheme, op_args)?; 67 | 68 | let mut mount_options = MountOptions::default(); 69 | let mut gid = nix::unistd::getgid().into(); 70 | mount_options.gid(gid); 71 | let mut uid = nix::unistd::getuid().into(); 72 | mount_options.uid(uid); 73 | 74 | #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))] 75 | let mut mount_handle = if nix::unistd::getuid().is_root() { 76 | if let Some(sudo_gid) = env::var("SUDO_GID") 77 | .ok() 78 | .and_then(|gid_str| gid_str.parse::().ok()) 79 | { 80 | mount_options.gid(sudo_gid); 81 | gid = sudo_gid; 82 | } 83 | 84 | if let Some(sudo_uid) = env::var("SUDO_UID") 85 | .ok() 86 | .and_then(|gid_str| gid_str.parse::().ok()) 87 | { 88 | mount_options.uid(uid); 89 | uid = sudo_uid; 90 | } 91 | 92 | let fs = fuse3_opendal::Filesystem::new(backend, uid, gid); 93 | Session::new(mount_options) 94 | .mount(fs, cfg.mount_path) 95 | .await? 96 | } else { 97 | let fs = fuse3_opendal::Filesystem::new(backend, uid, gid); 98 | Session::new(mount_options) 99 | .mount_with_unprivileged(fs, cfg.mount_path) 100 | .await? 101 | }; 102 | 103 | let handle = &mut mount_handle; 104 | tokio::select! { 105 | res = handle => res?, 106 | _ = tokio::signal::ctrl_c() => { 107 | mount_handle.unmount().await? 108 | } 109 | } 110 | 111 | Ok(()) 112 | } 113 | 114 | #[cfg(target_os = "windows")] 115 | async fn execute(cfg: Config) -> Result<()> { 116 | use std::path::PathBuf; 117 | use std::str::FromStr; 118 | 119 | use anyhow::Context; 120 | use cloud_filter::root::HydrationType; 121 | use cloud_filter::root::PopulationType; 122 | use cloud_filter::root::SecurityId; 123 | use cloud_filter::root::Session; 124 | use cloud_filter::root::SyncRootIdBuilder; 125 | use cloud_filter::root::SyncRootInfo; 126 | use opendal::Operator; 127 | use opendal::Scheme; 128 | use tokio::runtime::Handle; 129 | use tokio::signal; 130 | 131 | const PROVIDER_NAME: &str = "ofs"; 132 | 133 | if cfg.backend.has_host() { 134 | log::warn!("backend host will be ignored"); 135 | } 136 | 137 | let scheme_str = cfg.backend.scheme(); 138 | let op_args = cfg.backend.query_pairs().into_owned(); 139 | 140 | let scheme = match Scheme::from_str(scheme_str) { 141 | Ok(Scheme::Custom(_)) | Err(_) => Err(anyhow!("invalid scheme: {}", scheme_str)), 142 | Ok(s) => Ok(s), 143 | }?; 144 | let backend = Operator::via_iter(scheme, op_args).context("invalid arguments")?; 145 | 146 | let sync_root_id = SyncRootIdBuilder::new(PROVIDER_NAME) 147 | .user_security_id( 148 | SecurityId::current_user().expect("get current user security id, it might be a bug"), 149 | ) 150 | .build(); 151 | 152 | if !sync_root_id 153 | .is_registered() 154 | .expect("check if sync root is registered, it might be a bug") 155 | { 156 | sync_root_id 157 | .register( 158 | SyncRootInfo::default() 159 | .with_display_name(format!("ofs ({scheme_str})")) 160 | .with_hydration_type(HydrationType::Full) 161 | .with_population_type(PopulationType::Full) 162 | .with_icon("%SystemRoot%\\system32\\charmap.exe,0") 163 | .with_version(env!("CARGO_PKG_VERSION")) 164 | .with_recycle_bin_uri("http://cloudmirror.example.com/recyclebin") // FIXME 165 | .unwrap() 166 | .with_path(&cfg.mount_path) 167 | .context("mount_path is not a folder")?, 168 | ) 169 | .context("failed to register sync root")?; 170 | } 171 | 172 | let handle = Handle::current(); 173 | let connection = Session::new() 174 | .connect_async( 175 | &cfg.mount_path, 176 | cloudfilter_opendal::CloudFilter::new(backend, PathBuf::from(&cfg.mount_path)), 177 | move |f| handle.clone().block_on(f), 178 | ) 179 | .context("failed to connect to sync root")?; 180 | 181 | signal::ctrl_c().await.unwrap(); 182 | 183 | drop(connection); 184 | sync_root_id 185 | .unregister() 186 | .context("failed to unregister sync root")?; 187 | 188 | Ok(()) 189 | } 190 | -------------------------------------------------------------------------------- /integrations/cloud_filter/DEPENDENCIES.rust.tsv: -------------------------------------------------------------------------------- 1 | crate 0BSD Apache-2.0 Apache-2.0 WITH LLVM-exception BSD-2-Clause BSD-3-Clause BSL-1.0 CDLA-Permissive-2.0 ISC LGPL-2.1-or-later MIT Unicode-3.0 Unlicense Zlib 2 | addr2line@0.24.2 X X 3 | adler2@2.0.1 X X X 4 | android-tzdata@0.1.1 X X 5 | android_system_properties@0.1.5 X X 6 | anyhow@1.0.98 X X 7 | async-trait@0.1.88 X X 8 | autocfg@1.5.0 X X 9 | backon@1.5.1 X 10 | backtrace@0.3.75 X X 11 | base64@0.22.1 X X 12 | bincode@1.3.3 X 13 | bitflags@2.9.1 X X 14 | block-buffer@0.10.4 X X 15 | bumpalo@3.19.0 X X 16 | bytes@1.10.1 X 17 | cc@1.2.29 X X 18 | cfg-if@1.0.1 X X 19 | chrono@0.4.41 X X 20 | cloud-filter@0.0.6 X 21 | cloud_filter_opendal@0.0.12 X 22 | const-oid@0.9.6 X X 23 | core-foundation-sys@0.8.7 X X 24 | cpufeatures@0.2.17 X X 25 | crc32c@0.6.8 X X 26 | crypto-common@0.1.6 X X 27 | deranged@0.4.0 X X 28 | digest@0.10.7 X X 29 | displaydoc@0.2.5 X X 30 | dotenvy@0.15.7 X 31 | fastrand@2.3.0 X X 32 | flagset@0.4.7 X 33 | fnv@1.0.7 X X 34 | form_urlencoded@1.2.1 X X 35 | futures@0.3.31 X X 36 | futures-channel@0.3.31 X X 37 | futures-core@0.3.31 X X 38 | futures-executor@0.3.31 X X 39 | futures-io@0.3.31 X X 40 | futures-macro@0.3.31 X X 41 | futures-sink@0.3.31 X X 42 | futures-task@0.3.31 X X 43 | futures-util@0.3.31 X X 44 | generic-array@0.14.7 X 45 | getrandom@0.2.16 X X 46 | getrandom@0.3.3 X X 47 | gimli@0.31.1 X X 48 | gloo-timers@0.3.0 X X 49 | hex@0.4.3 X X 50 | hmac@0.12.1 X X 51 | home@0.5.11 X X 52 | http@1.3.1 X X 53 | http-body@1.0.1 X 54 | http-body-util@0.1.3 X 55 | httparse@1.10.1 X X 56 | hyper@1.6.0 X 57 | hyper-rustls@0.27.7 X X X 58 | hyper-util@0.1.15 X 59 | iana-time-zone@0.1.63 X X 60 | iana-time-zone-haiku@0.1.2 X X 61 | icu_collections@2.0.0 X 62 | icu_locale_core@2.0.0 X 63 | icu_normalizer@2.0.0 X 64 | icu_normalizer_data@2.0.0 X 65 | icu_properties@2.0.1 X 66 | icu_properties_data@2.0.1 X 67 | icu_provider@2.0.0 X 68 | idna@1.0.3 X X 69 | idna_adapter@1.2.1 X X 70 | io-uring@0.7.8 X X 71 | ipnet@2.11.0 X X 72 | iri-string@0.7.8 X X 73 | itoa@1.0.15 X X 74 | js-sys@0.3.77 X X 75 | libc@0.2.174 X X 76 | litemap@0.8.0 X 77 | log@0.4.27 X X 78 | md-5@0.10.6 X X 79 | memchr@2.7.5 X X 80 | memoffset@0.9.1 X 81 | miniz_oxide@0.8.9 X X X 82 | mio@1.0.4 X 83 | nt-time@0.8.1 X X 84 | num-conv@0.1.0 X X 85 | num-traits@0.2.19 X X 86 | object@0.36.7 X X 87 | once_cell@1.21.3 X X 88 | opendal@0.54.1 X 89 | percent-encoding@2.3.1 X X 90 | pin-project-lite@0.2.16 X X 91 | pin-utils@0.1.0 X X 92 | potential_utf@0.1.2 X 93 | powerfmt@0.2.0 X X 94 | ppv-lite86@0.2.21 X X 95 | proc-macro2@1.0.95 X X 96 | quick-xml@0.37.5 X 97 | quick-xml@0.38.0 X 98 | quote@1.0.40 X X 99 | r-efi@5.3.0 X X X 100 | rand@0.8.5 X X 101 | rand_chacha@0.3.1 X X 102 | rand_core@0.6.4 X X 103 | reqsign@0.16.5 X 104 | reqwest@0.12.22 X X 105 | ring@0.17.14 X X 106 | rustc-demangle@0.1.25 X X 107 | rustc_version@0.4.1 X X 108 | rustls@0.23.29 X X X 109 | rustls-pki-types@1.12.0 X X 110 | rustls-webpki@0.103.4 X 111 | rustversion@1.0.21 X X 112 | ryu@1.0.20 X X 113 | semver@1.0.26 X X 114 | serde@1.0.219 X X 115 | serde_derive@1.0.219 X X 116 | serde_json@1.0.140 X X 117 | serde_urlencoded@0.7.1 X X 118 | sha1@0.10.6 X X 119 | sha2@0.10.9 X X 120 | shlex@1.3.0 X X 121 | signal-hook-registry@1.4.5 X X 122 | slab@0.4.10 X 123 | smallvec@1.15.1 X X 124 | socket2@0.5.10 X X 125 | socket2@0.6.0 X X 126 | stable_deref_trait@1.2.0 X X 127 | subtle@2.6.1 X 128 | syn@2.0.104 X X 129 | sync_wrapper@1.0.2 X 130 | synstructure@0.13.2 X 131 | time@0.3.41 X X 132 | time-core@0.1.4 X X 133 | time-macros@0.2.22 X X 134 | tinystr@0.8.1 X 135 | tokio@1.47.1 X 136 | tokio-macros@2.5.0 X 137 | tokio-rustls@0.26.2 X X 138 | tokio-util@0.7.15 X 139 | tower@0.5.2 X 140 | tower-http@0.6.6 X 141 | tower-layer@0.3.3 X 142 | tower-service@0.3.3 X 143 | tracing@0.1.41 X 144 | tracing-core@0.1.34 X 145 | try-lock@0.2.5 X 146 | typenum@1.18.0 X X 147 | unicode-ident@1.0.18 X X X 148 | untrusted@0.9.0 X 149 | url@2.5.4 X X 150 | utf8_iter@1.0.4 X X 151 | uuid@1.17.0 X X 152 | version_check@0.9.5 X X 153 | want@0.3.1 X 154 | wasi@0.11.1+wasi-snapshot-preview1 X X X 155 | wasi@0.14.2+wasi-0.2.4 X X X 156 | wasm-bindgen@0.2.100 X X 157 | wasm-bindgen-backend@0.2.100 X X 158 | wasm-bindgen-futures@0.4.50 X X 159 | wasm-bindgen-macro@0.2.100 X X 160 | wasm-bindgen-macro-support@0.2.100 X X 161 | wasm-bindgen-shared@0.2.100 X X 162 | wasm-streams@0.4.2 X X 163 | web-sys@0.3.77 X X 164 | webpki-roots@1.0.1 X 165 | widestring@1.2.0 X X 166 | windows@0.58.0 X X 167 | windows-core@0.58.0 X X 168 | windows-core@0.61.2 X X 169 | windows-implement@0.58.0 X X 170 | windows-implement@0.60.0 X X 171 | windows-interface@0.58.0 X X 172 | windows-interface@0.59.1 X X 173 | windows-link@0.1.3 X X 174 | windows-result@0.2.0 X X 175 | windows-result@0.3.4 X X 176 | windows-strings@0.1.0 X X 177 | windows-strings@0.4.2 X X 178 | windows-sys@0.52.0 X X 179 | windows-sys@0.59.0 X X 180 | windows-targets@0.52.6 X X 181 | windows_aarch64_gnullvm@0.52.6 X X 182 | windows_aarch64_msvc@0.52.6 X X 183 | windows_i686_gnu@0.52.6 X X 184 | windows_i686_gnullvm@0.52.6 X X 185 | windows_i686_msvc@0.52.6 X X 186 | windows_x86_64_gnu@0.52.6 X X 187 | windows_x86_64_gnullvm@0.52.6 X X 188 | windows_x86_64_msvc@0.52.6 X X 189 | wit-bindgen-rt@0.39.0 X X X 190 | writeable@0.6.1 X 191 | yoke@0.8.0 X 192 | yoke-derive@0.8.0 X 193 | zerocopy@0.8.26 X X X 194 | zerofrom@0.1.6 X 195 | zerofrom-derive@0.1.6 X 196 | zeroize@1.8.1 X X 197 | zerotrie@0.2.2 X 198 | zerovec@0.11.2 X 199 | zerovec-derive@0.11.1 X 200 | -------------------------------------------------------------------------------- /integrations/fuse3/DEPENDENCIES.rust.tsv: -------------------------------------------------------------------------------- 1 | crate 0BSD Apache-2.0 Apache-2.0 WITH LLVM-exception BSD-3-Clause BSL-1.0 CDLA-Permissive-2.0 ISC LGPL-2.1-or-later MIT Unicode-3.0 Unlicense Zlib 2 | addr2line@0.24.2 X X 3 | adler2@2.0.1 X X X 4 | android-tzdata@0.1.1 X X 5 | android_system_properties@0.1.5 X X 6 | anyhow@1.0.98 X X 7 | async-notify@0.3.0 X 8 | autocfg@1.5.0 X X 9 | backon@1.5.1 X 10 | backtrace@0.3.75 X X 11 | base64@0.22.1 X X 12 | bincode@1.3.3 X 13 | bitflags@2.9.1 X X 14 | block-buffer@0.10.4 X X 15 | bumpalo@3.19.0 X X 16 | bytes@1.10.1 X 17 | cc@1.2.29 X X 18 | cfg-if@1.0.1 X X 19 | cfg_aliases@0.2.1 X 20 | chrono@0.4.41 X X 21 | concurrent-queue@2.5.0 X X 22 | core-foundation-sys@0.8.7 X X 23 | crossbeam-utils@0.8.21 X X 24 | crypto-common@0.1.6 X X 25 | digest@0.10.7 X X 26 | displaydoc@0.2.5 X X 27 | either@1.15.0 X X 28 | errno@0.3.13 X X 29 | event-listener@4.0.3 X X 30 | fastrand@2.3.0 X X 31 | fnv@1.0.7 X X 32 | form_urlencoded@1.2.1 X X 33 | fuse3@0.8.1 X 34 | fuse3_opendal@0.0.19 X 35 | futures@0.3.31 X X 36 | futures-channel@0.3.31 X X 37 | futures-core@0.3.31 X X 38 | futures-io@0.3.31 X X 39 | futures-macro@0.3.31 X X 40 | futures-sink@0.3.31 X X 41 | futures-task@0.3.31 X X 42 | futures-util@0.3.31 X X 43 | generic-array@0.14.7 X 44 | getrandom@0.2.16 X X 45 | getrandom@0.3.3 X X 46 | gimli@0.31.1 X X 47 | gloo-timers@0.3.0 X X 48 | home@0.5.11 X X 49 | http@1.3.1 X X 50 | http-body@1.0.1 X 51 | http-body-util@0.1.3 X 52 | httparse@1.10.1 X X 53 | hyper@1.6.0 X 54 | hyper-rustls@0.27.7 X X X 55 | hyper-util@0.1.15 X 56 | iana-time-zone@0.1.63 X X 57 | iana-time-zone-haiku@0.1.2 X X 58 | icu_collections@2.0.0 X 59 | icu_locale_core@2.0.0 X 60 | icu_normalizer@2.0.0 X 61 | icu_normalizer_data@2.0.0 X 62 | icu_properties@2.0.1 X 63 | icu_properties_data@2.0.1 X 64 | icu_provider@2.0.0 X 65 | idna@1.0.3 X X 66 | idna_adapter@1.2.1 X X 67 | io-uring@0.7.8 X X 68 | ipnet@2.11.0 X X 69 | iri-string@0.7.8 X X 70 | itoa@1.0.15 X X 71 | js-sys@0.3.77 X X 72 | lazy_static@1.5.0 X X 73 | libc@0.2.174 X X 74 | linux-raw-sys@0.4.15 X X X 75 | litemap@0.8.0 X 76 | log@0.4.27 X X 77 | md-5@0.10.6 X X 78 | memchr@2.7.5 X X 79 | memoffset@0.9.1 X 80 | miniz_oxide@0.8.9 X X X 81 | mio@1.0.4 X 82 | nix@0.29.0 X 83 | num-traits@0.2.19 X X 84 | object@0.36.7 X X 85 | once_cell@1.21.3 X X 86 | opendal@0.54.1 X 87 | parking@2.2.1 X X 88 | percent-encoding@2.3.1 X X 89 | pin-project-lite@0.2.16 X X 90 | pin-utils@0.1.0 X X 91 | potential_utf@0.1.2 X 92 | proc-macro2@1.0.95 X X 93 | quick-xml@0.38.3 X 94 | quote@1.0.40 X X 95 | r-efi@5.3.0 X X X 96 | reqwest@0.12.22 X X 97 | ring@0.17.14 X X 98 | rustc-demangle@0.1.25 X X 99 | rustix@0.38.44 X X X 100 | rustls@0.23.29 X X X 101 | rustls-pki-types@1.12.0 X X 102 | rustls-webpki@0.103.4 X 103 | rustversion@1.0.21 X X 104 | ryu@1.0.20 X X 105 | serde@1.0.219 X X 106 | serde_derive@1.0.219 X X 107 | serde_json@1.0.140 X X 108 | serde_urlencoded@0.7.1 X X 109 | sharded-slab@0.1.7 X 110 | shlex@1.3.0 X X 111 | signal-hook-registry@1.4.5 X X 112 | slab@0.4.10 X 113 | smallvec@1.15.1 X X 114 | socket2@0.5.10 X X 115 | socket2@0.6.0 X X 116 | stable_deref_trait@1.2.0 X X 117 | subtle@2.6.1 X 118 | syn@2.0.104 X X 119 | sync_wrapper@1.0.2 X 120 | synstructure@0.13.2 X 121 | tinystr@0.8.1 X 122 | tokio@1.47.1 X 123 | tokio-macros@2.5.0 X 124 | tokio-rustls@0.26.2 X X 125 | tokio-util@0.7.15 X 126 | tower@0.5.2 X 127 | tower-http@0.6.6 X 128 | tower-layer@0.3.3 X 129 | tower-service@0.3.3 X 130 | tracing@0.1.41 X 131 | tracing-attributes@0.1.30 X 132 | tracing-core@0.1.34 X 133 | trait-make@0.1.0 X X 134 | try-lock@0.2.5 X 135 | typenum@1.18.0 X X 136 | unicode-ident@1.0.18 X X X 137 | untrusted@0.9.0 X 138 | url@2.5.4 X X 139 | utf8_iter@1.0.4 X X 140 | uuid@1.17.0 X X 141 | version_check@0.9.5 X X 142 | want@0.3.1 X 143 | wasi@0.11.1+wasi-snapshot-preview1 X X X 144 | wasi@0.14.2+wasi-0.2.4 X X X 145 | wasm-bindgen@0.2.100 X X 146 | wasm-bindgen-backend@0.2.100 X X 147 | wasm-bindgen-futures@0.4.50 X X 148 | wasm-bindgen-macro@0.2.100 X X 149 | wasm-bindgen-macro-support@0.2.100 X X 150 | wasm-bindgen-shared@0.2.100 X X 151 | wasm-streams@0.4.2 X X 152 | web-sys@0.3.77 X X 153 | webpki-roots@1.0.1 X 154 | which@6.0.3 X 155 | windows-core@0.61.2 X X 156 | windows-implement@0.60.0 X X 157 | windows-interface@0.59.1 X X 158 | windows-link@0.1.3 X X 159 | windows-result@0.3.4 X X 160 | windows-strings@0.4.2 X X 161 | windows-sys@0.52.0 X X 162 | windows-sys@0.59.0 X X 163 | windows-sys@0.60.2 X X 164 | windows-targets@0.52.6 X X 165 | windows-targets@0.53.2 X X 166 | windows_aarch64_gnullvm@0.52.6 X X 167 | windows_aarch64_gnullvm@0.53.0 X X 168 | windows_aarch64_msvc@0.52.6 X X 169 | windows_aarch64_msvc@0.53.0 X X 170 | windows_i686_gnu@0.52.6 X X 171 | windows_i686_gnu@0.53.0 X X 172 | windows_i686_gnullvm@0.52.6 X X 173 | windows_i686_gnullvm@0.53.0 X X 174 | windows_i686_msvc@0.52.6 X X 175 | windows_i686_msvc@0.53.0 X X 176 | windows_x86_64_gnu@0.52.6 X X 177 | windows_x86_64_gnu@0.53.0 X X 178 | windows_x86_64_gnullvm@0.52.6 X X 179 | windows_x86_64_gnullvm@0.53.0 X X 180 | windows_x86_64_msvc@0.52.6 X X 181 | windows_x86_64_msvc@0.53.0 X X 182 | winsafe@0.0.19 X 183 | wit-bindgen-rt@0.39.0 X X X 184 | writeable@0.6.1 X 185 | yoke@0.8.0 X 186 | yoke-derive@0.8.0 X 187 | zerofrom@0.1.6 X 188 | zerofrom-derive@0.1.6 X 189 | zeroize@1.8.1 X X 190 | zerotrie@0.2.2 X 191 | zerovec@0.11.2 X 192 | zerovec-derive@0.11.1 X 193 | -------------------------------------------------------------------------------- /DEPENDENCIES.rust.tsv: -------------------------------------------------------------------------------- 1 | crate 0BSD Apache-2.0 Apache-2.0 WITH LLVM-exception BSD-2-Clause BSD-3-Clause BSL-1.0 CDLA-Permissive-2.0 ISC LGPL-2.1-or-later MIT Unicode-3.0 Unlicense Zlib 2 | addr2line@0.24.2 X X 3 | adler2@2.0.0 X X X 4 | aho-corasick@1.1.3 X X 5 | android-tzdata@0.1.1 X X 6 | android_system_properties@0.1.5 X X 7 | anstream@0.6.18 X X 8 | anstyle@1.0.10 X X 9 | anstyle-parse@0.2.6 X X 10 | anstyle-query@1.1.2 X X 11 | anstyle-wincon@3.0.7 X X 12 | anyhow@1.0.98 X X 13 | async-notify@0.3.0 X 14 | async-trait@0.1.88 X X 15 | autocfg@1.4.0 X X 16 | backon@1.5.0 X 17 | backtrace@0.3.75 X X 18 | base64@0.22.1 X X 19 | bincode@1.3.3 X 20 | bitflags@2.9.1 X X 21 | block-buffer@0.10.4 X X 22 | bumpalo@3.17.0 X X 23 | bytes@1.10.1 X 24 | cc@1.2.23 X X 25 | cfg-if@1.0.0 X X 26 | cfg_aliases@0.2.1 X 27 | chrono@0.4.41 X X 28 | clap@4.5.40 X X 29 | clap_builder@4.5.40 X X 30 | clap_derive@4.5.40 X X 31 | clap_lex@0.7.4 X X 32 | cloud-filter@0.0.6 X 33 | cloud_filter_opendal@0.0.12 X 34 | colorchoice@1.0.3 X X 35 | concurrent-queue@2.5.0 X X 36 | const-oid@0.9.6 X X 37 | core-foundation-sys@0.8.7 X X 38 | cpufeatures@0.2.17 X X 39 | crc32c@0.6.8 X X 40 | crossbeam-utils@0.8.21 X X 41 | crypto-common@0.1.6 X X 42 | deranged@0.4.0 X X 43 | digest@0.10.7 X X 44 | displaydoc@0.2.5 X X 45 | dotenvy@0.15.7 X 46 | either@1.15.0 X X 47 | env_filter@0.1.3 X X 48 | errno@0.3.12 X X 49 | event-listener@4.0.3 X X 50 | fastrand@2.3.0 X X 51 | flagset@0.4.7 X 52 | fnv@1.0.7 X X 53 | form_urlencoded@1.2.1 X X 54 | fuse3@0.8.1 X 55 | fuse3_opendal@0.0.19 X 56 | futures@0.3.31 X X 57 | futures-channel@0.3.31 X X 58 | futures-core@0.3.31 X X 59 | futures-executor@0.3.31 X X 60 | futures-io@0.3.31 X X 61 | futures-macro@0.3.31 X X 62 | futures-sink@0.3.31 X X 63 | futures-task@0.3.31 X X 64 | futures-util@0.3.31 X X 65 | generic-array@0.14.7 X 66 | getrandom@0.2.16 X X 67 | getrandom@0.3.3 X X 68 | gimli@0.31.1 X X 69 | gloo-timers@0.3.0 X X 70 | heck@0.5.0 X X 71 | hex@0.4.3 X X 72 | hmac@0.12.1 X X 73 | home@0.5.11 X X 74 | http@1.3.1 X X 75 | http-body@1.0.1 X 76 | http-body-util@0.1.3 X 77 | httparse@1.10.1 X X 78 | hyper@1.6.0 X 79 | hyper-rustls@0.27.5 X X X 80 | hyper-util@0.1.12 X 81 | iana-time-zone@0.1.63 X X 82 | iana-time-zone-haiku@0.1.2 X X 83 | icu_collections@2.0.0 X 84 | icu_locale_core@2.0.0 X 85 | icu_normalizer@2.0.0 X 86 | icu_normalizer_data@2.0.0 X 87 | icu_properties@2.0.1 X 88 | icu_properties_data@2.0.1 X 89 | icu_provider@2.0.0 X 90 | idna@1.0.3 X X 91 | idna_adapter@1.2.1 X X 92 | io-uring@0.7.8 X X 93 | ipnet@2.11.0 X X 94 | iri-string@0.7.8 X X 95 | is_terminal_polyfill@1.70.1 X X 96 | itoa@1.0.15 X X 97 | jiff@0.2.14 X X 98 | jiff-tzdb@0.1.4 X X 99 | jiff-tzdb-platform@0.1.3 X X 100 | js-sys@0.3.77 X X 101 | lazy_static@1.5.0 X X 102 | libc@0.2.172 X X 103 | linux-raw-sys@0.4.15 X X X 104 | litemap@0.8.0 X 105 | log@0.4.27 X X 106 | logforth@0.27.0 X 107 | md-5@0.10.6 X X 108 | memchr@2.7.4 X X 109 | memoffset@0.9.1 X 110 | miniz_oxide@0.8.8 X X X 111 | mio@1.0.3 X 112 | nix@0.29.0 X 113 | nix@0.30.1 X 114 | nt-time@0.8.1 X X 115 | num-conv@0.1.0 X X 116 | num-traits@0.2.19 X X 117 | object@0.36.7 X X 118 | ofs@0.0.24 X 119 | once_cell@1.21.3 X X 120 | opendal@0.54.1 X 121 | parking@2.2.1 X X 122 | percent-encoding@2.3.1 X X 123 | pin-project-lite@0.2.16 X X 124 | pin-utils@0.1.0 X X 125 | portable-atomic@1.11.0 X X 126 | portable-atomic-util@0.2.4 X X 127 | potential_utf@0.1.2 X 128 | powerfmt@0.2.0 X X 129 | ppv-lite86@0.2.21 X X 130 | proc-macro2@1.0.95 X X 131 | quick-xml@0.37.5 X 132 | quick-xml@0.38.0 X 133 | quote@1.0.40 X X 134 | r-efi@5.2.0 X X X 135 | rand@0.8.5 X X 136 | rand_chacha@0.3.1 X X 137 | rand_core@0.6.4 X X 138 | regex@1.11.1 X X 139 | regex-automata@0.4.9 X X 140 | regex-syntax@0.8.5 X X 141 | reqsign@0.16.5 X 142 | reqwest@0.12.22 X X 143 | ring@0.17.14 X X 144 | rustc-demangle@0.1.24 X X 145 | rustc_version@0.4.1 X X 146 | rustix@0.38.44 X X X 147 | rustls@0.23.27 X X X 148 | rustls-pki-types@1.12.0 X X 149 | rustls-webpki@0.103.3 X 150 | rustversion@1.0.20 X X 151 | ryu@1.0.20 X X 152 | semver@1.0.26 X X 153 | serde@1.0.219 X X 154 | serde_derive@1.0.219 X X 155 | serde_json@1.0.140 X X 156 | serde_urlencoded@0.7.1 X X 157 | sha1@0.10.6 X X 158 | sha2@0.10.9 X X 159 | sharded-slab@0.1.7 X 160 | shlex@1.3.0 X X 161 | signal-hook-registry@1.4.5 X X 162 | slab@0.4.9 X 163 | smallvec@1.15.0 X X 164 | socket2@0.5.9 X X 165 | socket2@0.6.0 X X 166 | stable_deref_trait@1.2.0 X X 167 | strsim@0.11.1 X 168 | subtle@2.6.1 X 169 | syn@2.0.101 X X 170 | sync_wrapper@1.0.2 X 171 | synstructure@0.13.2 X 172 | time@0.3.41 X X 173 | time-core@0.1.4 X X 174 | time-macros@0.2.22 X X 175 | tinystr@0.8.1 X 176 | tokio@1.47.0 X 177 | tokio-macros@2.5.0 X 178 | tokio-rustls@0.26.2 X X 179 | tokio-util@0.7.15 X 180 | tower@0.5.2 X 181 | tower-http@0.6.6 X 182 | tower-layer@0.3.3 X 183 | tower-service@0.3.3 X 184 | tracing@0.1.41 X 185 | tracing-attributes@0.1.28 X 186 | tracing-core@0.1.33 X 187 | trait-make@0.1.0 X X 188 | try-lock@0.2.5 X 189 | typenum@1.18.0 X X 190 | unicode-ident@1.0.18 X X X 191 | untrusted@0.9.0 X 192 | url@2.5.4 X X 193 | utf8_iter@1.0.4 X X 194 | utf8parse@0.2.2 X X 195 | uuid@1.17.0 X X 196 | version_check@0.9.5 X X 197 | want@0.3.1 X 198 | wasi@0.11.0+wasi-snapshot-preview1 X X X 199 | wasi@0.14.2+wasi-0.2.4 X X X 200 | wasm-bindgen@0.2.100 X X 201 | wasm-bindgen-backend@0.2.100 X X 202 | wasm-bindgen-futures@0.4.50 X X 203 | wasm-bindgen-macro@0.2.100 X X 204 | wasm-bindgen-macro-support@0.2.100 X X 205 | wasm-bindgen-shared@0.2.100 X X 206 | wasm-streams@0.4.2 X X 207 | web-sys@0.3.77 X X 208 | webpki-roots@0.26.11 X 209 | webpki-roots@1.0.0 X 210 | which@6.0.3 X 211 | widestring@1.2.0 X X 212 | windows@0.58.0 X X 213 | windows-core@0.58.0 X X 214 | windows-core@0.61.2 X X 215 | windows-implement@0.58.0 X X 216 | windows-implement@0.60.0 X X 217 | windows-interface@0.58.0 X X 218 | windows-interface@0.59.1 X X 219 | windows-link@0.1.1 X X 220 | windows-result@0.2.0 X X 221 | windows-result@0.3.4 X X 222 | windows-strings@0.1.0 X X 223 | windows-strings@0.4.2 X X 224 | windows-sys@0.52.0 X X 225 | windows-sys@0.59.0 X X 226 | windows-targets@0.52.6 X X 227 | windows_aarch64_gnullvm@0.52.6 X X 228 | windows_aarch64_msvc@0.52.6 X X 229 | windows_i686_gnu@0.52.6 X X 230 | windows_i686_gnullvm@0.52.6 X X 231 | windows_i686_msvc@0.52.6 X X 232 | windows_x86_64_gnu@0.52.6 X X 233 | windows_x86_64_gnullvm@0.52.6 X X 234 | windows_x86_64_msvc@0.52.6 X X 235 | winsafe@0.0.19 X 236 | wit-bindgen-rt@0.39.0 X X X 237 | writeable@0.6.1 X 238 | yoke@0.8.0 X 239 | yoke-derive@0.8.0 X 240 | zerocopy@0.8.25 X X X 241 | zerofrom@0.1.6 X 242 | zerofrom-derive@0.1.6 X 243 | zeroize@1.8.1 X X 244 | zerotrie@0.2.2 X 245 | zerovec@0.11.2 X 246 | zerovec-derive@0.11.1 X 247 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /integrations/cloud_filter/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The ASF licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | //! `cloud_filter_opendal` integrates OpenDAL with [cloud sync engines](https://learn.microsoft.com/en-us/windows/win32/cfapi/build-a-cloud-file-sync-engine). 19 | //! It provides a way to access various cloud storage on Windows. 20 | //! 21 | //! Note that `cloud_filter_opendal` is a read-only service, and it is not recommended to use it in production. 22 | //! 23 | //! # Example 24 | //! 25 | //! ```no_run 26 | //! use anyhow::Result; 27 | //! use cloud_filter::root::PopulationType; 28 | //! use cloud_filter::root::SecurityId; 29 | //! use cloud_filter::root::Session; 30 | //! use cloud_filter::root::SyncRootIdBuilder; 31 | //! use cloud_filter::root::SyncRootInfo; 32 | //! use opendal::services; 33 | //! use opendal::Operator; 34 | //! use tokio::runtime::Handle; 35 | //! use tokio::signal; 36 | //! 37 | //! #[tokio::main] 38 | //! async fn main() -> Result<()> { 39 | //! // Create any service desired 40 | //! let op = Operator::from_iter::([ 41 | //! ("bucket".to_string(), "my_bucket".to_string()), 42 | //! ("access_key".to_string(), "my_access_key".to_string()), 43 | //! ("secret_key".to_string(), "my_secret_key".to_string()), 44 | //! ("endpoint".to_string(), "my_endpoint".to_string()), 45 | //! ("region".to_string(), "my_region".to_string()), 46 | //! ])? 47 | //! .finish(); 48 | //! 49 | //! let client_path = std::env::var("CLIENT_PATH").expect("$CLIENT_PATH is set"); 50 | //! 51 | //! // Create a sync root id 52 | //! let sync_root_id = SyncRootIdBuilder::new("cloud_filter_opendal") 53 | //! .user_security_id(SecurityId::current_user()?) 54 | //! .build(); 55 | //! 56 | //! // Register the sync root if not exists 57 | //! if !sync_root_id.is_registered()? { 58 | //! sync_root_id.register( 59 | //! SyncRootInfo::default() 60 | //! .with_display_name("OpenDAL Cloud Filter") 61 | //! .with_population_type(PopulationType::Full) 62 | //! .with_icon("shell32.dll,3") 63 | //! .with_version("1.0.0") 64 | //! .with_recycle_bin_uri("http://cloudmirror.example.com/recyclebin")? 65 | //! .with_path(&client_path)?, 66 | //! )?; 67 | //! } 68 | //! 69 | //! let handle = Handle::current(); 70 | //! let connection = Session::new().connect_async( 71 | //! &client_path, 72 | //! cloud_filter_opendal::CloudFilter::new(op, client_path.clone().into()), 73 | //! move |f| handle.block_on(f), 74 | //! )?; 75 | //! 76 | //! signal::ctrl_c().await?; 77 | //! 78 | //! // Drop the connection before unregister the sync root 79 | //! drop(connection); 80 | //! sync_root_id.unregister()?; 81 | //! 82 | //! Ok(()) 83 | //! } 84 | //! `````` 85 | 86 | mod file; 87 | 88 | use std::{ 89 | cmp::min, 90 | fs::{self, File}, 91 | path::{Path, PathBuf}, 92 | }; 93 | 94 | use cloud_filter::{ 95 | error::{CResult, CloudErrorKind}, 96 | filter::{Filter, Request, info, ticket}, 97 | metadata::Metadata, 98 | placeholder::{ConvertOptions, Placeholder}, 99 | placeholder_file::PlaceholderFile, 100 | utility::{FileTime, WriteAt}, 101 | }; 102 | use file::FileBlob; 103 | use futures::StreamExt; 104 | use opendal::{Entry, Operator}; 105 | 106 | const BUF_SIZE: usize = 65536; 107 | 108 | /// CloudFilter is a adapter that adapts Windows cloud sync engines. 109 | pub struct CloudFilter { 110 | op: Operator, 111 | root: PathBuf, 112 | } 113 | 114 | impl CloudFilter { 115 | /// Create a new CloudFilter. 116 | pub fn new(op: Operator, root: PathBuf) -> Self { 117 | Self { op, root } 118 | } 119 | } 120 | 121 | impl Filter for CloudFilter { 122 | async fn fetch_data( 123 | &self, 124 | request: Request, 125 | ticket: ticket::FetchData, 126 | info: info::FetchData, 127 | ) -> CResult<()> { 128 | log::debug!("fetch_data: {}", request.path().display()); 129 | 130 | let _blob = bincode::deserialize::(request.file_blob()).map_err(|e| { 131 | log::warn!("failed to deserialize file blob: {e}"); 132 | CloudErrorKind::ValidationFailed 133 | })?; 134 | 135 | let range = info.required_file_range(); 136 | let path = request.path(); 137 | let remote_path = path 138 | .strip_prefix(&self.root) 139 | .map_err(|_| CloudErrorKind::NotUnderSyncRoot)?; 140 | 141 | let reader = self 142 | .op 143 | .reader_with(&remote_path.to_string_lossy().replace('\\', "/")) 144 | .await 145 | .map_err(|e| { 146 | log::warn!("failed to open file: {e}"); 147 | CloudErrorKind::Unsuccessful 148 | })?; 149 | 150 | let mut position = range.start; 151 | let mut buffer = Vec::with_capacity(BUF_SIZE); 152 | 153 | loop { 154 | let mut bytes_read = reader 155 | .read_into( 156 | &mut buffer, 157 | position..min(range.end, position + BUF_SIZE as u64), 158 | ) 159 | .await 160 | .map_err(|e| { 161 | log::warn!("failed to read file: {e}"); 162 | CloudErrorKind::Unsuccessful 163 | })?; 164 | 165 | let unaligned = bytes_read % 4096; 166 | if unaligned != 0 && position + (bytes_read as u64) < range.end { 167 | bytes_read -= unaligned; 168 | } 169 | 170 | ticket 171 | .write_at(&buffer[..bytes_read], position) 172 | .map_err(|e| { 173 | log::warn!("failed to write file: {e}"); 174 | CloudErrorKind::Unsuccessful 175 | })?; 176 | position += bytes_read as u64; 177 | 178 | if position >= range.end { 179 | break; 180 | } 181 | 182 | buffer.clear(); 183 | 184 | ticket.report_progress(range.end, position).map_err(|e| { 185 | log::warn!("failed to report progress: {e}"); 186 | CloudErrorKind::Unsuccessful 187 | })?; 188 | } 189 | 190 | Ok(()) 191 | } 192 | 193 | async fn fetch_placeholders( 194 | &self, 195 | request: Request, 196 | ticket: ticket::FetchPlaceholders, 197 | _info: info::FetchPlaceholders, 198 | ) -> CResult<()> { 199 | log::debug!("fetch_placeholders: {}", request.path().display()); 200 | 201 | let absolute = request.path(); 202 | let mut remote_path = absolute 203 | .strip_prefix(&self.root) 204 | .map_err(|_| CloudErrorKind::NotUnderSyncRoot)? 205 | .to_owned(); 206 | remote_path.push(""); 207 | 208 | let now = FileTime::now(); 209 | let mut entries = self 210 | .op 211 | .lister_with(&remote_path.to_string_lossy().replace('\\', "/")) 212 | .await 213 | .map_err(|e| { 214 | log::warn!("failed to list files: {e}"); 215 | CloudErrorKind::Unsuccessful 216 | })? 217 | .filter_map(|e| async { 218 | let entry = e.ok()?; 219 | let metadata = self.op.stat(entry.path()).await.ok()?; 220 | let entry_remote_path = PathBuf::from(entry.path()); 221 | let relative_path = entry_remote_path 222 | .strip_prefix(&remote_path) 223 | .expect("valid path"); 224 | check_in_sync(&entry, &self.root).then(|| { 225 | PlaceholderFile::new(relative_path) 226 | .metadata( 227 | match entry.metadata().is_dir() { 228 | true => Metadata::directory(), 229 | false => Metadata::file(), 230 | } 231 | .size(metadata.content_length()) 232 | .written( 233 | FileTime::from_unix_time( 234 | metadata 235 | .last_modified() 236 | .unwrap_or_default() 237 | .into_inner() 238 | .as_second(), 239 | ) 240 | .expect("valid time"), 241 | ) 242 | .created(now), 243 | ) 244 | .mark_in_sync() 245 | .blob( 246 | bincode::serialize(&FileBlob { 247 | ..Default::default() 248 | }) 249 | .expect("valid blob"), 250 | ) 251 | }) 252 | }) 253 | .collect::>() 254 | .await; 255 | 256 | _ = ticket.pass_with_placeholder(&mut entries).map_err(|e| { 257 | log::warn!("failed to pass placeholder: {e:?}"); 258 | }); 259 | 260 | Ok(()) 261 | } 262 | } 263 | 264 | /// Checks if the entry is in sync, then convert to placeholder. 265 | /// 266 | /// Returns `true` if the entry is not exists, `false` otherwise. 267 | fn check_in_sync(entry: &Entry, root: &Path) -> bool { 268 | let absolute = root.join(entry.path()); 269 | 270 | let Ok(metadata) = fs::metadata(&absolute) else { 271 | return true; 272 | }; 273 | 274 | if metadata.is_dir() != entry.metadata().is_dir() { 275 | return false; 276 | } else if metadata.is_file() { 277 | // FIXME: checksum 278 | if entry.metadata().content_length() != metadata.len() { 279 | return false; 280 | } 281 | } 282 | 283 | if metadata.is_dir() { 284 | let mut placeholder = Placeholder::open(absolute).unwrap(); 285 | _ = placeholder 286 | .convert_to_placeholder( 287 | ConvertOptions::default() 288 | .mark_in_sync() 289 | .has_children() 290 | .blob( 291 | bincode::serialize(&FileBlob { 292 | ..Default::default() 293 | }) 294 | .expect("valid blob"), 295 | ), 296 | None, 297 | ) 298 | .map_err(|e| { 299 | log::error!("failed to convert to placeholder: {e:?}"); 300 | }); 301 | } else { 302 | let mut placeholder = Placeholder::from(File::open(absolute).unwrap()); 303 | _ = placeholder 304 | .convert_to_placeholder( 305 | ConvertOptions::default().mark_in_sync().blob( 306 | bincode::serialize(&FileBlob { 307 | ..Default::default() 308 | }) 309 | .expect("valid blob"), 310 | ), 311 | None, 312 | ) 313 | .map_err(|e| log::error!("failed to convert to placeholder: {e:?}")); 314 | } 315 | 316 | false 317 | } 318 | -------------------------------------------------------------------------------- /integrations/fuse3/src/file_system.rs: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The ASF licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | use std::ffi::OsStr; 19 | use std::num::NonZeroU32; 20 | use std::path::PathBuf; 21 | use std::sync::Arc; 22 | use std::time::Duration; 23 | use std::time::SystemTime; 24 | 25 | use bytes::Bytes; 26 | use fuse3::Errno; 27 | use fuse3::Result; 28 | use fuse3::path::prelude::*; 29 | use futures_util::StreamExt; 30 | use futures_util::stream; 31 | use futures_util::stream::BoxStream; 32 | use opendal::EntryMode; 33 | use opendal::ErrorKind; 34 | use opendal::Metadata; 35 | use opendal::Operator; 36 | use opendal::raw::normalize_path; 37 | use sharded_slab::Slab; 38 | use tokio::sync::Mutex; 39 | 40 | use super::file::FileKey; 41 | use super::file::InnerWriter; 42 | use super::file::OpenedFile; 43 | 44 | const TTL: Duration = Duration::from_secs(1); // 1 second 45 | 46 | /// `Filesystem` represents the filesystem that implements [`PathFilesystem`] by opendal. 47 | /// 48 | /// `Filesystem` must be used along with `fuse3`'s `Session` like the following: 49 | /// 50 | /// ``` 51 | /// use fuse3::path::Session; 52 | /// use fuse3::MountOptions; 53 | /// use fuse3::Result; 54 | /// use fuse3_opendal::Filesystem; 55 | /// use opendal::services::Memory; 56 | /// use opendal::Operator; 57 | /// 58 | /// #[tokio::test] 59 | /// async fn test() -> Result<()> { 60 | /// // Build opendal Operator. 61 | /// let op = Operator::new(Memory::default())?.finish(); 62 | /// 63 | /// // Build fuse3 file system. 64 | /// let fs = Filesystem::new(op, 1000, 1000); 65 | /// 66 | /// // Configure mount options. 67 | /// let mount_options = MountOptions::default(); 68 | /// 69 | /// // Start a fuse3 session and mount it. 70 | /// let mut mount_handle = Session::new(mount_options) 71 | /// .mount_with_unprivileged(fs, "/tmp/mount_test") 72 | /// .await?; 73 | /// let handle = &mut mount_handle; 74 | /// 75 | /// tokio::select! { 76 | /// res = handle => res?, 77 | /// _ = tokio::signal::ctrl_c() => { 78 | /// mount_handle.unmount().await? 79 | /// } 80 | /// } 81 | /// 82 | /// Ok(()) 83 | /// } 84 | /// ``` 85 | pub struct Filesystem { 86 | op: Operator, 87 | gid: u32, 88 | uid: u32, 89 | 90 | opened_files: Slab, 91 | } 92 | 93 | impl Filesystem { 94 | /// Create a new filesystem with given operator, uid and gid. 95 | pub fn new(op: Operator, uid: u32, gid: u32) -> Self { 96 | Self { 97 | op, 98 | uid, 99 | gid, 100 | opened_files: Slab::new(), 101 | } 102 | } 103 | 104 | fn check_flags(&self, flags: u32) -> Result<(bool, bool, bool)> { 105 | let is_trunc = flags & libc::O_TRUNC as u32 != 0 || flags & libc::O_CREAT as u32 != 0; 106 | let is_append = flags & libc::O_APPEND as u32 != 0; 107 | 108 | let mode = flags & libc::O_ACCMODE as u32; 109 | let is_read = mode == libc::O_RDONLY as u32 || mode == libc::O_RDWR as u32; 110 | let is_write = mode == libc::O_WRONLY as u32 || mode == libc::O_RDWR as u32 || is_append; 111 | if !is_read && !is_write { 112 | Err(Errno::from(libc::EINVAL))?; 113 | } 114 | // OpenDAL only supports truncate write and append write, 115 | // so O_TRUNC or O_APPEND needs to be specified explicitly 116 | if (is_write && !is_trunc && !is_append) || is_trunc && !is_write { 117 | Err(Errno::from(libc::EINVAL))?; 118 | } 119 | 120 | let capability = self.op.info().full_capability(); 121 | if is_read && !capability.read { 122 | Err(Errno::from(libc::EACCES))?; 123 | } 124 | if is_trunc && !capability.write { 125 | Err(Errno::from(libc::EACCES))?; 126 | } 127 | if is_append && !capability.write_can_append { 128 | Err(Errno::from(libc::EACCES))?; 129 | } 130 | 131 | log::trace!( 132 | "check_flags: is_read={is_read}, is_write={is_write}, is_trunc={is_trunc}, is_append={is_append}" 133 | ); 134 | Ok((is_read, is_trunc, is_append)) 135 | } 136 | 137 | // Get opened file and check given path 138 | fn get_opened_file( 139 | &self, 140 | key: FileKey, 141 | path: Option<&OsStr>, 142 | ) -> Result> { 143 | let file = self 144 | .opened_files 145 | .get(key.0) 146 | .ok_or(Errno::from(libc::ENOENT))?; 147 | 148 | if matches!(path, Some(path) if path != file.path) { 149 | log::trace!( 150 | "get_opened_file: path not match: path={:?}, file={:?}", 151 | path, 152 | file.path 153 | ); 154 | Err(Errno::from(libc::EBADF))?; 155 | } 156 | 157 | Ok(file) 158 | } 159 | } 160 | 161 | impl PathFilesystem for Filesystem { 162 | // Init a fuse filesystem 163 | async fn init(&self, _req: Request) -> Result { 164 | Ok(ReplyInit { 165 | max_write: NonZeroU32::new(16 * 1024).unwrap(), 166 | }) 167 | } 168 | 169 | // Callback when fs is being destroyed 170 | async fn destroy(&self, _req: Request) {} 171 | 172 | async fn lookup(&self, _req: Request, parent: &OsStr, name: &OsStr) -> Result { 173 | log::debug!("lookup(parent={parent:?}, name={name:?})"); 174 | 175 | let path = PathBuf::from(parent).join(name); 176 | let metadata = self 177 | .op 178 | .stat(&path.to_string_lossy()) 179 | .await 180 | .map_err(opendal_error2errno)?; 181 | 182 | let now = SystemTime::now(); 183 | let attr = metadata2file_attr(&metadata, now, self.uid, self.gid); 184 | 185 | Ok(ReplyEntry { ttl: TTL, attr }) 186 | } 187 | 188 | async fn getattr( 189 | &self, 190 | _req: Request, 191 | path: Option<&OsStr>, 192 | fh: Option, 193 | flags: u32, 194 | ) -> Result { 195 | log::debug!("getattr(path={path:?}, fh={fh:?}, flags={flags:?})"); 196 | 197 | let fh_path = fh.and_then(|fh| { 198 | self.opened_files 199 | .get(FileKey::try_from(fh).ok()?.0) 200 | .map(|f| f.path.clone()) 201 | }); 202 | 203 | let file_path = match (path.map(Into::into), fh_path) { 204 | (Some(a), Some(b)) => { 205 | if a != b { 206 | Err(Errno::from(libc::EBADF))?; 207 | } 208 | Some(a) 209 | } 210 | (a, b) => a.or(b), 211 | }; 212 | 213 | let metadata = self 214 | .op 215 | .stat(&file_path.unwrap_or_default().to_string_lossy()) 216 | .await 217 | .map_err(opendal_error2errno)?; 218 | 219 | let now = SystemTime::now(); 220 | let attr = metadata2file_attr(&metadata, now, self.uid, self.gid); 221 | 222 | Ok(ReplyAttr { ttl: TTL, attr }) 223 | } 224 | 225 | async fn setattr( 226 | &self, 227 | _req: Request, 228 | path: Option<&OsStr>, 229 | fh: Option, 230 | set_attr: SetAttr, 231 | ) -> Result { 232 | log::debug!("setattr(path={path:?}, fh={fh:?}, set_attr={set_attr:?})"); 233 | 234 | self.getattr(_req, path, fh, 0).await 235 | } 236 | 237 | async fn symlink( 238 | &self, 239 | _req: Request, 240 | parent: &OsStr, 241 | name: &OsStr, 242 | link_path: &OsStr, 243 | ) -> Result { 244 | log::debug!("symlink(parent={parent:?}, name={name:?}, link_path={link_path:?})"); 245 | Err(libc::EOPNOTSUPP.into()) 246 | } 247 | 248 | async fn mknod( 249 | &self, 250 | _req: Request, 251 | parent: &OsStr, 252 | name: &OsStr, 253 | mode: u32, 254 | _rdev: u32, 255 | ) -> Result { 256 | log::debug!("mknod(parent={parent:?}, name={name:?}, mode=0o{mode:o})"); 257 | Err(libc::EOPNOTSUPP.into()) 258 | } 259 | 260 | async fn mkdir( 261 | &self, 262 | _req: Request, 263 | parent: &OsStr, 264 | name: &OsStr, 265 | mode: u32, 266 | _umask: u32, 267 | ) -> Result { 268 | log::debug!("mkdir(parent={parent:?}, name={name:?}, mode=0o{mode:o})"); 269 | 270 | let mut path = PathBuf::from(parent).join(name); 271 | path.push(""); // ref https://users.rust-lang.org/t/trailing-in-paths/43166 272 | self.op 273 | .create_dir(&path.to_string_lossy()) 274 | .await 275 | .map_err(opendal_error2errno)?; 276 | 277 | let now = SystemTime::now(); 278 | let attr = dummy_file_attr(FileType::Directory, now, self.uid, self.gid); 279 | 280 | Ok(ReplyEntry { ttl: TTL, attr }) 281 | } 282 | 283 | async fn unlink(&self, _req: Request, parent: &OsStr, name: &OsStr) -> Result<()> { 284 | log::debug!("unlink(parent={parent:?}, name={name:?})"); 285 | 286 | let path = PathBuf::from(parent).join(name); 287 | self.op 288 | .delete(&path.to_string_lossy()) 289 | .await 290 | .map_err(opendal_error2errno)?; 291 | 292 | Ok(()) 293 | } 294 | 295 | async fn rmdir(&self, _req: Request, parent: &OsStr, name: &OsStr) -> Result<()> { 296 | log::debug!("rmdir(parent={parent:?}, name={name:?})"); 297 | 298 | let path = PathBuf::from(parent).join(name); 299 | self.op 300 | .delete(&path.to_string_lossy()) 301 | .await 302 | .map_err(opendal_error2errno)?; 303 | 304 | Ok(()) 305 | } 306 | 307 | async fn rename( 308 | &self, 309 | _req: Request, 310 | origin_parent: &OsStr, 311 | origin_name: &OsStr, 312 | parent: &OsStr, 313 | name: &OsStr, 314 | ) -> Result<()> { 315 | log::debug!( 316 | "rename(p={origin_parent:?}, name={origin_name:?}, newp={parent:?}, newname={name:?})" 317 | ); 318 | 319 | if !self.op.info().full_capability().rename { 320 | return Err(Errno::from(libc::ENOTSUP))?; 321 | } 322 | 323 | let origin_path = PathBuf::from(origin_parent).join(origin_name); 324 | let path = PathBuf::from(parent).join(name); 325 | 326 | self.op 327 | .rename(&origin_path.to_string_lossy(), &path.to_string_lossy()) 328 | .await 329 | .map_err(opendal_error2errno)?; 330 | 331 | Ok(()) 332 | } 333 | 334 | async fn link( 335 | &self, 336 | _req: Request, 337 | path: &OsStr, 338 | new_parent: &OsStr, 339 | new_name: &OsStr, 340 | ) -> Result { 341 | log::debug!("link(path={path:?}, new_parent={new_parent:?}, new_name={new_name:?})"); 342 | Err(libc::EOPNOTSUPP.into()) 343 | } 344 | 345 | async fn opendir(&self, _req: Request, path: &OsStr, flags: u32) -> Result { 346 | log::debug!("opendir(path={path:?}, flags=0x{flags:x})"); 347 | Ok(ReplyOpen { fh: 0, flags }) 348 | } 349 | 350 | async fn open(&self, _req: Request, path: &OsStr, flags: u32) -> Result { 351 | log::debug!("open(path={path:?}, flags=0x{flags:x})"); 352 | 353 | let (is_read, is_trunc, is_append) = self.check_flags(flags)?; 354 | if flags & libc::O_CREAT as u32 != 0 { 355 | self.op 356 | .write(&path.to_string_lossy(), Bytes::new()) 357 | .await 358 | .map_err(opendal_error2errno)?; 359 | } 360 | 361 | let inner_writer = if is_trunc || is_append { 362 | let writer = self 363 | .op 364 | .writer_with(&path.to_string_lossy()) 365 | .append(is_append) 366 | .await 367 | .map_err(opendal_error2errno)?; 368 | let written = if is_append { 369 | self.op 370 | .stat(&path.to_string_lossy()) 371 | .await 372 | .map_err(opendal_error2errno)? 373 | .content_length() 374 | } else { 375 | 0 376 | }; 377 | Some(Arc::new(Mutex::new(InnerWriter { writer, written }))) 378 | } else { 379 | None 380 | }; 381 | 382 | let key = self 383 | .opened_files 384 | .insert(OpenedFile { 385 | path: path.into(), 386 | is_read, 387 | inner_writer, 388 | }) 389 | .ok_or(Errno::from(libc::EBUSY))?; 390 | 391 | Ok(ReplyOpen { 392 | fh: FileKey(key).to_fh(), 393 | flags, 394 | }) 395 | } 396 | 397 | async fn read( 398 | &self, 399 | _req: Request, 400 | path: Option<&OsStr>, 401 | fh: u64, 402 | offset: u64, 403 | size: u32, 404 | ) -> Result { 405 | log::debug!("read(path={path:?}, fh={fh}, offset={offset}, size={size})"); 406 | 407 | let file_path = { 408 | let file = self.get_opened_file(FileKey::try_from(fh)?, path)?; 409 | if !file.is_read { 410 | Err(Errno::from(libc::EACCES))?; 411 | } 412 | file.path.to_string_lossy().to_string() 413 | }; 414 | 415 | let data = self 416 | .op 417 | .read_with(&file_path) 418 | .range(offset..) 419 | .await 420 | .map_err(opendal_error2errno)?; 421 | 422 | Ok(ReplyData { 423 | data: data.to_bytes(), 424 | }) 425 | } 426 | 427 | async fn write( 428 | &self, 429 | _req: Request, 430 | path: Option<&OsStr>, 431 | fh: u64, 432 | offset: u64, 433 | data: &[u8], 434 | _write_flags: u32, 435 | flags: u32, 436 | ) -> Result { 437 | log::debug!( 438 | "write(path={:?}, fh={}, offset={}, data_len={}, flags=0x{:x})", 439 | path, 440 | fh, 441 | offset, 442 | data.len(), 443 | flags 444 | ); 445 | 446 | let Some(inner_writer) = ({ 447 | self.get_opened_file(FileKey::try_from(fh)?, path)? 448 | .inner_writer 449 | .clone() 450 | }) else { 451 | Err(Errno::from(libc::EACCES))? 452 | }; 453 | 454 | let mut inner = inner_writer.lock().await; 455 | // OpenDAL doesn't support random write 456 | if offset != inner.written { 457 | Err(Errno::from(libc::EINVAL))?; 458 | } 459 | 460 | inner 461 | .writer 462 | .write_from(data) 463 | .await 464 | .map_err(opendal_error2errno)?; 465 | inner.written += data.len() as u64; 466 | 467 | Ok(ReplyWrite { 468 | written: data.len() as _, 469 | }) 470 | } 471 | 472 | async fn release( 473 | &self, 474 | _req: Request, 475 | path: Option<&OsStr>, 476 | fh: u64, 477 | flags: u32, 478 | lock_owner: u64, 479 | flush: bool, 480 | ) -> Result<()> { 481 | log::debug!( 482 | "release(path={path:?}, fh={fh}, flags=0x{flags:x}, lock_owner={lock_owner}, flush={flush})" 483 | ); 484 | 485 | // Just take and forget it. 486 | let _ = self.opened_files.take(FileKey::try_from(fh)?.0); 487 | Ok(()) 488 | } 489 | 490 | /// In design, flush could be called multiple times for a single open. But there is the only 491 | /// place that we can handle the write operations. 492 | /// 493 | /// So we only support the use case that flush only be called once. 494 | async fn flush( 495 | &self, 496 | _req: Request, 497 | path: Option<&OsStr>, 498 | fh: u64, 499 | lock_owner: u64, 500 | ) -> Result<()> { 501 | log::debug!("flush(path={path:?}, fh={fh}, lock_owner={lock_owner})"); 502 | 503 | let file = self 504 | .opened_files 505 | .take(FileKey::try_from(fh)?.0) 506 | .ok_or(Errno::from(libc::EBADF))?; 507 | 508 | if let Some(inner_writer) = file.inner_writer { 509 | let mut lock = inner_writer.lock().await; 510 | let res = lock.writer.close().await.map_err(opendal_error2errno); 511 | return res.map(|_| ()); 512 | } 513 | 514 | if matches!(path, Some(ref p) if p != &file.path) { 515 | Err(Errno::from(libc::EBADF))?; 516 | } 517 | 518 | Ok(()) 519 | } 520 | 521 | type DirEntryStream<'a> = BoxStream<'a, Result>; 522 | 523 | async fn readdir<'a>( 524 | &'a self, 525 | _req: Request, 526 | path: &'a OsStr, 527 | fh: u64, 528 | offset: i64, 529 | ) -> Result>> { 530 | log::debug!("readdir(path={path:?}, fh={fh}, offset={offset})"); 531 | 532 | let mut current_dir = PathBuf::from(path); 533 | current_dir.push(""); // ref https://users.rust-lang.org/t/trailing-in-paths/43166 534 | let path = current_dir.to_string_lossy().to_string(); 535 | let children = self 536 | .op 537 | .lister(¤t_dir.to_string_lossy()) 538 | .await 539 | .map_err(opendal_error2errno)? 540 | .filter_map(move |entry| { 541 | let dir = normalize_path(path.as_str()); 542 | async move { 543 | match entry { 544 | Ok(e) if e.path() == dir => None, 545 | _ => Some(entry), 546 | } 547 | } 548 | }) 549 | .enumerate() 550 | .map(|(i, entry)| { 551 | entry 552 | .map(|e| DirectoryEntry { 553 | kind: entry_mode2file_type(e.metadata().mode()), 554 | name: e.name().trim_matches('/').into(), 555 | offset: (i + 3) as i64, 556 | }) 557 | .map_err(opendal_error2errno) 558 | }); 559 | 560 | let relative_paths = stream::iter([ 561 | Result::Ok(DirectoryEntry { 562 | kind: FileType::Directory, 563 | name: ".".into(), 564 | offset: 1, 565 | }), 566 | Result::Ok(DirectoryEntry { 567 | kind: FileType::Directory, 568 | name: "..".into(), 569 | offset: 2, 570 | }), 571 | ]); 572 | 573 | Ok(ReplyDirectory { 574 | entries: relative_paths.chain(children).skip(offset as usize).boxed(), 575 | }) 576 | } 577 | 578 | async fn access(&self, _req: Request, path: &OsStr, mask: u32) -> Result<()> { 579 | log::debug!("access(path={path:?}, mask=0x{mask:x})"); 580 | 581 | self.op 582 | .stat(&path.to_string_lossy()) 583 | .await 584 | .map_err(opendal_error2errno)?; 585 | 586 | Ok(()) 587 | } 588 | 589 | async fn create( 590 | &self, 591 | _req: Request, 592 | parent: &OsStr, 593 | name: &OsStr, 594 | mode: u32, 595 | flags: u32, 596 | ) -> Result { 597 | log::debug!("create(parent={parent:?}, name={name:?}, mode=0o{mode:o}, flags=0x{flags:x})"); 598 | 599 | let (is_read, is_trunc, is_append) = self.check_flags(flags | libc::O_CREAT as u32)?; 600 | 601 | let path = PathBuf::from(parent).join(name); 602 | 603 | let inner_writer = if is_trunc || is_append { 604 | let writer = self 605 | .op 606 | .writer_with(&path.to_string_lossy()) 607 | .chunk(4 * 1024 * 1024) 608 | .append(is_append) 609 | .await 610 | .map_err(opendal_error2errno)?; 611 | Some(Arc::new(Mutex::new(InnerWriter { writer, written: 0 }))) 612 | } else { 613 | None 614 | }; 615 | 616 | let now = SystemTime::now(); 617 | let attr = dummy_file_attr(FileType::RegularFile, now, self.uid, self.gid); 618 | 619 | let key = self 620 | .opened_files 621 | .insert(OpenedFile { 622 | path: path.into(), 623 | is_read, 624 | inner_writer, 625 | }) 626 | .ok_or(Errno::from(libc::EBUSY))?; 627 | 628 | Ok(ReplyCreated { 629 | ttl: TTL, 630 | attr, 631 | generation: 0, 632 | fh: FileKey(key).to_fh(), 633 | flags, 634 | }) 635 | } 636 | 637 | type DirEntryPlusStream<'a> = BoxStream<'a, Result>; 638 | 639 | async fn readdirplus<'a>( 640 | &'a self, 641 | _req: Request, 642 | parent: &'a OsStr, 643 | fh: u64, 644 | offset: u64, 645 | _lock_owner: u64, 646 | ) -> Result>> { 647 | log::debug!("readdirplus(parent={parent:?}, fh={fh}, offset={offset})"); 648 | 649 | let now = SystemTime::now(); 650 | let mut current_dir = PathBuf::from(parent); 651 | current_dir.push(""); // ref https://users.rust-lang.org/t/trailing-in-paths/43166 652 | let uid = self.uid; 653 | let gid = self.gid; 654 | 655 | let path = current_dir.to_string_lossy().to_string(); 656 | let children = self 657 | .op 658 | .lister_with(&path) 659 | .await 660 | .map_err(opendal_error2errno)? 661 | .filter_map(move |entry| { 662 | let dir = normalize_path(path.as_str()); 663 | async move { 664 | match entry { 665 | Ok(e) if e.path() == dir => None, 666 | _ => Some(entry), 667 | } 668 | } 669 | }) 670 | .enumerate() 671 | .map(move |(i, entry)| { 672 | entry 673 | .map(|e| { 674 | let metadata = e.metadata(); 675 | DirectoryEntryPlus { 676 | kind: entry_mode2file_type(metadata.mode()), 677 | name: e.name().trim_matches('/').into(), 678 | offset: (i + 3) as i64, 679 | attr: metadata2file_attr(metadata, now, uid, gid), 680 | entry_ttl: TTL, 681 | attr_ttl: TTL, 682 | } 683 | }) 684 | .map_err(opendal_error2errno) 685 | }); 686 | 687 | let relative_path_attr = dummy_file_attr(FileType::Directory, now, uid, gid); 688 | let relative_paths = stream::iter([ 689 | Result::Ok(DirectoryEntryPlus { 690 | kind: FileType::Directory, 691 | name: ".".into(), 692 | offset: 1, 693 | attr: relative_path_attr, 694 | entry_ttl: TTL, 695 | attr_ttl: TTL, 696 | }), 697 | Result::Ok(DirectoryEntryPlus { 698 | kind: FileType::Directory, 699 | name: "..".into(), 700 | offset: 2, 701 | attr: relative_path_attr, 702 | entry_ttl: TTL, 703 | attr_ttl: TTL, 704 | }), 705 | ]); 706 | 707 | Ok(ReplyDirectoryPlus { 708 | entries: relative_paths.chain(children).skip(offset as usize).boxed(), 709 | }) 710 | } 711 | 712 | async fn rename2( 713 | &self, 714 | req: Request, 715 | origin_parent: &OsStr, 716 | origin_name: &OsStr, 717 | parent: &OsStr, 718 | name: &OsStr, 719 | _flags: u32, 720 | ) -> Result<()> { 721 | log::debug!( 722 | "rename2(origin_parent={origin_parent:?}, origin_name={origin_name:?}, parent={parent:?}, name={name:?})" 723 | ); 724 | self.rename(req, origin_parent, origin_name, parent, name) 725 | .await 726 | } 727 | 728 | async fn copy_file_range( 729 | &self, 730 | req: Request, 731 | from_path: Option<&OsStr>, 732 | fh_in: u64, 733 | offset_in: u64, 734 | to_path: Option<&OsStr>, 735 | fh_out: u64, 736 | offset_out: u64, 737 | length: u64, 738 | flags: u64, 739 | ) -> Result { 740 | log::debug!( 741 | "copy_file_range(from_path={from_path:?}, fh_in={fh_in}, offset_in={offset_in}, to_path={to_path:?}, fh_out={fh_out}, offset_out={offset_out}, length={length}, flags={flags})" 742 | ); 743 | let data = self 744 | .read(req, from_path, fh_in, offset_in, length as _) 745 | .await?; 746 | 747 | let ReplyWrite { written } = self 748 | .write(req, to_path, fh_out, offset_out, &data.data, 0, flags as _) 749 | .await?; 750 | 751 | Ok(ReplyCopyFileRange { 752 | copied: u64::from(written), 753 | }) 754 | } 755 | 756 | async fn statfs(&self, _req: Request, path: &OsStr) -> Result { 757 | log::debug!("statfs(path={path:?})"); 758 | Ok(ReplyStatFs { 759 | blocks: 1, 760 | bfree: 0, 761 | bavail: 0, 762 | files: 1, 763 | ffree: 0, 764 | bsize: 4096, 765 | namelen: u32::MAX, 766 | frsize: 0, 767 | }) 768 | } 769 | } 770 | 771 | const fn entry_mode2file_type(mode: EntryMode) -> FileType { 772 | match mode { 773 | EntryMode::DIR => FileType::Directory, 774 | _ => FileType::RegularFile, 775 | } 776 | } 777 | 778 | fn metadata2file_attr(metadata: &Metadata, atime: SystemTime, uid: u32, gid: u32) -> FileAttr { 779 | let last_modified = match metadata.last_modified() { 780 | None => atime, 781 | Some(ts) => ts.into(), 782 | }; 783 | let kind = entry_mode2file_type(metadata.mode()); 784 | FileAttr { 785 | size: metadata.content_length(), 786 | mtime: last_modified, 787 | ctime: last_modified, 788 | ..dummy_file_attr(kind, atime, uid, gid) 789 | } 790 | } 791 | 792 | const fn dummy_file_attr(kind: FileType, now: SystemTime, uid: u32, gid: u32) -> FileAttr { 793 | FileAttr { 794 | size: 0, 795 | blocks: 0, 796 | atime: now, 797 | mtime: now, 798 | ctime: now, 799 | kind, 800 | perm: fuse3::perm_from_mode_and_kind(kind, 0o775), 801 | nlink: 0, 802 | uid, 803 | gid, 804 | rdev: 0, 805 | blksize: 4096, 806 | #[cfg(target_os = "macos")] 807 | crtime: now, 808 | #[cfg(target_os = "macos")] 809 | flags: 0, 810 | } 811 | } 812 | 813 | fn opendal_error2errno(err: opendal::Error) -> fuse3::Errno { 814 | log::trace!("opendal_error2errno: {err:?}"); 815 | match err.kind() { 816 | ErrorKind::Unsupported => Errno::from(libc::EOPNOTSUPP), 817 | ErrorKind::IsADirectory => Errno::from(libc::EISDIR), 818 | ErrorKind::NotFound => Errno::from(libc::ENOENT), 819 | ErrorKind::PermissionDenied => Errno::from(libc::EACCES), 820 | ErrorKind::AlreadyExists => Errno::from(libc::EEXIST), 821 | ErrorKind::NotADirectory => Errno::from(libc::ENOTDIR), 822 | ErrorKind::RangeNotSatisfied => Errno::from(libc::EINVAL), 823 | ErrorKind::RateLimited => Errno::from(libc::EBUSY), 824 | _ => Errno::from(libc::ENOENT), 825 | } 826 | } 827 | --------------------------------------------------------------------------------