opendal 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +89 -56
- data/Cargo.toml +1 -1
- data/DEPENDENCIES.rust.tsv +55 -41
- data/LICENSE +201 -0
- data/LICENSE-MPL-2.0.txt +373 -0
- data/NOTICE +5 -0
- data/core/CHANGELOG.md +175 -0
- data/core/Cargo.lock +225 -298
- data/core/Cargo.toml +100 -94
- data/core/DEPENDENCIES.rust.tsv +44 -73
- data/core/benches/metadata/README.md +21 -0
- data/core/benches/metadata/main.rs +541 -0
- data/core/benches/vs_fs/Cargo.toml +1 -1
- data/core/benches/vs_s3/Cargo.toml +1 -1
- data/core/core/Cargo.toml +7 -2
- data/core/core/src/blocking/delete.rs +5 -1
- data/core/core/src/blocking/operator.rs +12 -0
- data/core/core/src/blocking/read/std_reader.rs +25 -0
- data/core/core/src/docs/concepts.rs +70 -94
- data/core/core/src/docs/internals/accessor.rs +62 -293
- data/core/core/src/docs/internals/layer.rs +43 -59
- data/core/core/src/docs/internals/mod.rs +21 -23
- data/core/core/src/docs/mod.rs +10 -6
- data/core/core/src/docs/performance/concurrent_write.md +92 -55
- data/core/core/src/docs/performance/http_optimization.md +120 -104
- data/core/core/src/docs/performance/mod.rs +8 -6
- data/core/core/src/docs/rfcs/5485_conditional_reader.md +4 -0
- data/core/core/src/docs/rfcs/7182_restore_api.md +139 -557
- data/core/core/src/docs/rfcs/7945_limited_read.md +210 -0
- data/core/core/src/docs/rfcs/7948_s3_provider_presets.md +254 -0
- data/core/core/src/docs/rfcs/8109_writer_copy_from.md +330 -0
- data/core/core/src/docs/rfcs/8145_conflict_error_kind.md +210 -0
- data/core/core/src/docs/rfcs/8147_write_if_not_changed.md +233 -0
- data/core/core/src/docs/rfcs/8188_compose_api.md +292 -0
- data/core/core/src/docs/rfcs/8194_compact_metadata_and_operation_args.md +444 -0
- data/core/core/src/docs/rfcs/README.md +14 -3
- data/core/core/src/docs/rfcs/mod.rs +46 -22
- data/core/core/src/docs/specs/README.md +34 -0
- data/core/core/src/docs/specs/conditional-operations.md +156 -0
- data/core/core/src/docs/specs/metadata.md +63 -0
- data/core/core/src/docs/specs/mod.rs +26 -0
- data/core/core/src/docs/upgrade.md +114 -1
- data/core/core/src/layers/capability_override.rs +15 -2
- data/core/core/src/layers/complete.rs +347 -43
- data/core/core/src/layers/correctness_check.rs +718 -51
- data/core/core/src/layers/error_context.rs +76 -2
- data/core/core/src/layers/simulate.rs +91 -28
- data/core/core/src/lib.rs +1 -0
- data/core/core/src/raw/accessor.rs +91 -15
- data/core/core/src/raw/enum_utils.rs +7 -0
- data/core/core/src/raw/http_util/header.rs +22 -17
- data/core/core/src/raw/oio/compose/api.rs +92 -0
- data/core/core/src/raw/oio/compose/mod.rs +19 -0
- data/core/core/src/raw/oio/copy/api.rs +3 -3
- data/core/core/src/raw/oio/copy/block_copy.rs +32 -4
- data/core/core/src/raw/oio/copy/multipart_copy.rs +36 -7
- data/core/core/src/raw/oio/entry.rs +48 -22
- data/core/core/src/raw/oio/list/flat_list.rs +1 -1
- data/core/core/src/raw/oio/list/hierarchy_list.rs +3 -2
- data/core/core/src/raw/oio/mod.rs +3 -0
- data/core/core/src/raw/oio/read/position_read.rs +1 -1
- data/core/core/src/raw/oio/write/api.rs +40 -0
- data/core/core/src/raw/oio/write/append_write.rs +4 -4
- data/core/core/src/raw/oio/write/block_write.rs +2 -2
- data/core/core/src/raw/oio/write/multipart_write.rs +181 -48
- data/core/core/src/raw/oio/write/position_write.rs +1 -1
- data/core/core/src/raw/operation.rs +6 -0
- data/core/core/src/raw/ops.rs +1191 -456
- data/core/core/src/raw/rps.rs +11 -0
- data/core/core/src/raw/time.rs +14 -3
- data/core/core/src/services/memory/backend.rs +5 -10
- data/core/core/src/services/memory/lister.rs +4 -4
- data/core/core/src/services/memory/writer.rs +7 -6
- data/core/core/src/types/buffer.rs +102 -12
- data/core/core/src/types/bytes_range.rs +20 -1
- data/core/core/src/types/capability.rs +65 -0
- data/core/core/src/types/compact.rs +329 -0
- data/core/core/src/types/compose/composer.rs +183 -0
- data/core/core/src/types/compose/input.rs +57 -0
- data/core/core/src/types/compose/mod.rs +23 -0
- data/core/core/src/types/context/read.rs +16 -11
- data/core/core/src/types/context/write.rs +150 -2
- data/core/core/src/types/copy.rs +5 -3
- data/core/core/src/types/delete/deleter.rs +20 -12
- data/core/core/src/types/delete/input.rs +22 -64
- data/core/core/src/types/delete/mod.rs +0 -1
- data/core/core/src/types/entry.rs +8 -14
- data/core/core/src/types/error.rs +28 -11
- data/core/core/src/types/metadata.rs +709 -322
- data/core/core/src/types/mod.rs +9 -1
- data/core/core/src/types/operator/operator.rs +278 -29
- data/core/core/src/types/operator/operator_futures.rs +466 -105
- data/core/core/src/types/options.rs +631 -158
- data/core/core/src/types/read/futures_async_reader.rs +663 -50
- data/core/core/src/types/read/reader.rs +23 -5
- data/core/core/src/types/write/futures_async_writer.rs +27 -0
- data/core/core/src/types/write/writer.rs +47 -5
- data/core/http-transports/reqwest/Cargo.toml +1 -1
- data/core/layers/async-backtrace/Cargo.toml +2 -2
- data/core/layers/async-backtrace/src/lib.rs +21 -2
- data/core/layers/await-tree/Cargo.toml +2 -2
- data/core/layers/await-tree/src/lib.rs +25 -2
- data/core/layers/capability-check/Cargo.toml +2 -2
- data/core/layers/capability-check/src/lib.rs +98 -10
- data/core/layers/chaos/Cargo.toml +2 -2
- data/core/layers/chaos/src/lib.rs +18 -3
- data/core/layers/concurrent-limit/Cargo.toml +3 -3
- data/core/layers/concurrent-limit/src/lib.rs +53 -13
- data/core/layers/dtrace/Cargo.toml +2 -2
- data/core/layers/dtrace/src/lib.rs +29 -2
- data/core/layers/fastmetrics/Cargo.toml +3 -3
- data/core/layers/fastrace/Cargo.toml +2 -2
- data/core/layers/fastrace/src/lib.rs +28 -2
- data/core/layers/foyer/Cargo.toml +2 -2
- data/core/layers/foyer/src/full.rs +16 -19
- data/core/layers/foyer/src/lib.rs +67 -14
- data/core/layers/hotpath/Cargo.toml +2 -2
- data/core/layers/hotpath/src/lib.rs +20 -2
- data/core/layers/immutable-index/Cargo.toml +2 -2
- data/core/layers/immutable-index/src/lib.rs +48 -24
- data/core/layers/logging/Cargo.toml +2 -2
- data/core/layers/logging/src/lib.rs +46 -2
- data/core/layers/metrics/Cargo.toml +3 -3
- data/core/layers/mime-guess/Cargo.toml +2 -2
- data/core/layers/mime-guess/src/lib.rs +31 -4
- data/core/layers/observe-metrics-common/Cargo.toml +2 -2
- data/core/layers/observe-metrics-common/src/lib.rs +55 -2
- data/core/layers/otelmetrics/Cargo.toml +3 -3
- data/core/layers/oteltrace/Cargo.toml +2 -2
- data/core/layers/oteltrace/src/lib.rs +27 -2
- data/core/layers/prometheus/Cargo.toml +3 -3
- data/core/layers/prometheus-client/Cargo.toml +3 -3
- data/core/layers/retry/Cargo.toml +4 -4
- data/core/layers/retry/src/lib.rs +76 -19
- data/core/layers/route/Cargo.toml +3 -3
- data/core/layers/route/src/lib.rs +34 -7
- data/core/layers/tail-cut/Cargo.toml +2 -2
- data/core/layers/tail-cut/src/lib.rs +28 -2
- data/core/layers/throttle/Cargo.toml +2 -2
- data/core/layers/throttle/src/lib.rs +19 -2
- data/core/layers/timeout/Cargo.toml +3 -3
- data/core/layers/timeout/src/lib.rs +38 -13
- data/core/layers/tracing/Cargo.toml +2 -2
- data/core/layers/tracing/src/lib.rs +24 -3
- data/core/services/aliyun-drive/Cargo.toml +2 -2
- data/core/services/aliyun-drive/src/backend.rs +34 -24
- data/core/services/aliyun-drive/src/core.rs +143 -54
- data/core/services/aliyun-drive/src/lister.rs +19 -18
- data/core/services/aliyun-drive/src/reader.rs +4 -1
- data/core/services/aliyun-drive/src/writer.rs +3 -3
- data/core/services/alluxio/Cargo.toml +1 -1
- data/core/services/alluxio/src/backend.rs +1 -1
- data/core/services/alluxio/src/core.rs +113 -94
- data/core/services/alluxio/src/reader.rs +7 -3
- data/core/services/alluxio/src/writer.rs +2 -2
- data/core/services/azblob/Cargo.toml +2 -2
- data/core/services/azblob/src/backend.rs +17 -9
- data/core/services/azblob/src/copier.rs +41 -15
- data/core/services/azblob/src/core.rs +372 -135
- data/core/services/azblob/src/deleter.rs +41 -18
- data/core/services/azblob/src/lister.rs +16 -10
- data/core/services/azblob/src/reader.rs +4 -1
- data/core/services/azblob/src/writer.rs +48 -15
- data/core/services/azdls/Cargo.toml +3 -3
- data/core/services/azdls/src/backend.rs +45 -15
- data/core/services/azdls/src/config.rs +4 -1
- data/core/services/azdls/src/core.rs +276 -87
- data/core/services/azdls/src/deleter.rs +13 -4
- data/core/services/azdls/src/lister.rs +18 -12
- data/core/services/azdls/src/reader.rs +6 -2
- data/core/services/azdls/src/writer.rs +44 -23
- data/core/services/azfile/Cargo.toml +2 -2
- data/core/services/azfile/src/backend.rs +24 -8
- data/core/services/azfile/src/core.rs +195 -77
- data/core/services/azfile/src/deleter.rs +8 -1
- data/core/services/azfile/src/lister.rs +20 -14
- data/core/services/azfile/src/reader.rs +5 -2
- data/core/services/azfile/src/writer.rs +27 -15
- data/core/services/azure-common/Cargo.toml +1 -1
- data/core/services/b2/Cargo.toml +2 -2
- data/core/services/b2/src/backend.rs +30 -11
- data/core/services/b2/src/core.rs +134 -120
- data/core/services/b2/src/deleter.rs +1 -1
- data/core/services/b2/src/lister.rs +5 -2
- data/core/services/b2/src/reader.rs +5 -2
- data/core/services/b2/src/writer.rs +29 -12
- data/core/services/cacache/Cargo.toml +1 -1
- data/core/services/cacache/src/backend.rs +4 -5
- data/core/services/cacache/src/reader.rs +4 -1
- data/core/services/cacache/src/writer.rs +6 -2
- data/core/services/cloudflare-kv/Cargo.toml +1 -1
- data/core/services/cloudflare-kv/src/backend.rs +26 -15
- data/core/services/cloudflare-kv/src/config.rs +13 -2
- data/core/services/cloudflare-kv/src/core.rs +62 -56
- data/core/services/cloudflare-kv/src/deleter.rs +12 -3
- data/core/services/cloudflare-kv/src/lister.rs +66 -17
- data/core/services/cloudflare-kv/src/reader.rs +14 -9
- data/core/services/cloudflare-kv/src/writer.rs +10 -7
- data/core/services/compfs/Cargo.toml +1 -1
- data/core/services/compfs/src/backend.rs +11 -7
- data/core/services/compfs/src/lister.rs +7 -7
- data/core/services/compfs/src/writer.rs +1 -1
- data/core/services/cos/Cargo.toml +1 -1
- data/core/services/cos/src/backend.rs +129 -16
- data/core/services/cos/src/core.rs +198 -80
- data/core/services/cos/src/deleter.rs +4 -1
- data/core/services/cos/src/lister.rs +42 -27
- data/core/services/cos/src/reader.rs +6 -2
- data/core/services/cos/src/writer.rs +45 -21
- data/core/services/d1/Cargo.toml +1 -1
- data/core/services/d1/src/backend.rs +6 -5
- data/core/services/d1/src/core.rs +79 -63
- data/core/services/d1/src/reader.rs +4 -1
- data/core/services/d1/src/writer.rs +6 -2
- data/core/services/dashmap/Cargo.toml +1 -1
- data/core/services/dashmap/src/backend.rs +2 -2
- data/core/services/dashmap/src/reader.rs +4 -1
- data/core/services/dashmap/src/writer.rs +11 -8
- data/core/services/dbfs/Cargo.toml +1 -1
- data/core/services/dbfs/src/backend.rs +21 -15
- data/core/services/dbfs/src/core.rs +65 -60
- data/core/services/dbfs/src/deleter.rs +4 -1
- data/core/services/dbfs/src/lister.rs +10 -10
- data/core/services/dbfs/src/writer.rs +6 -3
- data/core/services/dropbox/Cargo.toml +2 -2
- data/core/services/dropbox/src/backend.rs +30 -23
- data/core/services/dropbox/src/core.rs +65 -63
- data/core/services/dropbox/src/deleter.rs +1 -1
- data/core/services/dropbox/src/lister.rs +17 -8
- data/core/services/dropbox/src/reader.rs +4 -1
- data/core/services/dropbox/src/writer.rs +13 -10
- data/core/services/etcd/Cargo.toml +1 -1
- data/core/services/etcd/src/backend.rs +8 -5
- data/core/services/etcd/src/core.rs +5 -11
- data/core/services/etcd/src/lister.rs +6 -1
- data/core/services/etcd/src/reader.rs +4 -1
- data/core/services/etcd/src/writer.rs +6 -3
- data/core/services/foundationdb/Cargo.toml +1 -1
- data/core/services/foundationdb/src/backend.rs +6 -5
- data/core/services/foundationdb/src/reader.rs +4 -1
- data/core/services/foundationdb/src/writer.rs +6 -2
- data/core/services/foyer/Cargo.toml +3 -3
- data/core/services/foyer/src/backend.rs +6 -5
- data/core/services/foyer/src/reader.rs +4 -1
- data/core/services/foyer/src/writer.rs +6 -2
- data/core/services/fs/Cargo.toml +1 -1
- data/core/services/fs/src/backend.rs +4 -3
- data/core/services/fs/src/core.rs +44 -26
- data/core/services/fs/src/lister.rs +9 -6
- data/core/services/fs/src/writer.rs +20 -13
- data/core/services/ftp/Cargo.toml +4 -6
- data/core/services/ftp/src/backend.rs +9 -5
- data/core/services/ftp/src/core.rs +9 -7
- data/core/services/ftp/src/lister.rs +7 -8
- data/core/services/ftp/src/reader.rs +3 -3
- data/core/services/ftp/src/writer.rs +3 -3
- data/core/services/gcs/Cargo.toml +4 -2
- data/core/services/gcs/src/backend.rs +53 -7
- data/core/services/gcs/src/composer.rs +705 -0
- data/core/services/gcs/src/copier.rs +14 -4
- data/core/services/gcs/src/core.rs +828 -124
- data/core/services/gcs/src/deleter.rs +33 -18
- data/core/services/gcs/src/lib.rs +1 -0
- data/core/services/gcs/src/lister.rs +21 -12
- data/core/services/gcs/src/reader.rs +12 -5
- data/core/services/gcs/src/writer.rs +632 -10
- data/core/services/gcs-grpc/Cargo.toml +58 -0
- data/core/services/gcs-grpc/README.md +19 -0
- data/core/services/gcs-grpc/proto/README.md +23 -0
- data/core/services/gcs-grpc/proto/google/storage/v2/storage.proto +171 -0
- data/core/services/gcs-grpc/proto/upstream-descriptor.bin +0 -0
- data/core/services/gcs-grpc/src/backend.rs +449 -0
- data/core/services/gcs-grpc/src/config.rs +116 -0
- data/core/services/gcs-grpc/src/copier.rs +98 -0
- data/core/services/gcs-grpc/src/core.rs +298 -0
- data/core/services/gcs-grpc/src/deleter.rs +65 -0
- data/core/services/gcs-grpc/src/docs.md +19 -0
- data/core/services/gcs-grpc/src/generated/google.storage.v2.rs +541 -0
- data/core/services/gcs-grpc/src/generated/mod.rs +24 -0
- data/core/services/gcs-grpc/src/lib.rs +43 -0
- data/core/services/gcs-grpc/src/lister.rs +125 -0
- data/core/services/gcs-grpc/src/reader.rs +140 -0
- data/core/services/gcs-grpc/src/writer.rs +780 -0
- data/core/services/gdrive/Cargo.toml +2 -2
- data/core/services/gdrive/src/backend.rs +123 -69
- data/core/services/gdrive/src/core.rs +87 -69
- data/core/services/gdrive/src/deleter.rs +8 -2
- data/core/services/gdrive/src/lister.rs +38 -26
- data/core/services/gdrive/src/path_index.rs +2 -2
- data/core/services/gdrive/src/reader.rs +9 -3
- data/core/services/gdrive/src/writer.rs +13 -7
- data/core/services/ghac/Cargo.toml +2 -2
- data/core/services/ghac/src/backend.rs +5 -2
- data/core/services/ghac/src/core.rs +59 -37
- data/core/services/ghac/src/reader.rs +5 -2
- data/core/services/ghac/src/writer.rs +16 -6
- data/core/services/github/Cargo.toml +1 -1
- data/core/services/github/src/backend.rs +16 -8
- data/core/services/github/src/core.rs +88 -76
- data/core/services/github/src/lister.rs +17 -15
- data/core/services/github/src/reader.rs +5 -2
- data/core/services/github/src/writer.rs +12 -6
- data/core/services/goosefs/Cargo.toml +2 -2
- data/core/services/goosefs/src/backend.rs +2 -2
- data/core/services/goosefs/src/core.rs +291 -208
- data/core/services/goosefs/src/docs.md +3 -0
- data/core/services/goosefs/src/lister.rs +2 -3
- data/core/services/goosefs/src/reader.rs +7 -3
- data/core/services/goosefs/src/writer.rs +21 -10
- data/core/services/goosefs/tests/safe_conditional_put.rs +37 -0
- data/core/services/gridfs/Cargo.toml +2 -2
- data/core/services/gridfs/src/backend.rs +12 -8
- data/core/services/gridfs/src/core.rs +1 -1
- data/core/services/gridfs/src/reader.rs +4 -1
- data/core/services/gridfs/src/writer.rs +6 -2
- data/core/services/hdfs/Cargo.toml +1 -1
- data/core/services/hdfs/src/backend.rs +1 -1
- data/core/services/hdfs/src/core.rs +7 -4
- data/core/services/hdfs/src/lister.rs +7 -9
- data/core/services/hdfs/src/writer.rs +5 -1
- data/core/services/hdfs-native/Cargo.toml +1 -1
- data/core/services/hdfs-native/src/backend.rs +1 -1
- data/core/services/hdfs-native/src/core.rs +44 -50
- data/core/services/hdfs-native/src/lister.rs +8 -10
- data/core/services/hdfs-native/src/writer.rs +5 -1
- data/core/services/hf/Cargo.toml +4 -3
- data/core/services/hf/src/backend.rs +4 -3
- data/core/services/hf/src/core.rs +907 -153
- data/core/services/hf/src/lister.rs +16 -6
- data/core/services/hf/src/reader.rs +377 -57
- data/core/services/hf/src/writer.rs +6 -2
- data/core/services/http/Cargo.toml +3 -3
- data/core/services/http/src/backend.rs +8 -6
- data/core/services/http/src/core.rs +57 -35
- data/core/services/http/src/reader.rs +5 -2
- data/core/services/ipfs/Cargo.toml +1 -1
- data/core/services/ipfs/src/backend.rs +20 -9
- data/core/services/ipfs/src/core.rs +43 -34
- data/core/services/ipfs/src/reader.rs +5 -2
- data/core/services/ipmfs/Cargo.toml +1 -1
- data/core/services/ipmfs/src/backend.rs +17 -8
- data/core/services/ipmfs/src/core.rs +66 -60
- data/core/services/ipmfs/src/deleter.rs +5 -2
- data/core/services/ipmfs/src/lister.rs +15 -12
- data/core/services/ipmfs/src/reader.rs +5 -2
- data/core/services/ipmfs/src/writer.rs +6 -3
- data/core/services/koofr/Cargo.toml +2 -2
- data/core/services/koofr/src/backend.rs +46 -17
- data/core/services/koofr/src/core.rs +64 -48
- data/core/services/koofr/src/deleter.rs +4 -1
- data/core/services/koofr/src/lister.rs +11 -10
- data/core/services/koofr/src/reader.rs +5 -2
- data/core/services/koofr/src/writer.rs +13 -7
- data/core/services/lakefs/Cargo.toml +1 -1
- data/core/services/lakefs/src/backend.rs +25 -8
- data/core/services/lakefs/src/core.rs +79 -73
- data/core/services/lakefs/src/deleter.rs +4 -1
- data/core/services/lakefs/src/lister.rs +17 -14
- data/core/services/lakefs/src/reader.rs +5 -2
- data/core/services/lakefs/src/writer.rs +11 -10
- data/core/services/memcached/Cargo.toml +1 -1
- data/core/services/memcached/src/backend.rs +57 -7
- data/core/services/memcached/src/config.rs +13 -2
- data/core/services/memcached/src/core.rs +11 -1
- data/core/services/memcached/src/reader.rs +4 -1
- data/core/services/memcached/src/writer.rs +6 -2
- data/core/services/mini_moka/Cargo.toml +1 -1
- data/core/services/mini_moka/src/backend.rs +8 -8
- data/core/services/mini_moka/src/reader.rs +4 -1
- data/core/services/mini_moka/src/writer.rs +11 -7
- data/core/services/moka/Cargo.toml +1 -1
- data/core/services/moka/src/backend.rs +7 -7
- data/core/services/moka/src/reader.rs +4 -1
- data/core/services/moka/src/writer.rs +10 -6
- data/core/services/mongodb/Cargo.toml +2 -2
- data/core/services/mongodb/src/backend.rs +7 -6
- data/core/services/mongodb/src/core.rs +1 -1
- data/core/services/mongodb/src/reader.rs +4 -1
- data/core/services/mongodb/src/writer.rs +6 -2
- data/core/services/monoiofs/Cargo.toml +2 -2
- data/core/services/monoiofs/src/backend.rs +16 -10
- data/core/services/monoiofs/src/core.rs +8 -9
- data/core/services/monoiofs/src/reader.rs +8 -10
- data/core/services/monoiofs/src/writer.rs +19 -24
- data/core/services/mysql/Cargo.toml +2 -2
- data/core/services/mysql/src/backend.rs +7 -6
- data/core/services/mysql/src/core.rs +1 -1
- data/core/services/mysql/src/lister.rs +7 -3
- data/core/services/mysql/src/reader.rs +4 -1
- data/core/services/mysql/src/writer.rs +6 -2
- data/core/services/obs/Cargo.toml +1 -1
- data/core/services/obs/src/backend.rs +29 -11
- data/core/services/obs/src/core.rs +81 -82
- data/core/services/obs/src/deleter.rs +4 -1
- data/core/services/obs/src/lister.rs +12 -5
- data/core/services/obs/src/reader.rs +5 -2
- data/core/services/obs/src/writer.rs +40 -19
- data/core/services/onedrive/Cargo.toml +2 -2
- data/core/services/onedrive/src/backend.rs +30 -21
- data/core/services/onedrive/src/config.rs +2 -2
- data/core/services/onedrive/src/core.rs +191 -90
- data/core/services/onedrive/src/deleter.rs +5 -2
- data/core/services/onedrive/src/graph_model.rs +10 -51
- data/core/services/onedrive/src/lister.rs +60 -24
- data/core/services/onedrive/src/reader.rs +17 -2
- data/core/services/onedrive/src/writer.rs +26 -15
- data/core/services/opfs/Cargo.toml +1 -1
- data/core/services/opfs/src/backend.rs +6 -13
- data/core/services/opfs/src/core.rs +33 -37
- data/core/services/opfs/src/lister.rs +4 -4
- data/core/services/opfs/src/writer.rs +1 -3
- data/core/services/oss/Cargo.toml +1 -1
- data/core/services/oss/src/backend.rs +40 -8
- data/core/services/oss/src/core.rs +192 -92
- data/core/services/oss/src/deleter.rs +16 -6
- data/core/services/oss/src/lister.rs +43 -27
- data/core/services/oss/src/reader.rs +6 -2
- data/core/services/oss/src/writer.rs +40 -22
- data/core/services/pcloud/Cargo.toml +1 -1
- data/core/services/pcloud/src/backend.rs +40 -6
- data/core/services/pcloud/src/core.rs +80 -66
- data/core/services/pcloud/src/deleter.rs +8 -1
- data/core/services/pcloud/src/lister.rs +4 -1
- data/core/services/pcloud/src/reader.rs +5 -2
- data/core/services/pcloud/src/writer.rs +6 -3
- data/core/services/persy/Cargo.toml +1 -1
- data/core/services/persy/src/backend.rs +6 -5
- data/core/services/persy/src/core.rs +60 -15
- data/core/services/persy/src/reader.rs +4 -1
- data/core/services/persy/src/writer.rs +6 -2
- data/core/services/postgresql/Cargo.toml +2 -2
- data/core/services/postgresql/src/backend.rs +7 -6
- data/core/services/postgresql/src/core.rs +1 -1
- data/core/services/postgresql/src/reader.rs +4 -1
- data/core/services/postgresql/src/writer.rs +6 -2
- data/core/services/redb/Cargo.toml +1 -1
- data/core/services/redb/src/backend.rs +6 -5
- data/core/services/redb/src/reader.rs +4 -1
- data/core/services/redb/src/writer.rs +6 -2
- data/core/services/redis/Cargo.toml +1 -1
- data/core/services/redis/src/backend.rs +18 -8
- data/core/services/redis/src/config.rs +13 -2
- data/core/services/redis/src/reader.rs +8 -4
- data/core/services/redis/src/writer.rs +6 -2
- data/core/services/rocksdb/Cargo.toml +1 -1
- data/core/services/rocksdb/src/backend.rs +6 -5
- data/core/services/rocksdb/src/lister.rs +6 -5
- data/core/services/rocksdb/src/reader.rs +4 -1
- data/core/services/rocksdb/src/writer.rs +6 -2
- data/core/services/s3/Cargo.toml +5 -4
- data/core/services/s3/src/backend.rs +995 -39
- data/core/services/s3/src/compatible_services.md +57 -0
- data/core/services/s3/src/config.rs +16 -0
- data/core/services/s3/src/copier.rs +190 -55
- data/core/services/s3/src/core.rs +299 -190
- data/core/services/s3/src/deleter.rs +26 -7
- data/core/services/s3/src/docs.md +57 -0
- data/core/services/s3/src/lister.rs +55 -36
- data/core/services/s3/src/reader.rs +22 -2
- data/core/services/s3/src/writer.rs +154 -32
- data/core/services/seafile/Cargo.toml +2 -2
- data/core/services/seafile/src/backend.rs +3 -3
- data/core/services/seafile/src/core.rs +105 -79
- data/core/services/seafile/src/lister.rs +11 -8
- data/core/services/seafile/src/reader.rs +5 -2
- data/core/services/seafile/src/writer.rs +6 -3
- data/core/services/sftp/Cargo.toml +1 -1
- data/core/services/sftp/src/backend.rs +19 -4
- data/core/services/sftp/src/core.rs +51 -46
- data/core/services/sftp/src/lister.rs +4 -4
- data/core/services/sftp/src/writer.rs +1 -1
- data/core/services/sled/Cargo.toml +1 -1
- data/core/services/sled/src/backend.rs +6 -5
- data/core/services/sled/src/core.rs +28 -6
- data/core/services/sled/src/lister.rs +6 -5
- data/core/services/sled/src/reader.rs +4 -1
- data/core/services/sled/src/writer.rs +6 -2
- data/core/services/sqlite/Cargo.toml +2 -2
- data/core/services/sqlite/src/backend.rs +12 -7
- data/core/services/sqlite/src/core.rs +3 -3
- data/core/services/sqlite/src/reader.rs +4 -1
- data/core/services/sqlite/src/writer.rs +7 -3
- data/core/services/surrealdb/Cargo.toml +2 -2
- data/core/services/surrealdb/src/backend.rs +7 -6
- data/core/services/surrealdb/src/core.rs +1 -1
- data/core/services/surrealdb/src/reader.rs +4 -1
- data/core/services/surrealdb/src/writer.rs +6 -2
- data/core/services/swift/Cargo.toml +1 -1
- data/core/services/swift/src/backend.rs +38 -8
- data/core/services/swift/src/core.rs +63 -67
- data/core/services/swift/src/deleter.rs +8 -2
- data/core/services/swift/src/lister.rs +11 -8
- data/core/services/swift/src/reader.rs +5 -2
- data/core/services/swift/src/writer.rs +17 -8
- data/core/services/tikv/Cargo.toml +2 -2
- data/core/services/tikv/src/backend.rs +7 -6
- data/core/services/tikv/src/core.rs +31 -14
- data/core/services/tikv/src/reader.rs +4 -1
- data/core/services/tikv/src/writer.rs +6 -2
- data/core/services/tos/Cargo.toml +2 -2
- data/core/services/tos/src/backend.rs +45 -21
- data/core/services/tos/src/copier.rs +44 -17
- data/core/services/tos/src/core.rs +147 -117
- data/core/services/tos/src/deleter.rs +9 -3
- data/core/services/tos/src/docs.md +1 -1
- data/core/services/tos/src/lister.rs +43 -28
- data/core/services/tos/src/reader.rs +5 -2
- data/core/services/tos/src/writer.rs +37 -16
- data/core/services/upyun/Cargo.toml +1 -1
- data/core/services/upyun/src/backend.rs +30 -7
- data/core/services/upyun/src/core.rs +122 -87
- data/core/services/upyun/src/deleter.rs +4 -1
- data/core/services/upyun/src/lister.rs +10 -10
- data/core/services/upyun/src/reader.rs +5 -2
- data/core/services/upyun/src/writer.rs +19 -7
- data/core/services/vercel-artifacts/Cargo.toml +1 -1
- data/core/services/vercel-artifacts/src/backend.rs +6 -3
- data/core/services/vercel-artifacts/src/core.rs +32 -27
- data/core/services/vercel-artifacts/src/reader.rs +5 -2
- data/core/services/vercel-artifacts/src/writer.rs +6 -3
- data/core/services/vercel-blob/Cargo.toml +1 -1
- data/core/services/vercel-blob/src/backend.rs +23 -6
- data/core/services/vercel-blob/src/core.rs +92 -81
- data/core/services/vercel-blob/src/lister.rs +12 -3
- data/core/services/vercel-blob/src/reader.rs +5 -2
- data/core/services/vercel-blob/src/writer.rs +19 -7
- data/core/services/webdav/Cargo.toml +2 -2
- data/core/services/webdav/src/backend.rs +25 -5
- data/core/services/webdav/src/core.rs +116 -59
- data/core/services/webdav/src/deleter.rs +4 -1
- data/core/services/webdav/src/lister.rs +4 -1
- data/core/services/webdav/src/reader.rs +6 -2
- data/core/services/webdav/src/writer.rs +17 -7
- data/core/services/webhdfs/Cargo.toml +2 -2
- data/core/services/webhdfs/src/backend.rs +24 -11
- data/core/services/webhdfs/src/core.rs +99 -98
- data/core/services/webhdfs/src/deleter.rs +5 -2
- data/core/services/webhdfs/src/lister.rs +21 -9
- data/core/services/webhdfs/src/reader.rs +5 -2
- data/core/services/webhdfs/src/writer.rs +49 -14
- data/core/services/yandex-disk/Cargo.toml +1 -1
- data/core/services/yandex-disk/src/backend.rs +28 -6
- data/core/services/yandex-disk/src/core.rs +97 -79
- data/core/services/yandex-disk/src/deleter.rs +4 -1
- data/core/services/yandex-disk/src/lister.rs +25 -9
- data/core/services/yandex-disk/src/reader.rs +5 -2
- data/core/services/yandex-disk/src/writer.rs +6 -3
- data/core/src/lib.rs +5 -0
- data/core/testkit/Cargo.toml +4 -4
- data/core/testkit/src/utils.rs +53 -8
- data/core/tests/behavior/async_compose.rs +297 -0
- data/core/tests/behavior/async_copy.rs +162 -1
- data/core/tests/behavior/async_delete.rs +364 -3
- data/core/tests/behavior/async_read.rs +86 -0
- data/core/tests/behavior/async_restore.rs +162 -0
- data/core/tests/behavior/async_stat.rs +90 -0
- data/core/tests/behavior/async_write.rs +330 -2
- data/core/tests/behavior/main.rs +4 -0
- data/core/upgrade.md +114 -1
- data/src/capability.rs +0 -1
- data/src/io.rs +0 -1
- data/src/lister.rs +0 -1
- data/src/metadata.rs +0 -1
- data/src/operator_info.rs +0 -1
- metadata +44 -2
|
@@ -23,8 +23,7 @@ use std::sync::LazyLock;
|
|
|
23
23
|
|
|
24
24
|
use base64::Engine;
|
|
25
25
|
use base64::prelude::BASE64_STANDARD;
|
|
26
|
-
use
|
|
27
|
-
use constants::X_AMZ_VERSION_ID;
|
|
26
|
+
use bytes::Buf;
|
|
28
27
|
use http::StatusCode;
|
|
29
28
|
use log::debug;
|
|
30
29
|
use log::warn;
|
|
@@ -34,6 +33,8 @@ use reqsign_aws_v4::AssumeRoleCredentialProvider;
|
|
|
34
33
|
use reqsign_aws_v4::Credential;
|
|
35
34
|
use reqsign_aws_v4::DefaultCredentialProvider;
|
|
36
35
|
use reqsign_aws_v4::RequestSigner as AwsV4Signer;
|
|
36
|
+
use reqsign_aws_v4::S3ExpressSessionConfig;
|
|
37
|
+
use reqsign_aws_v4::S3ExpressSessionProvider;
|
|
37
38
|
use reqsign_aws_v4::StaticCredentialProvider;
|
|
38
39
|
use reqsign_core::Context;
|
|
39
40
|
use reqsign_core::OsEnv;
|
|
@@ -56,6 +57,7 @@ use crate::lister::S3ObjectVersionsLister;
|
|
|
56
57
|
use crate::reader::*;
|
|
57
58
|
use crate::writer::S3Writer;
|
|
58
59
|
use crate::writer::S3Writers;
|
|
60
|
+
use opendal_core::raw::oio::Copy;
|
|
59
61
|
use opendal_core::raw::*;
|
|
60
62
|
use opendal_core::*;
|
|
61
63
|
|
|
@@ -146,6 +148,22 @@ impl S3Builder {
|
|
|
146
148
|
self
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
/// Set the AWS profile used by the default credential provider chain.
|
|
152
|
+
///
|
|
153
|
+
/// The configured profile takes precedence over the `AWS_PROFILE`
|
|
154
|
+
/// environment variable and applies to shared AWS config and credentials
|
|
155
|
+
/// files and SSO.
|
|
156
|
+
///
|
|
157
|
+
/// This setting has no effect when [`Self::disable_config_load`] is set or
|
|
158
|
+
/// when [`Self::credential_provider_chain`] replaces the default chain.
|
|
159
|
+
pub fn profile(mut self, profile: &str) -> Self {
|
|
160
|
+
if !profile.is_empty() {
|
|
161
|
+
self.config.profile = Some(profile.to_string())
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
self
|
|
165
|
+
}
|
|
166
|
+
|
|
149
167
|
/// Set access_key_id of this backend.
|
|
150
168
|
///
|
|
151
169
|
/// - If access_key_id is set, we will take user's input first.
|
|
@@ -545,6 +563,87 @@ impl S3Builder {
|
|
|
545
563
|
endpoint
|
|
546
564
|
}
|
|
547
565
|
|
|
566
|
+
fn invalid_s3_express_config(message: &'static str) -> Error {
|
|
567
|
+
Error::new(ErrorKind::ConfigInvalid, message)
|
|
568
|
+
.with_operation("Builder::build")
|
|
569
|
+
.with_context("service", S3_SCHEME)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
fn is_aws_host(host: &str) -> bool {
|
|
573
|
+
host == "s3.amazonaws.com"
|
|
574
|
+
|| host.ends_with(".amazonaws.com")
|
|
575
|
+
|| host.ends_with(".amazonaws.com.cn")
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
fn resolve_s3_express_config(
|
|
579
|
+
config: &S3Config,
|
|
580
|
+
region: &str,
|
|
581
|
+
) -> Result<Option<S3ExpressSessionConfig>> {
|
|
582
|
+
if !config.bucket.ends_with("--x-s3") {
|
|
583
|
+
return Ok(None);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let endpoint = match &config.endpoint {
|
|
587
|
+
Some(endpoint) => {
|
|
588
|
+
let endpoint = if endpoint.starts_with("http") {
|
|
589
|
+
endpoint.to_string()
|
|
590
|
+
} else {
|
|
591
|
+
format!("https://{endpoint}")
|
|
592
|
+
};
|
|
593
|
+
let endpoint = Url::parse(&endpoint).map_err(|err| {
|
|
594
|
+
Self::invalid_s3_express_config("invalid S3 endpoint").set_source(err)
|
|
595
|
+
})?;
|
|
596
|
+
let Some(host) = endpoint.host_str() else {
|
|
597
|
+
return Ok(None);
|
|
598
|
+
};
|
|
599
|
+
let host = host
|
|
600
|
+
.strip_prefix(&format!("{}.", config.bucket))
|
|
601
|
+
.unwrap_or(host);
|
|
602
|
+
if !Self::is_aws_host(host) {
|
|
603
|
+
return Ok(None);
|
|
604
|
+
}
|
|
605
|
+
if endpoint.scheme() != "https"
|
|
606
|
+
|| endpoint.path() != "/"
|
|
607
|
+
|| endpoint.query().is_some()
|
|
608
|
+
|| endpoint.fragment().is_some()
|
|
609
|
+
|| endpoint.port().is_some_and(|port| port != 443)
|
|
610
|
+
|| !endpoint.username().is_empty()
|
|
611
|
+
|| endpoint.password().is_some()
|
|
612
|
+
{
|
|
613
|
+
return Err(Self::invalid_s3_express_config(
|
|
614
|
+
"unsupported AWS endpoint for an S3 directory bucket",
|
|
615
|
+
));
|
|
616
|
+
}
|
|
617
|
+
Some(host.to_string())
|
|
618
|
+
}
|
|
619
|
+
None => None,
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
let express_config =
|
|
623
|
+
S3ExpressSessionConfig::from_bucket(&config.bucket, region).map_err(|err| {
|
|
624
|
+
Self::invalid_s3_express_config("invalid AWS S3 directory bucket configuration")
|
|
625
|
+
.set_source(err)
|
|
626
|
+
})?;
|
|
627
|
+
|
|
628
|
+
if let Some(endpoint) = endpoint {
|
|
629
|
+
let dns_suffix = express_config.partition().dns_suffix();
|
|
630
|
+
let region = express_config.region();
|
|
631
|
+
let regional = format!("s3.{region}.{dns_suffix}");
|
|
632
|
+
let zonal = format!(
|
|
633
|
+
"s3express-{}.{region}.{dns_suffix}",
|
|
634
|
+
express_config.zone_id()
|
|
635
|
+
);
|
|
636
|
+
let global = (dns_suffix == "amazonaws.com").then_some("s3.amazonaws.com");
|
|
637
|
+
if endpoint != regional && Some(endpoint.as_str()) != global && endpoint != zonal {
|
|
638
|
+
return Err(Self::invalid_s3_express_config(
|
|
639
|
+
"AWS endpoint does not match the S3 directory bucket",
|
|
640
|
+
));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
Ok(Some(express_config))
|
|
645
|
+
}
|
|
646
|
+
|
|
548
647
|
/// Deprecated: S3 delete batch capability is enabled by default.
|
|
549
648
|
#[deprecated(
|
|
550
649
|
since = "0.57.0",
|
|
@@ -838,8 +937,14 @@ impl Builder for S3Builder {
|
|
|
838
937
|
}
|
|
839
938
|
}
|
|
840
939
|
|
|
940
|
+
let s3_express_config = Self::resolve_s3_express_config(&config, ®ion)?;
|
|
941
|
+
let is_s3_express = s3_express_config.is_some();
|
|
942
|
+
|
|
841
943
|
// Building endpoint.
|
|
842
|
-
let endpoint =
|
|
944
|
+
let endpoint = match &s3_express_config {
|
|
945
|
+
Some(config) => config.endpoint().to_string(),
|
|
946
|
+
None => Self::build_endpoint(&config, ®ion),
|
|
947
|
+
};
|
|
843
948
|
debug!("backend use endpoint: {endpoint}");
|
|
844
949
|
|
|
845
950
|
// The base signer context only carries local config readers. HTTP
|
|
@@ -852,6 +957,12 @@ impl Builder for S3Builder {
|
|
|
852
957
|
|
|
853
958
|
if config.disable_config_load {
|
|
854
959
|
builder = builder.no_env().no_profile();
|
|
960
|
+
} else if let Some(profile) = config
|
|
961
|
+
.profile
|
|
962
|
+
.as_deref()
|
|
963
|
+
.filter(|profile| !profile.is_empty())
|
|
964
|
+
{
|
|
965
|
+
builder = builder.with_profile(profile);
|
|
855
966
|
}
|
|
856
967
|
|
|
857
968
|
if config.disable_ec2_metadata {
|
|
@@ -908,11 +1019,27 @@ impl Builder for S3Builder {
|
|
|
908
1019
|
provider
|
|
909
1020
|
};
|
|
910
1021
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1022
|
+
let signers = if is_s3_express {
|
|
1023
|
+
let provider = Arc::new(provider);
|
|
1024
|
+
let iam_signer = Signer::new(
|
|
1025
|
+
ctx.clone(),
|
|
1026
|
+
provider.clone(),
|
|
1027
|
+
AwsV4Signer::new("s3express", ®ion).with_standard_session_token(),
|
|
1028
|
+
);
|
|
1029
|
+
let session_provider =
|
|
1030
|
+
S3ExpressSessionProvider::new(bucket, provider).with_region(®ion);
|
|
1031
|
+
let session_signer = Signer::new(
|
|
1032
|
+
ctx,
|
|
1033
|
+
session_provider,
|
|
1034
|
+
AwsV4Signer::new("s3express", ®ion),
|
|
1035
|
+
);
|
|
1036
|
+
S3Signers::Express {
|
|
1037
|
+
iam: iam_signer,
|
|
1038
|
+
session: session_signer,
|
|
1039
|
+
}
|
|
1040
|
+
} else {
|
|
1041
|
+
S3Signers::General(Signer::new(ctx, provider, AwsV4Signer::new("s3", ®ion)))
|
|
1042
|
+
};
|
|
916
1043
|
|
|
917
1044
|
Ok(S3Backend {
|
|
918
1045
|
core: Arc::new(S3Core {
|
|
@@ -926,7 +1053,7 @@ impl Builder for S3Builder {
|
|
|
926
1053
|
stat_with_override_cache_control: true,
|
|
927
1054
|
stat_with_override_content_disposition: true,
|
|
928
1055
|
stat_with_override_content_type: true,
|
|
929
|
-
stat_with_version:
|
|
1056
|
+
stat_with_version: !is_s3_express,
|
|
930
1057
|
|
|
931
1058
|
read: true,
|
|
932
1059
|
read_with_if_match: true,
|
|
@@ -936,18 +1063,19 @@ impl Builder for S3Builder {
|
|
|
936
1063
|
read_with_override_cache_control: true,
|
|
937
1064
|
read_with_override_content_disposition: true,
|
|
938
1065
|
read_with_override_content_type: true,
|
|
939
|
-
read_with_version:
|
|
1066
|
+
read_with_version: !is_s3_express,
|
|
940
1067
|
read_with_suffix: true,
|
|
941
1068
|
|
|
942
1069
|
write: true,
|
|
943
1070
|
write_can_empty: true,
|
|
944
1071
|
write_can_multi: true,
|
|
945
1072
|
write_can_append: true,
|
|
1073
|
+
write_can_copy_from: checksum_algorithm.is_none(),
|
|
946
1074
|
|
|
947
1075
|
write_with_cache_control: true,
|
|
948
1076
|
write_with_content_type: true,
|
|
949
1077
|
write_with_content_disposition: true,
|
|
950
|
-
write_with_content_encoding:
|
|
1078
|
+
write_with_content_encoding: !is_s3_express,
|
|
951
1079
|
write_with_if_match: true,
|
|
952
1080
|
write_with_if_not_exists: true,
|
|
953
1081
|
write_with_user_metadata: true,
|
|
@@ -975,13 +1103,14 @@ impl Builder for S3Builder {
|
|
|
975
1103
|
|
|
976
1104
|
delete: true,
|
|
977
1105
|
delete_max_size: Some(DEFAULT_BATCH_MAX_OPERATIONS),
|
|
978
|
-
delete_with_version:
|
|
1106
|
+
delete_with_version: !is_s3_express,
|
|
1107
|
+
delete_with_if_match: true,
|
|
979
1108
|
|
|
980
1109
|
copy: true,
|
|
981
1110
|
copy_can_multi: true,
|
|
982
1111
|
copy_with_if_not_exists: true,
|
|
983
1112
|
copy_with_if_match: true,
|
|
984
|
-
copy_with_source_version:
|
|
1113
|
+
copy_with_source_version: !is_s3_express,
|
|
985
1114
|
// The min multipart size of S3 is 5 MiB.
|
|
986
1115
|
//
|
|
987
1116
|
// ref: <https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html>
|
|
@@ -995,12 +1124,16 @@ impl Builder for S3Builder {
|
|
|
995
1124
|
Some(usize::MAX)
|
|
996
1125
|
},
|
|
997
1126
|
|
|
1127
|
+
restore: !is_s3_express,
|
|
1128
|
+
restore_with_version: !is_s3_express,
|
|
1129
|
+
restore_with_if_not_exists: !is_s3_express,
|
|
1130
|
+
|
|
998
1131
|
list: true,
|
|
999
1132
|
list_with_limit: true,
|
|
1000
|
-
list_with_start_after:
|
|
1001
|
-
list_with_recursive:
|
|
1002
|
-
list_with_versions:
|
|
1003
|
-
list_with_deleted:
|
|
1133
|
+
list_with_start_after: !is_s3_express,
|
|
1134
|
+
list_with_recursive: !is_s3_express,
|
|
1135
|
+
list_with_versions: !is_s3_express,
|
|
1136
|
+
list_with_deleted: !is_s3_express,
|
|
1004
1137
|
|
|
1005
1138
|
presign: true,
|
|
1006
1139
|
presign_stat: true,
|
|
@@ -1024,7 +1157,7 @@ impl Builder for S3Builder {
|
|
|
1024
1157
|
skip_signature: config.skip_signature,
|
|
1025
1158
|
disable_list_objects_v2: config.disable_list_objects_v2,
|
|
1026
1159
|
enable_request_payer: config.enable_request_payer,
|
|
1027
|
-
|
|
1160
|
+
signers,
|
|
1028
1161
|
checksum_algorithm,
|
|
1029
1162
|
default_acl: config.default_acl,
|
|
1030
1163
|
}),
|
|
@@ -1044,6 +1177,7 @@ impl Service for S3Backend {
|
|
|
1044
1177
|
type Lister = S3Listers;
|
|
1045
1178
|
type Deleter = oio::BatchDeleter<S3Deleter>;
|
|
1046
1179
|
type Copier = S3Copiers;
|
|
1180
|
+
type Composer = ();
|
|
1047
1181
|
|
|
1048
1182
|
fn info(&self) -> ServiceInfo {
|
|
1049
1183
|
self.core.info.clone()
|
|
@@ -1066,27 +1200,15 @@ impl Service for S3Backend {
|
|
|
1066
1200
|
}
|
|
1067
1201
|
|
|
1068
1202
|
async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
|
|
1203
|
+
let error_ctx = ErrorContext::new(ServiceOperation("HeadObject"))
|
|
1204
|
+
.with_caller_condition(args.is_conditional());
|
|
1069
1205
|
let resp = self.core.s3_head_object(ctx, path, args).await?;
|
|
1070
1206
|
|
|
1071
1207
|
let status = resp.status();
|
|
1072
1208
|
|
|
1073
1209
|
match status {
|
|
1074
|
-
StatusCode::OK =>
|
|
1075
|
-
|
|
1076
|
-
let mut meta = parse_into_metadata(path, headers)?;
|
|
1077
|
-
|
|
1078
|
-
let user_meta = parse_prefixed_headers(headers, X_AMZ_META_PREFIX);
|
|
1079
|
-
if !user_meta.is_empty() {
|
|
1080
|
-
meta = meta.with_user_metadata(user_meta);
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
if let Some(v) = parse_header_to_str(headers, X_AMZ_VERSION_ID)? {
|
|
1084
|
-
meta.set_version(v);
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
Ok(RpStat::new(meta))
|
|
1088
|
-
}
|
|
1089
|
-
_ => Err(parse_error(resp)),
|
|
1210
|
+
StatusCode::OK => Ok(RpStat::new(parse_into_s3_metadata(path, resp.headers())?)),
|
|
1211
|
+
_ => Err(parse_error(error_ctx, resp)),
|
|
1090
1212
|
}
|
|
1091
1213
|
}
|
|
1092
1214
|
fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
|
|
@@ -1172,10 +1294,9 @@ impl Service for S3Backend {
|
|
|
1172
1294
|
from: &str,
|
|
1173
1295
|
to: &str,
|
|
1174
1296
|
args: OpCopy,
|
|
1175
|
-
opts: OpCopier,
|
|
1176
1297
|
) -> Result<Self::Copier> {
|
|
1177
1298
|
let output: S3Copiers = {
|
|
1178
|
-
let copier = new_s3_copier(self.core.clone(), ctx, from, to, args
|
|
1299
|
+
let copier = new_s3_copier(self.core.clone(), ctx, from, to, args)?;
|
|
1179
1300
|
Ok(copier)
|
|
1180
1301
|
}?;
|
|
1181
1302
|
|
|
@@ -1195,6 +1316,92 @@ impl Service for S3Backend {
|
|
|
1195
1316
|
))
|
|
1196
1317
|
}
|
|
1197
1318
|
|
|
1319
|
+
async fn restore(
|
|
1320
|
+
&self,
|
|
1321
|
+
ctx: &OperationContext,
|
|
1322
|
+
path: &str,
|
|
1323
|
+
args: OpRestore,
|
|
1324
|
+
) -> Result<RpRestore> {
|
|
1325
|
+
if let Some(version) = args.version() {
|
|
1326
|
+
let copy_args = OpCopy::from_options(
|
|
1327
|
+
&self.capability(),
|
|
1328
|
+
options::CopyOptions {
|
|
1329
|
+
source_version: Some(version.to_owned()),
|
|
1330
|
+
if_not_exists: args.if_not_exists(),
|
|
1331
|
+
..Default::default()
|
|
1332
|
+
},
|
|
1333
|
+
)?;
|
|
1334
|
+
let mut copier = new_s3_copier(self.core.clone(), ctx, path, path, copy_args)?;
|
|
1335
|
+
|
|
1336
|
+
return match copier.close().await {
|
|
1337
|
+
Ok(_) => Ok(RpRestore::new()),
|
|
1338
|
+
Err(err) => {
|
|
1339
|
+
let _ = copier.abort().await;
|
|
1340
|
+
Err(err)
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if args.if_not_exists() {
|
|
1346
|
+
return Err(Error::new(
|
|
1347
|
+
ErrorKind::ConfigInvalid,
|
|
1348
|
+
"if_not_exists requires a restore version",
|
|
1349
|
+
));
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
let resp = self
|
|
1353
|
+
.core
|
|
1354
|
+
.s3_list_object_versions(ctx, path, "", Some(1), "", "")
|
|
1355
|
+
.await?;
|
|
1356
|
+
if resp.status() != StatusCode::OK {
|
|
1357
|
+
return Err(parse_error(
|
|
1358
|
+
ErrorContext::new(ServiceOperation("ListObjectVersions")),
|
|
1359
|
+
resp,
|
|
1360
|
+
));
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
let output: ListObjectVersionsOutput =
|
|
1364
|
+
quick_xml::de::from_reader(resp.into_body().reader())
|
|
1365
|
+
.map_err(new_xml_deserialize_error)
|
|
1366
|
+
.map_err(Error::set_temporary)?;
|
|
1367
|
+
let abs_path = build_abs_path(&self.core.root, path);
|
|
1368
|
+
|
|
1369
|
+
if output
|
|
1370
|
+
.version
|
|
1371
|
+
.iter()
|
|
1372
|
+
.any(|version| version.key == abs_path && version.is_latest)
|
|
1373
|
+
{
|
|
1374
|
+
return Ok(RpRestore::new());
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
let Some(marker) = output
|
|
1378
|
+
.delete_marker
|
|
1379
|
+
.into_iter()
|
|
1380
|
+
.find(|marker| marker.key == abs_path && marker.is_latest)
|
|
1381
|
+
else {
|
|
1382
|
+
return Err(Error::new(
|
|
1383
|
+
ErrorKind::NotFound,
|
|
1384
|
+
"no live object or current delete marker exists",
|
|
1385
|
+
));
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
let delete_args = OpDelete::from_options(
|
|
1389
|
+
&self.capability(),
|
|
1390
|
+
options::DeleteOptions {
|
|
1391
|
+
version: Some(marker.version_id.clone()),
|
|
1392
|
+
..Default::default()
|
|
1393
|
+
},
|
|
1394
|
+
)?;
|
|
1395
|
+
let resp = self.core.s3_delete_object(ctx, path, &delete_args).await?;
|
|
1396
|
+
match resp.status() {
|
|
1397
|
+
StatusCode::NO_CONTENT | StatusCode::NOT_FOUND => Ok(RpRestore::new()),
|
|
1398
|
+
_ => Err(parse_error(
|
|
1399
|
+
ErrorContext::new(ServiceOperation("DeleteObject")),
|
|
1400
|
+
resp,
|
|
1401
|
+
)),
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1198
1405
|
async fn presign(
|
|
1199
1406
|
&self,
|
|
1200
1407
|
ctx: &OperationContext,
|
|
@@ -1232,8 +1439,650 @@ impl Service for S3Backend {
|
|
|
1232
1439
|
}
|
|
1233
1440
|
#[cfg(test)]
|
|
1234
1441
|
mod tests {
|
|
1442
|
+
use std::sync::Arc;
|
|
1443
|
+
use std::sync::Mutex;
|
|
1444
|
+
|
|
1445
|
+
use bytes::Bytes;
|
|
1446
|
+
use http::HeaderMap;
|
|
1447
|
+
use http::Method;
|
|
1448
|
+
use http::Request;
|
|
1449
|
+
use http::Response;
|
|
1450
|
+
|
|
1235
1451
|
use super::*;
|
|
1236
1452
|
|
|
1453
|
+
#[derive(Clone, Copy)]
|
|
1454
|
+
enum CreateSessionOutcome {
|
|
1455
|
+
Success,
|
|
1456
|
+
Body(&'static str),
|
|
1457
|
+
Status(StatusCode),
|
|
1458
|
+
TemporaryTransportError,
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
#[derive(Clone)]
|
|
1462
|
+
struct CapturedRequest {
|
|
1463
|
+
method: Method,
|
|
1464
|
+
uri: http::Uri,
|
|
1465
|
+
headers: HeaderMap,
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
#[derive(Clone)]
|
|
1469
|
+
struct S3ExpressMockTransport {
|
|
1470
|
+
outcome: CreateSessionOutcome,
|
|
1471
|
+
source_content_length: u64,
|
|
1472
|
+
source_version: Option<&'static str>,
|
|
1473
|
+
requests: Arc<Mutex<Vec<CapturedRequest>>>,
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
impl S3ExpressMockTransport {
|
|
1477
|
+
fn new(outcome: CreateSessionOutcome) -> Self {
|
|
1478
|
+
Self {
|
|
1479
|
+
outcome,
|
|
1480
|
+
source_content_length: 1,
|
|
1481
|
+
source_version: None,
|
|
1482
|
+
requests: Arc::new(Mutex::new(Vec::new())),
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
fn with_source_content_length(mut self, source_content_length: u64) -> Self {
|
|
1487
|
+
self.source_content_length = source_content_length;
|
|
1488
|
+
self
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
fn with_source_version(mut self, source_version: &'static str) -> Self {
|
|
1492
|
+
self.source_version = Some(source_version);
|
|
1493
|
+
self
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
fn requests(&self) -> Vec<CapturedRequest> {
|
|
1497
|
+
self.requests.lock().expect("lock poisoned").clone()
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
fn response(status: StatusCode, body: impl Into<Bytes>) -> Response<HttpBody> {
|
|
1501
|
+
let body = Buffer::from(body.into());
|
|
1502
|
+
let size = body.len() as u64;
|
|
1503
|
+
Response::builder()
|
|
1504
|
+
.status(status)
|
|
1505
|
+
.body(HttpBody::new(
|
|
1506
|
+
futures::stream::iter(vec![Ok(body)]),
|
|
1507
|
+
Some(size),
|
|
1508
|
+
))
|
|
1509
|
+
.expect("mock response must build")
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
impl HttpTransport for S3ExpressMockTransport {
|
|
1514
|
+
async fn fetch(&self, req: Request<Buffer>) -> Result<Response<HttpBody>> {
|
|
1515
|
+
let is_create_session = req.uri().query() == Some("session");
|
|
1516
|
+
self.requests
|
|
1517
|
+
.lock()
|
|
1518
|
+
.expect("lock poisoned")
|
|
1519
|
+
.push(CapturedRequest {
|
|
1520
|
+
method: req.method().clone(),
|
|
1521
|
+
uri: req.uri().clone(),
|
|
1522
|
+
headers: req.headers().clone(),
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
if is_create_session {
|
|
1526
|
+
tokio::task::yield_now().await;
|
|
1527
|
+
return match self.outcome {
|
|
1528
|
+
CreateSessionOutcome::Success => Ok(Self::response(
|
|
1529
|
+
StatusCode::OK,
|
|
1530
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
|
|
1531
|
+
<CreateSessionResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
|
|
1532
|
+
<Credentials>\
|
|
1533
|
+
<SessionToken>session-token</SessionToken>\
|
|
1534
|
+
<SecretAccessKey>session-secret-key</SecretAccessKey>\
|
|
1535
|
+
<AccessKeyId>session-access-key</AccessKeyId>\
|
|
1536
|
+
<Expiration>2099-01-01T00:05:00Z</Expiration>\
|
|
1537
|
+
</Credentials>\
|
|
1538
|
+
</CreateSessionResult>",
|
|
1539
|
+
)),
|
|
1540
|
+
CreateSessionOutcome::Body(body) => Ok(Self::response(StatusCode::OK, body)),
|
|
1541
|
+
CreateSessionOutcome::Status(status) => {
|
|
1542
|
+
Ok(Self::response(status, Bytes::new()))
|
|
1543
|
+
}
|
|
1544
|
+
CreateSessionOutcome::TemporaryTransportError => Err(Error::new(
|
|
1545
|
+
ErrorKind::Unexpected,
|
|
1546
|
+
"temporary mock transport failure",
|
|
1547
|
+
)
|
|
1548
|
+
.set_temporary()),
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
if req.method() == Method::HEAD {
|
|
1553
|
+
let mut response = Self::response(StatusCode::OK, Bytes::new());
|
|
1554
|
+
response.headers_mut().insert(
|
|
1555
|
+
http::header::CONTENT_LENGTH,
|
|
1556
|
+
self.source_content_length
|
|
1557
|
+
.to_string()
|
|
1558
|
+
.parse()
|
|
1559
|
+
.expect("source content length must be a valid header"),
|
|
1560
|
+
);
|
|
1561
|
+
response.headers_mut().insert(
|
|
1562
|
+
http::header::ETAG,
|
|
1563
|
+
"\"source-etag\""
|
|
1564
|
+
.parse()
|
|
1565
|
+
.expect("source ETag must be a valid header"),
|
|
1566
|
+
);
|
|
1567
|
+
if let Some(version) = self.source_version {
|
|
1568
|
+
response.headers_mut().insert(
|
|
1569
|
+
constants::X_AMZ_VERSION_ID,
|
|
1570
|
+
version
|
|
1571
|
+
.parse()
|
|
1572
|
+
.expect("source version must be a valid header"),
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
return Ok(response);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
let query = req.uri().query().unwrap_or_default();
|
|
1579
|
+
if req.headers().contains_key(constants::X_AMZ_COPY_SOURCE) {
|
|
1580
|
+
return Ok(Self::response(
|
|
1581
|
+
StatusCode::OK,
|
|
1582
|
+
"<CopyObjectResult><ETag>\"etag\"</ETag>\
|
|
1583
|
+
<LastModified>2026-08-24T00:00:00Z</LastModified></CopyObjectResult>",
|
|
1584
|
+
));
|
|
1585
|
+
}
|
|
1586
|
+
if query == "uploads" {
|
|
1587
|
+
return Ok(Self::response(
|
|
1588
|
+
StatusCode::OK,
|
|
1589
|
+
"<InitiateMultipartUploadResult><UploadId>upload-id</UploadId>\
|
|
1590
|
+
</InitiateMultipartUploadResult>",
|
|
1591
|
+
));
|
|
1592
|
+
}
|
|
1593
|
+
if query.contains("list-type=2") {
|
|
1594
|
+
return Ok(Self::response(
|
|
1595
|
+
StatusCode::OK,
|
|
1596
|
+
"<ListBucketResult><IsTruncated>false</IsTruncated></ListBucketResult>",
|
|
1597
|
+
));
|
|
1598
|
+
}
|
|
1599
|
+
if req.method() == Method::POST && query.contains("uploadId=") {
|
|
1600
|
+
return Ok(Self::response(
|
|
1601
|
+
StatusCode::OK,
|
|
1602
|
+
"<CompleteMultipartUploadResult><Bucket>example</Bucket><Key>target</Key>\
|
|
1603
|
+
<Location>https://example.invalid/target</Location><ETag>\"etag\"</ETag>\
|
|
1604
|
+
</CompleteMultipartUploadResult>",
|
|
1605
|
+
));
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
Ok(Self::response(StatusCode::OK, Bytes::new()))
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
fn s3_express_operator(transport: S3ExpressMockTransport) -> Operator {
|
|
1613
|
+
Operator::new(
|
|
1614
|
+
S3Builder::default()
|
|
1615
|
+
.bucket("example--usw2-az1--x-s3")
|
|
1616
|
+
.region("us-west-2")
|
|
1617
|
+
.access_key_id("source-access-key")
|
|
1618
|
+
.secret_access_key("source-secret-key")
|
|
1619
|
+
.session_token("source-session-token")
|
|
1620
|
+
.disable_config_load()
|
|
1621
|
+
.disable_ec2_metadata(),
|
|
1622
|
+
)
|
|
1623
|
+
.expect("S3 Express operator must build")
|
|
1624
|
+
.with_context(OperationContext::new().with_http_transport(HttpTransporter::new(transport)))
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
fn s3_general_operator(transport: S3ExpressMockTransport) -> Operator {
|
|
1628
|
+
Operator::new(
|
|
1629
|
+
S3Builder::default()
|
|
1630
|
+
.bucket("example")
|
|
1631
|
+
.region("us-west-2")
|
|
1632
|
+
.access_key_id("source-access-key")
|
|
1633
|
+
.secret_access_key("source-secret-key")
|
|
1634
|
+
.session_token("source-session-token")
|
|
1635
|
+
.disable_config_load()
|
|
1636
|
+
.disable_ec2_metadata(),
|
|
1637
|
+
)
|
|
1638
|
+
.expect("S3 operator must build")
|
|
1639
|
+
.with_context(OperationContext::new().with_http_transport(HttpTransporter::new(transport)))
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
fn header<'a>(request: &'a CapturedRequest, name: &str) -> &'a str {
|
|
1643
|
+
request
|
|
1644
|
+
.headers
|
|
1645
|
+
.get(name)
|
|
1646
|
+
.unwrap_or_else(|| panic!("missing {name} header"))
|
|
1647
|
+
.to_str()
|
|
1648
|
+
.expect("header must be text")
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
#[tokio::test]
|
|
1652
|
+
async fn test_s3_express_signing_chain_and_session_reuse() {
|
|
1653
|
+
let transport = S3ExpressMockTransport::new(CreateSessionOutcome::Success);
|
|
1654
|
+
let op = s3_express_operator(transport.clone());
|
|
1655
|
+
|
|
1656
|
+
let (first, second) = tokio::join!(
|
|
1657
|
+
op.write("first", "first-body"),
|
|
1658
|
+
op.write("second", "second-body")
|
|
1659
|
+
);
|
|
1660
|
+
first.expect("first write must succeed");
|
|
1661
|
+
second.expect("second write must succeed");
|
|
1662
|
+
|
|
1663
|
+
let requests = transport.requests();
|
|
1664
|
+
assert_eq!(
|
|
1665
|
+
requests[0].uri.query(),
|
|
1666
|
+
Some("session"),
|
|
1667
|
+
"CreateSession must complete before object requests are signed"
|
|
1668
|
+
);
|
|
1669
|
+
let create_sessions = requests
|
|
1670
|
+
.iter()
|
|
1671
|
+
.filter(|request| request.uri.query() == Some("session"))
|
|
1672
|
+
.collect::<Vec<_>>();
|
|
1673
|
+
assert_eq!(create_sessions.len(), 1, "session must be reused");
|
|
1674
|
+
|
|
1675
|
+
let create_session = create_sessions[0];
|
|
1676
|
+
assert_eq!(create_session.method, Method::GET);
|
|
1677
|
+
assert_eq!(
|
|
1678
|
+
header(create_session, "x-amz-create-session-mode"),
|
|
1679
|
+
"ReadWrite"
|
|
1680
|
+
);
|
|
1681
|
+
assert_eq!(
|
|
1682
|
+
header(create_session, "x-amz-security-token"),
|
|
1683
|
+
"source-session-token"
|
|
1684
|
+
);
|
|
1685
|
+
assert!(!create_session.headers.contains_key("x-amz-s3session-token"));
|
|
1686
|
+
assert!(header(create_session, "authorization").contains("source-access-key/"));
|
|
1687
|
+
|
|
1688
|
+
let puts = requests
|
|
1689
|
+
.iter()
|
|
1690
|
+
.filter(|request| request.method == Method::PUT)
|
|
1691
|
+
.collect::<Vec<_>>();
|
|
1692
|
+
assert_eq!(puts.len(), 2);
|
|
1693
|
+
for request in puts {
|
|
1694
|
+
assert_eq!(header(request, "x-amz-s3session-token"), "session-token");
|
|
1695
|
+
assert!(!request.headers.contains_key("x-amz-security-token"));
|
|
1696
|
+
assert!(header(request, "authorization").contains("session-access-key/"));
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
#[tokio::test]
|
|
1701
|
+
async fn test_s3_express_list_uses_session_credentials() {
|
|
1702
|
+
let transport = S3ExpressMockTransport::new(CreateSessionOutcome::Success);
|
|
1703
|
+
let op = s3_express_operator(transport.clone());
|
|
1704
|
+
|
|
1705
|
+
let entries = op.list("").await.expect("list must succeed");
|
|
1706
|
+
assert!(entries.is_empty());
|
|
1707
|
+
|
|
1708
|
+
let requests = transport.requests();
|
|
1709
|
+
let list = requests
|
|
1710
|
+
.iter()
|
|
1711
|
+
.find(|request| {
|
|
1712
|
+
request
|
|
1713
|
+
.uri
|
|
1714
|
+
.query()
|
|
1715
|
+
.is_some_and(|query| query.contains("list-type=2"))
|
|
1716
|
+
})
|
|
1717
|
+
.expect("ListObjectsV2 request must be captured");
|
|
1718
|
+
assert_eq!(header(list, "x-amz-s3session-token"), "session-token");
|
|
1719
|
+
assert!(!list.headers.contains_key("x-amz-security-token"));
|
|
1720
|
+
assert!(header(list, "authorization").contains("session-access-key/"));
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
#[tokio::test]
|
|
1724
|
+
async fn test_s3_general_bucket_always_uses_iam_credentials() {
|
|
1725
|
+
let transport = S3ExpressMockTransport::new(CreateSessionOutcome::Success);
|
|
1726
|
+
let op = s3_general_operator(transport.clone());
|
|
1727
|
+
|
|
1728
|
+
op.write("source", "body")
|
|
1729
|
+
.await
|
|
1730
|
+
.expect("write must succeed");
|
|
1731
|
+
op.copy_with("source", "target")
|
|
1732
|
+
.source_content_length_hint(1)
|
|
1733
|
+
.await
|
|
1734
|
+
.expect("copy must succeed");
|
|
1735
|
+
|
|
1736
|
+
let requests = transport.requests();
|
|
1737
|
+
assert_eq!(requests.len(), 2);
|
|
1738
|
+
for request in requests {
|
|
1739
|
+
assert_eq!(
|
|
1740
|
+
header(&request, "x-amz-security-token"),
|
|
1741
|
+
"source-session-token"
|
|
1742
|
+
);
|
|
1743
|
+
assert!(!request.headers.contains_key("x-amz-s3session-token"));
|
|
1744
|
+
assert!(header(&request, "authorization").contains("source-access-key/"));
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
#[tokio::test]
|
|
1749
|
+
async fn test_s3_copy_hint_skips_source_snapshot() {
|
|
1750
|
+
let transport = S3ExpressMockTransport::new(CreateSessionOutcome::Success);
|
|
1751
|
+
let op = s3_general_operator(transport.clone());
|
|
1752
|
+
|
|
1753
|
+
let metadata = op
|
|
1754
|
+
.copy_with("source", "target")
|
|
1755
|
+
.source_content_length_hint(7)
|
|
1756
|
+
.await
|
|
1757
|
+
.expect("copy must succeed");
|
|
1758
|
+
|
|
1759
|
+
assert_eq!(metadata.content_length(), 7);
|
|
1760
|
+
let requests = transport.requests();
|
|
1761
|
+
assert_eq!(requests.len(), 1);
|
|
1762
|
+
assert_eq!(requests[0].method, Method::PUT);
|
|
1763
|
+
assert!(
|
|
1764
|
+
!requests[0]
|
|
1765
|
+
.headers
|
|
1766
|
+
.contains_key(constants::X_AMZ_COPY_SOURCE_IF_MATCH)
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
#[tokio::test]
|
|
1771
|
+
async fn test_s3_copy_hint_preserves_explicit_source_version() {
|
|
1772
|
+
let transport = S3ExpressMockTransport::new(CreateSessionOutcome::Success);
|
|
1773
|
+
let op = s3_general_operator(transport.clone());
|
|
1774
|
+
|
|
1775
|
+
op.copy_with("source", "target")
|
|
1776
|
+
.source_version("source-version")
|
|
1777
|
+
.source_content_length_hint(7)
|
|
1778
|
+
.await
|
|
1779
|
+
.expect("copy must succeed");
|
|
1780
|
+
|
|
1781
|
+
let requests = transport.requests();
|
|
1782
|
+
assert_eq!(requests.len(), 1);
|
|
1783
|
+
assert!(
|
|
1784
|
+
header(&requests[0], constants::X_AMZ_COPY_SOURCE).contains("versionId=source-version")
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
#[tokio::test]
|
|
1789
|
+
async fn test_s3_copy_without_hint_pins_source_etag() {
|
|
1790
|
+
let transport = S3ExpressMockTransport::new(CreateSessionOutcome::Success)
|
|
1791
|
+
.with_source_content_length(7);
|
|
1792
|
+
let op = s3_general_operator(transport.clone());
|
|
1793
|
+
|
|
1794
|
+
let metadata = op
|
|
1795
|
+
.copy("source", "target")
|
|
1796
|
+
.await
|
|
1797
|
+
.expect("copy must succeed");
|
|
1798
|
+
|
|
1799
|
+
assert_eq!(metadata.content_length(), 7);
|
|
1800
|
+
let requests = transport.requests();
|
|
1801
|
+
assert_eq!(
|
|
1802
|
+
requests
|
|
1803
|
+
.iter()
|
|
1804
|
+
.filter(|request| request.method == Method::HEAD)
|
|
1805
|
+
.count(),
|
|
1806
|
+
1
|
|
1807
|
+
);
|
|
1808
|
+
let copy = requests
|
|
1809
|
+
.iter()
|
|
1810
|
+
.find(|request| request.headers.contains_key(constants::X_AMZ_COPY_SOURCE))
|
|
1811
|
+
.expect("CopyObject request must be captured");
|
|
1812
|
+
assert_eq!(
|
|
1813
|
+
header(copy, constants::X_AMZ_COPY_SOURCE_IF_MATCH),
|
|
1814
|
+
"\"source-etag\""
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
#[tokio::test]
|
|
1819
|
+
async fn test_s3_copy_uses_version_from_source_snapshot() {
|
|
1820
|
+
let transport = S3ExpressMockTransport::new(CreateSessionOutcome::Success)
|
|
1821
|
+
.with_source_version("source-version");
|
|
1822
|
+
let op = s3_general_operator(transport.clone());
|
|
1823
|
+
|
|
1824
|
+
op.copy_with("source", "target")
|
|
1825
|
+
.await
|
|
1826
|
+
.expect("copy must succeed");
|
|
1827
|
+
|
|
1828
|
+
let requests = transport.requests();
|
|
1829
|
+
let copy = requests
|
|
1830
|
+
.iter()
|
|
1831
|
+
.find(|request| request.headers.contains_key(constants::X_AMZ_COPY_SOURCE))
|
|
1832
|
+
.expect("CopyObject request must be captured");
|
|
1833
|
+
assert!(header(copy, constants::X_AMZ_COPY_SOURCE).contains("versionId=source-version"));
|
|
1834
|
+
assert_eq!(
|
|
1835
|
+
header(copy, constants::X_AMZ_COPY_SOURCE_IF_MATCH),
|
|
1836
|
+
"\"source-etag\""
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
#[tokio::test]
|
|
1841
|
+
async fn test_s3_express_copy_and_presign_use_iam_credentials() {
|
|
1842
|
+
let transport = S3ExpressMockTransport::new(CreateSessionOutcome::Success);
|
|
1843
|
+
let op = s3_express_operator(transport.clone());
|
|
1844
|
+
|
|
1845
|
+
op.copy_with("source", "target")
|
|
1846
|
+
.source_content_length_hint(1)
|
|
1847
|
+
.await
|
|
1848
|
+
.expect("copy must succeed");
|
|
1849
|
+
let requests = transport.requests();
|
|
1850
|
+
assert_eq!(
|
|
1851
|
+
requests
|
|
1852
|
+
.iter()
|
|
1853
|
+
.filter(|request| request.uri.query() == Some("session"))
|
|
1854
|
+
.count(),
|
|
1855
|
+
0,
|
|
1856
|
+
"a source length hint must skip HeadObject"
|
|
1857
|
+
);
|
|
1858
|
+
let copy = requests
|
|
1859
|
+
.iter()
|
|
1860
|
+
.find(|request| request.headers.contains_key(constants::X_AMZ_COPY_SOURCE))
|
|
1861
|
+
.expect("CopyObject request must be captured");
|
|
1862
|
+
assert_eq!(header(copy, "x-amz-security-token"), "source-session-token");
|
|
1863
|
+
assert!(!copy.headers.contains_key("x-amz-s3session-token"));
|
|
1864
|
+
assert!(header(copy, "authorization").contains("source-access-key/"));
|
|
1865
|
+
assert!(
|
|
1866
|
+
!copy
|
|
1867
|
+
.headers
|
|
1868
|
+
.contains_key(constants::X_AMZ_COPY_SOURCE_IF_MATCH)
|
|
1869
|
+
);
|
|
1870
|
+
|
|
1871
|
+
let presigned = op
|
|
1872
|
+
.presign_read("source", Duration::from_secs(60))
|
|
1873
|
+
.await
|
|
1874
|
+
.expect("presign must succeed");
|
|
1875
|
+
let query = presigned.uri().query().expect("presign query must exist");
|
|
1876
|
+
assert!(query.contains("X-Amz-Credential=source-access-key%2F"));
|
|
1877
|
+
assert!(query.contains("X-Amz-Security-Token=source-session-token"));
|
|
1878
|
+
assert!(!query.contains("session-access-key"));
|
|
1879
|
+
assert!(!query.contains("x-amz-s3session-token"));
|
|
1880
|
+
assert_eq!(transport.requests().len(), requests.len());
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
#[tokio::test]
|
|
1884
|
+
async fn test_s3_express_upload_part_copy_uses_iam_credentials() {
|
|
1885
|
+
let source_content_length = 5 * 1024 * 1024 + 1;
|
|
1886
|
+
let transport = S3ExpressMockTransport::new(CreateSessionOutcome::Success)
|
|
1887
|
+
.with_source_content_length(source_content_length);
|
|
1888
|
+
let op = s3_express_operator(transport.clone());
|
|
1889
|
+
|
|
1890
|
+
op.copy_with("source", "target")
|
|
1891
|
+
.chunk(5 * 1024 * 1024)
|
|
1892
|
+
.source_content_length_hint(source_content_length)
|
|
1893
|
+
.await
|
|
1894
|
+
.expect("multipart copy must succeed");
|
|
1895
|
+
|
|
1896
|
+
let requests = transport.requests();
|
|
1897
|
+
let part_copies = requests
|
|
1898
|
+
.iter()
|
|
1899
|
+
.filter(|request| {
|
|
1900
|
+
request.headers.contains_key(constants::X_AMZ_COPY_SOURCE)
|
|
1901
|
+
&& request
|
|
1902
|
+
.uri
|
|
1903
|
+
.query()
|
|
1904
|
+
.is_some_and(|query| query.contains("partNumber="))
|
|
1905
|
+
})
|
|
1906
|
+
.collect::<Vec<_>>();
|
|
1907
|
+
assert_eq!(part_copies.len(), 2);
|
|
1908
|
+
for request in part_copies {
|
|
1909
|
+
assert_eq!(
|
|
1910
|
+
header(request, "x-amz-security-token"),
|
|
1911
|
+
"source-session-token"
|
|
1912
|
+
);
|
|
1913
|
+
assert!(!request.headers.contains_key("x-amz-s3session-token"));
|
|
1914
|
+
assert!(header(request, "authorization").contains("source-access-key/"));
|
|
1915
|
+
assert!(
|
|
1916
|
+
!request
|
|
1917
|
+
.headers
|
|
1918
|
+
.contains_key(constants::X_AMZ_COPY_SOURCE_IF_MATCH)
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
assert_eq!(
|
|
1922
|
+
requests
|
|
1923
|
+
.iter()
|
|
1924
|
+
.filter(|request| request.uri.query() == Some("session"))
|
|
1925
|
+
.count(),
|
|
1926
|
+
1,
|
|
1927
|
+
"session operations in multipart copy must share one session"
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
#[tokio::test]
|
|
1932
|
+
async fn test_s3_multipart_copy_without_hint_pins_every_part() {
|
|
1933
|
+
let source_content_length = 5 * 1024 * 1024 + 1;
|
|
1934
|
+
let transport = S3ExpressMockTransport::new(CreateSessionOutcome::Success)
|
|
1935
|
+
.with_source_content_length(source_content_length);
|
|
1936
|
+
let op = s3_general_operator(transport.clone());
|
|
1937
|
+
|
|
1938
|
+
op.copy_with("source", "target")
|
|
1939
|
+
.chunk(5 * 1024 * 1024)
|
|
1940
|
+
.await
|
|
1941
|
+
.expect("multipart copy must succeed");
|
|
1942
|
+
|
|
1943
|
+
let requests = transport.requests();
|
|
1944
|
+
let part_copies = requests
|
|
1945
|
+
.iter()
|
|
1946
|
+
.filter(|request| {
|
|
1947
|
+
request.headers.contains_key(constants::X_AMZ_COPY_SOURCE)
|
|
1948
|
+
&& request
|
|
1949
|
+
.uri
|
|
1950
|
+
.query()
|
|
1951
|
+
.is_some_and(|query| query.contains("partNumber="))
|
|
1952
|
+
})
|
|
1953
|
+
.collect::<Vec<_>>();
|
|
1954
|
+
assert_eq!(part_copies.len(), 2);
|
|
1955
|
+
for request in part_copies {
|
|
1956
|
+
assert_eq!(
|
|
1957
|
+
header(request, constants::X_AMZ_COPY_SOURCE_IF_MATCH),
|
|
1958
|
+
"\"source-etag\""
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
#[tokio::test]
|
|
1964
|
+
async fn test_s3_express_writer_copy_from_uses_iam_credentials() {
|
|
1965
|
+
let transport = S3ExpressMockTransport::new(CreateSessionOutcome::Success);
|
|
1966
|
+
let op = s3_express_operator(transport.clone());
|
|
1967
|
+
|
|
1968
|
+
let mut writer = op.writer("target").await.expect("writer must open");
|
|
1969
|
+
writer
|
|
1970
|
+
.copy_from("source", 0..5 * 1024 * 1024_u64)
|
|
1971
|
+
.await
|
|
1972
|
+
.expect("writer copy must succeed");
|
|
1973
|
+
writer.close().await.expect("writer must close");
|
|
1974
|
+
|
|
1975
|
+
let requests = transport.requests();
|
|
1976
|
+
let part_copy = requests
|
|
1977
|
+
.iter()
|
|
1978
|
+
.find(|request| {
|
|
1979
|
+
request.headers.contains_key(constants::X_AMZ_COPY_SOURCE)
|
|
1980
|
+
&& request
|
|
1981
|
+
.uri
|
|
1982
|
+
.query()
|
|
1983
|
+
.is_some_and(|query| query.contains("partNumber="))
|
|
1984
|
+
})
|
|
1985
|
+
.expect("UploadPartCopy request must be captured");
|
|
1986
|
+
assert_eq!(
|
|
1987
|
+
header(part_copy, "x-amz-security-token"),
|
|
1988
|
+
"source-session-token"
|
|
1989
|
+
);
|
|
1990
|
+
assert!(!part_copy.headers.contains_key("x-amz-s3session-token"));
|
|
1991
|
+
assert!(header(part_copy, "authorization").contains("source-access-key/"));
|
|
1992
|
+
assert_eq!(
|
|
1993
|
+
requests
|
|
1994
|
+
.iter()
|
|
1995
|
+
.filter(|request| request.uri.query() == Some("session"))
|
|
1996
|
+
.count(),
|
|
1997
|
+
1,
|
|
1998
|
+
"writer multipart operations must share one session"
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
#[tokio::test]
|
|
2003
|
+
async fn test_s3_express_create_session_error_mapping() {
|
|
2004
|
+
for (outcome, kind, temporary) in [
|
|
2005
|
+
(
|
|
2006
|
+
CreateSessionOutcome::Status(StatusCode::FORBIDDEN),
|
|
2007
|
+
ErrorKind::PermissionDenied,
|
|
2008
|
+
false,
|
|
2009
|
+
),
|
|
2010
|
+
(
|
|
2011
|
+
CreateSessionOutcome::Status(StatusCode::TOO_MANY_REQUESTS),
|
|
2012
|
+
ErrorKind::RateLimited,
|
|
2013
|
+
true,
|
|
2014
|
+
),
|
|
2015
|
+
(
|
|
2016
|
+
CreateSessionOutcome::Status(StatusCode::INTERNAL_SERVER_ERROR),
|
|
2017
|
+
ErrorKind::Unexpected,
|
|
2018
|
+
true,
|
|
2019
|
+
),
|
|
2020
|
+
(
|
|
2021
|
+
CreateSessionOutcome::TemporaryTransportError,
|
|
2022
|
+
ErrorKind::Unexpected,
|
|
2023
|
+
true,
|
|
2024
|
+
),
|
|
2025
|
+
(
|
|
2026
|
+
CreateSessionOutcome::Body(
|
|
2027
|
+
"<CreateSessionResult><Credentials>\
|
|
2028
|
+
<SecretAccessKey>session-secret-key</SecretAccessKey>\
|
|
2029
|
+
<AccessKeyId>session-access-key</AccessKeyId>\
|
|
2030
|
+
<Expiration>2099-01-01T00:05:00Z</Expiration>\
|
|
2031
|
+
</Credentials></CreateSessionResult>",
|
|
2032
|
+
),
|
|
2033
|
+
ErrorKind::Unexpected,
|
|
2034
|
+
false,
|
|
2035
|
+
),
|
|
2036
|
+
(
|
|
2037
|
+
CreateSessionOutcome::Body(
|
|
2038
|
+
"<CreateSessionResult><Credentials>\
|
|
2039
|
+
<SessionToken>session-token</SessionToken>\
|
|
2040
|
+
<SecretAccessKey>session-secret-key</SecretAccessKey>\
|
|
2041
|
+
<AccessKeyId>session-access-key</AccessKeyId>\
|
|
2042
|
+
<Expiration>2020-01-01T00:05:00Z</Expiration>\
|
|
2043
|
+
</Credentials></CreateSessionResult>",
|
|
2044
|
+
),
|
|
2045
|
+
ErrorKind::Unexpected,
|
|
2046
|
+
false,
|
|
2047
|
+
),
|
|
2048
|
+
] {
|
|
2049
|
+
let transport = S3ExpressMockTransport::new(outcome);
|
|
2050
|
+
let op = s3_express_operator(transport);
|
|
2051
|
+
let err = op
|
|
2052
|
+
.write("test", "body")
|
|
2053
|
+
.await
|
|
2054
|
+
.expect_err("CreateSession failure must reach the caller");
|
|
2055
|
+
assert_eq!(err.kind(), kind);
|
|
2056
|
+
assert_eq!(err.is_temporary(), temporary);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
#[test]
|
|
2061
|
+
fn test_profile() {
|
|
2062
|
+
let builder = S3Builder::default().profile("selected");
|
|
2063
|
+
assert_eq!(builder.config.profile.as_deref(), Some("selected"));
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
#[test]
|
|
2067
|
+
fn test_write_can_copy_from_capability() {
|
|
2068
|
+
let backend = S3Builder::default()
|
|
2069
|
+
.bucket("test")
|
|
2070
|
+
.region("us-east-1")
|
|
2071
|
+
.endpoint("http://127.0.0.1:9000")
|
|
2072
|
+
.build()
|
|
2073
|
+
.unwrap();
|
|
2074
|
+
assert!(backend.capability().write_can_copy_from);
|
|
2075
|
+
|
|
2076
|
+
let backend = S3Builder::default()
|
|
2077
|
+
.bucket("test")
|
|
2078
|
+
.region("us-east-1")
|
|
2079
|
+
.endpoint("http://127.0.0.1:9000")
|
|
2080
|
+
.checksum_algorithm("crc32c")
|
|
2081
|
+
.build()
|
|
2082
|
+
.unwrap();
|
|
2083
|
+
assert!(!backend.capability().write_can_copy_from);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
1237
2086
|
#[test]
|
|
1238
2087
|
fn test_is_valid_bucket() {
|
|
1239
2088
|
let bucket_cases = vec![
|
|
@@ -1287,6 +2136,98 @@ mod tests {
|
|
|
1287
2136
|
}
|
|
1288
2137
|
}
|
|
1289
2138
|
|
|
2139
|
+
#[test]
|
|
2140
|
+
fn test_resolve_s3_express_config() {
|
|
2141
|
+
let directory_bucket = "example--usw2-az1--x-s3";
|
|
2142
|
+
|
|
2143
|
+
for endpoint in [
|
|
2144
|
+
None,
|
|
2145
|
+
Some("s3.amazonaws.com"),
|
|
2146
|
+
Some("https://s3.us-west-2.amazonaws.com"),
|
|
2147
|
+
Some("https://s3.us-west-2.amazonaws.com:443"),
|
|
2148
|
+
Some("https://example--usw2-az1--x-s3.s3express-usw2-az1.us-west-2.amazonaws.com"),
|
|
2149
|
+
] {
|
|
2150
|
+
let mut builder = S3Builder::default().bucket(directory_bucket);
|
|
2151
|
+
if let Some(endpoint) = endpoint {
|
|
2152
|
+
builder = builder.endpoint(endpoint);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
let config = S3Builder::resolve_s3_express_config(&builder.config, "us-west-2")
|
|
2156
|
+
.expect("valid directory bucket must resolve")
|
|
2157
|
+
.expect("AWS directory bucket must enable session authentication");
|
|
2158
|
+
assert_eq!(
|
|
2159
|
+
config.endpoint(),
|
|
2160
|
+
"https://example--usw2-az1--x-s3.s3express-usw2-az1.us-west-2.amazonaws.com"
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
#[test]
|
|
2166
|
+
fn test_s3_express_classification_does_not_change_compatible_services() {
|
|
2167
|
+
let builder = S3Builder::default()
|
|
2168
|
+
.bucket("example--usw2-az1--x-s3")
|
|
2169
|
+
.endpoint("https://s3.example.com");
|
|
2170
|
+
|
|
2171
|
+
assert!(
|
|
2172
|
+
S3Builder::resolve_s3_express_config(&builder.config, "us-west-2")
|
|
2173
|
+
.expect("custom endpoint classification must succeed")
|
|
2174
|
+
.is_none()
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
#[test]
|
|
2179
|
+
fn test_s3_express_capabilities() {
|
|
2180
|
+
let backend = S3Builder::default()
|
|
2181
|
+
.bucket("example--usw2-az1--x-s3")
|
|
2182
|
+
.region("us-west-2")
|
|
2183
|
+
.disable_config_load()
|
|
2184
|
+
.disable_ec2_metadata()
|
|
2185
|
+
.build()
|
|
2186
|
+
.expect("S3 Express backend must build");
|
|
2187
|
+
let capability = backend.capability();
|
|
2188
|
+
|
|
2189
|
+
assert!(!capability.stat_with_version);
|
|
2190
|
+
assert!(!capability.read_with_version);
|
|
2191
|
+
assert!(!capability.write_with_content_encoding);
|
|
2192
|
+
assert!(!capability.delete_with_version);
|
|
2193
|
+
assert!(!capability.copy_with_source_version);
|
|
2194
|
+
assert!(!capability.restore);
|
|
2195
|
+
assert!(!capability.restore_with_version);
|
|
2196
|
+
assert!(!capability.restore_with_if_not_exists);
|
|
2197
|
+
assert!(!capability.list_with_start_after);
|
|
2198
|
+
assert!(!capability.list_with_recursive);
|
|
2199
|
+
assert!(!capability.list_with_versions);
|
|
2200
|
+
assert!(!capability.list_with_deleted);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
#[test]
|
|
2204
|
+
fn test_invalid_aws_directory_bucket_configuration_is_rejected() {
|
|
2205
|
+
let builder = S3Builder::default().bucket("example--usw2-az1--x-s3");
|
|
2206
|
+
|
|
2207
|
+
let err = S3Builder::resolve_s3_express_config(&builder.config, "us-east-1")
|
|
2208
|
+
.expect_err("zone and region mismatch must be rejected");
|
|
2209
|
+
assert_eq!(err.kind(), ErrorKind::ConfigInvalid);
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
#[test]
|
|
2213
|
+
fn test_unsupported_aws_directory_bucket_endpoints_are_rejected() {
|
|
2214
|
+
for endpoint in [
|
|
2215
|
+
"https://s3.dualstack.us-west-2.amazonaws.com",
|
|
2216
|
+
"https://example--usw2-az1--x-s3.s3express-usw2-az1.dualstack.us-west-2.amazonaws.com",
|
|
2217
|
+
"https://example--usw2-az1--x-s3.s3express-use1-az1.us-west-2.amazonaws.com",
|
|
2218
|
+
"https://s3.us-west-2.amazonaws.com/path",
|
|
2219
|
+
"https://s3.us-west-2.amazonaws.com.cn",
|
|
2220
|
+
] {
|
|
2221
|
+
let builder = S3Builder::default()
|
|
2222
|
+
.bucket("example--usw2-az1--x-s3")
|
|
2223
|
+
.endpoint(endpoint);
|
|
2224
|
+
|
|
2225
|
+
let err = S3Builder::resolve_s3_express_config(&builder.config, "us-west-2")
|
|
2226
|
+
.expect_err("unsupported AWS directory bucket endpoint must be rejected");
|
|
2227
|
+
assert_eq!(err.kind(), ErrorKind::ConfigInvalid, "endpoint: {endpoint}");
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
|
|
1290
2231
|
#[tokio::test]
|
|
1291
2232
|
async fn test_detect_region() {
|
|
1292
2233
|
let cases = vec![
|
|
@@ -1345,7 +2286,14 @@ mod tests {
|
|
|
1345
2286
|
.build()
|
|
1346
2287
|
.expect("build");
|
|
1347
2288
|
|
|
1348
|
-
let op = OpWrite::
|
|
2289
|
+
let (op, _) = OpWrite::from_options(
|
|
2290
|
+
&backend.capability(),
|
|
2291
|
+
options::WriteOptions {
|
|
2292
|
+
content_type: Some("application/json".to_owned()),
|
|
2293
|
+
..Default::default()
|
|
2294
|
+
},
|
|
2295
|
+
)
|
|
2296
|
+
.unwrap();
|
|
1349
2297
|
let args = OpPresign::new(op, Duration::from_secs(3600));
|
|
1350
2298
|
let ctx = OperationContext::new();
|
|
1351
2299
|
let presigned = backend
|
|
@@ -1371,7 +2319,11 @@ mod tests {
|
|
|
1371
2319
|
.build()
|
|
1372
2320
|
.expect("build");
|
|
1373
2321
|
|
|
1374
|
-
let op =
|
|
2322
|
+
let op: OpStat = options::StatOptions {
|
|
2323
|
+
version: Some("a+b/c=d%25&e".to_owned()),
|
|
2324
|
+
..Default::default()
|
|
2325
|
+
}
|
|
2326
|
+
.into();
|
|
1375
2327
|
let args = OpPresign::new(op, Duration::from_secs(3600));
|
|
1376
2328
|
let ctx = OperationContext::new();
|
|
1377
2329
|
let presigned = backend
|
|
@@ -1397,7 +2349,11 @@ mod tests {
|
|
|
1397
2349
|
.build()
|
|
1398
2350
|
.expect("build");
|
|
1399
2351
|
|
|
1400
|
-
let op =
|
|
2352
|
+
let (_, op, _) = options::ReadOptions {
|
|
2353
|
+
version: Some("a+b/c=d%25&e".to_owned()),
|
|
2354
|
+
..Default::default()
|
|
2355
|
+
}
|
|
2356
|
+
.into();
|
|
1401
2357
|
let args = OpPresign::new(
|
|
1402
2358
|
PresignOperation::Read(BytesRange::default(), op),
|
|
1403
2359
|
Duration::from_secs(3600),
|