This is page 49 of 55. Use http://codebase.md/apache/opendal?page={x} to view the full context.
# Directory Structure
```
├── .asf.yaml
├── .config
│ └── nextest.toml
├── .devcontainer
│ ├── devcontainer.json
│ └── post_create.sh
├── .editorconfig
├── .env.example
├── .gitattributes
├── .github
│ ├── actions
│ │ ├── fuzz_test
│ │ │ └── action.yaml
│ │ ├── setup
│ │ │ └── action.yaml
│ │ ├── setup-hadoop
│ │ │ └── action.yaml
│ │ ├── setup-ocaml
│ │ │ └── action.yaml
│ │ ├── test_behavior_binding_c
│ │ │ └── action.yaml
│ │ ├── test_behavior_binding_cpp
│ │ │ └── action.yaml
│ │ ├── test_behavior_binding_go
│ │ │ └── action.yaml
│ │ ├── test_behavior_binding_java
│ │ │ └── action.yaml
│ │ ├── test_behavior_binding_nodejs
│ │ │ └── action.yaml
│ │ ├── test_behavior_binding_python
│ │ │ └── action.yaml
│ │ ├── test_behavior_core
│ │ │ └── action.yaml
│ │ └── test_behavior_integration_object_store
│ │ └── action.yml
│ ├── CODEOWNERS
│ ├── dependabot.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── 1-bug-report.yml
│ │ ├── 2-feature-request.yml
│ │ ├── 3-new-release.md
│ │ └── config.yml
│ ├── pull_request_template.md
│ ├── release.yml
│ ├── scripts
│ │ ├── test_behavior
│ │ │ ├── __init__.py
│ │ │ ├── plan.py
│ │ │ └── test_plan.py
│ │ ├── test_go_binding
│ │ │ ├── generate_test_scheme.py
│ │ │ └── matrix.yaml
│ │ └── weekly_update
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── main.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── services
│ │ ├── aliyun_drive
│ │ │ └── aliyun_drive
│ │ │ └── disable_action.yml
│ │ ├── alluxio
│ │ │ └── alluxio
│ │ │ └── action.yml
│ │ ├── azblob
│ │ │ ├── azure_azblob
│ │ │ │ └── action.yml
│ │ │ └── azurite_azblob
│ │ │ └── action.yml
│ │ ├── azdls
│ │ │ └── azdls
│ │ │ └── action.yml
│ │ ├── azfile
│ │ │ └── azfile
│ │ │ └── action.yml
│ │ ├── b2
│ │ │ └── b2
│ │ │ └── action.yml
│ │ ├── cacache
│ │ │ └── cacache
│ │ │ └── action.yml
│ │ ├── compfs
│ │ │ └── compfs
│ │ │ └── action.yml
│ │ ├── cos
│ │ │ └── cos
│ │ │ └── action.yml
│ │ ├── dashmap
│ │ │ └── dashmap
│ │ │ └── action.yml
│ │ ├── dropbox
│ │ │ └── dropbox
│ │ │ └── disable_action.yml
│ │ ├── etcd
│ │ │ ├── etcd
│ │ │ │ └── action.yml
│ │ │ ├── etcd-cluster
│ │ │ │ └── action.yml
│ │ │ └── etcd-tls
│ │ │ └── action.yml
│ │ ├── fs
│ │ │ └── local_fs
│ │ │ └── action.yml
│ │ ├── ftp
│ │ │ └── vsftpd
│ │ │ └── disable_action.yml
│ │ ├── gcs
│ │ │ ├── gcs
│ │ │ │ └── action.yml
│ │ │ └── gcs_with_default_storage_class
│ │ │ └── action.yml
│ │ ├── gdrive
│ │ │ └── gdrive
│ │ │ └── action.yml
│ │ ├── gridfs
│ │ │ ├── gridfs
│ │ │ │ └── action.yml
│ │ │ └── gridfs_with_basic_auth
│ │ │ └── action.yml
│ │ ├── hdfs
│ │ │ ├── hdfs_cluster
│ │ │ │ └── action.yml
│ │ │ ├── hdfs_cluster_with_atomic_write_dir
│ │ │ │ └── action.yml
│ │ │ ├── hdfs_default
│ │ │ │ └── action.yml
│ │ │ ├── hdfs_default_gcs
│ │ │ │ └── action.yml
│ │ │ ├── hdfs_default_on_azurite_azblob
│ │ │ │ └── action.yml
│ │ │ ├── hdfs_default_on_minio_s3
│ │ │ │ └── action.yml
│ │ │ └── hdfs_default_with_atomic_write_dir
│ │ │ └── action.yml
│ │ ├── hdfs_native
│ │ │ └── hdfs_native_cluster
│ │ │ └── action.yml
│ │ ├── http
│ │ │ ├── caddy
│ │ │ │ └── action.yml
│ │ │ └── nginx
│ │ │ └── action.yml
│ │ ├── huggingface
│ │ │ └── huggingface
│ │ │ └── action.yml
│ │ ├── koofr
│ │ │ └── koofr
│ │ │ └── disable_action.yml
│ │ ├── memcached
│ │ │ ├── memcached
│ │ │ │ └── action.yml
│ │ │ └── memcached_with_auth
│ │ │ └── action.yml
│ │ ├── memory
│ │ │ └── memory
│ │ │ └── action.yml
│ │ ├── mini_moka
│ │ │ └── mini_moka
│ │ │ └── action.yml
│ │ ├── moka
│ │ │ └── moka
│ │ │ └── action.yml
│ │ ├── mongodb
│ │ │ ├── mongodb_with_basic_auth
│ │ │ │ └── action.yml
│ │ │ └── mongodb_with_no_auth
│ │ │ └── action.yml
│ │ ├── monoiofs
│ │ │ └── monoiofs
│ │ │ └── action.yml
│ │ ├── mysql
│ │ │ └── mysql
│ │ │ └── action.yml
│ │ ├── oss
│ │ │ ├── oss
│ │ │ │ └── action.yml
│ │ │ └── oss_with_versioning
│ │ │ └── action.yml
│ │ ├── persy
│ │ │ └── persy
│ │ │ └── action.yml
│ │ ├── postgresql
│ │ │ └── postgresql
│ │ │ └── action.yml
│ │ ├── redb
│ │ │ └── redb
│ │ │ └── action.yml
│ │ ├── redis
│ │ │ ├── dragonfly
│ │ │ │ └── action.yml
│ │ │ ├── kvrocks
│ │ │ │ └── action.yml
│ │ │ ├── redis
│ │ │ │ └── action.yml
│ │ │ ├── redis_tls
│ │ │ │ └── action.yml
│ │ │ ├── redis_with_cluster
│ │ │ │ └── action.yml
│ │ │ └── redis_with_cluster_tls
│ │ │ └── action.yml
│ │ ├── rocksdb
│ │ │ └── rocksdb
│ │ │ └── action.yml
│ │ ├── s3
│ │ │ ├── 0_minio_s3
│ │ │ │ └── action.yml
│ │ │ ├── aws_s3
│ │ │ │ └── action.yml
│ │ │ ├── aws_s3_with_list_objects_v1
│ │ │ │ └── action.yml
│ │ │ ├── aws_s3_with_sse_c
│ │ │ │ └── action.yml
│ │ │ ├── aws_s3_with_versioning
│ │ │ │ └── action.yml
│ │ │ ├── aws_s3_with_virtual_host
│ │ │ │ └── action.yml
│ │ │ ├── ceph_radios_s3_with_versioning
│ │ │ │ └── disable_action.yml
│ │ │ ├── ceph_rados_s3
│ │ │ │ └── disable_action.yml
│ │ │ ├── minio_s3_with_anonymous
│ │ │ │ └── action.yml
│ │ │ ├── minio_s3_with_list_objects_v1
│ │ │ │ └── action.yml
│ │ │ ├── minio_s3_with_versioning
│ │ │ │ └── action.yml
│ │ │ └── r2
│ │ │ └── disabled_action.yml
│ │ ├── seafile
│ │ │ └── seafile
│ │ │ └── action.yml
│ │ ├── sftp
│ │ │ ├── sftp
│ │ │ │ └── action.yml
│ │ │ └── sftp_with_default_root
│ │ │ └── action.yml
│ │ ├── sled
│ │ │ ├── sled
│ │ │ │ └── action.yml
│ │ │ └── sled_with_tree
│ │ │ └── action.yml
│ │ ├── sqlite
│ │ │ └── sqlite
│ │ │ └── action.yml
│ │ ├── swift
│ │ │ ├── ceph_rados_swift
│ │ │ │ └── action.yml
│ │ │ └── swift
│ │ │ └── action.yml
│ │ ├── tikv
│ │ │ └── tikv
│ │ │ └── disable_action.yml
│ │ ├── webdav
│ │ │ ├── 0_nginx
│ │ │ │ └── action.yml
│ │ │ ├── jfrog
│ │ │ │ └── disabled_action.yml
│ │ │ ├── nextcloud
│ │ │ │ └── action.yml
│ │ │ ├── nginx_with_empty_password
│ │ │ │ └── action.yml
│ │ │ ├── nginx_with_password
│ │ │ │ └── action.yml
│ │ │ ├── nginx_with_redirect
│ │ │ │ └── action.yml
│ │ │ └── owncloud
│ │ │ └── action.yml
│ │ └── webhdfs
│ │ ├── webhdfs
│ │ │ └── action.yml
│ │ ├── webhdfs_with_list_batch_disabled
│ │ │ └── action.yml
│ │ └── webhdfs_with_user_name
│ │ └── action.yml
│ └── workflows
│ ├── ci_bindings_c.yml
│ ├── ci_bindings_cpp.yml
│ ├── ci_bindings_d.yml
│ ├── ci_bindings_dart.yml
│ ├── ci_bindings_dotnet.yml
│ ├── ci_bindings_go.yml
│ ├── ci_bindings_haskell.yml
│ ├── ci_bindings_java.yml
│ ├── ci_bindings_lua.yml
│ ├── ci_bindings_nodejs.yml
│ ├── ci_bindings_ocaml.yml
│ ├── ci_bindings_php.yml
│ ├── ci_bindings_python.yml
│ ├── ci_bindings_ruby.yml
│ ├── ci_bindings_swift.yml
│ ├── ci_bindings_zig.yml
│ ├── ci_check.yml
│ ├── ci_core.yml
│ ├── ci_integration_dav_server.yml
│ ├── ci_integration_object_store.yml
│ ├── ci_integration_parquet.yml
│ ├── ci_integration_spring.yml
│ ├── ci_integration_unftp_sbe.yml
│ ├── ci_odev.yml
│ ├── ci_weekly_update.yml
│ ├── discussion-thread-link.yml
│ ├── docs.yml
│ ├── full-ci-promote.yml
│ ├── release_dart.yml
│ ├── release_java.yml
│ ├── release_nodejs.yml
│ ├── release_python.yml
│ ├── release_ruby.yml
│ ├── release_rust.yml
│ ├── service_test_ghac.yml
│ ├── test_behavior_binding_c.yml
│ ├── test_behavior_binding_cpp.yml
│ ├── test_behavior_binding_go.yml
│ ├── test_behavior_binding_java.yml
│ ├── test_behavior_binding_nodejs.yml
│ ├── test_behavior_binding_python.yml
│ ├── test_behavior_core.yml
│ ├── test_behavior_integration_object_store.yml
│ ├── test_behavior.yml
│ ├── test_edge.yml
│ └── test_fuzz.yml
├── .gitignore
├── .taplo.toml
├── .typos.toml
├── .vscode
│ └── settings.json
├── .yamlfmt
├── AGENTS.md
├── bindings
│ ├── java
│ │ ├── .cargo
│ │ │ └── config.toml
│ │ ├── .gitignore
│ │ ├── .mvn
│ │ │ └── wrapper
│ │ │ └── maven-wrapper.properties
│ │ ├── Cargo.toml
│ │ ├── DEPENDENCIES.md
│ │ ├── DEPENDENCIES.rust.tsv
│ │ ├── mvnw
│ │ ├── mvnw.cmd
│ │ ├── pom.xml
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── async_operator.rs
│ │ │ ├── convert.rs
│ │ │ ├── error.rs
│ │ │ ├── executor.rs
│ │ │ ├── layer.rs
│ │ │ ├── lib.rs
│ │ │ ├── main
│ │ │ │ ├── java
│ │ │ │ │ └── org
│ │ │ │ │ └── apache
│ │ │ │ │ └── opendal
│ │ │ │ │ ├── AsyncExecutor.java
│ │ │ │ │ ├── AsyncOperator.java
│ │ │ │ │ ├── Capability.java
│ │ │ │ │ ├── Entry.java
│ │ │ │ │ ├── Environment.java
│ │ │ │ │ ├── layer
│ │ │ │ │ │ ├── ConcurrentLimitLayer.java
│ │ │ │ │ │ ├── package-info.java
│ │ │ │ │ │ └── RetryLayer.java
│ │ │ │ │ ├── Layer.java
│ │ │ │ │ ├── ListOptions.java
│ │ │ │ │ ├── Metadata.java
│ │ │ │ │ ├── NativeLibrary.java
│ │ │ │ │ ├── NativeObject.java
│ │ │ │ │ ├── OpenDAL.java
│ │ │ │ │ ├── OpenDALException.java
│ │ │ │ │ ├── Operator.java
│ │ │ │ │ ├── OperatorInfo.java
│ │ │ │ │ ├── OperatorInputStream.java
│ │ │ │ │ ├── OperatorOutputStream.java
│ │ │ │ │ ├── package-info.java
│ │ │ │ │ ├── PresignedRequest.java
│ │ │ │ │ ├── ReadOptions.java
│ │ │ │ │ ├── ServiceConfig.java
│ │ │ │ │ ├── StatOptions.java
│ │ │ │ │ └── WriteOptions.java
│ │ │ │ └── resources
│ │ │ │ ├── bindings.properties
│ │ │ │ └── META-INF
│ │ │ │ └── NOTICE
│ │ │ ├── operator_input_stream.rs
│ │ │ ├── operator_output_stream.rs
│ │ │ ├── operator.rs
│ │ │ ├── test
│ │ │ │ └── java
│ │ │ │ └── org
│ │ │ │ └── apache
│ │ │ │ └── opendal
│ │ │ │ └── test
│ │ │ │ ├── AsyncExecutorTest.java
│ │ │ │ ├── behavior
│ │ │ │ │ ├── AsyncCopyTest.java
│ │ │ │ │ ├── AsyncCreateDirTest.java
│ │ │ │ │ ├── AsyncListTest.java
│ │ │ │ │ ├── AsyncPresignTest.java
│ │ │ │ │ ├── AsyncReadOnlyTest.java
│ │ │ │ │ ├── AsyncRenameTest.java
│ │ │ │ │ ├── AsyncStatOptionsTest.java
│ │ │ │ │ ├── AsyncWriteOptionsTest.java
│ │ │ │ │ ├── AsyncWriteTest.java
│ │ │ │ │ ├── BehaviorExtension.java
│ │ │ │ │ ├── BehaviorTestBase.java
│ │ │ │ │ ├── BlockingCopyTest.java
│ │ │ │ │ ├── BlockingCreateDirTest.java
│ │ │ │ │ ├── BlockingListTest.java
│ │ │ │ │ ├── BlockingReadOnlyTest.java
│ │ │ │ │ ├── BlockingRenameTest.java
│ │ │ │ │ ├── BlockingStatOptionsTest.java
│ │ │ │ │ ├── BlockingWriteOptionTest.java
│ │ │ │ │ ├── BlockingWriteTest.java
│ │ │ │ │ └── RegressionTest.java
│ │ │ │ ├── condition
│ │ │ │ │ └── OpenDALExceptionCondition.java
│ │ │ │ ├── LayerTest.java
│ │ │ │ ├── MetadataTest.java
│ │ │ │ ├── OperatorDuplicateTest.java
│ │ │ │ ├── OperatorInfoTest.java
│ │ │ │ ├── OperatorInputOutputStreamTest.java
│ │ │ │ ├── OperatorUtf8DecodeTest.java
│ │ │ │ └── UtilityTest.java
│ │ │ └── utility.rs
│ │ ├── tools
│ │ │ └── build.py
│ │ ├── upgrade.md
│ │ └── users.md
│ ├── nodejs
│ │ ├── .cargo
│ │ │ └── config.toml
│ │ ├── .gitignore
│ │ ├── .node-version
│ │ ├── .npmignore
│ │ ├── .npmrc
│ │ ├── .prettierignore
│ │ ├── benchmark
│ │ │ ├── deno.ts
│ │ │ ├── node.js
│ │ │ └── README.md
│ │ ├── build.rs
│ │ ├── Cargo.toml
│ │ ├── CONTRIBUTING.md
│ │ ├── DEPENDENCIES.md
│ │ ├── DEPENDENCIES.rust.tsv
│ │ ├── devbox.json
│ │ ├── devbox.lock
│ │ ├── generated.d.ts
│ │ ├── generated.js
│ │ ├── index.cjs
│ │ ├── index.d.ts
│ │ ├── index.mjs
│ │ ├── npm
│ │ │ ├── darwin-arm64
│ │ │ │ ├── package.json
│ │ │ │ └── README.md
│ │ │ ├── darwin-x64
│ │ │ │ ├── package.json
│ │ │ │ └── README.md
│ │ │ ├── linux-arm64-gnu
│ │ │ │ ├── package.json
│ │ │ │ └── README.md
│ │ │ ├── linux-arm64-musl
│ │ │ │ ├── package.json
│ │ │ │ └── README.md
│ │ │ ├── linux-x64-gnu
│ │ │ │ ├── package.json
│ │ │ │ └── README.md
│ │ │ ├── linux-x64-musl
│ │ │ │ ├── package.json
│ │ │ │ └── README.md
│ │ │ ├── win32-arm64-msvc
│ │ │ │ ├── package.json
│ │ │ │ └── README.md
│ │ │ └── win32-x64-msvc
│ │ │ ├── package.json
│ │ │ └── README.md
│ │ ├── package.json
│ │ ├── pnpm-lock.yaml
│ │ ├── README.md
│ │ ├── scripts
│ │ │ └── header.mjs
│ │ ├── src
│ │ │ ├── capability.rs
│ │ │ ├── layer.rs
│ │ │ ├── lib.rs
│ │ │ └── options.rs
│ │ ├── tests
│ │ │ ├── service.test.mjs
│ │ │ ├── suites
│ │ │ │ ├── async.suite.mjs
│ │ │ │ ├── asyncDeleteOptions.suite.mjs
│ │ │ │ ├── asyncLister.suite.mjs
│ │ │ │ ├── asyncListOptions.suite.mjs
│ │ │ │ ├── asyncReadOptions.suite.mjs
│ │ │ │ ├── asyncStatOptions.suite.mjs
│ │ │ │ ├── asyncWriteOptions.suite.mjs
│ │ │ │ ├── index.mjs
│ │ │ │ ├── layer.suite.mjs
│ │ │ │ ├── services.suite.mjs
│ │ │ │ ├── sync.suite.mjs
│ │ │ │ ├── syncDeleteOptions.suite.mjs
│ │ │ │ ├── syncLister.suite.mjs
│ │ │ │ ├── syncListOptions.suite.mjs
│ │ │ │ ├── syncReadOptions.suite.mjs
│ │ │ │ ├── syncStatOptions.suite.mjs
│ │ │ │ └── syncWriteOptions.suite.mjs
│ │ │ └── utils.mjs
│ │ ├── theme
│ │ │ ├── index.tsx
│ │ │ └── package.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.theme.json
│ │ ├── typedoc.json
│ │ ├── upgrade.md
│ │ └── vitest.config.mjs
│ ├── python
│ │ ├── .gitignore
│ │ ├── benchmark
│ │ │ ├── async_opendal_benchmark.py
│ │ │ ├── async_origin_s3_benchmark_with_gevent.py
│ │ │ └── README.md
│ │ ├── Cargo.toml
│ │ ├── CONTRIBUTING.md
│ │ ├── DEPENDENCIES.md
│ │ ├── DEPENDENCIES.rust.tsv
│ │ ├── docs
│ │ │ ├── api
│ │ │ │ ├── async_file.md
│ │ │ │ ├── async_operator.md
│ │ │ │ ├── capability.md
│ │ │ │ ├── exceptions.md
│ │ │ │ ├── file.md
│ │ │ │ ├── layers.md
│ │ │ │ ├── operator.md
│ │ │ │ └── types.md
│ │ │ └── index.md
│ │ ├── justfile
│ │ ├── mkdocs.yml
│ │ ├── pyproject.toml
│ │ ├── pyrightconfig.json
│ │ ├── python
│ │ │ └── opendal
│ │ │ ├── __init__.py
│ │ │ ├── capability.pyi
│ │ │ ├── exceptions.pyi
│ │ │ ├── file.pyi
│ │ │ ├── layers.pyi
│ │ │ ├── operator.pyi
│ │ │ ├── py.typed
│ │ │ ├── services.pyi
│ │ │ └── types.pyi
│ │ ├── README.md
│ │ ├── ruff.toml
│ │ ├── src
│ │ │ ├── capability.rs
│ │ │ ├── errors.rs
│ │ │ ├── file.rs
│ │ │ ├── layers.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ ├── metadata.rs
│ │ │ ├── operator.rs
│ │ │ ├── options.rs
│ │ │ ├── services.rs
│ │ │ └── utils.rs
│ │ ├── template
│ │ │ └── module.html.jinja2
│ │ ├── tests
│ │ │ ├── conftest.py
│ │ │ ├── test_async_check.py
│ │ │ ├── test_async_copy.py
│ │ │ ├── test_async_delete.py
│ │ │ ├── test_async_exists.py
│ │ │ ├── test_async_list.py
│ │ │ ├── test_async_pickle_types.py
│ │ │ ├── test_async_rename.py
│ │ │ ├── test_capability.py
│ │ │ ├── test_exceptions.py
│ │ │ ├── test_pickle_rw.py
│ │ │ ├── test_read.py
│ │ │ ├── test_sync_check.py
│ │ │ ├── test_sync_copy.py
│ │ │ ├── test_sync_delete.py
│ │ │ ├── test_sync_exists.py
│ │ │ ├── test_sync_list.py
│ │ │ ├── test_sync_pickle_types.py
│ │ │ ├── test_sync_rename.py
│ │ │ └── test_write.py
│ │ ├── upgrade.md
│ │ ├── users.md
│ │ └── uv.lock
│ └── README.md
├── CHANGELOG.md
├── CITATION.cff
├── CLAUDE.md
├── CONTRIBUTING.md
├── core
│ ├── benches
│ │ ├── ops
│ │ │ ├── main.rs
│ │ │ ├── read.rs
│ │ │ ├── README.md
│ │ │ ├── utils.rs
│ │ │ └── write.rs
│ │ ├── README.md
│ │ ├── types
│ │ │ ├── buffer.rs
│ │ │ ├── main.rs
│ │ │ ├── README.md
│ │ │ └── tasks.rs
│ │ ├── vs_fs
│ │ │ ├── Cargo.toml
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ └── main.rs
│ │ └── vs_s3
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ └── src
│ │ └── main.rs
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── CHANGELOG.md
│ ├── CONTRIBUTING.md
│ ├── core
│ │ ├── Cargo.toml
│ │ └── src
│ │ ├── blocking
│ │ │ ├── delete.rs
│ │ │ ├── list.rs
│ │ │ ├── mod.rs
│ │ │ ├── operator.rs
│ │ │ ├── read
│ │ │ │ ├── buffer_iterator.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── reader.rs
│ │ │ │ ├── std_bytes_iterator.rs
│ │ │ │ └── std_reader.rs
│ │ │ └── write
│ │ │ ├── mod.rs
│ │ │ ├── std_writer.rs
│ │ │ └── writer.rs
│ │ ├── docs
│ │ │ ├── comparisons
│ │ │ │ ├── mod.rs
│ │ │ │ └── vs_object_store.md
│ │ │ ├── concepts.rs
│ │ │ ├── internals
│ │ │ │ ├── accessor.rs
│ │ │ │ ├── layer.rs
│ │ │ │ └── mod.rs
│ │ │ ├── mod.rs
│ │ │ ├── performance
│ │ │ │ ├── concurrent_write.md
│ │ │ │ ├── http_optimization.md
│ │ │ │ └── mod.rs
│ │ │ ├── rfcs
│ │ │ │ ├── 0000_example.md
│ │ │ │ ├── 0041_object_native_api.md
│ │ │ │ ├── 0044_error_handle.md
│ │ │ │ ├── 0057_auto_region.md
│ │ │ │ ├── 0069_object_stream.md
│ │ │ │ ├── 0090_limited_reader.md
│ │ │ │ ├── 0112_path_normalization.md
│ │ │ │ ├── 0191_async_streaming_io.md
│ │ │ │ ├── 0203_remove_credential.md
│ │ │ │ ├── 0221_create_dir.md
│ │ │ │ ├── 0247_retryable_error.md
│ │ │ │ ├── 0293_object_id.md
│ │ │ │ ├── 0337_dir_entry.md
│ │ │ │ ├── 0409_accessor_capabilities.md
│ │ │ │ ├── 0413_presign.md
│ │ │ │ ├── 0423_command_line_interface.md
│ │ │ │ ├── 0429_init_from_iter.md
│ │ │ │ ├── 0438_multipart.md
│ │ │ │ ├── 0443_gateway.md
│ │ │ │ ├── 0501_new_builder.md
│ │ │ │ ├── 0554_write_refactor.md
│ │ │ │ ├── 0561_list_metadata_reuse.md
│ │ │ │ ├── 0599_blocking_api.md
│ │ │ │ ├── 0623_redis_service.md
│ │ │ │ ├── 0627_split_capabilities.md
│ │ │ │ ├── 0661_path_in_accessor.md
│ │ │ │ ├── 0793_generic_kv_services.md
│ │ │ │ ├── 0926_object_reader.md
│ │ │ │ ├── 0977_refactor_error.md
│ │ │ │ ├── 1085_object_handler.md
│ │ │ │ ├── 1391_object_metadataer.md
│ │ │ │ ├── 1398_query_based_metadata.md
│ │ │ │ ├── 1420_object_writer.md
│ │ │ │ ├── 1477_remove_object_concept.md
│ │ │ │ ├── 1735_operation_extension.md
│ │ │ │ ├── 2083_writer_sink_api.md
│ │ │ │ ├── 2133_append_api.md
│ │ │ │ ├── 2299_chain_based_operator_api.md
│ │ │ │ ├── 2602_object_versioning.md
│ │ │ │ ├── 2758_merge_append_into_write.md
│ │ │ │ ├── 2774_lister_api.md
│ │ │ │ ├── 2779_list_with_metakey.md
│ │ │ │ ├── 2852_native_capability.md
│ │ │ │ ├── 2884_merge_range_read_into_read.md
│ │ │ │ ├── 3017_remove_write_copy_from.md
│ │ │ │ ├── 3197_config.md
│ │ │ │ ├── 3232_align_list_api.md
│ │ │ │ ├── 3243_list_prefix.md
│ │ │ │ ├── 3356_lazy_reader.md
│ │ │ │ ├── 3526_list_recursive.md
│ │ │ │ ├── 3574_concurrent_stat_in_list.md
│ │ │ │ ├── 3734_buffered_reader.md
│ │ │ │ ├── 3898_concurrent_writer.md
│ │ │ │ ├── 3911_deleter_api.md
│ │ │ │ ├── 4382_range_based_read.md
│ │ │ │ ├── 4638_executor.md
│ │ │ │ ├── 5314_remove_metakey.md
│ │ │ │ ├── 5444_operator_from_uri.md
│ │ │ │ ├── 5479_context.md
│ │ │ │ ├── 5485_conditional_reader.md
│ │ │ │ ├── 5495_list_with_deleted.md
│ │ │ │ ├── 5556_write_returns_metadata.md
│ │ │ │ ├── 5871_read_returns_metadata.md
│ │ │ │ ├── 6189_remove_native_blocking.md
│ │ │ │ ├── 6209_glob_support.md
│ │ │ │ ├── 6213_options_api.md
│ │ │ │ ├── 6370_foyer_integration.md
│ │ │ │ ├── 6678_simulate_layer.md
│ │ │ │ ├── 6707_capability_override_layer.md
│ │ │ │ ├── 6817_checksum.md
│ │ │ │ ├── 6828_core.md
│ │ │ │ ├── 7130_route_layer.md
│ │ │ │ ├── mod.rs
│ │ │ │ └── README.md
│ │ │ └── upgrade.md
│ │ ├── layers
│ │ │ ├── complete.rs
│ │ │ ├── correctness_check.rs
│ │ │ ├── error_context.rs
│ │ │ ├── http_client.rs
│ │ │ ├── mod.rs
│ │ │ ├── simulate.rs
│ │ │ └── type_eraser.rs
│ │ ├── lib.rs
│ │ ├── raw
│ │ │ ├── accessor.rs
│ │ │ ├── atomic_util.rs
│ │ │ ├── enum_utils.rs
│ │ │ ├── futures_util.rs
│ │ │ ├── http_util
│ │ │ │ ├── body.rs
│ │ │ │ ├── bytes_content_range.rs
│ │ │ │ ├── bytes_range.rs
│ │ │ │ ├── client.rs
│ │ │ │ ├── error.rs
│ │ │ │ ├── header.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── multipart.rs
│ │ │ │ └── uri.rs
│ │ │ ├── layer.rs
│ │ │ ├── mod.rs
│ │ │ ├── oio
│ │ │ │ ├── buf
│ │ │ │ │ ├── flex_buf.rs
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ ├── pooled_buf.rs
│ │ │ │ │ └── queue_buf.rs
│ │ │ │ ├── delete
│ │ │ │ │ ├── api.rs
│ │ │ │ │ ├── batch_delete.rs
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ └── one_shot_delete.rs
│ │ │ │ ├── entry.rs
│ │ │ │ ├── list
│ │ │ │ │ ├── api.rs
│ │ │ │ │ ├── flat_list.rs
│ │ │ │ │ ├── hierarchy_list.rs
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ ├── page_list.rs
│ │ │ │ │ └── prefix_list.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── read
│ │ │ │ │ ├── api.rs
│ │ │ │ │ └── mod.rs
│ │ │ │ └── write
│ │ │ │ ├── api.rs
│ │ │ │ ├── append_write.rs
│ │ │ │ ├── block_write.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── multipart_write.rs
│ │ │ │ ├── one_shot_write.rs
│ │ │ │ └── position_write.rs
│ │ │ ├── operation.rs
│ │ │ ├── ops.rs
│ │ │ ├── path_cache.rs
│ │ │ ├── path.rs
│ │ │ ├── rps.rs
│ │ │ ├── serde_util.rs
│ │ │ ├── std_io_util.rs
│ │ │ ├── time.rs
│ │ │ ├── tokio_util.rs
│ │ │ └── version.rs
│ │ ├── services
│ │ │ ├── memory
│ │ │ │ ├── backend.rs
│ │ │ │ ├── config.rs
│ │ │ │ ├── core.rs
│ │ │ │ ├── deleter.rs
│ │ │ │ ├── docs.md
│ │ │ │ ├── lister.rs
│ │ │ │ ├── mod.rs
│ │ │ │ └── writer.rs
│ │ │ └── mod.rs
│ │ └── types
│ │ ├── buffer.rs
│ │ ├── builder.rs
│ │ ├── capability.rs
│ │ ├── context
│ │ │ ├── mod.rs
│ │ │ ├── read.rs
│ │ │ └── write.rs
│ │ ├── delete
│ │ │ ├── deleter.rs
│ │ │ ├── futures_delete_sink.rs
│ │ │ ├── input.rs
│ │ │ └── mod.rs
│ │ ├── entry.rs
│ │ ├── error.rs
│ │ ├── execute
│ │ │ ├── api.rs
│ │ │ ├── executor.rs
│ │ │ ├── executors
│ │ │ │ ├── mod.rs
│ │ │ │ └── tokio_executor.rs
│ │ │ └── mod.rs
│ │ ├── list.rs
│ │ ├── metadata.rs
│ │ ├── mod.rs
│ │ ├── mode.rs
│ │ ├── operator
│ │ │ ├── builder.rs
│ │ │ ├── info.rs
│ │ │ ├── mod.rs
│ │ │ ├── operator_futures.rs
│ │ │ ├── operator.rs
│ │ │ ├── registry.rs
│ │ │ └── uri.rs
│ │ ├── options.rs
│ │ ├── read
│ │ │ ├── buffer_stream.rs
│ │ │ ├── futures_async_reader.rs
│ │ │ ├── futures_bytes_stream.rs
│ │ │ ├── mod.rs
│ │ │ └── reader.rs
│ │ └── write
│ │ ├── buffer_sink.rs
│ │ ├── futures_async_writer.rs
│ │ ├── futures_bytes_sink.rs
│ │ ├── mod.rs
│ │ └── writer.rs
│ ├── DEPENDENCIES.md
│ ├── DEPENDENCIES.rust.tsv
│ ├── edge
│ │ ├── file_write_on_full_disk
│ │ │ ├── Cargo.toml
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ └── main.rs
│ │ ├── README.md
│ │ ├── s3_aws_assume_role_with_web_identity
│ │ │ ├── Cargo.toml
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ └── main.rs
│ │ └── s3_read_on_wasm
│ │ ├── .gitignore
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── src
│ │ │ └── lib.rs
│ │ └── webdriver.json
│ ├── fuzz
│ │ ├── .gitignore
│ │ ├── Cargo.toml
│ │ ├── fuzz_reader.rs
│ │ ├── fuzz_writer.rs
│ │ └── README.md
│ ├── layers
│ │ ├── async-backtrace
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── await-tree
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── capability-check
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── chaos
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── concurrent-limit
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── dtrace
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── fastmetrics
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── fastrace
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── foyer
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── hotpath
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── immutable-index
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── logging
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── metrics
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── mime-guess
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── observe-metrics-common
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── otelmetrics
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── oteltrace
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── prometheus
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── prometheus-client
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── retry
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── route
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── tail-cut
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── throttle
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── timeout
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ └── tracing
│ │ ├── Cargo.toml
│ │ └── src
│ │ └── lib.rs
│ ├── LICENSE
│ ├── README.md
│ ├── services
│ │ ├── aliyun-drive
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── alluxio
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── azblob
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── azdls
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── azfile
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── azure-common
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ │ ├── b2
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── cacache
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── cloudflare-kv
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ ├── model.rs
│ │ │ └── writer.rs
│ │ ├── compfs
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ ├── reader.rs
│ │ │ └── writer.rs
│ │ ├── cos
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── d1
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── model.rs
│ │ │ └── writer.rs
│ │ ├── dashmap
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── dbfs
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── dropbox
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── builder.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── etcd
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── foundationdb
│ │ │ ├── build.rs
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── fs
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ ├── reader.rs
│ │ │ └── writer.rs
│ │ ├── ftp
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── err.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ ├── reader.rs
│ │ │ └── writer.rs
│ │ ├── gcs
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ ├── uri.rs
│ │ │ └── writer.rs
│ │ ├── gdrive
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── builder.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── ghac
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── github
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ ├── mod.rs
│ │ │ └── writer.rs
│ │ ├── gridfs
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── hdfs
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ ├── reader.rs
│ │ │ └── writer.rs
│ │ ├── hdfs-native
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ ├── reader.rs
│ │ │ └── writer.rs
│ │ ├── http
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ └── lib.rs
│ │ ├── huggingface
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ └── lister.rs
│ │ ├── ipfs
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── ipld.rs
│ │ │ └── lib.rs
│ │ ├── ipmfs
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── builder.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── koofr
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── lakefs
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── memcached
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── binary.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── mini_moka
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── moka
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── mongodb
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── monoiofs
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ ├── reader.rs
│ │ │ └── writer.rs
│ │ ├── mysql
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── obs
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── onedrive
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── builder.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── graph_model.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── opfs
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ └── utils.rs
│ │ ├── oss
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── pcloud
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── persy
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── postgresql
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── redb
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── redis
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── delete.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── rocksdb
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── s3
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── compatible_services.md
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ ├── mod.rs
│ │ │ └── writer.rs
│ │ ├── seafile
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── sftp
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ ├── reader.rs
│ │ │ ├── utils.rs
│ │ │ └── writer.rs
│ │ ├── sled
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── sqlite
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── surrealdb
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── swift
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── compatible_services.md
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── tikv
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── upyun
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── vercel-artifacts
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── builder.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ └── writer.rs
│ │ ├── vercel-blob
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── webdav
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ └── writer.rs
│ │ ├── webhdfs
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── backend.rs
│ │ │ ├── config.rs
│ │ │ ├── core.rs
│ │ │ ├── deleter.rs
│ │ │ ├── docs.md
│ │ │ ├── error.rs
│ │ │ ├── lib.rs
│ │ │ ├── lister.rs
│ │ │ ├── message.rs
│ │ │ └── writer.rs
│ │ └── yandex-disk
│ │ ├── Cargo.toml
│ │ └── src
│ │ ├── backend.rs
│ │ ├── config.rs
│ │ ├── core.rs
│ │ ├── deleter.rs
│ │ ├── docs.md
│ │ ├── error.rs
│ │ ├── lib.rs
│ │ ├── lister.rs
│ │ └── writer.rs
│ ├── src
│ │ └── lib.rs
│ ├── testkit
│ │ ├── Cargo.toml
│ │ └── src
│ │ ├── lib.rs
│ │ ├── read.rs
│ │ ├── utils.rs
│ │ └── write.rs
│ ├── tests
│ │ ├── behavior
│ │ │ ├── async_copy.rs
│ │ │ ├── async_create_dir.rs
│ │ │ ├── async_delete.rs
│ │ │ ├── async_list.rs
│ │ │ ├── async_presign.rs
│ │ │ ├── async_read.rs
│ │ │ ├── async_rename.rs
│ │ │ ├── async_stat.rs
│ │ │ ├── async_write.rs
│ │ │ ├── main.rs
│ │ │ ├── README.md
│ │ │ └── utils.rs
│ │ └── data
│ │ ├── normal_dir
│ │ │ └── .gitkeep
│ │ ├── normal_file.txt
│ │ ├── special_dir !@#$%^&()_+-=;',
│ │ │ └── .gitkeep
│ │ └── special_file !@#$%^&()_+-=;',.txt
│ ├── upgrade.md
│ └── users.md
├── deny.toml
├── DEPENDENCIES.md
├── dev
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── README.md
│ └── src
│ ├── generate
│ │ ├── java.j2
│ │ ├── java.rs
│ │ ├── mod.rs
│ │ ├── parser.rs
│ │ ├── python.j2
│ │ └── python.rs
│ ├── main.rs
│ └── release
│ ├── mod.rs
│ └── package.rs
├── doap.rdf
├── fixtures
│ ├── alluxio
│ │ └── docker-compose-alluxio.yml
│ ├── azblob
│ │ └── docker-compose-azurite.yml
│ ├── data
│ │ ├── normal_dir
│ │ │ └── .gitkeep
│ │ ├── normal_file.txt
│ │ ├── special_dir !@#$%^&()_+-=;',
│ │ │ └── .gitkeep
│ │ └── special_file !@#$%^&()_+-=;',.txt
│ ├── etcd
│ │ ├── ca-key.pem
│ │ ├── ca.pem
│ │ ├── client-key.pem
│ │ ├── client.pem
│ │ ├── docker-compose-cluster.yml
│ │ ├── docker-compose-standalone-tls.yml
│ │ ├── docker-compose-standalone.yml
│ │ ├── server-key.pem
│ │ └── server.pem
│ ├── ftp
│ │ └── docker-compose-vsftpd.yml
│ ├── hdfs
│ │ ├── azurite-azblob-core-site.xml
│ │ ├── docker-compose-hdfs-cluster.yml
│ │ ├── gcs-core-site.xml
│ │ ├── hdfs-site.xml
│ │ └── minio-s3-core-site.xml
│ ├── http
│ │ ├── Caddyfile
│ │ ├── docker-compose-caddy.yml
│ │ ├── docker-compose-nginx.yml
│ │ └── nginx.conf
│ ├── libsql
│ │ ├── docker-compose-auth.yml
│ │ └── docker-compose.yml
│ ├── memcached
│ │ ├── docker-compose-memcached-with-auth.yml
│ │ └── docker-compose-memcached.yml
│ ├── mongodb
│ │ ├── docker-compose-basic-auth.yml
│ │ └── docker-compose-no-auth.yml
│ ├── mysql
│ │ ├── docker-compose.yml
│ │ └── init.sql
│ ├── postgresql
│ │ ├── docker-compose.yml
│ │ └── init.sql
│ ├── redis
│ │ ├── docker-compose-dragonfly.yml
│ │ ├── docker-compose-kvrocks.yml
│ │ ├── docker-compose-redis-cluster-tls.yml
│ │ ├── docker-compose-redis-cluster.yml
│ │ ├── docker-compose-redis-tls.yml
│ │ ├── docker-compose-redis.yml
│ │ └── ssl
│ │ ├── .gitignore
│ │ ├── ca.crt
│ │ ├── ca.key
│ │ ├── ca.srl
│ │ ├── README.md
│ │ ├── redis.crt
│ │ ├── redis.key
│ │ └── req.conf
│ ├── s3
│ │ ├── docker-compose-ceph-rados.yml
│ │ └── docker-compose-minio.yml
│ ├── seafile
│ │ └── docker-compose-seafile.yml
│ ├── sftp
│ │ ├── change_root_dir.sh
│ │ ├── docker-compose-sftp-with-default-root.yml
│ │ ├── docker-compose-sftp.yml
│ │ ├── health-check.sh
│ │ ├── test_ssh_key
│ │ └── test_ssh_key.pub
│ ├── sqlite
│ │ └── data.sql
│ ├── swift
│ │ ├── docker-compose-ceph-rados.yml
│ │ └── docker-compose-swift.yml
│ ├── tikv
│ │ ├── gen_cert.sh
│ │ ├── pd-tls.toml
│ │ ├── pd.toml
│ │ ├── ssl
│ │ │ ├── ca-key.pem
│ │ │ ├── ca.pem
│ │ │ ├── client-key.pem
│ │ │ ├── client.pem
│ │ │ ├── pd-server-key.pem
│ │ │ ├── pd-server.pem
│ │ │ ├── tikv-server-key.pem
│ │ │ └── tikv-server.pem
│ │ ├── tikv-tls.toml
│ │ └── tikv.toml
│ ├── webdav
│ │ ├── config
│ │ │ └── nginx
│ │ │ └── http.conf
│ │ ├── docker-compose-webdav-jfrog.yml
│ │ ├── docker-compose-webdav-nextcloud.yml
│ │ ├── docker-compose-webdav-owncloud.yml
│ │ ├── docker-compose-webdav-with-auth.yml
│ │ ├── docker-compose-webdav-with-empty-passwd.yml
│ │ ├── docker-compose-webdav.yml
│ │ └── health-check-nextcloud.sh
│ └── webhdfs
│ └── docker-compose-webhdfs.yml
├── justfile
├── LICENSE
├── licenserc.toml
├── NOTICE
├── README.md
├── rust-toolchain.toml
├── rustfmt.toml
└── scripts
├── constants.py
├── dependencies.py
├── merge_local_staging.py
├── README.md
├── verify.py
└── workspace.py
```
# Files
--------------------------------------------------------------------------------
/core/core/src/types/operator/operator_futures.rs:
--------------------------------------------------------------------------------
```rust
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Futures provides the futures generated by [`Operator`]
//!
//! By using futures, users can add more options for operation.
use std::collections::HashMap;
use std::future::IntoFuture;
use std::ops::RangeBounds;
use crate::raw::*;
use crate::*;
use futures::Future;
/// OperatorFuture is the future generated by [`Operator`].
///
/// The future will consume all the input to generate a future.
///
/// # NOTES
///
/// This struct is by design to keep in crate. We don't want
/// users to use this struct directly.
pub struct OperatorFuture<I, O, F: Future<Output = Result<O>>> {
/// The accessor to the underlying object storage
acc: Accessor,
/// The path of string
path: String,
/// The input args
args: I,
/// The function which will move all the args and return a static future
f: fn(Accessor, String, I) -> F,
}
impl<I, O, F: Future<Output = Result<O>>> OperatorFuture<I, O, F> {
/// # NOTES
///
/// This struct is by design to keep in crate. We don't want
/// users to use this struct directly.
pub(crate) fn new(
inner: Accessor,
path: String,
args: I,
f: fn(Accessor, String, I) -> F,
) -> Self {
OperatorFuture {
acc: inner,
path,
args,
f,
}
}
}
impl<I, O, F> IntoFuture for OperatorFuture<I, O, F>
where
F: Future<Output = Result<O>>,
{
type Output = Result<O>;
type IntoFuture = F;
fn into_future(self) -> Self::IntoFuture {
(self.f)(self.acc, self.path, self.args)
}
}
/// Future that generated by [`Operator::stat_with`].
///
/// Users can add more options by public functions provided by this struct.
pub type FutureStat<F> = OperatorFuture<options::StatOptions, Metadata, F>;
impl<F: Future<Output = Result<Metadata>>> FutureStat<F> {
/// Set the If-Match for this operation.
///
/// Refer to [`options::StatOptions::if_match`] for more details.
pub fn if_match(mut self, v: &str) -> Self {
self.args.if_match = Some(v.to_string());
self
}
/// Set the If-None-Match for this operation.
///
/// Refer to [`options::StatOptions::if_none_match`] for more details.
pub fn if_none_match(mut self, v: &str) -> Self {
self.args.if_none_match = Some(v.to_string());
self
}
/// Set the If-Modified-Since for this operation.
///
/// Refer to [`options::StatOptions::if_modified_since`] for more details.
pub fn if_modified_since(mut self, v: Timestamp) -> Self {
self.args.if_modified_since = Some(v);
self
}
/// Set the If-Unmodified-Since for this operation.
///
/// Refer to [`options::StatOptions::if_unmodified_since`] for more details.
pub fn if_unmodified_since(mut self, v: Timestamp) -> Self {
self.args.if_unmodified_since = Some(v);
self
}
/// Set the version for this operation.
///
/// Refer to [`options::StatOptions::version`] for more details.
pub fn version(mut self, v: &str) -> Self {
self.args.version = Some(v.to_string());
self
}
}
/// Future that generated by [`Operator::presign_stat_with`].
///
/// Users can add more options by public functions provided by this struct.
pub type FuturePresignStat<F> =
OperatorFuture<(options::StatOptions, Duration), PresignedRequest, F>;
impl<F: Future<Output = Result<PresignedRequest>>> FuturePresignStat<F> {
/// Refer to [`options::StatOptions::override_content_disposition`] for more details.
pub fn override_content_disposition(mut self, v: &str) -> Self {
self.args.0.override_content_disposition = Some(v.to_string());
self
}
/// Refer to [`options::StatOptions::override_cache_control`] for more details.
pub fn override_cache_control(mut self, v: &str) -> Self {
self.args.0.override_cache_control = Some(v.to_string());
self
}
/// Refer to [`options::StatOptions::override_content_type`] for more details.
pub fn override_content_type(mut self, v: &str) -> Self {
self.args.0.override_content_type = Some(v.to_string());
self
}
/// Refer to [`options::StatOptions::if_match`] for more details.
pub fn if_match(mut self, v: &str) -> Self {
self.args.0.if_match = Some(v.to_string());
self
}
/// Refer to [`options::StatOptions::if_none_match`] for more details.
pub fn if_none_match(mut self, v: &str) -> Self {
self.args.0.if_none_match = Some(v.to_string());
self
}
}
/// Future that generated by [`Operator::presign_delete_with`].
///
/// Users can add more options by public functions provided by this struct.
pub type FuturePresignDelete<F> =
OperatorFuture<(options::DeleteOptions, Duration), PresignedRequest, F>;
impl<F: Future<Output = Result<PresignedRequest>>> FuturePresignDelete<F> {}
/// Future that generated by [`Operator::presign_read_with`].
///
/// Users can add more options by public functions provided by this struct.
pub type FuturePresignRead<F> =
OperatorFuture<(options::ReadOptions, Duration), PresignedRequest, F>;
impl<F: Future<Output = Result<PresignedRequest>>> FuturePresignRead<F> {
/// Refer to [`options::ReadOptions::override_content_disposition`] for more details.
pub fn override_content_disposition(mut self, v: &str) -> Self {
self.args.0.override_content_disposition = Some(v.to_string());
self
}
/// Refer to [`options::ReadOptions::override_cache_control`] for more details.
pub fn override_cache_control(mut self, v: &str) -> Self {
self.args.0.override_cache_control = Some(v.to_string());
self
}
/// Refer to [`options::ReadOptions::override_content_type`] for more details.
pub fn override_content_type(mut self, v: &str) -> Self {
self.args.0.override_content_type = Some(v.to_string());
self
}
/// Refer to [`options::ReadOptions::if_match`] for more details.
pub fn if_match(mut self, v: &str) -> Self {
self.args.0.if_match = Some(v.to_string());
self
}
/// Refer to [`options::ReadOptions::if_none_match`] for more details.
pub fn if_none_match(mut self, v: &str) -> Self {
self.args.0.if_none_match = Some(v.to_string());
self
}
}
/// Future that generated by [`Operator::presign_write_with`].
///
/// Users can add more options by public functions provided by this struct.
pub type FuturePresignWrite<F> =
OperatorFuture<(options::WriteOptions, Duration), PresignedRequest, F>;
impl<F: Future<Output = Result<PresignedRequest>>> FuturePresignWrite<F> {
/// Refer to [`options::WriteOptions::content_type`] for more details.
pub fn content_type(mut self, v: &str) -> Self {
self.args.0.content_type = Some(v.to_string());
self
}
/// Refer to [`options::WriteOptions::content_disposition`] for more details.
pub fn content_disposition(mut self, v: &str) -> Self {
self.args.0.content_disposition = Some(v.to_string());
self
}
/// Refer to [`options::WriteOptions::content_encoding`] for more details.
pub fn content_encoding(mut self, v: &str) -> Self {
self.args.0.content_encoding = Some(v.to_string());
self
}
/// Refer to [`options::WriteOptions::cache_control`] for more details.
pub fn cache_control(mut self, v: &str) -> Self {
self.args.0.cache_control = Some(v.to_string());
self
}
}
/// Future that generated by [`Operator::read_with`].
///
/// Users can add more options by public functions provided by this struct.
pub type FutureRead<F> = OperatorFuture<options::ReadOptions, Buffer, F>;
impl<F: Future<Output = Result<Buffer>>> FutureRead<F> {
/// Set `range` for this `read` request.
///
/// If we have a file with size `n`.
///
/// - `..` means read bytes in range `[0, n)` of file.
/// - `0..1024` and `..1024` means read bytes in range `[0, 1024)` of file
/// - `1024..` means read bytes in range `[1024, n)` of file
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::TryStreamExt;
/// # async fn test(op: Operator) -> Result<()> {
/// let bs = op.read_with("path/to/file").range(0..1024).await?;
/// # Ok(())
/// # }
/// ```
pub fn range(mut self, range: impl RangeBounds<u64>) -> Self {
self.args.range = range.into();
self
}
/// Set `concurrent` for the reader.
///
/// OpenDAL by default to write file without concurrent. This is not efficient for cases when users
/// read large chunks of data. By setting `concurrent`, opendal will read files concurrently
/// on support storage services.
///
/// By setting `concurrent`, opendal will fetch chunks concurrently with
/// the given chunk size.
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # async fn test(op: Operator) -> Result<()> {
/// let r = op.read_with("path/to/file").concurrent(8).await?;
/// # Ok(())
/// # }
/// ```
pub fn concurrent(mut self, concurrent: usize) -> Self {
self.args.concurrent = concurrent.max(1);
self
}
/// OpenDAL will use services' preferred chunk size by default. Users can set chunk based on their own needs.
///
/// This following example will make opendal read data in 4MiB chunks:
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # async fn test(op: Operator) -> Result<()> {
/// let r = op.read_with("path/to/file").chunk(4 * 1024 * 1024).await?;
/// # Ok(())
/// # }
/// ```
pub fn chunk(mut self, chunk_size: usize) -> Self {
self.args.chunk = Some(chunk_size);
self
}
/// Set `version` for this `read` request.
///
/// This feature can be used to retrieve the data of a specified version of the given path.
///
/// If the version doesn't exist, an error with kind [`ErrorKind::NotFound`] will be returned.
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
///
/// # async fn test(op: Operator, version: &str) -> Result<()> {
/// let mut bs = op.read_with("path/to/file").version(version).await?;
/// # Ok(())
/// # }
/// ```
pub fn version(mut self, v: &str) -> Self {
self.args.version = Some(v.to_string());
self
}
/// Set `if_match` for this `read` request.
///
/// This feature can be used to check if the file's `ETag` matches the given `ETag`.
///
/// If file exists and it's etag doesn't match, an error with kind [`ErrorKind::ConditionNotMatch`]
/// will be returned.
///
/// ```
/// # use opendal_core::Result;
/// use opendal_core::Operator;
/// # async fn test(op: Operator, etag: &str) -> Result<()> {
/// let mut metadata = op.read_with("path/to/file").if_match(etag).await?;
/// # Ok(())
/// # }
/// ```
pub fn if_match(mut self, v: &str) -> Self {
self.args.if_match = Some(v.to_string());
self
}
/// Set `if_none_match` for this `read` request.
///
/// This feature can be used to check if the file's `ETag` doesn't match the given `ETag`.
///
/// If file exists and it's etag match, an error with kind [`ErrorKind::ConditionNotMatch`]
/// will be returned.
///
/// ```
/// # use opendal_core::Result;
/// use opendal_core::Operator;
/// # async fn test(op: Operator, etag: &str) -> Result<()> {
/// let mut metadata = op.read_with("path/to/file").if_none_match(etag).await?;
/// # Ok(())
/// # }
/// ```
pub fn if_none_match(mut self, v: &str) -> Self {
self.args.if_none_match = Some(v.to_string());
self
}
/// ## `if_modified_since`
///
/// Set `if_modified_since` for this `read` request.
///
/// This feature can be used to check if the file has been modified since the given timestamp.
///
/// If file exists and it hasn't been modified since the specified time, an error with kind
/// [`ErrorKind::ConditionNotMatch`] will be returned.
///
/// ```
/// # use opendal_core::Result;
/// use jiff::Timestamp;
/// use opendal_core::Operator;
/// # async fn test(op: Operator, time: Timestamp) -> Result<()> {
/// let mut metadata = op.read_with("path/to/file").if_modified_since(time).await?;
/// # Ok(())
/// # }
/// ```
pub fn if_modified_since(mut self, v: impl Into<Timestamp>) -> Self {
self.args.if_modified_since = Some(v.into());
self
}
/// Set `if_unmodified_since` for this `read` request.
///
/// This feature can be used to check if the file hasn't been modified since the given timestamp.
///
/// If file exists and it has been modified since the specified time, an error with kind
/// [`ErrorKind::ConditionNotMatch`] will be returned.
///
/// ```
/// # use opendal_core::Result;
/// use jiff::Timestamp;
/// use opendal_core::Operator;
/// # async fn test(op: Operator, time: Timestamp) -> Result<()> {
/// let mut metadata = op
/// .read_with("path/to/file")
/// .if_unmodified_since(time)
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn if_unmodified_since(mut self, v: impl Into<Timestamp>) -> Self {
self.args.if_unmodified_since = Some(v.into());
self
}
}
/// Future that generated by [`Operator::read_with`] or [`Operator::reader_with`].
///
/// Users can add more options by public functions provided by this struct.
///
/// # Notes
///
/// `(OpRead, ())` is a trick to make sure `FutureReader` is different from `FutureRead`
pub type FutureReader<F> = OperatorFuture<options::ReaderOptions, Reader, F>;
impl<F: Future<Output = Result<Reader>>> FutureReader<F> {
/// Set `version` for this `reader`.
///
/// This feature can be used to retrieve the data of a specified version of the given path.
///
/// If the version doesn't exist, an error with kind [`ErrorKind::NotFound`] will be returned.
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
///
/// # async fn test(op: Operator, version: &str) -> Result<()> {
/// let mut r = op.reader_with("path/to/file").version(version).await?;
/// # Ok(())
/// # }
/// ```
pub fn version(mut self, v: &str) -> Self {
self.args.version = Some(v.to_string());
self
}
/// Set `concurrent` for the reader.
///
/// OpenDAL by default to write file without concurrent. This is not efficient for cases when users
/// read large chunks of data. By setting `concurrent`, opendal will reading files concurrently
/// on support storage services.
///
/// By setting `concurrent`, opendal will fetch chunks concurrently with
/// the give chunk size.
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # async fn test(op: Operator) -> Result<()> {
/// let r = op.reader_with("path/to/file").concurrent(8).await?;
/// # Ok(())
/// # }
/// ```
pub fn concurrent(mut self, concurrent: usize) -> Self {
self.args.concurrent = concurrent.max(1);
self
}
/// OpenDAL will use services' preferred chunk size by default. Users can set chunk based on their own needs.
///
/// This following example will make opendal read data in 4MiB chunks:
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # async fn test(op: Operator) -> Result<()> {
/// let r = op
/// .reader_with("path/to/file")
/// .chunk(4 * 1024 * 1024)
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn chunk(mut self, chunk_size: usize) -> Self {
self.args.chunk = Some(chunk_size);
self
}
/// Controls the optimization strategy for range reads in [`Reader::fetch`].
///
/// When performing range reads, if the gap between two requested ranges is smaller than
/// the configured `gap` size, OpenDAL will merge these ranges into a single read request
/// and discard the unrequested data in between. This helps reduce the number of API calls
/// to remote storage services.
///
/// This optimization is particularly useful when performing multiple small range reads
/// that are close to each other, as it reduces the overhead of multiple network requests
/// at the cost of transferring some additional data.
///
/// In this example, if two requested ranges are separated by less than 1MiB,
/// they will be merged into a single read request:
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # async fn test(op: Operator) -> Result<()> {
/// let r = op
/// .reader_with("path/to/file")
/// .chunk(4 * 1024 * 1024)
/// .gap(1024 * 1024) // 1MiB gap
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn gap(mut self, gap_size: usize) -> Self {
self.args.gap = Some(gap_size);
self
}
/// Set `if-match` for this `read` request.
///
/// This feature can be used to check if the file's `ETag` matches the given `ETag`.
///
/// If file exists and it's etag doesn't match, an error with kind [`ErrorKind::ConditionNotMatch`]
/// will be returned.
///
/// ```
/// # use opendal_core::Result;
/// use opendal_core::Operator;
/// # async fn test(op: Operator, etag: &str) -> Result<()> {
/// let mut r = op.reader_with("path/to/file").if_match(etag).await?;
/// # Ok(())
/// # }
/// ```
pub fn if_match(mut self, etag: &str) -> Self {
self.args.if_match = Some(etag.to_string());
self
}
/// Set `if-none-match` for this `read` request.
///
/// This feature can be used to check if the file's `ETag` doesn't match the given `ETag`.
///
/// If file exists and it's etag match, an error with kind [`ErrorKind::ConditionNotMatch`]
/// will be returned.
///
/// ```
/// # use opendal_core::Result;
/// use opendal_core::Operator;
/// # async fn test(op: Operator, etag: &str) -> Result<()> {
/// let mut r = op.reader_with("path/to/file").if_none_match(etag).await?;
/// # Ok(())
/// # }
/// ```
pub fn if_none_match(mut self, etag: &str) -> Self {
self.args.if_none_match = Some(etag.to_string());
self
}
/// Set `if-modified-since` for this `read` request.
///
/// This feature can be used to check if the file has been modified since the given timestamp.
///
/// If file exists and it hasn't been modified since the specified time, an error with kind
/// [`ErrorKind::ConditionNotMatch`] will be returned.
///
/// ```
/// # use opendal_core::Result;
/// use jiff::Timestamp;
/// use opendal_core::Operator;
/// # async fn test(op: Operator, time: Timestamp) -> Result<()> {
/// let mut r = op
/// .reader_with("path/to/file")
/// .if_modified_since(time)
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn if_modified_since(mut self, v: impl Into<Timestamp>) -> Self {
self.args.if_modified_since = Some(v.into());
self
}
/// Set `if-unmodified-since` for this `read` request.
///
/// This feature can be used to check if the file hasn't been modified since the given timestamp.
///
/// If file exists and it has been modified since the specified time, an error with kind
/// [`ErrorKind::ConditionNotMatch`] will be returned.
///
/// ```
/// # use opendal_core::Result;
/// use jiff::Timestamp;
/// use opendal_core::Operator;
/// # async fn test(op: Operator, time: Timestamp) -> Result<()> {
/// let mut r = op
/// .reader_with("path/to/file")
/// .if_unmodified_since(time)
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn if_unmodified_since(mut self, v: impl Into<Timestamp>) -> Self {
self.args.if_unmodified_since = Some(v.into());
self
}
}
/// Future that generated by [`Operator::write_with`].
///
/// Users can add more options by public functions provided by this struct.
pub type FutureWrite<F> = OperatorFuture<(options::WriteOptions, Buffer), Metadata, F>;
impl<F: Future<Output = Result<Metadata>>> FutureWrite<F> {
/// Sets append mode for this write request.
///
/// Refer to [`options::WriteOptions::append`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let _ = op
/// .write_with("path/to/file", vec![0; 4096])
/// .append(true)
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn append(mut self, v: bool) -> Self {
self.args.0.append = v;
self
}
/// Sets chunk size for buffered writes.
///
/// Refer to [`options::WriteOptions::chunk`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// // Set 8MiB chunk size - data will be sent in one API call at close
/// let _ = op
/// .write_with("path/to/file", vec![0; 4096])
/// .chunk(8 * 1024 * 1024)
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn chunk(mut self, v: usize) -> Self {
self.args.0.chunk = Some(v);
self
}
/// Sets concurrent write operations for this writer.
///
/// Refer to [`options::WriteOptions::concurrent`] for more details.
///
/// ## Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// // Enable concurrent writes with 8 parallel operations at 128B chunk.
/// let _ = op
/// .write_with("path/to/file", vec![0; 4096])
/// .chunk(128)
/// .concurrent(8)
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn concurrent(mut self, v: usize) -> Self {
self.args.0.concurrent = v.max(1);
self
}
/// Sets Cache-Control header for this write operation.
///
/// Refer to [`options::WriteOptions::cache_control`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// // Cache content for 7 days (604800 seconds)
/// let _ = op
/// .write_with("path/to/file", vec![0; 4096])
/// .cache_control("max-age=604800")
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn cache_control(mut self, v: &str) -> Self {
self.args.0.cache_control = Some(v.to_string());
self
}
/// Sets `Content-Type` header for this write operation.
///
/// Refer to [`options::WriteOptions::content_type`] for more details.
///
/// ## Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// // Set content type for plain text file
/// let _ = op
/// .write_with("path/to/file", vec![0; 4096])
/// .content_type("text/plain")
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn content_type(mut self, v: &str) -> Self {
self.args.0.content_type = Some(v.to_string());
self
}
/// Sets Content-Disposition header for this write request.
///
/// Refer to [`options::WriteOptions::content_disposition`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let _ = op
/// .write_with("path/to/file", vec![0; 4096])
/// .content_disposition("attachment; filename=\"filename.jpg\"")
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn content_disposition(mut self, v: &str) -> Self {
self.args.0.content_disposition = Some(v.to_string());
self
}
/// Sets Content-Encoding header for this write request.
///
/// Refer to [`options::WriteOptions::content_encoding`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let _ = op
/// .write_with("path/to/file", vec![0; 4096])
/// .content_encoding("gzip")
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn content_encoding(mut self, v: &str) -> Self {
self.args.0.content_encoding = Some(v.to_string());
self
}
/// Sets If-Match header for this write request.
///
/// Refer to [`options::WriteOptions::if_match`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let _ = op
/// .write_with("path/to/file", vec![0; 4096])
/// .if_match("\"686897696a7c876b7e\"")
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn if_match(mut self, s: &str) -> Self {
self.args.0.if_match = Some(s.to_string());
self
}
/// Sets If-None-Match header for this write request.
///
/// Refer to [`options::WriteOptions::if_none_match`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let _ = op
/// .write_with("path/to/file", vec![0; 4096])
/// .if_none_match("\"686897696a7c876b7e\"")
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn if_none_match(mut self, s: &str) -> Self {
self.args.0.if_none_match = Some(s.to_string());
self
}
/// Sets the condition that write operation will succeed only if target does not exist.
///
/// Refer to [`options::WriteOptions::if_not_exists`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let _ = op
/// .write_with("path/to/file", vec![0; 4096])
/// .if_not_exists(true)
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn if_not_exists(mut self, b: bool) -> Self {
self.args.0.if_not_exists = b;
self
}
/// Sets user metadata for this write request.
///
/// Refer to [`options::WriteOptions::user_metadata`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let _ = op
/// .write_with("path/to/file", vec![0; 4096])
/// .user_metadata([
/// ("language".to_string(), "rust".to_string()),
/// ("author".to_string(), "OpenDAL".to_string()),
/// ])
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn user_metadata(mut self, data: impl IntoIterator<Item = (String, String)>) -> Self {
self.args.0.user_metadata = Some(HashMap::from_iter(data));
self
}
}
/// Future that generated by [`Operator::writer_with`].
///
/// Users can add more options by public functions provided by this struct.
pub type FutureWriter<F> = OperatorFuture<options::WriteOptions, Writer, F>;
impl<F: Future<Output = Result<Writer>>> FutureWriter<F> {
/// Sets append mode for this write request.
///
/// Refer to [`options::WriteOptions::append`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let mut w = op.writer_with("path/to/file").append(true).await?;
/// w.write(vec![0; 4096]).await?;
/// w.write(vec![1; 4096]).await?;
/// w.close().await?;
/// # Ok(())
/// # }
/// ```
pub fn append(mut self, v: bool) -> Self {
self.args.append = v;
self
}
/// Sets chunk size for buffered writes.
///
/// Refer to [`options::WriteOptions::chunk`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// // Set 8MiB chunk size - data will be sent in one API call at close
/// let mut w = op
/// .writer_with("path/to/file")
/// .chunk(8 * 1024 * 1024)
/// .await?;
/// w.write(vec![0; 4096]).await?;
/// w.write(vec![1; 4096]).await?;
/// w.close().await?;
/// # Ok(())
/// # }
/// ```
pub fn chunk(mut self, v: usize) -> Self {
self.args.chunk = Some(v);
self
}
/// Sets concurrent write operations for this writer.
///
/// Refer to [`options::WriteOptions::concurrent`] for more details.
///
/// ## Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// // Enable concurrent writes with 8 parallel operations
/// let mut w = op.writer_with("path/to/file").concurrent(8).await?;
///
/// // First write starts immediately
/// w.write(vec![0; 4096]).await?;
///
/// // Second write runs concurrently with first
/// w.write(vec![1; 4096]).await?;
///
/// // Ensures all writes complete successfully and in order
/// w.close().await?;
/// # Ok(())
/// # }
/// ```
pub fn concurrent(mut self, v: usize) -> Self {
self.args.concurrent = v.max(1);
self
}
/// Sets Cache-Control header for this write operation.
///
/// Refer to [`options::WriteOptions::cache_control`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// // Cache content for 7 days (604800 seconds)
/// let mut w = op
/// .writer_with("path/to/file")
/// .cache_control("max-age=604800")
/// .await?;
/// w.write(vec![0; 4096]).await?;
/// w.write(vec![1; 4096]).await?;
/// w.close().await?;
/// # Ok(())
/// # }
/// ```
pub fn cache_control(mut self, v: &str) -> Self {
self.args.cache_control = Some(v.to_string());
self
}
/// Sets `Content-Type` header for this write operation.
///
/// Refer to [`options::WriteOptions::content_type`] for more details.
///
/// ## Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// // Set content type for plain text file
/// let mut w = op
/// .writer_with("path/to/file")
/// .content_type("text/plain")
/// .await?;
/// w.write(vec![0; 4096]).await?;
/// w.write(vec![1; 4096]).await?;
/// w.close().await?;
/// # Ok(())
/// # }
/// ```
pub fn content_type(mut self, v: &str) -> Self {
self.args.content_type = Some(v.to_string());
self
}
/// Sets Content-Disposition header for this write request.
///
/// Refer to [`options::WriteOptions::content_disposition`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let mut w = op
/// .writer_with("path/to/file")
/// .content_disposition("attachment; filename=\"filename.jpg\"")
/// .await?;
/// w.write(vec![0; 4096]).await?;
/// w.write(vec![1; 4096]).await?;
/// w.close().await?;
/// # Ok(())
/// # }
/// ```
pub fn content_disposition(mut self, v: &str) -> Self {
self.args.content_disposition = Some(v.to_string());
self
}
/// Sets Content-Encoding header for this write request.
///
/// Refer to [`options::WriteOptions::content_encoding`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let mut w = op
/// .writer_with("path/to/file")
/// .content_encoding("gzip")
/// .await?;
/// w.write(vec![0; 4096]).await?;
/// w.write(vec![1; 4096]).await?;
/// w.close().await?;
/// # Ok(())
/// # }
/// ```
pub fn content_encoding(mut self, v: &str) -> Self {
self.args.content_encoding = Some(v.to_string());
self
}
/// Sets If-Match header for this write request.
///
/// Refer to [`options::WriteOptions::if_match`] for more details.
///
/// ### Behavior
///
/// - If supported, the write operation will only succeed if the target's ETag matches the specified value
/// - The value should be a valid ETag string
/// - Common values include:
/// - A specific ETag value like `"686897696a7c876b7e"`
/// - `*` - Matches any existing resource
/// - If not supported, the value will be ignored
///
/// This operation provides conditional write functionality based on ETag matching,
/// helping prevent unintended overwrites in concurrent scenarios.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let mut w = op
/// .writer_with("path/to/file")
/// .if_match("\"686897696a7c876b7e\"")
/// .await?;
/// w.write(vec![0; 4096]).await?;
/// w.write(vec![1; 4096]).await?;
/// w.close().await?;
/// # Ok(())
/// # }
/// ```
pub fn if_match(mut self, s: &str) -> Self {
self.args.if_match = Some(s.to_string());
self
}
/// Sets If-None-Match header for this write request.
///
/// Refer to [`options::WriteOptions::if_none_match`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let mut w = op
/// .writer_with("path/to/file")
/// .if_none_match("\"686897696a7c876b7e\"")
/// .await?;
/// w.write(vec![0; 4096]).await?;
/// w.write(vec![1; 4096]).await?;
/// w.close().await?;
/// # Ok(())
/// # }
/// ```
pub fn if_none_match(mut self, s: &str) -> Self {
self.args.if_none_match = Some(s.to_string());
self
}
/// Sets the condition that write operation will succeed only if target does not exist.
///
/// Refer to [`options::WriteOptions::if_not_exists`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let mut w = op.writer_with("path/to/file").if_not_exists(true).await?;
/// w.write(vec![0; 4096]).await?;
/// w.write(vec![1; 4096]).await?;
/// w.close().await?;
/// # Ok(())
/// # }
/// ```
pub fn if_not_exists(mut self, b: bool) -> Self {
self.args.if_not_exists = b;
self
}
/// Sets user metadata for this write request.
///
/// Refer to [`options::WriteOptions::user_metadata`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
/// # use futures::StreamExt;
/// # use futures::SinkExt;
/// use bytes::Bytes;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let mut w = op
/// .writer_with("path/to/file")
/// .user_metadata([
/// ("content-type".to_string(), "text/plain".to_string()),
/// ("author".to_string(), "OpenDAL".to_string()),
/// ])
/// .await?;
/// w.write(vec![0; 4096]).await?;
/// w.close().await?;
/// # Ok(())
/// # }
/// ```
pub fn user_metadata(mut self, data: impl IntoIterator<Item = (String, String)>) -> Self {
self.args.user_metadata = Some(HashMap::from_iter(data));
self
}
}
/// Future that generated by [`Operator::delete_with`].
///
/// Users can add more options by public functions provided by this struct.
pub type FutureDelete<F> = OperatorFuture<options::DeleteOptions, (), F>;
impl<F: Future<Output = Result<()>>> FutureDelete<F> {
/// Change the version of this delete operation.
pub fn version(mut self, v: &str) -> Self {
self.args.version = Some(v.to_string());
self
}
/// Enable recursive deletion.
pub fn recursive(mut self, recursive: bool) -> Self {
self.args.recursive = recursive;
self
}
}
/// Future that generated by [`Operator::deleter_with`].
///
/// Users can add more options by public functions provided by this struct.
pub type FutureDeleter<F> = OperatorFuture<OpDeleter, (), F>;
/// Future that generated by [`Operator::list_with`] or [`Operator::lister_with`].
///
/// Users can add more options by public functions provided by this struct.
pub type FutureList<F> = OperatorFuture<options::ListOptions, Vec<Entry>, F>;
impl<F: Future<Output = Result<Vec<Entry>>>> FutureList<F> {
/// The limit passed to underlying service to specify the max results
/// that could return per-request.
///
/// Users could use this to control the memory usage of list operation.
pub fn limit(mut self, v: usize) -> Self {
self.args.limit = Some(v);
self
}
/// The start_after passes to underlying service to specify the specified key
/// to start listing from.
pub fn start_after(mut self, v: &str) -> Self {
self.args.start_after = Some(v.to_string());
self
}
/// The recursive is used to control whether the list operation is recursive.
///
/// - If `false`, list operation will only list the entries under the given path.
/// - If `true`, list operation will list all entries that starts with given path.
///
/// Default to `false`.
pub fn recursive(mut self, v: bool) -> Self {
self.args.recursive = v;
self
}
/// Controls whether the `list` operation should return file versions.
///
/// This function allows you to specify if the `list` operation, when executed, should include
/// information about different versions of files, if versioning is supported and enabled.
///
/// If `true`, subsequent `list` operations will include version information for each file.
/// If `false`, version information will be omitted from the `list` results.
///
/// Default to `false`
pub fn versions(mut self, v: bool) -> Self {
self.args.versions = v;
self
}
/// Controls whether the `list` operation should include deleted files (or versions).
///
/// This function allows you to specify if the `list` operation, when executed, should include
/// entries for files or versions that have been marked as deleted. This is particularly relevant
/// in object storage systems that support soft deletion or versioning.
///
/// If `true`, subsequent `list` operations will include deleted files or versions.
/// If `false`, deleted files or versions will be excluded from the `list` results.
pub fn deleted(mut self, v: bool) -> Self {
self.args.deleted = v;
self
}
}
/// Future that generated by [`Operator::list_with`] or [`Operator::lister_with`].
///
/// Users can add more options by public functions provided by this struct.
pub type FutureLister<F> = OperatorFuture<options::ListOptions, Lister, F>;
impl<F: Future<Output = Result<Lister>>> FutureLister<F> {
/// The limit passed to underlying service to specify the max results
/// that could return per-request.
///
/// Users could use this to control the memory usage of list operation.
pub fn limit(mut self, v: usize) -> Self {
self.args.limit = Some(v);
self
}
/// The start_after passes to underlying service to specify the specified key
/// to start listing from.
pub fn start_after(mut self, v: &str) -> Self {
self.args.start_after = Some(v.to_string());
self
}
/// The recursive is used to control whether the list operation is recursive.
///
/// - If `false`, list operation will only list the entries under the given path.
/// - If `true`, list operation will list all entries that starts with given path.
///
/// Default to `false`.
pub fn recursive(mut self, v: bool) -> Self {
self.args.recursive = v;
self
}
/// Controls whether the `list` operation should return file versions.
///
/// This function allows you to specify if the `list` operation, when executed, should include
/// information about different versions of files, if versioning is supported and enabled.
///
/// If `true`, subsequent `list` operations will include version information for each file.
/// If `false`, version information will be omitted from the `list` results.
///
/// Default to `false`
pub fn versions(mut self, v: bool) -> Self {
self.args.versions = v;
self
}
/// Controls whether the `list` operation should include deleted files (or versions).
///
/// This function allows you to specify if the `list` operation, when executed, should include
/// entries for files or versions that have been marked as deleted. This is particularly relevant
/// in object storage systems that support soft deletion or versioning.
///
/// If `true`, subsequent `list` operations will include deleted files or versions.
/// If `false`, deleted files or versions will be excluded from the `list` results.
pub fn deleted(mut self, v: bool) -> Self {
self.args.deleted = v;
self
}
}
/// Future that generated by [`Operator::copy_with`].
///
/// Users can add more options by public functions provided by this struct.
pub type FutureCopy<F> = OperatorFuture<(options::CopyOptions, String), (), F>;
impl<F: Future<Output = Result<()>>> FutureCopy<F> {
/// Sets the condition that copy operation will succeed only if target does not exist.
///
/// Refer to [`options::CopyOptions::if_not_exists`] for more details.
///
/// ### Example
///
/// ```
/// # use opendal_core::Result;
/// # use opendal_core::Operator;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let _ = op
/// .copy_with("source/path", "target/path")
/// .if_not_exists(true)
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn if_not_exists(mut self, v: bool) -> Self {
self.args.0.if_not_exists = v;
self
}
}
```
--------------------------------------------------------------------------------
/core/services/webdav/src/core.rs:
--------------------------------------------------------------------------------
```rust
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use std::collections::HashMap;
use std::collections::VecDeque;
use std::fmt::Debug;
use std::io::Cursor;
use std::sync::Arc;
use bytes::Bytes;
use http::Request;
use http::Response;
use http::StatusCode;
use http::header;
use quick_xml::Reader;
use quick_xml::Writer;
use quick_xml::escape::escape;
use quick_xml::events::BytesEnd;
use quick_xml::events::BytesStart;
use quick_xml::events::BytesText;
use quick_xml::events::Event;
use serde::Deserialize;
use super::error::parse_error;
use opendal_core::raw::*;
use opendal_core::*;
/// Default namespace prefix for user-defined properties in WebDAV.
/// Users can override this via configuration.
pub const DEFAULT_USER_METADATA_PREFIX: &str = "opendal";
/// Default namespace URI for user-defined properties in WebDAV.
/// Users can override this via configuration.
pub const DEFAULT_USER_METADATA_URI: &str = "https://opendal.apache.org/ns";
/// The request to query all properties of a file or directory.
///
/// rfc4918 9.1: retrieve all properties define in specification
static PROPFIND_REQUEST: &str = r#"<?xml version="1.0" encoding="utf-8" ?><D:propfind xmlns:D="DAV:"><D:allprop/></D:propfind>"#;
/// The header to specify the depth of the query.
///
/// Valid values are `0`, `1`, `infinity`.
///
/// - `0`: only to the resource itself.
/// - `1`: to the resource and its internal members only.
/// - `infinity`: to the resource and all its members.
///
/// reference: [RFC4918: 10.2. Depth Header](https://datatracker.ietf.org/doc/html/rfc4918#section-10.2)
static HEADER_DEPTH: &str = "Depth";
/// The header to specify the destination of the query.
///
/// The Destination request header specifies the URI that identifies a
/// destination resource for methods such as COPY and MOVE, which take
/// two URIs as parameters.
///
/// reference: [RFC4918: 10.3. Destination Header](https://datatracker.ietf.org/doc/html/rfc4918#section-10.3)
static HEADER_DESTINATION: &str = "Destination";
/// The header to specify the overwrite behavior of the query
///
/// The Overwrite request header specifies whether the server should
/// overwrite a resource mapped to the destination URL during a COPY or
/// MOVE.
///
/// Valid values are `T` and `F`.
///
/// A value of "F" states that the server must not perform the COPY or MOVE operation
/// if the destination URL does map to a resource.
///
/// reference: [RFC4918: 10.6. Overwrite Header](https://datatracker.ietf.org/doc/html/rfc4918#section-10.6)
static HEADER_OVERWRITE: &str = "Overwrite";
pub struct WebdavCore {
pub info: Arc<AccessorInfo>,
pub endpoint: String,
pub server_path: String,
pub root: String,
pub authorization: Option<String>,
/// XML namespace prefix for user metadata properties.
pub user_metadata_prefix: String,
/// XML namespace URI for user metadata properties.
pub user_metadata_uri: String,
}
impl Debug for WebdavCore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WebdavCore")
.field("endpoint", &self.endpoint)
.field("root", &self.root)
.field("user_metadata_prefix", &self.user_metadata_prefix)
.field("user_metadata_uri", &self.user_metadata_uri)
.finish_non_exhaustive()
}
}
impl WebdavCore {
pub async fn webdav_stat(&self, path: &str) -> Result<Metadata> {
let path = build_rooted_abs_path(&self.root, path);
self.webdav_stat_rooted_abs_path(&path).await
}
/// Input path must be `rooted_abs_path`.
async fn webdav_stat_rooted_abs_path(&self, rooted_abs_path: &str) -> Result<Metadata> {
let url = format!("{}{}", self.endpoint, percent_encode_path(rooted_abs_path));
let mut req = Request::builder().method("PROPFIND").uri(url);
req = req.header(header::CONTENT_TYPE, "application/xml");
req = req.header(header::CONTENT_LENGTH, PROPFIND_REQUEST.len());
if let Some(auth) = &self.authorization {
req = req.header(header::AUTHORIZATION, auth);
}
// Only stat the resource itself.
req = req.header(HEADER_DEPTH, "0");
let req = req
.extension(Operation::Stat)
.body(Buffer::from(Bytes::from(PROPFIND_REQUEST)))
.map_err(new_request_build_error)?;
let resp = self.info.http_client().send(req).await?;
if !resp.status().is_success() {
return Err(parse_error(resp));
}
let bs = resp.into_body();
let xml_bytes = bs.to_bytes();
let xml_str = String::from_utf8_lossy(&xml_bytes);
let result: Multistatus = deserialize_multistatus(&xml_bytes)?;
let propfind_resp = result.response.first().ok_or_else(|| {
Error::new(
ErrorKind::NotFound,
"propfind response is empty, the resource is not exist",
)
})?;
let mut metadata = parse_propstat(&propfind_resp.propstat)?;
// Parse user metadata from the raw XML response using configured namespace
let user_metadata = parse_user_metadata_from_xml(&xml_str, &self.user_metadata_uri);
if !user_metadata.is_empty() {
metadata = metadata.with_user_metadata(user_metadata);
}
Ok(metadata)
}
pub async fn webdav_get(
&self,
path: &str,
range: BytesRange,
_: &OpRead,
) -> Result<Response<HttpBody>> {
let path = build_rooted_abs_path(&self.root, path);
let url: String = format!("{}{}", self.endpoint, percent_encode_path(&path));
let mut req = Request::get(&url);
if let Some(auth) = &self.authorization {
req = req.header(header::AUTHORIZATION, auth.clone())
}
if !range.is_full() {
req = req.header(header::RANGE, range.to_header());
}
let req = req
.extension(Operation::Read)
.body(Buffer::new())
.map_err(new_request_build_error)?;
self.info.http_client().fetch(req).await
}
pub async fn webdav_put(
&self,
path: &str,
size: Option<u64>,
args: &OpWrite,
body: Buffer,
) -> Result<Response<Buffer>> {
let path = build_rooted_abs_path(&self.root, path);
let url = format!("{}{}", self.endpoint, percent_encode_path(&path));
let mut req = Request::put(&url);
if let Some(v) = &self.authorization {
req = req.header(header::AUTHORIZATION, v)
}
if let Some(v) = size {
req = req.header(header::CONTENT_LENGTH, v)
}
if let Some(v) = args.content_type() {
req = req.header(header::CONTENT_TYPE, v)
}
if let Some(v) = args.content_disposition() {
req = req.header(header::CONTENT_DISPOSITION, v)
}
let req = req
.extension(Operation::Write)
.body(body)
.map_err(new_request_build_error)?;
self.info.http_client().send(req).await
}
/// Set user-defined metadata using WebDAV PROPPATCH method.
///
/// This method uses the OpenDAL custom namespace to store user metadata
/// as DAV dead properties. Each key-value pair in the user_metadata map
/// is stored as a property with the configured namespace prefix.
///
/// # Reference
/// - [RFC4918: 9.2 PROPPATCH Method](https://datatracker.ietf.org/doc/html/rfc4918#section-9.2)
pub async fn webdav_proppatch(
&self,
path: &str,
user_metadata: &HashMap<String, String>,
) -> Result<Response<Buffer>> {
let path = build_rooted_abs_path(&self.root, path);
let url = format!("{}{}", self.endpoint, percent_encode_path(&path));
let mut req = Request::builder().method("PROPPATCH").uri(&url);
req = req.header(header::CONTENT_TYPE, "application/xml; charset=utf-8");
if let Some(auth) = &self.authorization {
req = req.header(header::AUTHORIZATION, auth);
}
// Build the PROPPATCH XML request body using configured namespace
let proppatch_body = build_proppatch_request(
user_metadata,
&self.user_metadata_prefix,
&self.user_metadata_uri,
);
let req = req
.extension(Operation::Write)
.body(Buffer::from(Bytes::from(proppatch_body)))
.map_err(new_request_build_error)?;
self.info.http_client().send(req).await
}
pub async fn webdav_delete(&self, path: &str) -> Result<Response<Buffer>> {
let path = build_rooted_abs_path(&self.root, path);
let url = format!("{}{}", self.endpoint, percent_encode_path(&path));
let mut req = Request::delete(&url);
if let Some(auth) = &self.authorization {
req = req.header(header::AUTHORIZATION, auth.clone())
}
let req = req
.extension(Operation::Delete)
.body(Buffer::new())
.map_err(new_request_build_error)?;
self.info.http_client().send(req).await
}
pub async fn webdav_copy(&self, from: &str, to: &str) -> Result<Response<Buffer>> {
// Check if source file exists.
let _ = self.webdav_stat(from).await?;
// Make sure target's dir is exist.
self.webdav_mkcol(get_parent(to)).await?;
let source = build_rooted_abs_path(&self.root, from);
let source_uri = format!("{}{}", self.endpoint, percent_encode_path(&source));
let target = build_rooted_abs_path(&self.root, to);
let target_uri = format!("{}{}", self.endpoint, percent_encode_path(&target));
let mut req = Request::builder().method("COPY").uri(&source_uri);
if let Some(auth) = &self.authorization {
req = req.header(header::AUTHORIZATION, auth);
}
req = req.header(HEADER_DESTINATION, target_uri);
req = req.header(HEADER_OVERWRITE, "T");
let req = req
.extension(Operation::Copy)
.body(Buffer::new())
.map_err(new_request_build_error)?;
self.info.http_client().send(req).await
}
pub async fn webdav_move(&self, from: &str, to: &str) -> Result<Response<Buffer>> {
// Check if source file exists.
let _ = self.webdav_stat(from).await?;
// Make sure target's dir is exist.
self.webdav_mkcol(get_parent(to)).await?;
let source = build_rooted_abs_path(&self.root, from);
let source_uri = format!("{}{}", self.endpoint, percent_encode_path(&source));
let target = build_rooted_abs_path(&self.root, to);
let target_uri = format!("{}{}", self.endpoint, percent_encode_path(&target));
let mut req = Request::builder().method("MOVE").uri(&source_uri);
if let Some(auth) = &self.authorization {
req = req.header(header::AUTHORIZATION, auth);
}
req = req.header(HEADER_DESTINATION, target_uri);
req = req.header(HEADER_OVERWRITE, "T");
let req = req
.extension(Operation::Rename)
.body(Buffer::new())
.map_err(new_request_build_error)?;
self.info.http_client().send(req).await
}
pub async fn webdav_list(&self, path: &str, args: &OpList) -> Result<Response<Buffer>> {
let path = build_rooted_abs_path(&self.root, path);
let url = format!("{}{}", self.endpoint, percent_encode_path(&path));
let mut req = Request::builder().method("PROPFIND").uri(&url);
req = req.header(header::CONTENT_TYPE, "application/xml");
req = req.header(header::CONTENT_LENGTH, PROPFIND_REQUEST.len());
if let Some(auth) = &self.authorization {
req = req.header(header::AUTHORIZATION, auth);
}
if args.recursive() {
req = req.header(HEADER_DEPTH, "infinity");
} else {
req = req.header(HEADER_DEPTH, "1");
}
let req = req
.extension(Operation::List)
.body(Buffer::from(Bytes::from(PROPFIND_REQUEST)))
.map_err(new_request_build_error)?;
self.info.http_client().send(req).await
}
/// Create dir recursively for given path.
///
/// # Notes
///
/// We only expose this method to the backend since there are dependencies on input path.
pub async fn webdav_mkcol(&self, path: &str) -> Result<()> {
let path = build_rooted_abs_path(&self.root, path);
let mut path = path.as_str();
let mut dirs = VecDeque::default();
loop {
match self.webdav_stat_rooted_abs_path(path).await {
// Dir exists, break the loop.
Ok(_) => {
break;
}
// Dir not found, keep going.
Err(err) if err.kind() == ErrorKind::NotFound => {
dirs.push_front(path);
path = get_parent(path);
}
// Unexpected error found, return it.
Err(err) => return Err(err),
}
if path == "/" {
break;
}
}
for dir in dirs {
self.webdav_mkcol_rooted_abs_path(dir).await?;
}
Ok(())
}
/// Create a dir
///
/// Input path must be `rooted_abs_path`
///
/// Reference: [RFC4918: 9.3.1. MKCOL Status Codes](https://datatracker.ietf.org/doc/html/rfc4918#section-9.3.1)
async fn webdav_mkcol_rooted_abs_path(&self, rooted_abs_path: &str) -> Result<()> {
let url = format!("{}{}", self.endpoint, percent_encode_path(rooted_abs_path));
let mut req = Request::builder().method("MKCOL").uri(&url);
if let Some(auth) = &self.authorization {
req = req.header(header::AUTHORIZATION, auth.clone())
}
let req = req
.extension(Operation::CreateDir)
.body(Buffer::new())
.map_err(new_request_build_error)?;
let resp = self.info.http_client().send(req).await?;
let status = resp.status();
match status {
// 201 (Created) - The collection was created.
StatusCode::CREATED
// 405 (Method Not Allowed) - MKCOL can only be executed on an unmapped URL.
//
// The MKCOL method can only be performed on a deleted or non-existent resource.
// This error means the directory already exists which is allowed by create_dir.
| StatusCode::METHOD_NOT_ALLOWED => {
Ok(())
}
_ => Err(parse_error(resp)),
}
}
}
/// Build a PROPPATCH request body to set user-defined metadata.
///
/// The request uses the specified namespace to store metadata as dead properties
/// on the WebDAV server.
///
/// # Arguments
/// * `user_metadata` - Key-value pairs to store as properties
/// * `namespace_prefix` - XML namespace prefix (e.g., "opendal")
/// * `namespace_uri` - XML namespace URI (e.g., "https://opendal.apache.org/ns")
///
/// # Example output
/// ```xml
/// <?xml version="1.0" encoding="utf-8"?>
/// <D:propertyupdate xmlns:D="DAV:" xmlns:opendal="https://opendal.apache.org/ns">
/// <D:set>
/// <D:prop>
/// <opendal:key1>value1</opendal:key1>
/// <opendal:key2>value2</opendal:key2>
/// </D:prop>
/// </D:set>
/// </D:propertyupdate>
/// ```
pub fn build_proppatch_request(
user_metadata: &HashMap<String, String>,
namespace_prefix: &str,
namespace_uri: &str,
) -> String {
let mut writer = Writer::new(Cursor::new(Vec::new()));
// Write XML declaration
writer
.write_event(Event::Decl(quick_xml::events::BytesDecl::new(
"1.0",
Some("utf-8"),
None,
)))
.expect("write xml decl");
// Write <D:propertyupdate> with namespace declarations
let mut propertyupdate = BytesStart::new("D:propertyupdate");
propertyupdate.push_attribute(("xmlns:D", "DAV:"));
propertyupdate.push_attribute((
format!("xmlns:{}", namespace_prefix).as_str(),
namespace_uri,
));
writer
.write_event(Event::Start(propertyupdate))
.expect("write propertyupdate");
// Write <D:set>
writer
.write_event(Event::Start(BytesStart::new("D:set")))
.expect("write set");
// Write <D:prop>
writer
.write_event(Event::Start(BytesStart::new("D:prop")))
.expect("write prop");
// Write each user metadata property
for (key, value) in user_metadata {
// Note: key needs to be escaped for XML tag name, value is handled by BytesText
let escaped_key = escape(key);
let tag_name = format!("{}:{}", namespace_prefix, escaped_key);
writer
.write_event(Event::Start(BytesStart::new(&tag_name)))
.expect("write prop start");
// BytesText::new expects unescaped content and will escape it automatically
writer
.write_event(Event::Text(BytesText::new(value)))
.expect("write prop value");
writer
.write_event(Event::End(BytesEnd::new(&tag_name)))
.expect("write prop end");
}
// Close tags
writer
.write_event(Event::End(BytesEnd::new("D:prop")))
.expect("write prop end");
writer
.write_event(Event::End(BytesEnd::new("D:set")))
.expect("write set end");
writer
.write_event(Event::End(BytesEnd::new("D:propertyupdate")))
.expect("write propertyupdate end");
String::from_utf8(writer.into_inner().into_inner()).expect("valid utf8")
}
/// Parse user metadata from the raw XML response using quick-xml Reader.
///
/// This function extracts properties in the specified namespace
/// from the PROPFIND response and returns them as a HashMap.
///
/// Note: WebDAV servers like Nextcloud/ownCloud may use dynamic namespace prefixes
/// (e.g., x1:, x2:) instead of the prefix we send. The namespace URI is the
/// reliable identifier, not the prefix.
///
/// # Arguments
/// * `xml` - The raw XML response string
/// * `namespace_uri` - The namespace URI to look for
pub fn parse_user_metadata_from_xml(xml: &str, namespace_uri: &str) -> HashMap<String, String> {
let mut user_metadata = HashMap::new();
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(true);
// Track namespace prefix -> URI mappings
let mut ns_prefixes: HashMap<String, String> = HashMap::new();
// Track which prefixes map to our target namespace URI
let mut target_prefixes: Vec<String> = Vec::new();
// Current element state for tracking property values
let mut current_prop_key: Option<String> = None;
let mut current_prop_value = String::new();
let mut buf = Vec::new();
loop {
let event = reader.read_event_into(&mut buf);
match event {
Ok(Event::Start(ref e)) => {
// Extract namespace declarations from attributes
for attr in e.attributes().flatten() {
let key = String::from_utf8_lossy(attr.key.as_ref()).to_string();
if key.starts_with("xmlns:") {
let prefix = key.strip_prefix("xmlns:").unwrap_or("").to_string();
let uri = String::from_utf8_lossy(&attr.value).to_string();
if uri == namespace_uri && !target_prefixes.contains(&prefix) {
target_prefixes.push(prefix.clone());
}
ns_prefixes.insert(prefix, uri);
}
}
// Check if this element is in our target namespace
let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
if let Some(colon_pos) = name.find(':') {
let prefix = &name[..colon_pos];
let local_name = &name[colon_pos + 1..];
if target_prefixes.contains(&prefix.to_string()) {
current_prop_key = Some(local_name.to_string());
current_prop_value.clear();
}
}
}
Ok(Event::Empty(ref e)) => {
// Extract namespace declarations from attributes
for attr in e.attributes().flatten() {
let key = String::from_utf8_lossy(attr.key.as_ref()).to_string();
if key.starts_with("xmlns:") {
let prefix = key.strip_prefix("xmlns:").unwrap_or("").to_string();
let uri = String::from_utf8_lossy(&attr.value).to_string();
if uri == namespace_uri && !target_prefixes.contains(&prefix) {
target_prefixes.push(prefix.clone());
}
ns_prefixes.insert(prefix, uri);
}
}
// For Empty events (self-closing tags), immediately insert with empty value
let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
if let Some(colon_pos) = name.find(':') {
let prefix = &name[..colon_pos];
let local_name = &name[colon_pos + 1..];
if target_prefixes.contains(&prefix.to_string()) {
user_metadata.insert(local_name.to_string(), String::new());
}
}
}
Ok(Event::Text(ref e)) => {
if current_prop_key.is_some() {
// Text content - add directly (no escaping needed)
let text_str = String::from_utf8_lossy(e.as_ref());
current_prop_value.push_str(&text_str);
}
}
Ok(Event::GeneralRef(ref e)) => {
if current_prop_key.is_some() {
// Handle XML entity references (e.g., < > & " ')
let entity_name = String::from_utf8_lossy(e.as_ref());
let decoded = match entity_name.as_ref() {
"lt" => "<",
"gt" => ">",
"amp" => "&",
"quot" => "\"",
"apos" => "'",
_ => "", // Unknown entity, skip
};
current_prop_value.push_str(decoded);
}
}
Ok(Event::End(ref e)) => {
let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
if let Some(colon_pos) = name.find(':') {
let prefix = &name[..colon_pos];
let local_name = &name[colon_pos + 1..];
if target_prefixes.contains(&prefix.to_string()) {
if let Some(key) = current_prop_key.take() {
if key == local_name {
user_metadata.insert(key, current_prop_value.clone());
current_prop_value.clear();
}
}
}
}
}
Ok(Event::Eof) => break,
Err(_) => break,
_ => {}
}
buf.clear();
}
user_metadata
}
/// Check if a PROPPATCH 207 Multi-Status response indicates success using quick-xml Reader.
///
/// A 207 status code only indicates that there is detailed information available,
/// not success or failure. We need to parse the response body to check if all
/// property updates were successful (status 200).
///
/// Returns Ok(()) if all properties were successfully updated, or an error
/// with details about the failure.
pub fn check_proppatch_response(xml: &str) -> Result<()> {
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut in_status = false;
let mut status_text = String::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let name = String::from_utf8_lossy(e.name().as_ref()).to_lowercase();
// Match status element regardless of namespace prefix
if name.ends_with(":status") || name == "status" {
in_status = true;
status_text.clear();
}
}
Ok(Event::Text(ref e)) => {
if in_status {
let text_str = String::from_utf8_lossy(e.as_ref());
status_text.push_str(&text_str);
}
}
Ok(Event::End(ref e)) => {
let name = String::from_utf8_lossy(e.name().as_ref()).to_lowercase();
if name.ends_with(":status") || name == "status" {
// Parse status code from "HTTP/1.1 XXX Description"
if let Some(code_str) = status_text.split_whitespace().nth(1) {
if let Ok(code) = code_str.parse::<u16>() {
if !(200..300).contains(&code) {
return Err(Error::new(
ErrorKind::Unexpected,
format!("PROPPATCH failed with status: {status_text}"),
));
}
}
}
in_status = false;
}
}
Ok(Event::Eof) => break,
Err(_) => break,
_ => {}
}
buf.clear();
}
Ok(())
}
pub fn deserialize_multistatus(bs: &[u8]) -> Result<Multistatus> {
let s = String::from_utf8_lossy(bs);
// HACKS! HACKS! HACKS!
//
// Make sure the string is escaped.
// Related to <https://github.com/tafia/quick-xml/issues/719>
//
// This is a temporary solution, we should find a better way to handle this.
let s = s.replace("&()_+-=;", "%26%28%29_%2B-%3D%3B");
quick_xml::de::from_str(&s).map_err(new_xml_deserialize_error)
}
pub fn parse_propstat(propstat: &Propstat) -> Result<Metadata> {
let Propstat {
prop:
Prop {
getlastmodified,
getcontentlength,
getcontenttype,
getetag,
resourcetype,
..
},
status,
} = propstat;
if let [_, code, text] = status.splitn(3, ' ').collect::<Vec<_>>()[..3] {
// As defined in https://tools.ietf.org/html/rfc2068#section-6.1
let code = code.parse::<u16>().unwrap();
if code >= 400 {
return Err(Error::new(
ErrorKind::Unexpected,
format!("propfind response is unexpected: {code} {text}"),
));
}
}
let mode: EntryMode = if resourcetype.value == Some(ResourceType::Collection) {
EntryMode::DIR
} else {
EntryMode::FILE
};
let mut m = Metadata::new(mode);
if let Some(v) = getcontentlength {
m.set_content_length(v.parse::<u64>().unwrap());
}
if let Some(v) = getcontenttype {
m.set_content_type(v);
}
if let Some(v) = getetag {
m.set_etag(v);
}
// https://www.rfc-editor.org/rfc/rfc4918#section-14.18
m.set_last_modified(Timestamp::parse_rfc2822(getlastmodified)?);
// the storage services have returned all the properties
Ok(m)
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
#[serde(default)]
pub struct Multistatus {
pub response: Vec<PropfindResponse>,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct PropfindResponse {
pub href: String,
pub propstat: Propstat,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct Propstat {
pub status: String,
pub prop: Prop,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct Prop {
pub getlastmodified: String,
pub getetag: Option<String>,
pub getcontentlength: Option<String>,
pub getcontenttype: Option<String>,
pub resourcetype: ResourceTypeContainer,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct ResourceTypeContainer {
#[serde(rename = "$value")]
pub value: Option<ResourceType>,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "lowercase")]
pub enum ResourceType {
Collection,
}
#[cfg(test)]
mod tests {
use quick_xml::de::from_str;
use super::*;
#[test]
fn test_propstat() {
let xml = r#"<D:propstat>
<D:prop>
<D:displayname>/</D:displayname>
<D:getlastmodified>Tue, 01 May 2022 06:39:47 GMT</D:getlastmodified>
<D:resourcetype><D:collection/></D:resourcetype>
<D:lockdiscovery/>
<D:supportedlock>
<D:lockentry>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
</D:supportedlock>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>"#;
let propstat = from_str::<Propstat>(xml).unwrap();
assert_eq!(
propstat.prop.getlastmodified,
"Tue, 01 May 2022 06:39:47 GMT"
);
assert_eq!(
propstat.prop.resourcetype.value.unwrap(),
ResourceType::Collection
);
assert_eq!(propstat.status, "HTTP/1.1 200 OK");
}
#[test]
fn test_response_simple() {
let xml = r#"<D:response>
<D:href>/</D:href>
<D:propstat>
<D:prop>
<D:displayname>/</D:displayname>
<D:getlastmodified>Tue, 01 May 2022 06:39:47 GMT</D:getlastmodified>
<D:resourcetype><D:collection/></D:resourcetype>
<D:lockdiscovery/>
<D:supportedlock>
<D:lockentry>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
</D:supportedlock>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>"#;
let response = from_str::<PropfindResponse>(xml).unwrap();
assert_eq!(response.href, "/");
assert_eq!(
response.propstat.prop.getlastmodified,
"Tue, 01 May 2022 06:39:47 GMT"
);
assert_eq!(
response.propstat.prop.resourcetype.value.unwrap(),
ResourceType::Collection
);
assert_eq!(response.propstat.status, "HTTP/1.1 200 OK");
}
#[test]
fn test_response_file() {
let xml = r#"<D:response>
<D:href>/test_file</D:href>
<D:propstat>
<D:prop>
<D:displayname>test_file</D:displayname>
<D:getcontentlength>1</D:getcontentlength>
<D:getlastmodified>Tue, 07 May 2022 05:52:22 GMT</D:getlastmodified>
<D:resourcetype></D:resourcetype>
<D:lockdiscovery />
<D:supportedlock>
<D:lockentry>
<D:lockscope>
<D:exclusive />
</D:lockscope>
<D:locktype>
<D:write />
</D:locktype>
</D:lockentry>
</D:supportedlock>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>"#;
let response = from_str::<PropfindResponse>(xml).unwrap();
assert_eq!(response.href, "/test_file");
assert_eq!(
response.propstat.prop.getlastmodified,
"Tue, 07 May 2022 05:52:22 GMT"
);
assert_eq!(response.propstat.prop.getcontentlength.unwrap(), "1");
assert_eq!(response.propstat.prop.resourcetype.value, None);
assert_eq!(response.propstat.status, "HTTP/1.1 200 OK");
}
#[test]
fn test_with_multiple_items_simple() {
let xml = r#"<D:multistatus xmlns:D="DAV:">
<D:response>
<D:href>/</D:href>
<D:propstat>
<D:prop>
<D:displayname>/</D:displayname>
<D:getlastmodified>Tue, 01 May 2022 06:39:47 GMT</D:getlastmodified>
<D:resourcetype><D:collection/></D:resourcetype>
<D:lockdiscovery/>
<D:supportedlock>
<D:lockentry>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
</D:supportedlock>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/</D:href>
<D:propstat>
<D:prop>
<D:displayname>/</D:displayname>
<D:getlastmodified>Tue, 01 May 2022 06:39:47 GMT</D:getlastmodified>
<D:resourcetype><D:collection/></D:resourcetype>
<D:lockdiscovery/>
<D:supportedlock>
<D:lockentry>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
</D:supportedlock>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>"#;
let multistatus = from_str::<Multistatus>(xml).unwrap();
let response = multistatus.response;
assert_eq!(response.len(), 2);
assert_eq!(response[0].href, "/");
assert_eq!(
response[0].propstat.prop.getlastmodified,
"Tue, 01 May 2022 06:39:47 GMT"
);
}
#[test]
fn test_with_multiple_items_mixed() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:">
<D:response>
<D:href>/</D:href>
<D:propstat>
<D:prop>
<D:displayname>/</D:displayname>
<D:getlastmodified>Tue, 07 May 2022 06:39:47 GMT</D:getlastmodified>
<D:resourcetype>
<D:collection />
</D:resourcetype>
<D:lockdiscovery />
<D:supportedlock>
<D:lockentry>
<D:lockscope>
<D:exclusive />
</D:lockscope>
<D:locktype>
<D:write />
</D:locktype>
</D:lockentry>
</D:supportedlock>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/testdir/</D:href>
<D:propstat>
<D:prop>
<D:displayname>testdir</D:displayname>
<D:getlastmodified>Tue, 07 May 2022 06:40:10 GMT</D:getlastmodified>
<D:resourcetype>
<D:collection />
</D:resourcetype>
<D:lockdiscovery />
<D:supportedlock>
<D:lockentry>
<D:lockscope>
<D:exclusive />
</D:lockscope>
<D:locktype>
<D:write />
</D:locktype>
</D:lockentry>
</D:supportedlock>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/test_file</D:href>
<D:propstat>
<D:prop>
<D:displayname>test_file</D:displayname>
<D:getcontentlength>1</D:getcontentlength>
<D:getlastmodified>Tue, 07 May 2022 05:52:22 GMT</D:getlastmodified>
<D:resourcetype></D:resourcetype>
<D:lockdiscovery />
<D:supportedlock>
<D:lockentry>
<D:lockscope>
<D:exclusive />
</D:lockscope>
<D:locktype>
<D:write />
</D:locktype>
</D:lockentry>
</D:supportedlock>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>"#;
let multistatus = from_str::<Multistatus>(xml).unwrap();
let response = multistatus.response;
assert_eq!(response.len(), 3);
let first_response = &response[0];
assert_eq!(first_response.href, "/");
assert_eq!(
first_response.propstat.prop.getlastmodified,
"Tue, 07 May 2022 06:39:47 GMT"
);
let second_response = &response[1];
assert_eq!(second_response.href, "/testdir/");
assert_eq!(
second_response.propstat.prop.getlastmodified,
"Tue, 07 May 2022 06:40:10 GMT"
);
let third_response = &response[2];
assert_eq!(third_response.href, "/test_file");
assert_eq!(
third_response.propstat.prop.getlastmodified,
"Tue, 07 May 2022 05:52:22 GMT"
);
}
#[test]
fn test_with_multiple_items_mixed_nginx() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:">
<D:response>
<D:href>/</D:href>
<D:propstat>
<D:prop>
<D:getlastmodified>Fri, 17 Feb 2023 03:37:22 GMT</D:getlastmodified>
<D:resourcetype>
<D:collection />
</D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/test_file_75</D:href>
<D:propstat>
<D:prop>
<D:getcontentlength>1</D:getcontentlength>
<D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
<D:resourcetype></D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/test_file_36</D:href>
<D:propstat>
<D:prop>
<D:getcontentlength>1</D:getcontentlength>
<D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
<D:resourcetype></D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/test_file_38</D:href>
<D:propstat>
<D:prop>
<D:getcontentlength>1</D:getcontentlength>
<D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
<D:resourcetype></D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/test_file_59</D:href>
<D:propstat>
<D:prop>
<D:getcontentlength>1</D:getcontentlength>
<D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
<D:resourcetype></D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/test_file_9</D:href>
<D:propstat>
<D:prop>
<D:getcontentlength>1</D:getcontentlength>
<D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
<D:resourcetype></D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/test_file_93</D:href>
<D:propstat>
<D:prop>
<D:getcontentlength>1</D:getcontentlength>
<D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
<D:resourcetype></D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/test_file_43</D:href>
<D:propstat>
<D:prop>
<D:getcontentlength>1</D:getcontentlength>
<D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
<D:resourcetype></D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/test_file_95</D:href>
<D:propstat>
<D:prop>
<D:getcontentlength>1</D:getcontentlength>
<D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
<D:resourcetype></D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>
"#;
let multistatus: Multistatus = from_str(xml).unwrap();
let response = multistatus.response;
assert_eq!(response.len(), 9);
let first_response = &response[0];
assert_eq!(first_response.href, "/");
assert_eq!(
first_response.propstat.prop.getlastmodified,
"Fri, 17 Feb 2023 03:37:22 GMT"
);
}
#[test]
fn test_build_proppatch_request() {
let mut user_metadata = HashMap::new();
user_metadata.insert("key1".to_string(), "value1".to_string());
user_metadata.insert("key2".to_string(), "value2".to_string());
let request = build_proppatch_request(
&user_metadata,
DEFAULT_USER_METADATA_PREFIX,
DEFAULT_USER_METADATA_URI,
);
// Check that the request contains the expected XML structure
assert!(request.contains(r#"<?xml version="1.0" encoding="utf-8"?>"#));
assert!(request.contains(r#"<D:propertyupdate"#));
assert!(request.contains(r#"xmlns:D="DAV:""#));
assert!(request.contains(&format!(
r#"xmlns:{DEFAULT_USER_METADATA_PREFIX}="{DEFAULT_USER_METADATA_URI}""#
)));
assert!(request.contains(r#"<D:set>"#));
assert!(request.contains(r#"<D:prop>"#));
// Check that user metadata is included (order may vary)
assert!(request.contains(&format!(
"<{DEFAULT_USER_METADATA_PREFIX}:key1>value1</{DEFAULT_USER_METADATA_PREFIX}:key1>"
)));
assert!(request.contains(&format!(
"<{DEFAULT_USER_METADATA_PREFIX}:key2>value2</{DEFAULT_USER_METADATA_PREFIX}:key2>"
)));
}
#[test]
fn test_build_proppatch_request_with_special_chars() {
let mut user_metadata = HashMap::new();
user_metadata.insert("key".to_string(), "value<>&\"'".to_string());
let request = build_proppatch_request(
&user_metadata,
DEFAULT_USER_METADATA_PREFIX,
DEFAULT_USER_METADATA_URI,
);
// Check that special characters are properly escaped
assert!(request.contains("value<>&"'"));
}
#[test]
fn test_build_proppatch_request_custom_namespace() {
let mut user_metadata = HashMap::new();
user_metadata.insert("key1".to_string(), "value1".to_string());
let request = build_proppatch_request(&user_metadata, "custom", "http://example.com/ns");
// Check that custom namespace is used
assert!(request.contains(r#"xmlns:custom="http://example.com/ns""#));
assert!(request.contains("<custom:key1>value1</custom:key1>"));
}
#[test]
fn test_parse_user_metadata_from_xml() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:" xmlns:opendal="https://opendal.apache.org/ns">
<D:response>
<D:propstat>
<D:prop>
<D:getlastmodified>Fri, 17 Feb 2023 03:37:22 GMT</D:getlastmodified>
<opendal:key1>value1</opendal:key1>
<opendal:key2>value2</opendal:key2>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>"#;
let user_metadata = parse_user_metadata_from_xml(xml, DEFAULT_USER_METADATA_URI);
assert_eq!(user_metadata.len(), 2);
assert_eq!(user_metadata.get("key1"), Some(&"value1".to_string()));
assert_eq!(user_metadata.get("key2"), Some(&"value2".to_string()));
}
#[test]
fn test_parse_user_metadata_from_xml_with_special_chars() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:" xmlns:opendal="https://opendal.apache.org/ns">
<D:response>
<D:propstat>
<D:prop>
<opendal:key>value<>&"'</opendal:key>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>"#;
let user_metadata = parse_user_metadata_from_xml(xml, DEFAULT_USER_METADATA_URI);
assert_eq!(user_metadata.len(), 1);
assert_eq!(user_metadata.get("key"), Some(&"value<>&\"'".to_string()));
}
#[test]
fn test_parse_user_metadata_from_xml_empty() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:">
<D:response>
<D:propstat>
<D:prop>
<D:getlastmodified>Fri, 17 Feb 2023 03:37:22 GMT</D:getlastmodified>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>"#;
let user_metadata = parse_user_metadata_from_xml(xml, DEFAULT_USER_METADATA_URI);
assert!(user_metadata.is_empty());
}
#[test]
fn test_parse_user_metadata_from_xml_dynamic_prefix() {
// Nextcloud/ownCloud style: uses dynamic namespace prefixes (x1:, x2:, etc.)
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:propstat>
<d:prop>
<d:getlastmodified>Fri, 17 Feb 2023 03:37:22 GMT</d:getlastmodified>
<x1:key1 xmlns:x1="https://opendal.apache.org/ns">value1</x1:key1>
<x2:key2 xmlns:x2="https://opendal.apache.org/ns">value2</x2:key2>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>"#;
let user_metadata = parse_user_metadata_from_xml(xml, DEFAULT_USER_METADATA_URI);
assert_eq!(user_metadata.len(), 2);
assert_eq!(user_metadata.get("key1"), Some(&"value1".to_string()));
assert_eq!(user_metadata.get("key2"), Some(&"value2".to_string()));
}
#[test]
fn test_parse_user_metadata_from_xml_namespace_at_root() {
// Alternative: namespace declared at root level
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<d:multistatus xmlns:d="DAV:" xmlns:custom="https://opendal.apache.org/ns">
<d:response>
<d:propstat>
<d:prop>
<custom:location>everywhere</custom:location>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>"#;
let user_metadata = parse_user_metadata_from_xml(xml, DEFAULT_USER_METADATA_URI);
assert_eq!(user_metadata.len(), 1);
assert_eq!(
user_metadata.get("location"),
Some(&"everywhere".to_string())
);
}
#[test]
fn test_parse_user_metadata_from_xml_custom_namespace() {
// Test with a custom namespace (like what users might configure)
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<d:multistatus xmlns:d="DAV:" xmlns:myapp="http://myapp.example.com/ns">
<d:response>
<d:propstat>
<d:prop>
<myapp:author>John Doe</myapp:author>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>"#;
let user_metadata = parse_user_metadata_from_xml(xml, "http://myapp.example.com/ns");
assert_eq!(user_metadata.len(), 1);
assert_eq!(user_metadata.get("author"), Some(&"John Doe".to_string()));
}
#[test]
fn test_check_proppatch_response_success() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:" xmlns:opendal="https://opendal.apache.org/ns">
<D:response>
<D:href>/test.txt</D:href>
<D:propstat>
<D:prop>
<opendal:location/>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>"#;
assert!(check_proppatch_response(xml).is_ok());
}
#[test]
fn test_check_proppatch_response_failure() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:" xmlns:opendal="https://opendal.apache.org/ns">
<D:response>
<D:href>/test.txt</D:href>
<D:propstat>
<D:prop>
<opendal:location/>
</D:prop>
<D:status>HTTP/1.1 403 Forbidden</D:status>
</D:propstat>
</D:response>
</D:multistatus>"#;
assert!(check_proppatch_response(xml).is_err());
}
#[test]
fn test_check_proppatch_response_lowercase() {
// Nextcloud might use lowercase "d:" prefix
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:href>/test.txt</d:href>
<d:propstat>
<d:prop/>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>"#;
assert!(check_proppatch_response(xml).is_ok());
}
}
```