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
|
@@ -15,13 +15,18 @@
|
|
|
15
15
|
// specific language governing permissions and limitations
|
|
16
16
|
// under the License.
|
|
17
17
|
|
|
18
|
+
use asyncband::mutex::Mutex;
|
|
19
|
+
use asyncband::once::OnceCell;
|
|
18
20
|
use bytes::Buf;
|
|
19
21
|
use bytes::Bytes;
|
|
20
22
|
use http::Request;
|
|
21
23
|
use http::Response;
|
|
22
24
|
use http::header;
|
|
25
|
+
use log::debug;
|
|
23
26
|
use serde::{Deserialize, Serialize};
|
|
24
27
|
use std::fmt::Debug;
|
|
28
|
+
use std::sync::Arc;
|
|
29
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
25
30
|
|
|
26
31
|
use xet::xet_session::{XetDownloadStreamGroup, XetSession, XetSessionBuilder, XetUploadCommit};
|
|
27
32
|
|
|
@@ -154,12 +159,74 @@ pub(super) struct MixedCommitPayload {
|
|
|
154
159
|
|
|
155
160
|
// API response types
|
|
156
161
|
|
|
157
|
-
#[derive(Deserialize)]
|
|
162
|
+
#[derive(Deserialize, Clone)]
|
|
158
163
|
pub(super) struct XetFileResponse {
|
|
159
164
|
pub hash: String,
|
|
160
165
|
pub size: u64,
|
|
161
166
|
}
|
|
162
167
|
|
|
168
|
+
#[derive(Deserialize)]
|
|
169
|
+
pub(super) struct RepoInfoResponse {
|
|
170
|
+
pub id: String,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Response shape of HF's `xet-{read,write}-token` endpoint. Matches
|
|
174
|
+
/// `CasJWTInfo` in the vendored `xet` crate (`xet_client::hub_client::types`)
|
|
175
|
+
/// — the same three fields regardless of read or write token.
|
|
176
|
+
#[derive(Deserialize)]
|
|
177
|
+
#[serde(rename_all = "camelCase")]
|
|
178
|
+
struct XetTokenResponse {
|
|
179
|
+
cas_url: String,
|
|
180
|
+
exp: u64,
|
|
181
|
+
access_token: String,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// A cached CAS access token, so a new [`XetDownloadStreamGroup`] doesn't
|
|
185
|
+
/// have to fetch its own on every creation.
|
|
186
|
+
#[derive(Clone)]
|
|
187
|
+
struct XetToken {
|
|
188
|
+
cas_url: String,
|
|
189
|
+
access_token: String,
|
|
190
|
+
/// Unix timestamp (seconds) at which HF says this token actually
|
|
191
|
+
/// expires.
|
|
192
|
+
expires_at: u64,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// How much earlier than a token's real expiry to refresh it ourselves.
|
|
196
|
+
/// Larger than the `xet` crate's own internal 30s buffer
|
|
197
|
+
/// (`xet_client::cas_client::auth::REFRESH_BUFFER_SEC`) so a group we just
|
|
198
|
+
/// built has headroom left, instead of immediately tripping that buffer and
|
|
199
|
+
/// refreshing again itself via its own unmocked, uncounted HTTP client.
|
|
200
|
+
const XET_TOKEN_REFRESH_BUFFER_SECS: u64 = 120;
|
|
201
|
+
|
|
202
|
+
/// Which CAS token scope to fetch/cache -- reads and writes are distinct HF
|
|
203
|
+
/// API token scopes, each with their own cache slot on [`HfCore`].
|
|
204
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
205
|
+
pub(super) enum XetTokenScope {
|
|
206
|
+
Read,
|
|
207
|
+
Write,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
impl XetTokenScope {
|
|
211
|
+
fn as_str(self) -> &'static str {
|
|
212
|
+
match self {
|
|
213
|
+
Self::Read => "read",
|
|
214
|
+
Self::Write => "write",
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// The `Operation` a fetch for this scope's token should be tagged
|
|
219
|
+
/// with, so a token-less writer hits `request()`'s local
|
|
220
|
+
/// `PermissionDenied` fast-path instead of an unauthenticated network
|
|
221
|
+
/// call.
|
|
222
|
+
fn operation(self) -> Operation {
|
|
223
|
+
match self {
|
|
224
|
+
Self::Read => Operation::Read,
|
|
225
|
+
Self::Write => Operation::Write,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
163
230
|
#[derive(serde::Deserialize, Debug)]
|
|
164
231
|
pub(super) struct CommitResponse {
|
|
165
232
|
#[allow(dead_code)]
|
|
@@ -200,23 +267,26 @@ impl PathInfo {
|
|
|
200
267
|
|
|
201
268
|
pub fn metadata(&self) -> Result<Metadata> {
|
|
202
269
|
let mode = self.entry_mode();
|
|
203
|
-
let mut meta =
|
|
270
|
+
let mut meta = match mode {
|
|
271
|
+
EntryMode::FILE => MetadataBuilder::file(self.size),
|
|
272
|
+
EntryMode::DIR => MetadataBuilder::dir(),
|
|
273
|
+
EntryMode::Unknown => MetadataBuilder::unknown(),
|
|
274
|
+
};
|
|
204
275
|
|
|
205
276
|
if let Some(commit_info) = self.last_commit.as_ref() {
|
|
206
|
-
meta.
|
|
277
|
+
meta.last_modified(commit_info.date.parse::<Timestamp>()?);
|
|
207
278
|
}
|
|
208
279
|
|
|
209
280
|
if mode == EntryMode::FILE {
|
|
210
|
-
meta.set_content_length(self.size);
|
|
211
281
|
// For buckets, oid may be None; for regular repos, prefer lfs.oid then oid
|
|
212
282
|
if let Some(lfs) = &self.lfs {
|
|
213
|
-
meta.
|
|
283
|
+
meta.etag(&lfs.oid);
|
|
214
284
|
} else if let Some(oid) = &self.oid {
|
|
215
|
-
meta.
|
|
285
|
+
meta.etag(oid);
|
|
216
286
|
}
|
|
217
287
|
}
|
|
218
288
|
|
|
219
|
-
Ok(meta)
|
|
289
|
+
Ok(meta.build())
|
|
220
290
|
}
|
|
221
291
|
}
|
|
222
292
|
|
|
@@ -244,6 +314,14 @@ pub struct HfCore {
|
|
|
244
314
|
pub endpoint: String,
|
|
245
315
|
pub xet_session: XetSession,
|
|
246
316
|
pub download_mode: HfDownloadMode,
|
|
317
|
+
canonical_repo: OnceCell<HfRepo>,
|
|
318
|
+
/// Cached CAS read token, shared by every `XetDownloadStreamGroup` this
|
|
319
|
+
/// core creates, so at most one `xet-read-token` request happens per
|
|
320
|
+
/// token lifetime instead of one per group (one per file read).
|
|
321
|
+
xet_read_token: Arc<Mutex<Option<XetToken>>>,
|
|
322
|
+
/// Same idea for `XetUploadCommit`'s `xet-write-token`. Separate slot:
|
|
323
|
+
/// read and write are distinct HF API token scopes.
|
|
324
|
+
xet_write_token: Arc<Mutex<Option<XetToken>>>,
|
|
247
325
|
}
|
|
248
326
|
|
|
249
327
|
impl Debug for HfCore {
|
|
@@ -277,6 +355,9 @@ impl HfCore {
|
|
|
277
355
|
endpoint,
|
|
278
356
|
xet_session,
|
|
279
357
|
download_mode,
|
|
358
|
+
canonical_repo: OnceCell::new(),
|
|
359
|
+
xet_read_token: Arc::new(Mutex::new(None)),
|
|
360
|
+
xet_write_token: Arc::new(Mutex::new(None)),
|
|
280
361
|
}
|
|
281
362
|
}
|
|
282
363
|
|
|
@@ -315,18 +396,24 @@ impl HfCore {
|
|
|
315
396
|
headers
|
|
316
397
|
}
|
|
317
398
|
|
|
318
|
-
/// Create a new XET upload commit with
|
|
319
|
-
///
|
|
320
|
-
///
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
399
|
+
/// Create a new XET upload commit, seeded with a cached CAS write token
|
|
400
|
+
/// so it doesn't have to fetch its own. Still creates a fresh commit
|
|
401
|
+
/// each call -- only the token (and its `/api` request) is reused; see
|
|
402
|
+
/// [`Self::xet_download_group`] for why the object itself isn't cached.
|
|
403
|
+
pub(super) async fn xet_upload_commit(
|
|
404
|
+
&self,
|
|
405
|
+
ctx: &OperationContext,
|
|
406
|
+
) -> Result<XetUploadCommit> {
|
|
407
|
+
let (token, refresh_url, refresh_headers) =
|
|
408
|
+
self.xet_auth(ctx, XetTokenScope::Write).await?;
|
|
324
409
|
self.xet_session
|
|
325
410
|
.new_upload_commit()
|
|
326
411
|
.map_err(|err| {
|
|
327
412
|
Error::new(ErrorKind::Unexpected, "failed to create xet upload commit")
|
|
328
413
|
.set_source(err)
|
|
329
414
|
})?
|
|
415
|
+
.with_endpoint(token.cas_url)
|
|
416
|
+
.with_token_info(token.access_token, token.expires_at)
|
|
330
417
|
.with_token_refresh_url(refresh_url, refresh_headers)
|
|
331
418
|
.build()
|
|
332
419
|
.await
|
|
@@ -336,12 +423,97 @@ impl HfCore {
|
|
|
336
423
|
})
|
|
337
424
|
}
|
|
338
425
|
|
|
339
|
-
///
|
|
340
|
-
///
|
|
341
|
-
///
|
|
342
|
-
|
|
343
|
-
|
|
426
|
+
/// A cached token for `scope`, plus the URL/headers to configure
|
|
427
|
+
/// automatic refresh on a builder that outlives this call. Shared by
|
|
428
|
+
/// [`Self::xet_upload_commit`] and [`Self::xet_download_group`], which
|
|
429
|
+
/// otherwise differ only in which builder they seed with it. Uses the
|
|
430
|
+
/// canonical repo id so the refresh URL doesn't depend on redirect
|
|
431
|
+
/// behavior (see [`Self::canonical_repo`]). Resolves the repo once and
|
|
432
|
+
/// hands it to [`Self::cached_xet_token`] so the token lookup doesn't
|
|
433
|
+
/// resolve it again.
|
|
434
|
+
async fn xet_auth(
|
|
435
|
+
&self,
|
|
436
|
+
ctx: &OperationContext,
|
|
437
|
+
scope: XetTokenScope,
|
|
438
|
+
) -> Result<(XetToken, String, http::HeaderMap)> {
|
|
439
|
+
let repo = self.canonical_repo(ctx).await?;
|
|
440
|
+
let token = self.cached_xet_token(ctx, &repo, scope).await?;
|
|
441
|
+
let refresh_url = repo.xet_token_url(&self.endpoint, scope);
|
|
344
442
|
let refresh_headers = self.xet_token_refresh_headers();
|
|
443
|
+
Ok((token, refresh_url, refresh_headers))
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/// Get a still-valid cached token for `scope` against the already
|
|
447
|
+
/// resolved `repo`, fetching and caching a fresh one if missing or
|
|
448
|
+
/// close to expiry. The lock is held across the refresh request so
|
|
449
|
+
/// concurrent callers single-flight onto one fetch; `repo` is resolved
|
|
450
|
+
/// by the caller beforehand so that lookup doesn't extend this
|
|
451
|
+
/// critical section.
|
|
452
|
+
///
|
|
453
|
+
/// On a clock failure, `now` falls back to `u64::MAX` so the token is
|
|
454
|
+
/// treated as stale (extra `/api` traffic) rather than `0`, which would
|
|
455
|
+
/// pin a stale token as valid forever.
|
|
456
|
+
async fn cached_xet_token(
|
|
457
|
+
&self,
|
|
458
|
+
ctx: &OperationContext,
|
|
459
|
+
repo: &HfRepo,
|
|
460
|
+
scope: XetTokenScope,
|
|
461
|
+
) -> Result<XetToken> {
|
|
462
|
+
let cache = match scope {
|
|
463
|
+
XetTokenScope::Read => &self.xet_read_token,
|
|
464
|
+
XetTokenScope::Write => &self.xet_write_token,
|
|
465
|
+
};
|
|
466
|
+
let mut cached = cache.lock().await;
|
|
467
|
+
|
|
468
|
+
let now = SystemTime::now()
|
|
469
|
+
.duration_since(UNIX_EPOCH)
|
|
470
|
+
.map(|d| d.as_secs())
|
|
471
|
+
.unwrap_or(u64::MAX);
|
|
472
|
+
if let Some(token) = cached.as_ref()
|
|
473
|
+
&& token.expires_at > now.saturating_add(XET_TOKEN_REFRESH_BUFFER_SECS)
|
|
474
|
+
{
|
|
475
|
+
return Ok(token.clone());
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
let url = repo.xet_token_url(&self.endpoint, scope);
|
|
479
|
+
let req = self
|
|
480
|
+
.request(http::Method::GET, &url, scope.operation(), "XetToken")?
|
|
481
|
+
.body(Buffer::new())
|
|
482
|
+
.map_err(new_request_build_error)?;
|
|
483
|
+
let resp = ctx.http_transport().fetch(req).await?;
|
|
484
|
+
if !resp.status().is_success() {
|
|
485
|
+
let (parts, _) = resp.into_parts();
|
|
486
|
+
return Err(parse_error(
|
|
487
|
+
ErrorContext::new(ServiceOperation("XetToken")),
|
|
488
|
+
parts,
|
|
489
|
+
));
|
|
490
|
+
}
|
|
491
|
+
let (_, mut body) = resp.into_parts();
|
|
492
|
+
let buffer = body.to_buffer().await?;
|
|
493
|
+
let info: XetTokenResponse =
|
|
494
|
+
serde_json::from_reader(buffer.reader()).map_err(new_json_deserialize_error)?;
|
|
495
|
+
let fresh = XetToken {
|
|
496
|
+
cas_url: info.cas_url,
|
|
497
|
+
access_token: info.access_token,
|
|
498
|
+
expires_at: info.exp,
|
|
499
|
+
};
|
|
500
|
+
*cached = Some(fresh.clone());
|
|
501
|
+
Ok(fresh)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/// Create a new XET download stream group, seeded with a cached CAS read
|
|
505
|
+
/// token so it doesn't have to fetch its own. Called once per `HfReader`
|
|
506
|
+
/// (see `xet_group` in reader.rs) rather than cached on `HfCore`: its
|
|
507
|
+
/// internal progress tracking (`GroupProgress.items`) has no eviction, so
|
|
508
|
+
/// sharing one group across every reader on an `Operator` would grow it
|
|
509
|
+
/// without bound. Scoping to a reader's lifetime keeps that growth
|
|
510
|
+
/// bounded by the reader instead. Same reasoning applies to
|
|
511
|
+
/// `xet_upload_commit`'s commit objects.
|
|
512
|
+
pub(super) async fn xet_download_group(
|
|
513
|
+
&self,
|
|
514
|
+
ctx: &OperationContext,
|
|
515
|
+
) -> Result<XetDownloadStreamGroup> {
|
|
516
|
+
let (token, refresh_url, refresh_headers) = self.xet_auth(ctx, XetTokenScope::Read).await?;
|
|
345
517
|
self.xet_session
|
|
346
518
|
.new_download_stream_group()
|
|
347
519
|
.map_err(|err| {
|
|
@@ -351,6 +523,8 @@ impl HfCore {
|
|
|
351
523
|
)
|
|
352
524
|
.set_source(err)
|
|
353
525
|
})?
|
|
526
|
+
.with_endpoint(token.cas_url)
|
|
527
|
+
.with_token_info(token.access_token, token.expires_at)
|
|
354
528
|
.with_token_refresh_url(refresh_url, refresh_headers)
|
|
355
529
|
.build()
|
|
356
530
|
.await
|
|
@@ -374,6 +548,12 @@ impl HfCore {
|
|
|
374
548
|
op: Operation,
|
|
375
549
|
service_operation: &'static str,
|
|
376
550
|
) -> Result<http::request::Builder> {
|
|
551
|
+
// Every outbound HF/CAS request is built here, so this is the one
|
|
552
|
+
// place that can show what's actually being queried -- enable with
|
|
553
|
+
// `RUST_LOG=opendal_service_hf::core=debug`.
|
|
554
|
+
debug!(
|
|
555
|
+
"hf request: service_operation={service_operation} operation={op} method={method} url={url}"
|
|
556
|
+
);
|
|
377
557
|
let mut req = Request::builder()
|
|
378
558
|
.method(method)
|
|
379
559
|
.uri(url)
|
|
@@ -396,9 +576,58 @@ impl HfCore {
|
|
|
396
576
|
Ok(req)
|
|
397
577
|
}
|
|
398
578
|
|
|
399
|
-
///
|
|
400
|
-
|
|
401
|
-
|
|
579
|
+
/// Return the repo handle with the server's canonical repo id, resolving
|
|
580
|
+
/// and caching it on first use.
|
|
581
|
+
///
|
|
582
|
+
/// HF resolves repo ids case-insensitively but replies 307 to any request
|
|
583
|
+
/// for a non-canonically-cased id (e.g. `user/repo` for `user/Repo`).
|
|
584
|
+
/// The transport cannot replay bodied requests (commit, paths-info)
|
|
585
|
+
/// through a redirect. Building every URL from the canonical id avoids
|
|
586
|
+
/// depending on redirect behavior entirely, removing the inconsistency across
|
|
587
|
+
/// operations.
|
|
588
|
+
pub(super) async fn canonical_repo(&self, ctx: &OperationContext) -> Result<HfRepo> {
|
|
589
|
+
// Buckets are addressed by opaque ids
|
|
590
|
+
if self.repo.is_bucket() {
|
|
591
|
+
return Ok(self.repo.clone());
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
self.canonical_repo
|
|
595
|
+
.get_or_try_init(|| async {
|
|
596
|
+
let url = format!(
|
|
597
|
+
"{}/api/{}/{}",
|
|
598
|
+
self.endpoint,
|
|
599
|
+
self.repo.repo_type.as_plural_str(),
|
|
600
|
+
self.repo.repo_id,
|
|
601
|
+
);
|
|
602
|
+
let req = self
|
|
603
|
+
.request(http::Method::GET, &url, Operation::Stat, "RepoInfo")?
|
|
604
|
+
.body(Buffer::new())
|
|
605
|
+
.map_err(new_request_build_error)?;
|
|
606
|
+
let resp = ctx.http_transport().fetch(req).await?;
|
|
607
|
+
if !resp.status().is_success() {
|
|
608
|
+
let (parts, _) = resp.into_parts();
|
|
609
|
+
return Err(parse_error(
|
|
610
|
+
ErrorContext::new(ServiceOperation("RepoInfo")),
|
|
611
|
+
parts,
|
|
612
|
+
));
|
|
613
|
+
}
|
|
614
|
+
let (_, mut body) = resp.into_parts();
|
|
615
|
+
let buffer = body.to_buffer().await?;
|
|
616
|
+
let info: RepoInfoResponse =
|
|
617
|
+
serde_json::from_reader(buffer.reader()).map_err(new_json_deserialize_error)?;
|
|
618
|
+
|
|
619
|
+
let mut repo = self.repo.clone();
|
|
620
|
+
repo.repo_id = info.id;
|
|
621
|
+
Ok(repo)
|
|
622
|
+
})
|
|
623
|
+
.await
|
|
624
|
+
.cloned()
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/// Build an [`HfUri`] for the given operator-relative path, using the
|
|
628
|
+
/// canonical repo id.
|
|
629
|
+
pub(super) async fn canonical_uri(&self, ctx: &OperationContext, path: &str) -> Result<HfUri> {
|
|
630
|
+
Ok(self.canonical_repo(ctx).await?.uri(&self.root, path))
|
|
402
631
|
}
|
|
403
632
|
|
|
404
633
|
/// Convert an operator-relative path to a repo-absolute path
|
|
@@ -409,43 +638,8 @@ impl HfCore {
|
|
|
409
638
|
.to_string()
|
|
410
639
|
}
|
|
411
640
|
|
|
412
|
-
/// Send a request and return the successful streaming response or a parsed error.
|
|
413
|
-
///
|
|
414
|
-
/// Uses `fetch` so error response bodies are never read into memory —
|
|
415
|
-
/// `parse_error` reads only response headers.
|
|
416
|
-
async fn send(
|
|
417
|
-
&self,
|
|
418
|
-
ctx: &OperationContext,
|
|
419
|
-
req: Request<Buffer>,
|
|
420
|
-
) -> Result<Response<HttpBody>> {
|
|
421
|
-
let resp = ctx.http_transport().fetch(req).await?;
|
|
422
|
-
let (parts, body) = resp.into_parts();
|
|
423
|
-
if parts.status.is_success() {
|
|
424
|
-
Ok(Response::from_parts(parts, body))
|
|
425
|
-
} else {
|
|
426
|
-
// Drop the streaming body without reading it.
|
|
427
|
-
Err(parse_error(parts))
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/// Send a request, check for success, and deserialize the JSON response.
|
|
432
|
-
///
|
|
433
|
-
/// Returns the response parts (status, headers, etc.) alongside the
|
|
434
|
-
/// deserialized body so callers can inspect headers when needed.
|
|
435
|
-
pub(super) async fn send_parse<T: serde::de::DeserializeOwned>(
|
|
436
|
-
&self,
|
|
437
|
-
ctx: &OperationContext,
|
|
438
|
-
req: Request<Buffer>,
|
|
439
|
-
) -> Result<(http::response::Parts, T)> {
|
|
440
|
-
let (parts, mut body) = self.send(ctx, req).await?.into_parts();
|
|
441
|
-
let buffer = body.to_buffer().await?;
|
|
442
|
-
let parsed =
|
|
443
|
-
serde_json::from_reader(buffer.reader()).map_err(new_json_deserialize_error)?;
|
|
444
|
-
Ok((parts, parsed))
|
|
445
|
-
}
|
|
446
|
-
|
|
447
641
|
pub(super) async fn path_info(&self, ctx: &OperationContext, path: &str) -> Result<PathInfo> {
|
|
448
|
-
let uri = self.
|
|
642
|
+
let uri = self.canonical_uri(ctx, path).await?;
|
|
449
643
|
let url = uri.paths_info_url(&self.endpoint);
|
|
450
644
|
let form_body = format!("paths={}&expand=True", percent_encode_path(&uri.path));
|
|
451
645
|
|
|
@@ -454,7 +648,18 @@ impl HfCore {
|
|
|
454
648
|
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
|
|
455
649
|
.body(Buffer::from(Bytes::from(form_body)))
|
|
456
650
|
.map_err(new_request_build_error)?;
|
|
457
|
-
let
|
|
651
|
+
let resp = ctx.http_transport().fetch(req).await?;
|
|
652
|
+
if !resp.status().is_success() {
|
|
653
|
+
let (parts, _) = resp.into_parts();
|
|
654
|
+
return Err(parse_error(
|
|
655
|
+
ErrorContext::new(ServiceOperation("PathInfo")),
|
|
656
|
+
parts,
|
|
657
|
+
));
|
|
658
|
+
}
|
|
659
|
+
let (_, mut body) = resp.into_parts();
|
|
660
|
+
let buffer = body.to_buffer().await?;
|
|
661
|
+
let mut files: Vec<PathInfo> =
|
|
662
|
+
serde_json::from_reader(buffer.reader()).map_err(new_json_deserialize_error)?;
|
|
458
663
|
|
|
459
664
|
// NOTE: if the file is not found, the server will return 200 with an empty array
|
|
460
665
|
if files.is_empty() {
|
|
@@ -468,7 +673,9 @@ impl HfCore {
|
|
|
468
673
|
///
|
|
469
674
|
/// In `Xet` mode adds `Accept: application/vnd.xet-fileinfo+json` so the
|
|
470
675
|
/// server returns XET metadata instead of redirecting; in `Http` mode the
|
|
471
|
-
/// redirect is followed and the file bytes are streamed directly.
|
|
676
|
+
/// redirect is followed and the file bytes are streamed directly. Either
|
|
677
|
+
/// way, the response may carry an `x-xet-hash` header -- interpreting
|
|
678
|
+
/// that is up to the caller, not this method's concern.
|
|
472
679
|
pub(super) async fn resolve(
|
|
473
680
|
&self,
|
|
474
681
|
ctx: &OperationContext,
|
|
@@ -476,7 +683,7 @@ impl HfCore {
|
|
|
476
683
|
range: BytesRange,
|
|
477
684
|
mode: HfDownloadMode,
|
|
478
685
|
) -> Result<Response<HttpBody>> {
|
|
479
|
-
let uri = self.
|
|
686
|
+
let uri = self.canonical_uri(ctx, path).await?;
|
|
480
687
|
let url = uri.resolve_url(&self.endpoint, self.repo.revision());
|
|
481
688
|
|
|
482
689
|
let mut req = self.request(http::Method::GET, &url, Operation::Read, "Resolve")?;
|
|
@@ -498,7 +705,7 @@ impl HfCore {
|
|
|
498
705
|
// only response headers, so there is no need to buffer the body
|
|
499
706
|
// (which may be a large HTML error page).
|
|
500
707
|
let (parts, _) = resp.into_parts();
|
|
501
|
-
let mut err = parse_error(parts);
|
|
708
|
+
let mut err = parse_error(ErrorContext::new(ServiceOperation("Resolve")), parts);
|
|
502
709
|
if status == http::StatusCode::NOT_FOUND && self.path_info(ctx, path).await.is_ok() {
|
|
503
710
|
err = err.set_temporary();
|
|
504
711
|
}
|
|
@@ -519,7 +726,10 @@ impl HfCore {
|
|
|
519
726
|
deleted_files: Vec<DeletedFile>,
|
|
520
727
|
deleted_folders: Vec<DeletedFolder>,
|
|
521
728
|
) -> Result<CommitResponse> {
|
|
522
|
-
let url = self
|
|
729
|
+
let url = self
|
|
730
|
+
.canonical_repo(ctx)
|
|
731
|
+
.await?
|
|
732
|
+
.git_commit_url(&self.endpoint);
|
|
523
733
|
|
|
524
734
|
let payload = MixedCommitPayload {
|
|
525
735
|
summary: "Commit via OpenDAL".to_string(),
|
|
@@ -538,8 +748,17 @@ impl HfCore {
|
|
|
538
748
|
.body(Buffer::from(json_body))
|
|
539
749
|
.map_err(new_request_build_error)?;
|
|
540
750
|
|
|
541
|
-
let
|
|
542
|
-
|
|
751
|
+
let resp = ctx.http_transport().fetch(req).await?;
|
|
752
|
+
if !resp.status().is_success() {
|
|
753
|
+
let (parts, _) = resp.into_parts();
|
|
754
|
+
return Err(parse_error(
|
|
755
|
+
ErrorContext::new(ServiceOperation("CommitGit")),
|
|
756
|
+
parts,
|
|
757
|
+
));
|
|
758
|
+
}
|
|
759
|
+
let (_, mut body) = resp.into_parts();
|
|
760
|
+
let buffer = body.to_buffer().await?;
|
|
761
|
+
serde_json::from_reader(buffer.reader()).map_err(new_json_deserialize_error)
|
|
543
762
|
}
|
|
544
763
|
|
|
545
764
|
/// Commit file changes to a bucket repo via the NDJSON batch API.
|
|
@@ -573,7 +792,14 @@ impl HfCore {
|
|
|
573
792
|
.body(Buffer::from(Bytes::from(body)))
|
|
574
793
|
.map_err(new_request_build_error)?;
|
|
575
794
|
|
|
576
|
-
|
|
795
|
+
let resp = ctx.http_transport().fetch(req).await?;
|
|
796
|
+
if !resp.status().is_success() {
|
|
797
|
+
let (parts, _) = resp.into_parts();
|
|
798
|
+
return Err(parse_error(
|
|
799
|
+
ErrorContext::new(ServiceOperation("CommitBucket")),
|
|
800
|
+
parts,
|
|
801
|
+
));
|
|
802
|
+
}
|
|
577
803
|
Ok(())
|
|
578
804
|
}
|
|
579
805
|
}
|
|
@@ -590,6 +816,21 @@ pub(crate) mod test_utils {
|
|
|
590
816
|
pub(crate) struct MockHttpTransport {
|
|
591
817
|
url: Arc<Mutex<Option<String>>>,
|
|
592
818
|
body: Arc<Mutex<Option<String>>>,
|
|
819
|
+
request_count: Arc<Mutex<usize>>,
|
|
820
|
+
/// `exp` returned by the mocked `xet-{read,write}-token` endpoint.
|
|
821
|
+
xet_token_expires_at: Arc<Mutex<u64>>,
|
|
822
|
+
/// Number of upcoming `fetch` calls that must fail with a 500
|
|
823
|
+
/// before resuming normal responses.
|
|
824
|
+
fail_next: Arc<Mutex<usize>>,
|
|
825
|
+
/// When set, a `/resolve/` request carries `x-xet-hash` and the
|
|
826
|
+
/// mocked [`XetFileResponse`] body instead of plain bytes, so tests
|
|
827
|
+
/// can exercise the XET classification path without real network.
|
|
828
|
+
xet_file: Arc<Mutex<Option<XetFileResponse>>>,
|
|
829
|
+
/// `Range` header of the most recent XET-classifying `/resolve/`
|
|
830
|
+
/// request (one that hit the `xet_file` branch above), so tests can
|
|
831
|
+
/// tell which racing caller's range a single-flighted classification
|
|
832
|
+
/// actually sent.
|
|
833
|
+
classify_range_header: Arc<Mutex<Option<String>>>,
|
|
593
834
|
}
|
|
594
835
|
|
|
595
836
|
impl MockHttpTransport {
|
|
@@ -597,9 +838,36 @@ pub(crate) mod test_utils {
|
|
|
597
838
|
Self {
|
|
598
839
|
url: Arc::new(Mutex::new(None)),
|
|
599
840
|
body: Arc::new(Mutex::new(None)),
|
|
841
|
+
request_count: Arc::new(Mutex::new(0)),
|
|
842
|
+
xet_token_expires_at: Arc::new(Mutex::new(u64::MAX)),
|
|
843
|
+
fail_next: Arc::new(Mutex::new(0)),
|
|
844
|
+
xet_file: Arc::new(Mutex::new(None)),
|
|
845
|
+
classify_range_header: Arc::new(Mutex::new(None)),
|
|
600
846
|
}
|
|
601
847
|
}
|
|
602
848
|
|
|
849
|
+
/// Controls the `exp` field returned by the mocked
|
|
850
|
+
/// `xet-{read,write}-token` endpoint for subsequent requests.
|
|
851
|
+
pub(crate) fn set_xet_token_expires_at(&self, expires_at: u64) {
|
|
852
|
+
*self.xet_token_expires_at.lock().unwrap() = expires_at;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/// Makes the next `n` calls to `fetch` return a 500 response instead
|
|
856
|
+
/// of their normal mocked payload.
|
|
857
|
+
pub(crate) fn fail_next_requests(&self, n: usize) {
|
|
858
|
+
*self.fail_next.lock().unwrap() = n;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/// Makes every subsequent `/resolve/` request report the path as
|
|
862
|
+
/// XET-backed with the given hash/size, via `x-xet-hash` and a
|
|
863
|
+
/// mocked [`XetFileResponse`] body.
|
|
864
|
+
pub(crate) fn set_xet_backed(&self, hash: &str, size: u64) {
|
|
865
|
+
*self.xet_file.lock().unwrap() = Some(XetFileResponse {
|
|
866
|
+
hash: hash.to_string(),
|
|
867
|
+
size,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
603
871
|
pub(crate) fn get_captured_url(&self) -> String {
|
|
604
872
|
self.url.lock().unwrap().clone().unwrap()
|
|
605
873
|
}
|
|
@@ -607,20 +875,77 @@ pub(crate) mod test_utils {
|
|
|
607
875
|
pub(crate) fn get_captured_body(&self) -> String {
|
|
608
876
|
self.body.lock().unwrap().clone().unwrap_or_default()
|
|
609
877
|
}
|
|
878
|
+
|
|
879
|
+
pub(crate) fn request_count(&self) -> usize {
|
|
880
|
+
*self.request_count.lock().unwrap()
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/// `Range` header of the most recent XET-classifying `/resolve/`
|
|
884
|
+
/// request, or `None` if that range was full (no `Range` header) or
|
|
885
|
+
/// no such request has happened yet.
|
|
886
|
+
pub(crate) fn get_captured_classify_range_header(&self) -> Option<String> {
|
|
887
|
+
self.classify_range_header.lock().unwrap().clone()
|
|
888
|
+
}
|
|
610
889
|
}
|
|
611
890
|
|
|
612
891
|
impl HttpTransport for MockHttpTransport {
|
|
613
892
|
async fn fetch(&self, req: Request<Buffer>) -> Result<Response<HttpBody>> {
|
|
614
|
-
|
|
893
|
+
// Yield once so concurrent callers (`futures::join!` in the
|
|
894
|
+
// single-flight tests) actually interleave instead of running to
|
|
895
|
+
// completion one at a time -- without this, a fully synchronous
|
|
896
|
+
// mock never gives the executor a chance to poll a second caller
|
|
897
|
+
// while the first is still "in flight", so tests asserting on
|
|
898
|
+
// single-flighted locking would pass even if the locking were
|
|
899
|
+
// broken.
|
|
900
|
+
tokio::task::yield_now().await;
|
|
901
|
+
*self.request_count.lock().unwrap() += 1;
|
|
902
|
+
|
|
903
|
+
{
|
|
904
|
+
let mut fail_next = self.fail_next.lock().unwrap();
|
|
905
|
+
if *fail_next > 0 {
|
|
906
|
+
*fail_next -= 1;
|
|
907
|
+
return Ok(Response::builder()
|
|
908
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
909
|
+
.header("x-error-message", "mock injected failure")
|
|
910
|
+
.body(HttpBody::new(futures::stream::empty(), Some(0)))
|
|
911
|
+
.unwrap());
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
let url = req.uri().to_string();
|
|
916
|
+
*self.url.lock().unwrap() = Some(url.clone());
|
|
615
917
|
*self.body.lock().unwrap() = Some(
|
|
616
918
|
String::from_utf8(req.body().to_bytes().to_vec())
|
|
617
919
|
.expect("request body must be utf-8 for test payloads"),
|
|
618
920
|
);
|
|
619
921
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|| req.uri().to_string().contains("/tree/")
|
|
922
|
+
if url.contains("/resolve/")
|
|
923
|
+
&& let Some(info) = self.xet_file.lock().unwrap().clone()
|
|
623
924
|
{
|
|
925
|
+
*self.classify_range_header.lock().unwrap() = req
|
|
926
|
+
.headers()
|
|
927
|
+
.get(header::RANGE)
|
|
928
|
+
.and_then(|v| v.to_str().ok())
|
|
929
|
+
.map(str::to_string);
|
|
930
|
+
let data = Bytes::from(format!(
|
|
931
|
+
r#"{{"hash":"{}","size":{}}}"#,
|
|
932
|
+
info.hash, info.size
|
|
933
|
+
));
|
|
934
|
+
let size = data.len() as u64;
|
|
935
|
+
let buffer = Buffer::from(data);
|
|
936
|
+
return Ok(Response::builder()
|
|
937
|
+
.status(StatusCode::OK)
|
|
938
|
+
.header(header::CONTENT_LENGTH, size)
|
|
939
|
+
.header("x-xet-hash", info.hash)
|
|
940
|
+
.body(HttpBody::new(
|
|
941
|
+
futures::stream::iter(vec![Ok(buffer)]),
|
|
942
|
+
Some(size),
|
|
943
|
+
))
|
|
944
|
+
.unwrap());
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Return a minimal valid JSON response for API requests
|
|
948
|
+
let (body, content_length) = if url.contains("/paths-info/") || url.contains("/tree/") {
|
|
624
949
|
let data =
|
|
625
950
|
Bytes::from(r#"[{"type":"file","oid":"abc123","size":100,"path":"test.txt"}]"#);
|
|
626
951
|
let size = data.len() as u64;
|
|
@@ -629,7 +954,7 @@ pub(crate) mod test_utils {
|
|
|
629
954
|
HttpBody::new(futures::stream::iter(vec![Ok(buffer)]), Some(size)),
|
|
630
955
|
size,
|
|
631
956
|
)
|
|
632
|
-
} else if
|
|
957
|
+
} else if url.contains("/commit/") {
|
|
633
958
|
let data = Bytes::from(r#"{}"#);
|
|
634
959
|
let size = data.len() as u64;
|
|
635
960
|
let buffer = Buffer::from(data);
|
|
@@ -637,6 +962,27 @@ pub(crate) mod test_utils {
|
|
|
637
962
|
HttpBody::new(futures::stream::iter(vec![Ok(buffer)]), Some(size)),
|
|
638
963
|
size,
|
|
639
964
|
)
|
|
965
|
+
} else if url.contains("xet-read-token") || url.contains("xet-write-token") {
|
|
966
|
+
let exp = *self.xet_token_expires_at.lock().unwrap();
|
|
967
|
+
let data = Bytes::from(format!(
|
|
968
|
+
r#"{{"casUrl":"https://cas.example.com","exp":{exp},"accessToken":"mock-token"}}"#
|
|
969
|
+
));
|
|
970
|
+
let size = data.len() as u64;
|
|
971
|
+
let buffer = Buffer::from(data);
|
|
972
|
+
(
|
|
973
|
+
HttpBody::new(futures::stream::iter(vec![Ok(buffer)]), Some(size)),
|
|
974
|
+
size,
|
|
975
|
+
)
|
|
976
|
+
} else if let Some(rest) = req.uri().path().strip_prefix("/api/") {
|
|
977
|
+
// Repo-info: echo the requested id back as the canonical id.
|
|
978
|
+
let id = rest.split_once('/').map(|(_, id)| id).unwrap_or(rest);
|
|
979
|
+
let data = Bytes::from(format!(r#"{{"id":"{id}"}}"#));
|
|
980
|
+
let size = data.len() as u64;
|
|
981
|
+
let buffer = Buffer::from(data);
|
|
982
|
+
(
|
|
983
|
+
HttpBody::new(futures::stream::iter(vec![Ok(buffer)]), Some(size)),
|
|
984
|
+
size,
|
|
985
|
+
)
|
|
640
986
|
} else {
|
|
641
987
|
let data = Bytes::from_static(b"hello");
|
|
642
988
|
let size = data.len() as u64;
|
|
@@ -688,6 +1034,11 @@ pub(crate) mod test_utils {
|
|
|
688
1034
|
|
|
689
1035
|
#[cfg(test)]
|
|
690
1036
|
mod tests {
|
|
1037
|
+
use std::sync::{Arc, Mutex};
|
|
1038
|
+
|
|
1039
|
+
use http::Response;
|
|
1040
|
+
use http::StatusCode;
|
|
1041
|
+
|
|
691
1042
|
use super::super::core::HfRepoType;
|
|
692
1043
|
use super::test_utils::create_test_core;
|
|
693
1044
|
use super::*;
|
|
@@ -771,69 +1122,407 @@ mod tests {
|
|
|
771
1122
|
|
|
772
1123
|
Ok(())
|
|
773
1124
|
}
|
|
774
|
-
}
|
|
775
1125
|
|
|
776
|
-
|
|
777
|
-
|
|
1126
|
+
/// A scripted transport mirroring how HF serves a repo whose configured id
|
|
1127
|
+
/// is not canonically cased: repo-info returns the canonical id, and only
|
|
1128
|
+
/// the canonical commit URL accepts the commit.
|
|
1129
|
+
#[derive(Clone, Default)]
|
|
1130
|
+
struct CanonicalCaseTransport {
|
|
1131
|
+
requests: Arc<Mutex<Vec<(String, String, String)>>>,
|
|
1132
|
+
}
|
|
778
1133
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
.
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
(
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1134
|
+
impl HttpTransport for CanonicalCaseTransport {
|
|
1135
|
+
async fn fetch(&self, req: Request<Buffer>) -> Result<Response<HttpBody>> {
|
|
1136
|
+
let uri = req.uri().to_string();
|
|
1137
|
+
let auth = req
|
|
1138
|
+
.headers()
|
|
1139
|
+
.get(header::AUTHORIZATION)
|
|
1140
|
+
.and_then(|v| v.to_str().ok())
|
|
1141
|
+
.unwrap_or_default()
|
|
1142
|
+
.to_string();
|
|
1143
|
+
self.requests
|
|
1144
|
+
.lock()
|
|
1145
|
+
.unwrap()
|
|
1146
|
+
.push((req.method().to_string(), uri.clone(), auth));
|
|
1147
|
+
|
|
1148
|
+
let body = match uri.as_str() {
|
|
1149
|
+
"https://huggingface.co/api/models/test-user/uppercase-repo" => {
|
|
1150
|
+
Bytes::from_static(br#"{"id":"test-user/Uppercase-Repo"}"#)
|
|
1151
|
+
}
|
|
1152
|
+
"https://huggingface.co/api/models/test-user/Uppercase-Repo/commit/main" => {
|
|
1153
|
+
Bytes::from_static(b"{}")
|
|
1154
|
+
}
|
|
1155
|
+
other => panic!("unexpected request to {other}"),
|
|
1156
|
+
};
|
|
1157
|
+
let len = body.len() as u64;
|
|
1158
|
+
Ok(Response::builder()
|
|
1159
|
+
.status(http::StatusCode::OK)
|
|
1160
|
+
.header(header::CONTENT_LENGTH, len)
|
|
1161
|
+
.body(HttpBody::new(
|
|
1162
|
+
futures::stream::iter(vec![Ok(Buffer::from(body))]),
|
|
1163
|
+
Some(len),
|
|
1164
|
+
))
|
|
1165
|
+
.unwrap())
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
#[tokio::test]
|
|
1170
|
+
async fn test_commit_uses_canonical_repo_id() -> Result<()> {
|
|
1171
|
+
let transport = CanonicalCaseTransport::default();
|
|
1172
|
+
let ctx = OperationContext::from_parts(
|
|
1173
|
+
HttpTransporter::new(transport.clone()),
|
|
1174
|
+
Executor::default(),
|
|
1175
|
+
);
|
|
1176
|
+
|
|
1177
|
+
let xet_session = XetSessionBuilder::new()
|
|
1178
|
+
.build()
|
|
1179
|
+
.expect("failed to create xet session");
|
|
1180
|
+
let core = HfCore::new(
|
|
1181
|
+
ServiceInfo::new("hf", "", ""),
|
|
1182
|
+
Capability::default(),
|
|
1183
|
+
HfRepo::new(
|
|
1184
|
+
HfRepoType::Model,
|
|
1185
|
+
"test-user/uppercase-repo".to_string(),
|
|
1186
|
+
Some("main".to_string()),
|
|
1187
|
+
),
|
|
1188
|
+
"/".to_string(),
|
|
1189
|
+
Some("hf_dummy".to_string()),
|
|
1190
|
+
"https://huggingface.co".to_string(),
|
|
1191
|
+
xet_session,
|
|
1192
|
+
HfDownloadMode::Xet,
|
|
1193
|
+
);
|
|
1194
|
+
|
|
1195
|
+
let lfs_file = |path: &str| LfsFile {
|
|
1196
|
+
path: path.to_string(),
|
|
1197
|
+
oid: "deadbeef".to_string(),
|
|
1198
|
+
algo: "sha256".to_string(),
|
|
1199
|
+
size: 2812,
|
|
814
1200
|
};
|
|
1201
|
+
core.commit_git(&ctx, vec![], vec![lfs_file("a.md")], vec![], vec![])
|
|
1202
|
+
.await?;
|
|
1203
|
+
core.commit_git(&ctx, vec![], vec![lfs_file("b.md")], vec![], vec![])
|
|
1204
|
+
.await?;
|
|
1205
|
+
|
|
1206
|
+
let requests = transport.requests.lock().unwrap();
|
|
1207
|
+
let expected = [
|
|
1208
|
+
(
|
|
1209
|
+
"GET",
|
|
1210
|
+
"https://huggingface.co/api/models/test-user/uppercase-repo",
|
|
1211
|
+
),
|
|
1212
|
+
(
|
|
1213
|
+
"POST",
|
|
1214
|
+
"https://huggingface.co/api/models/test-user/Uppercase-Repo/commit/main",
|
|
1215
|
+
),
|
|
1216
|
+
// The canonical id is cached: no second repo-info request.
|
|
1217
|
+
(
|
|
1218
|
+
"POST",
|
|
1219
|
+
"https://huggingface.co/api/models/test-user/Uppercase-Repo/commit/main",
|
|
1220
|
+
),
|
|
1221
|
+
];
|
|
1222
|
+
assert_eq!(requests.len(), expected.len());
|
|
1223
|
+
for ((method, uri, auth), (exp_method, exp_uri)) in requests.iter().zip(expected) {
|
|
1224
|
+
assert_eq!(method, exp_method);
|
|
1225
|
+
assert_eq!(uri, exp_uri);
|
|
1226
|
+
assert_eq!(auth, "Bearer hf_dummy");
|
|
1227
|
+
}
|
|
815
1228
|
|
|
816
|
-
|
|
1229
|
+
Ok(())
|
|
1230
|
+
}
|
|
817
1231
|
|
|
818
|
-
|
|
1232
|
+
#[tokio::test]
|
|
1233
|
+
async fn test_xet_read_token_is_cached_across_calls() -> Result<()> {
|
|
1234
|
+
let (core, ctx, mock_client) = create_test_core(
|
|
1235
|
+
HfRepoType::Model,
|
|
1236
|
+
"test-user/test-repo",
|
|
1237
|
+
"main",
|
|
1238
|
+
"https://huggingface.co",
|
|
1239
|
+
);
|
|
1240
|
+
mock_client.set_xet_token_expires_at(u64::MAX);
|
|
1241
|
+
|
|
1242
|
+
// 2 requests: one to resolve the canonical repo id, one for the
|
|
1243
|
+
// token itself.
|
|
1244
|
+
let repo = core.canonical_repo(&ctx).await?;
|
|
1245
|
+
let first = core
|
|
1246
|
+
.cached_xet_token(&ctx, &repo, XetTokenScope::Read)
|
|
1247
|
+
.await?;
|
|
1248
|
+
assert_eq!(mock_client.request_count(), 2);
|
|
1249
|
+
assert_eq!(first.access_token, "mock-token");
|
|
1250
|
+
assert_eq!(first.cas_url, "https://cas.example.com");
|
|
1251
|
+
|
|
1252
|
+
// A second call with the cached token nowhere near expiry must not
|
|
1253
|
+
// hit the network again -- the canonical repo id is cached too.
|
|
1254
|
+
let second = core
|
|
1255
|
+
.cached_xet_token(&ctx, &repo, XetTokenScope::Read)
|
|
1256
|
+
.await?;
|
|
1257
|
+
assert_eq!(mock_client.request_count(), 2);
|
|
1258
|
+
assert_eq!(second.access_token, first.access_token);
|
|
819
1259
|
|
|
820
|
-
|
|
821
|
-
|
|
1260
|
+
Ok(())
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
#[tokio::test]
|
|
1264
|
+
async fn test_xet_read_token_refreshes_once_stale() -> Result<()> {
|
|
1265
|
+
let (core, ctx, mock_client) = create_test_core(
|
|
1266
|
+
HfRepoType::Model,
|
|
1267
|
+
"test-user/test-repo",
|
|
1268
|
+
"main",
|
|
1269
|
+
"https://huggingface.co",
|
|
1270
|
+
);
|
|
1271
|
+
// Already within the refresh buffer of "now" -- immediately stale.
|
|
1272
|
+
mock_client.set_xet_token_expires_at(0);
|
|
1273
|
+
|
|
1274
|
+
// 2 requests: one to resolve the canonical repo id, one for the
|
|
1275
|
+
// token itself.
|
|
1276
|
+
let repo = core.canonical_repo(&ctx).await?;
|
|
1277
|
+
core.cached_xet_token(&ctx, &repo, XetTokenScope::Read)
|
|
1278
|
+
.await?;
|
|
1279
|
+
assert_eq!(mock_client.request_count(), 2);
|
|
1280
|
+
|
|
1281
|
+
// The canonical repo id is cached, so only the token is re-fetched.
|
|
1282
|
+
core.cached_xet_token(&ctx, &repo, XetTokenScope::Read)
|
|
1283
|
+
.await?;
|
|
1284
|
+
assert_eq!(
|
|
1285
|
+
mock_client.request_count(),
|
|
1286
|
+
3,
|
|
1287
|
+
"a token that's always stale must be refreshed on every call, not cached"
|
|
1288
|
+
);
|
|
1289
|
+
|
|
1290
|
+
Ok(())
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/// A failed refresh must not poison the cache: the next call should
|
|
1294
|
+
/// retry against the network rather than being permanently stuck on an
|
|
1295
|
+
/// error or on a stale/absent token.
|
|
1296
|
+
#[tokio::test]
|
|
1297
|
+
async fn test_cached_xet_token_retries_after_fetch_failure() -> Result<()> {
|
|
1298
|
+
let (core, ctx, mock_client) = create_test_core(
|
|
1299
|
+
HfRepoType::Model,
|
|
1300
|
+
"test-user/test-repo",
|
|
1301
|
+
"main",
|
|
1302
|
+
"https://huggingface.co",
|
|
1303
|
+
);
|
|
1304
|
+
mock_client.set_xet_token_expires_at(u64::MAX);
|
|
1305
|
+
mock_client.fail_next_requests(1);
|
|
1306
|
+
|
|
1307
|
+
// The injected failure hits the canonical repo id lookup, the first
|
|
1308
|
+
// request resolving it makes.
|
|
1309
|
+
match core.canonical_repo(&ctx).await {
|
|
1310
|
+
Err(err) => assert!(err.to_string().contains("mock injected failure")),
|
|
1311
|
+
Ok(_) => panic!("a failed canonical repo lookup must surface as an error"),
|
|
822
1312
|
}
|
|
1313
|
+
assert_eq!(mock_client.request_count(), 1);
|
|
823
1314
|
|
|
824
|
-
|
|
1315
|
+
// Retry: canonical repo id lookup succeeds, then the token itself.
|
|
1316
|
+
let repo = core.canonical_repo(&ctx).await?;
|
|
1317
|
+
let token = core
|
|
1318
|
+
.cached_xet_token(&ctx, &repo, XetTokenScope::Read)
|
|
1319
|
+
.await?;
|
|
1320
|
+
assert_eq!(token.access_token, "mock-token");
|
|
1321
|
+
assert_eq!(mock_client.request_count(), 3);
|
|
1322
|
+
|
|
1323
|
+
Ok(())
|
|
825
1324
|
}
|
|
826
1325
|
|
|
827
|
-
#[
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1326
|
+
#[tokio::test]
|
|
1327
|
+
async fn test_xet_read_and_write_tokens_are_cached_independently() -> Result<()> {
|
|
1328
|
+
let (mut core, ctx, mock_client) = create_test_core(
|
|
1329
|
+
HfRepoType::Model,
|
|
1330
|
+
"test-user/test-repo",
|
|
1331
|
+
"main",
|
|
1332
|
+
"https://huggingface.co",
|
|
1333
|
+
);
|
|
1334
|
+
// A write-scope token fetch now goes through `request()` tagged as
|
|
1335
|
+
// `Operation::Write`, which requires a token to be configured.
|
|
1336
|
+
core.token = Some("hf_dummy".to_string());
|
|
1337
|
+
mock_client.set_xet_token_expires_at(u64::MAX);
|
|
1338
|
+
|
|
1339
|
+
// 2 requests: one to resolve the canonical repo id, one for the
|
|
1340
|
+
// read token itself.
|
|
1341
|
+
let repo = core.canonical_repo(&ctx).await?;
|
|
1342
|
+
core.cached_xet_token(&ctx, &repo, XetTokenScope::Read)
|
|
1343
|
+
.await?;
|
|
1344
|
+
assert_eq!(mock_client.request_count(), 2);
|
|
1345
|
+
|
|
1346
|
+
// A write token request must not be satisfied by the read token's
|
|
1347
|
+
// cache slot -- read and write are distinct HF API scopes. The
|
|
1348
|
+
// canonical repo id is already cached, so only the token is fetched.
|
|
1349
|
+
core.cached_xet_token(&ctx, &repo, XetTokenScope::Write)
|
|
1350
|
+
.await?;
|
|
1351
|
+
assert_eq!(mock_client.request_count(), 3);
|
|
1352
|
+
assert!(mock_client.get_captured_url().contains("write"));
|
|
1353
|
+
|
|
1354
|
+
// Both are now warm; neither call should hit the network again.
|
|
1355
|
+
core.cached_xet_token(&ctx, &repo, XetTokenScope::Read)
|
|
1356
|
+
.await?;
|
|
1357
|
+
core.cached_xet_token(&ctx, &repo, XetTokenScope::Write)
|
|
1358
|
+
.await?;
|
|
1359
|
+
assert_eq!(mock_client.request_count(), 3);
|
|
831
1360
|
|
|
832
|
-
|
|
1361
|
+
Ok(())
|
|
1362
|
+
}
|
|
833
1363
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1364
|
+
/// The point of caching isn't just `cached_xet_token` in isolation --
|
|
1365
|
+
/// `xet_download_group`/`xet_upload_commit` must actually go through it.
|
|
1366
|
+
/// A regression that bypassed the cache in their wiring wouldn't be
|
|
1367
|
+
/// caught by the `cached_xet_token`-only tests above.
|
|
1368
|
+
#[tokio::test]
|
|
1369
|
+
async fn test_xet_download_group_reuses_cached_read_token() -> Result<()> {
|
|
1370
|
+
let (core, ctx, mock_client) = create_test_core(
|
|
1371
|
+
HfRepoType::Model,
|
|
1372
|
+
"test-user/test-repo",
|
|
1373
|
+
"main",
|
|
1374
|
+
"https://huggingface.co",
|
|
1375
|
+
);
|
|
1376
|
+
mock_client.set_xet_token_expires_at(u64::MAX);
|
|
1377
|
+
|
|
1378
|
+
// 2 requests: one to resolve the canonical repo id, one for the
|
|
1379
|
+
// read token itself.
|
|
1380
|
+
core.xet_download_group(&ctx).await?;
|
|
1381
|
+
assert_eq!(mock_client.request_count(), 2);
|
|
1382
|
+
|
|
1383
|
+
// A second group build must reuse the cached read token (and
|
|
1384
|
+
// canonical repo id) rather than fetching its own.
|
|
1385
|
+
core.xet_download_group(&ctx).await?;
|
|
1386
|
+
assert_eq!(mock_client.request_count(), 2);
|
|
1387
|
+
|
|
1388
|
+
Ok(())
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
#[tokio::test]
|
|
1392
|
+
async fn test_xet_upload_commit_reuses_cached_write_token() -> Result<()> {
|
|
1393
|
+
let (mut core, ctx, mock_client) = create_test_core(
|
|
1394
|
+
HfRepoType::Model,
|
|
1395
|
+
"test-user/test-repo",
|
|
1396
|
+
"main",
|
|
1397
|
+
"https://huggingface.co",
|
|
1398
|
+
);
|
|
1399
|
+
core.token = Some("hf_dummy".to_string());
|
|
1400
|
+
mock_client.set_xet_token_expires_at(u64::MAX);
|
|
1401
|
+
|
|
1402
|
+
// 2 requests: one to resolve the canonical repo id, one for the
|
|
1403
|
+
// write token itself.
|
|
1404
|
+
core.xet_upload_commit(&ctx).await?;
|
|
1405
|
+
assert_eq!(mock_client.request_count(), 2);
|
|
1406
|
+
assert!(mock_client.get_captured_url().contains("write"));
|
|
1407
|
+
|
|
1408
|
+
// A second commit build must reuse the cached write token (and
|
|
1409
|
+
// canonical repo id) rather than fetching its own.
|
|
1410
|
+
core.xet_upload_commit(&ctx).await?;
|
|
1411
|
+
assert_eq!(mock_client.request_count(), 2);
|
|
1412
|
+
|
|
1413
|
+
Ok(())
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/// A write-token fetch must be tagged `Operation::Write`, not `Read`, so
|
|
1417
|
+
/// a token-less writer hits `request()`'s local `PermissionDenied`
|
|
1418
|
+
/// fast-path instead of firing a real unauthenticated network call.
|
|
1419
|
+
#[tokio::test]
|
|
1420
|
+
async fn test_xet_upload_commit_without_token_fails_locally() -> Result<()> {
|
|
1421
|
+
let (core, ctx, mock_client) = create_test_core(
|
|
1422
|
+
HfRepoType::Model,
|
|
1423
|
+
"test-user/test-repo",
|
|
1424
|
+
"main",
|
|
1425
|
+
"https://huggingface.co",
|
|
1426
|
+
);
|
|
1427
|
+
mock_client.set_xet_token_expires_at(u64::MAX);
|
|
1428
|
+
|
|
1429
|
+
match core.xet_upload_commit(&ctx).await {
|
|
1430
|
+
Err(err) => assert_eq!(err.kind(), ErrorKind::PermissionDenied),
|
|
1431
|
+
Ok(_) => panic!("an upload commit without a token must fail locally"),
|
|
1432
|
+
}
|
|
1433
|
+
// The canonical repo id lookup doesn't require a token, but the
|
|
1434
|
+
// write-token fetch itself must be rejected before it reaches the
|
|
1435
|
+
// network.
|
|
1436
|
+
assert_eq!(mock_client.request_count(), 1);
|
|
1437
|
+
|
|
1438
|
+
Ok(())
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/// Locks in the strict `>` in `cached_xet_token`'s freshness check: a
|
|
1442
|
+
/// token expiring exactly at the edge of the refresh buffer counts as
|
|
1443
|
+
/// stale (refetched), not fresh.
|
|
1444
|
+
#[tokio::test]
|
|
1445
|
+
async fn test_token_at_refresh_buffer_boundary_is_stale() -> Result<()> {
|
|
1446
|
+
let (core, ctx, mock_client) = create_test_core(
|
|
1447
|
+
HfRepoType::Model,
|
|
1448
|
+
"test-user/test-repo",
|
|
1449
|
+
"main",
|
|
1450
|
+
"https://huggingface.co",
|
|
1451
|
+
);
|
|
1452
|
+
|
|
1453
|
+
let now = SystemTime::now()
|
|
1454
|
+
.duration_since(UNIX_EPOCH)
|
|
1455
|
+
.unwrap()
|
|
1456
|
+
.as_secs();
|
|
1457
|
+
|
|
1458
|
+
// Exactly at the buffer boundary: `expires_at > now + BUFFER` is
|
|
1459
|
+
// false, so this must be treated as stale. The first call also pays
|
|
1460
|
+
// for the one-time canonical repo id lookup.
|
|
1461
|
+
mock_client.set_xet_token_expires_at(now + XET_TOKEN_REFRESH_BUFFER_SECS);
|
|
1462
|
+
let repo = core.canonical_repo(&ctx).await?;
|
|
1463
|
+
core.cached_xet_token(&ctx, &repo, XetTokenScope::Read)
|
|
1464
|
+
.await?;
|
|
1465
|
+
assert_eq!(mock_client.request_count(), 2);
|
|
1466
|
+
core.cached_xet_token(&ctx, &repo, XetTokenScope::Read)
|
|
1467
|
+
.await?;
|
|
1468
|
+
assert_eq!(
|
|
1469
|
+
mock_client.request_count(),
|
|
1470
|
+
3,
|
|
1471
|
+
"a token expiring exactly at the refresh buffer must be refetched, not reused"
|
|
1472
|
+
);
|
|
1473
|
+
|
|
1474
|
+
// One second past the boundary: now fresh, so cached and reused.
|
|
1475
|
+
mock_client.set_xet_token_expires_at(now + XET_TOKEN_REFRESH_BUFFER_SECS + 1);
|
|
1476
|
+
core.cached_xet_token(&ctx, &repo, XetTokenScope::Read)
|
|
1477
|
+
.await?;
|
|
1478
|
+
assert_eq!(mock_client.request_count(), 4);
|
|
1479
|
+
core.cached_xet_token(&ctx, &repo, XetTokenScope::Read)
|
|
1480
|
+
.await?;
|
|
1481
|
+
assert_eq!(
|
|
1482
|
+
mock_client.request_count(),
|
|
1483
|
+
4,
|
|
1484
|
+
"a token expiring past the refresh buffer must be cached and reused"
|
|
1485
|
+
);
|
|
1486
|
+
|
|
1487
|
+
Ok(())
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
/// The whole point of holding the cache's mutex across the refresh
|
|
1491
|
+
/// call: concurrent callers racing on a stale/missing token queue
|
|
1492
|
+
/// behind one in-flight refresh instead of each firing their own.
|
|
1493
|
+
#[tokio::test]
|
|
1494
|
+
async fn test_concurrent_stale_token_refresh_is_single_flighted() -> Result<()> {
|
|
1495
|
+
let (core, ctx, mock_client) = create_test_core(
|
|
1496
|
+
HfRepoType::Model,
|
|
1497
|
+
"test-user/test-repo",
|
|
1498
|
+
"main",
|
|
1499
|
+
"https://huggingface.co",
|
|
1500
|
+
);
|
|
1501
|
+
mock_client.set_xet_token_expires_at(u64::MAX);
|
|
1502
|
+
|
|
1503
|
+
let repo = core.canonical_repo(&ctx).await?;
|
|
1504
|
+
let (r1, r2, r3) = futures::join!(
|
|
1505
|
+
core.cached_xet_token(&ctx, &repo, XetTokenScope::Read),
|
|
1506
|
+
core.cached_xet_token(&ctx, &repo, XetTokenScope::Read),
|
|
1507
|
+
core.cached_xet_token(&ctx, &repo, XetTokenScope::Read),
|
|
1508
|
+
);
|
|
1509
|
+
r1?;
|
|
1510
|
+
r2?;
|
|
1511
|
+
r3?;
|
|
1512
|
+
|
|
1513
|
+
assert_eq!(
|
|
1514
|
+
mock_client.request_count(),
|
|
1515
|
+
2,
|
|
1516
|
+
"concurrent callers racing on a cold/stale token must share one refresh \
|
|
1517
|
+
(plus the one-time canonical repo id lookup)"
|
|
1518
|
+
);
|
|
1519
|
+
|
|
1520
|
+
Ok(())
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
#[test]
|
|
1524
|
+
fn test_parse_error_branch_update_conflict_is_temporary() {
|
|
1525
|
+
let (parts, _) = Response::builder()
|
|
837
1526
|
.status(StatusCode::PRECONDITION_FAILED)
|
|
838
1527
|
.header(
|
|
839
1528
|
"x-error-message",
|
|
@@ -843,35 +1532,92 @@ mod error {
|
|
|
843
1532
|
.unwrap()
|
|
844
1533
|
.into_parts();
|
|
845
1534
|
|
|
846
|
-
|
|
1535
|
+
let err = parse_error(ErrorContext::new(ServiceOperation("Test")), parts);
|
|
847
1536
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1537
|
+
assert_eq!(err.kind(), ErrorKind::Conflict);
|
|
1538
|
+
assert!(err.is_temporary());
|
|
1539
|
+
}
|
|
851
1540
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1541
|
+
#[test]
|
|
1542
|
+
fn test_parse_error_other_precondition_failed_is_not_temporary() {
|
|
1543
|
+
let (parts, _) = Response::builder()
|
|
1544
|
+
.status(StatusCode::PRECONDITION_FAILED)
|
|
1545
|
+
.header("x-error-message", "etag mismatch")
|
|
1546
|
+
.body(())
|
|
1547
|
+
.unwrap()
|
|
1548
|
+
.into_parts();
|
|
860
1549
|
|
|
861
|
-
|
|
1550
|
+
let err = parse_error(ErrorContext::new(ServiceOperation("Test")), parts);
|
|
862
1551
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1552
|
+
assert_eq!(err.kind(), ErrorKind::Unexpected);
|
|
1553
|
+
assert!(!err.is_temporary());
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
use http::StatusCode;
|
|
1558
|
+
|
|
1559
|
+
/// Context needed to classify an error from this service.
|
|
1560
|
+
#[derive(Clone, Copy, Debug)]
|
|
1561
|
+
pub(crate) struct ErrorContext {
|
|
1562
|
+
service_operation: ServiceOperation,
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
impl ErrorContext {
|
|
1566
|
+
pub(crate) const fn new(service_operation: ServiceOperation) -> Self {
|
|
1567
|
+
Self { service_operation }
|
|
866
1568
|
}
|
|
867
1569
|
}
|
|
868
1570
|
|
|
869
|
-
|
|
1571
|
+
/// Parse an error response using its service request context.
|
|
1572
|
+
pub(crate) fn parse_error(ctx: ErrorContext, parts: http::response::Parts) -> Error {
|
|
1573
|
+
// HF sets x-error-message on every error response with a short human-readable
|
|
1574
|
+
// description. Using the header avoids reading the response body, which can be
|
|
1575
|
+
// a large HTML error page (e.g. 52 KB on 404s from the /resolve/ endpoint).
|
|
1576
|
+
let message = parts
|
|
1577
|
+
.headers
|
|
1578
|
+
.get("x-error-message")
|
|
1579
|
+
.and_then(|v| v.to_str().ok())
|
|
1580
|
+
.unwrap_or("unknown error")
|
|
1581
|
+
.to_string();
|
|
1582
|
+
|
|
1583
|
+
// HF git-style commit APIs reject stale branch snapshots with 412.
|
|
1584
|
+
// Treat this specific conflict as temporary so RetryLayer can replay
|
|
1585
|
+
// the whole write/delete close sequence on a fresh branch head.
|
|
1586
|
+
let branch_updated_conflict = parts.status == StatusCode::PRECONDITION_FAILED
|
|
1587
|
+
&& message
|
|
1588
|
+
.to_ascii_lowercase()
|
|
1589
|
+
.contains("branch was updated since you opened this page");
|
|
1590
|
+
|
|
1591
|
+
let (kind, retryable) = match parts.status {
|
|
1592
|
+
StatusCode::NOT_FOUND => (ErrorKind::NotFound, false),
|
|
1593
|
+
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => (ErrorKind::PermissionDenied, false),
|
|
1594
|
+
StatusCode::PRECONDITION_FAILED if branch_updated_conflict => (ErrorKind::Conflict, true),
|
|
1595
|
+
StatusCode::PRECONDITION_FAILED => (ErrorKind::Unexpected, false),
|
|
1596
|
+
StatusCode::TOO_MANY_REQUESTS => (ErrorKind::RateLimited, true),
|
|
1597
|
+
StatusCode::INTERNAL_SERVER_ERROR
|
|
1598
|
+
| StatusCode::BAD_GATEWAY
|
|
1599
|
+
| StatusCode::SERVICE_UNAVAILABLE
|
|
1600
|
+
| StatusCode::GATEWAY_TIMEOUT => (ErrorKind::Unexpected, true),
|
|
1601
|
+
_ => (ErrorKind::Unexpected, false),
|
|
1602
|
+
};
|
|
1603
|
+
|
|
1604
|
+
let mut err = Error::new(kind, message);
|
|
1605
|
+
|
|
1606
|
+
err = err.with_context("service_operation", ctx.service_operation.0);
|
|
1607
|
+
err = with_error_response_context(err, parts);
|
|
1608
|
+
|
|
1609
|
+
if retryable {
|
|
1610
|
+
err = err.set_temporary();
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
err
|
|
1614
|
+
}
|
|
870
1615
|
|
|
871
1616
|
mod uri {
|
|
872
1617
|
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
|
|
873
1618
|
|
|
874
1619
|
pub use super::HfRepoType;
|
|
1620
|
+
use super::XetTokenScope;
|
|
875
1621
|
use crate::HUGGINGFACE_SCHEME;
|
|
876
1622
|
use opendal_core::raw::*;
|
|
877
1623
|
|
|
@@ -931,7 +1677,8 @@ mod uri {
|
|
|
931
1677
|
}
|
|
932
1678
|
|
|
933
1679
|
/// Build the XET token API URL for this repository.
|
|
934
|
-
pub fn xet_token_url(&self, endpoint: &str,
|
|
1680
|
+
pub fn xet_token_url(&self, endpoint: &str, scope: XetTokenScope) -> String {
|
|
1681
|
+
let token_type = scope.as_str();
|
|
935
1682
|
match self.repo_type {
|
|
936
1683
|
HfRepoType::Bucket => {
|
|
937
1684
|
format!(
|
|
@@ -1056,21 +1803,15 @@ mod uri {
|
|
|
1056
1803
|
|
|
1057
1804
|
// Match special refs: refs/(convert|pr)/<segment>
|
|
1058
1805
|
if let Some(rest) = rev_and_path.strip_prefix("refs/convert/") {
|
|
1059
|
-
return
|
|
1060
|
-
(
|
|
1061
|
-
|
|
1062
|
-
rest[slash + 1..].to_string(),
|
|
1063
|
-
)
|
|
1064
|
-
} else {
|
|
1065
|
-
(rev_and_path.to_string(), String::new())
|
|
1806
|
+
return match rest.split_once('/') {
|
|
1807
|
+
Some((segment, path)) => (format!("refs/convert/{segment}"), path.to_string()),
|
|
1808
|
+
None => (rev_and_path.to_string(), String::new()),
|
|
1066
1809
|
};
|
|
1067
1810
|
}
|
|
1068
1811
|
if let Some(rest) = rev_and_path.strip_prefix("refs/pr/") {
|
|
1069
|
-
return
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
} else {
|
|
1073
|
-
(rev_and_path.to_string(), String::new())
|
|
1812
|
+
return match rest.split_once('/') {
|
|
1813
|
+
Some((segment, path)) => (format!("refs/pr/{segment}"), path.to_string()),
|
|
1814
|
+
None => (rev_and_path.to_string(), String::new()),
|
|
1074
1815
|
};
|
|
1075
1816
|
}
|
|
1076
1817
|
|
|
@@ -1266,6 +2007,15 @@ mod uri {
|
|
|
1266
2007
|
assert_eq!(p.path, "");
|
|
1267
2008
|
}
|
|
1268
2009
|
|
|
2010
|
+
#[test]
|
|
2011
|
+
fn resolve_refs_convert_revision_with_path() {
|
|
2012
|
+
let p = resolve("datasets/squad@refs/convert/parquet/default/train/0000.parquet");
|
|
2013
|
+
assert_eq!(p.repo.repo_type, HfRepoType::Dataset);
|
|
2014
|
+
assert_eq!(p.repo.repo_id, "squad");
|
|
2015
|
+
assert_eq!(p.repo.revision.as_deref(), Some("refs/convert/parquet"));
|
|
2016
|
+
assert_eq!(p.path, "default/train/0000.parquet");
|
|
2017
|
+
}
|
|
2018
|
+
|
|
1269
2019
|
#[test]
|
|
1270
2020
|
fn resolve_refs_pr_revision() {
|
|
1271
2021
|
let p = resolve("username/my_model@refs/pr/10");
|
|
@@ -1384,8 +2134,12 @@ mod uri {
|
|
|
1384
2134
|
#[test]
|
|
1385
2135
|
fn test_bucket_xet_token_urls() {
|
|
1386
2136
|
let p = resolve("buckets/user/bucket");
|
|
1387
|
-
let read_url = p
|
|
1388
|
-
|
|
2137
|
+
let read_url = p
|
|
2138
|
+
.repo
|
|
2139
|
+
.xet_token_url("https://huggingface.co", XetTokenScope::Read);
|
|
2140
|
+
let write_url = p
|
|
2141
|
+
.repo
|
|
2142
|
+
.xet_token_url("https://huggingface.co", XetTokenScope::Write);
|
|
1389
2143
|
assert_eq!(
|
|
1390
2144
|
read_url,
|
|
1391
2145
|
"https://huggingface.co/api/buckets/user/bucket/xet-read-token"
|