opendal 0.1.13 → 0.1.15

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.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +24 -24
  3. data/Cargo.toml +1 -1
  4. data/DEPENDENCIES.rust.tsv +24 -24
  5. data/core/CHANGELOG.md +9 -1
  6. data/core/Cargo.lock +110 -110
  7. data/core/Cargo.toml +94 -94
  8. data/core/DEPENDENCIES.rust.tsv +12 -12
  9. data/core/benches/types/buffer.rs +61 -0
  10. data/core/benches/vs_fs/Cargo.toml +1 -1
  11. data/core/benches/vs_s3/Cargo.toml +1 -1
  12. data/core/core/CHANGELOG.md +9 -1
  13. data/core/core/src/raw/futures_util.rs +5 -0
  14. data/core/core/src/raw/oio/copy/block_copy.rs +1 -1
  15. data/core/core/src/raw/oio/copy/multipart_copy.rs +1 -1
  16. data/core/core/src/raw/oio/write/block_write.rs +1 -1
  17. data/core/core/src/raw/oio/write/multipart_write.rs +1 -1
  18. data/core/core/src/raw/oio/write/position_write.rs +41 -1
  19. data/core/core/src/types/buffer.rs +131 -0
  20. data/core/core/src/types/delete/deleter.rs +13 -0
  21. data/core/core/src/types/mod.rs +1 -0
  22. data/core/http-transports/reqwest/Cargo.toml +1 -1
  23. data/core/layers/async-backtrace/Cargo.toml +2 -2
  24. data/core/layers/await-tree/Cargo.toml +2 -2
  25. data/core/layers/capability-check/Cargo.toml +2 -2
  26. data/core/layers/chaos/Cargo.toml +2 -2
  27. data/core/layers/concurrent-limit/Cargo.toml +2 -2
  28. data/core/layers/dtrace/Cargo.toml +2 -2
  29. data/core/layers/fastmetrics/Cargo.toml +3 -3
  30. data/core/layers/fastrace/Cargo.toml +2 -2
  31. data/core/layers/foyer/Cargo.toml +2 -2
  32. data/core/layers/hotpath/Cargo.toml +2 -2
  33. data/core/layers/immutable-index/Cargo.toml +2 -2
  34. data/core/layers/logging/Cargo.toml +2 -2
  35. data/core/layers/metrics/Cargo.toml +3 -3
  36. data/core/layers/mime-guess/Cargo.toml +2 -2
  37. data/core/layers/observe-metrics-common/Cargo.toml +1 -1
  38. data/core/layers/otelmetrics/Cargo.toml +3 -3
  39. data/core/layers/oteltrace/Cargo.toml +2 -2
  40. data/core/layers/prometheus/Cargo.toml +3 -3
  41. data/core/layers/prometheus-client/Cargo.toml +3 -3
  42. data/core/layers/retry/Cargo.toml +4 -4
  43. data/core/layers/route/Cargo.toml +3 -3
  44. data/core/layers/tail-cut/Cargo.toml +2 -2
  45. data/core/layers/throttle/Cargo.toml +2 -2
  46. data/core/layers/timeout/Cargo.toml +3 -3
  47. data/core/layers/tracing/Cargo.toml +2 -2
  48. data/core/services/aliyun-drive/Cargo.toml +1 -1
  49. data/core/services/alluxio/Cargo.toml +1 -1
  50. data/core/services/azblob/Cargo.toml +2 -2
  51. data/core/services/azdls/Cargo.toml +2 -2
  52. data/core/services/azfile/Cargo.toml +2 -2
  53. data/core/services/azure-common/Cargo.toml +1 -1
  54. data/core/services/b2/Cargo.toml +1 -1
  55. data/core/services/cacache/Cargo.toml +1 -1
  56. data/core/services/cloudflare-kv/Cargo.toml +1 -1
  57. data/core/services/compfs/Cargo.toml +1 -1
  58. data/core/services/cos/Cargo.toml +1 -1
  59. data/core/services/d1/Cargo.toml +1 -1
  60. data/core/services/dashmap/Cargo.toml +1 -1
  61. data/core/services/dbfs/Cargo.toml +1 -1
  62. data/core/services/dropbox/Cargo.toml +1 -1
  63. data/core/services/etcd/Cargo.toml +1 -1
  64. data/core/services/foundationdb/Cargo.toml +1 -1
  65. data/core/services/foyer/Cargo.toml +3 -3
  66. data/core/services/fs/Cargo.toml +1 -1
  67. data/core/services/ftp/Cargo.toml +1 -1
  68. data/core/services/gcs/Cargo.toml +1 -1
  69. data/core/services/gcs-grpc/Cargo.toml +1 -1
  70. data/core/services/gdrive/Cargo.toml +1 -1
  71. data/core/services/ghac/Cargo.toml +2 -2
  72. data/core/services/github/Cargo.toml +1 -1
  73. data/core/services/goosefs/Cargo.toml +2 -2
  74. data/core/services/goosefs/src/backend.rs +43 -16
  75. data/core/services/goosefs/src/config.rs +4 -1
  76. data/core/services/goosefs/src/docs.md +1 -1
  77. data/core/services/gridfs/Cargo.toml +1 -1
  78. data/core/services/hdfs/Cargo.toml +2 -2
  79. data/core/services/hdfs/src/backend.rs +15 -7
  80. data/core/services/hdfs/src/core.rs +55 -16
  81. data/core/services/hdfs/src/docs.md +9 -1
  82. data/core/services/hdfs-native/Cargo.toml +1 -1
  83. data/core/services/hf/Cargo.toml +5 -2
  84. data/core/services/hf/src/backend.rs +8 -3
  85. data/core/services/hf/src/config.rs +4 -2
  86. data/core/services/hf/src/docs.md +6 -2
  87. data/core/services/hf/src/reader.rs +163 -68
  88. data/core/services/http/Cargo.toml +3 -3
  89. data/core/services/ipfs/Cargo.toml +1 -1
  90. data/core/services/ipmfs/Cargo.toml +1 -1
  91. data/core/services/koofr/Cargo.toml +1 -1
  92. data/core/services/lakefs/Cargo.toml +1 -1
  93. data/core/services/memcached/Cargo.toml +1 -1
  94. data/core/services/mini_moka/Cargo.toml +1 -1
  95. data/core/services/moka/Cargo.toml +1 -1
  96. data/core/services/mongodb/Cargo.toml +1 -1
  97. data/core/services/monoiofs/Cargo.toml +1 -1
  98. data/core/services/mysql/Cargo.toml +1 -1
  99. data/core/services/obs/Cargo.toml +1 -1
  100. data/core/services/onedrive/Cargo.toml +1 -1
  101. data/core/services/opfs/Cargo.toml +1 -1
  102. data/core/services/oss/Cargo.toml +1 -1
  103. data/core/services/oss/src/backend.rs +143 -2
  104. data/core/services/oss/src/docs.md +12 -0
  105. data/core/services/pcloud/Cargo.toml +1 -1
  106. data/core/services/persy/Cargo.toml +1 -1
  107. data/core/services/postgresql/Cargo.toml +1 -1
  108. data/core/services/redb/Cargo.toml +1 -1
  109. data/core/services/redis/Cargo.toml +1 -1
  110. data/core/services/rocksdb/Cargo.toml +1 -1
  111. data/core/services/s3/Cargo.toml +1 -1
  112. data/core/services/seafile/Cargo.toml +1 -1
  113. data/core/services/sftp/Cargo.toml +1 -1
  114. data/core/services/sled/Cargo.toml +1 -1
  115. data/core/services/sqlite/Cargo.toml +1 -1
  116. data/core/services/surrealdb/Cargo.toml +1 -1
  117. data/core/services/swift/Cargo.toml +1 -1
  118. data/core/services/tikv/Cargo.toml +1 -1
  119. data/core/services/tos/Cargo.toml +1 -1
  120. data/core/services/upyun/Cargo.toml +1 -1
  121. data/core/services/vercel-artifacts/Cargo.toml +1 -1
  122. data/core/services/vercel-blob/Cargo.toml +1 -1
  123. data/core/services/webdav/Cargo.toml +1 -1
  124. data/core/services/webdav/src/backend.rs +10 -21
  125. data/core/services/webdav/src/config.rs +5 -14
  126. data/core/services/webhdfs/Cargo.toml +1 -1
  127. data/core/services/yandex-disk/Cargo.toml +1 -1
  128. data/core/testkit/Cargo.toml +4 -4
  129. metadata +2 -2
@@ -32,7 +32,7 @@ version = { workspace = true }
32
32
  all-features = true
33
33
 
34
34
  [dependencies]
35
- opendal-core = { path = "../../core", version = "0.59.2", default-features = false }
35
+ opendal-core = { path = "../../core", version = "0.59.4", default-features = false }
36
36
 
37
37
  asyncband = { workspace = true, features = ["mutex", "singleflight"] }
38
38
  bytes = { workspace = true }
@@ -36,8 +36,8 @@ bytes = { workspace = true }
36
36
  ghac = { version = "0.3.0", default-features = false }
37
37
  http = { workspace = true }
38
38
  log = { workspace = true }
39
- opendal-core = { path = "../../core", version = "0.59.2", default-features = false }
40
- opendal-service-azblob = { path = "../azblob", version = "0.59.2", default-features = false }
39
+ opendal-core = { path = "../../core", version = "0.59.4", default-features = false }
40
+ opendal-service-azblob = { path = "../azblob", version = "0.59.4", default-features = false }
41
41
  prost = { version = "0.14.3", default-features = false }
42
42
  reqsign-azure-storage = { version = "3.1.1", default-features = false }
43
43
  reqsign-core = { version = "3.2.0", default-features = false }
@@ -36,7 +36,7 @@ base64 = { workspace = true }
36
36
  bytes = { workspace = true }
37
37
  http = { workspace = true }
38
38
  log = { workspace = true }
39
- opendal-core = { path = "../../core", version = "0.59.2", default-features = false }
39
+ opendal-core = { path = "../../core", version = "0.59.4", default-features = false }
40
40
  serde = { workspace = true, features = ["derive"] }
41
41
  serde_json = { workspace = true }
42
42
 
@@ -45,11 +45,11 @@ asyncband = { workspace = true, features = ["rwlock"] }
45
45
  bytes = { workspace = true }
46
46
  goosefs-sdk = { version = "0.2.1", default-features = false }
47
47
  log = { workspace = true }
48
- opendal-core = { path = "../../core", version = "0.59.2", default-features = false }
48
+ opendal-core = { path = "../../core", version = "0.59.4", default-features = false }
49
49
  serde = { workspace = true, features = ["derive"] }
50
50
 
51
51
  [dev-dependencies]
52
- opendal = { path = "../..", version = "0.59.2", features = [
52
+ opendal = { path = "../..", version = "0.59.4", features = [
53
53
  "services-goosefs",
54
54
  ] }
55
55
  tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
@@ -90,7 +90,9 @@ impl GoosefsBuilder {
90
90
 
91
91
  /// Set default write type.
92
92
  ///
93
- /// Values: `"must_cache"`, `"cache_through"`, `"through"`, `"async_through"`
93
+ /// Values: `"must_cache"`, `"try_cache"`, `"cache_through"`, `"through"`,
94
+ /// `"async_through"`. Matching is case-insensitive. `build()` fails with
95
+ /// [`ErrorKind::ConfigInvalid`] when the value is not one of these.
94
96
  pub fn write_type(mut self, wt: &str) -> Self {
95
97
  if !wt.is_empty() {
96
98
  self.config.write_type = Some(wt.to_string());
@@ -340,22 +342,18 @@ impl Builder for GoosefsBuilder {
340
342
  goosefs_config.chunk_size = chunk_size;
341
343
  }
342
344
 
343
- // Parse write_type string → goosefs_sdk::WritePType i32.
344
- //
345
- // Normalise case once up front so we don't need to enumerate both
346
- // `must_cache` and `MUST_CACHE` branches — this mirrors how the
347
- // GooseFS server-side config parser (`WritePType::valueOf`) treats
348
- // the value as case-insensitive.
345
+ // Parse write_type through the SDK so unknown values fail instead of
346
+ // silently becoming MUST_CACHE. `with_write_type_str` is
347
+ // case-insensitive, matching GooseFS `WritePType::valueOf`.
349
348
  if let Some(ref wt) = self.config.write_type {
350
- let wt_i32 = match wt.to_lowercase().as_str() {
351
- "must_cache" => 1,
352
- "try_cache" => 2,
353
- "cache_through" => 3,
354
- "through" => 4,
355
- "async_through" => 5,
356
- _ => 1, // default to MUST_CACHE
357
- };
358
- goosefs_config.write_type = Some(wt_i32);
349
+ goosefs_config = goosefs_config.with_write_type_str(wt).map_err(|e| {
350
+ Error::new(
351
+ ErrorKind::ConfigInvalid,
352
+ format!("invalid write_type: {}", e),
353
+ )
354
+ .with_operation("Builder::build")
355
+ .with_context("service", GOOSEFS_SCHEME)
356
+ })?;
359
357
  }
360
358
 
361
359
  // Parse auth_type string → goosefs_sdk::auth::AuthType
@@ -775,6 +773,35 @@ mod tests {
775
773
  );
776
774
  }
777
775
 
776
+ #[test]
777
+ fn test_builder_unknown_write_type_fails() {
778
+ let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
779
+ for wt in ["INVALID_WT", "cache_throughh"] {
780
+ let err = GoosefsBuilder::default()
781
+ .root("/data")
782
+ .master_addr("127.0.0.1:9200")
783
+ .write_type(wt)
784
+ .build()
785
+ .expect_err("build must fail on unknown write_type");
786
+ assert_eq!(err.kind(), ErrorKind::ConfigInvalid);
787
+ assert!(
788
+ err.to_string().contains("write_type"),
789
+ "unexpected error message for {wt}: {err}"
790
+ );
791
+ }
792
+ }
793
+
794
+ #[test]
795
+ fn test_builder_write_type_is_case_insensitive() {
796
+ let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
797
+ let backend = GoosefsBuilder::default()
798
+ .root("/data")
799
+ .master_addr("127.0.0.1:9200")
800
+ .write_type("CACHE_THROUGH")
801
+ .build();
802
+ assert!(backend.is_ok());
803
+ }
804
+
778
805
  #[test]
779
806
  fn test_capability_rename_with_if_not_exists() {
780
807
  let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
@@ -69,7 +69,10 @@ pub struct GoosefsConfig {
69
69
 
70
70
  /// Default write type for new files.
71
71
  ///
72
- /// Supported values: `"must_cache"`, `"cache_through"`, `"through"`, `"async_through"`.
72
+ /// Supported values: `"must_cache"`, `"try_cache"`, `"cache_through"`,
73
+ /// `"through"`, `"async_through"`. Matching is case-insensitive.
74
+ /// `build()` fails with `ConfigInvalid` when the value is not one of
75
+ /// these.
73
76
  /// Default: `"must_cache"`.
74
77
  pub write_type: Option<String>,
75
78
 
@@ -28,7 +28,7 @@ Features:
28
28
  - **HA support**: Comma-separated master addresses for automatic Primary Master discovery.
29
29
  - **Block-level I/O**: Data reads/writes go through block-level gRPC bidirectional streaming.
30
30
  - **Consistent hash routing**: Worker selection uses consistent hashing on block IDs.
31
- - **All WriteTypes**: Supports MUST_CACHE, CACHE_THROUGH, THROUGH, and ASYNC_THROUGH.
31
+ - **All WriteTypes**: Supports MUST_CACHE, TRY_CACHE, CACHE_THROUGH, THROUGH, and ASYNC_THROUGH. Unknown `write_type` values fail at `build()` with `ConfigInvalid`.
32
32
  - **Conditional Create**: `write_with_if_not_exists` publishes via Master no-replace
33
33
  rename (`rename_with_if_not_exists`); destination is never deleted on the Create path.
34
34
  - **Rename parent directories**: `rename` creates a missing destination parent only
@@ -35,7 +35,7 @@ all-features = true
35
35
  asyncband = { workspace = true, features = ["once-cell"] }
36
36
  futures = { workspace = true }
37
37
  mongodb = "3.3.0"
38
- opendal-core = { path = "../../core", version = "0.59.2", default-features = false }
38
+ opendal-core = { path = "../../core", version = "0.59.4", default-features = false }
39
39
  serde = { workspace = true, features = ["derive"] }
40
40
 
41
41
  [dev-dependencies]
@@ -32,11 +32,11 @@ version = { workspace = true }
32
32
  all-features = true
33
33
 
34
34
  [dependencies]
35
- opendal-core = { path = "../../core", version = "0.59.2", default-features = false }
35
+ opendal-core = { path = "../../core", version = "0.59.4", default-features = false }
36
36
 
37
37
  bytes = { workspace = true }
38
38
  futures = { workspace = true }
39
- hdrs = { version = "0.3.2", features = ["async_file"] }
39
+ hdrs = { version = "0.3.3", features = ["async_file"] }
40
40
  log = { workspace = true }
41
41
  serde = { workspace = true, features = ["derive"] }
42
42
  tokio = { workspace = true, features = ["rt"] }
@@ -169,6 +169,8 @@ impl Builder for HdfsBuilder {
169
169
 
170
170
  list: true,
171
171
 
172
+ copy: true,
173
+
172
174
  rename: true,
173
175
  rename_with_if_not_exists: true,
174
176
 
@@ -195,7 +197,7 @@ impl Service for HdfsBackend {
195
197
  type Writer = HdfsLazyWriter;
196
198
  type Lister = Option<HdfsLister>;
197
199
  type Deleter = oio::OneShotDeleter<HdfsDeleter>;
198
- type Copier = ();
200
+ type Copier = oio::OneShotCopier;
199
201
  type Composer = ();
200
202
 
201
203
  fn info(&self) -> ServiceInfo {
@@ -258,14 +260,20 @@ impl Service for HdfsBackend {
258
260
  fn copy(
259
261
  &self,
260
262
  _ctx: &OperationContext,
261
- _from: &str,
262
- _to: &str,
263
+ from: &str,
264
+ to: &str,
263
265
  _args: OpCopy,
264
266
  ) -> Result<Self::Copier> {
265
- Err(Error::new(
266
- ErrorKind::Unsupported,
267
- "operation is not supported",
268
- ))
267
+ let core = self.core.clone();
268
+ let from = from.to_string();
269
+ let to = to.to_string();
270
+ // Recreate the future so retry layers can rerun the copy after a temporary error.
271
+ Ok(oio::OneShotCopier::new_with(move || {
272
+ let core = core.clone();
273
+ let from = from.clone();
274
+ let to = to.clone();
275
+ async move { core.hdfs_copy(&from, &to).await }
276
+ }))
269
277
  }
270
278
 
271
279
  async fn rename(
@@ -124,8 +124,7 @@ impl HdfsCore {
124
124
  });
125
125
 
126
126
  if !target_exists {
127
- let parent = get_parent(&target_path);
128
- self.client.create_dir(parent).map_err(new_std_io_error)?;
127
+ self.ensure_parent_dir(&target_path)?;
129
128
  }
130
129
  if !should_append {
131
130
  initial_size = 0;
@@ -175,20 +174,7 @@ impl HdfsCore {
175
174
  return Err(new_std_io_error(err));
176
175
  }
177
176
 
178
- let parent = std::path::PathBuf::from(&to_path)
179
- .parent()
180
- .ok_or_else(|| {
181
- Error::new(
182
- ErrorKind::Unexpected,
183
- "path should have parent but not, it must be malformed",
184
- )
185
- .with_context("to", &to_path)
186
- })?
187
- .to_path_buf();
188
-
189
- self.client
190
- .create_dir(&parent.to_string_lossy())
191
- .map_err(new_std_io_error)?;
177
+ self.ensure_parent_dir(&to_path)?;
192
178
  }
193
179
  Ok(metadata) => {
194
180
  if metadata.is_file() {
@@ -215,6 +201,59 @@ impl HdfsCore {
215
201
 
216
202
  Ok(())
217
203
  }
204
+
205
+ pub async fn hdfs_copy(&self, from: &str, to: &str) -> Result<Metadata> {
206
+ let from_path = build_rooted_abs_path(&self.root, from);
207
+ // OpenDAL copy is file-to-file only. Reject directory sources before
208
+ // the HDFS API can recursively copy their contents.
209
+ let from_meta = self.client.metadata(&from_path).map_err(new_std_io_error)?;
210
+ if !from_meta.is_file() {
211
+ return Err(
212
+ Error::new(ErrorKind::IsADirectory, "from path should be a file")
213
+ .with_context("from", &from_path),
214
+ );
215
+ }
216
+
217
+ let to_path = build_rooted_abs_path(&self.root, to);
218
+ match self.client.metadata(&to_path) {
219
+ Ok(meta) => {
220
+ // OpenDAL treats `to` as the exact destination file path. Reject
221
+ // an existing directory instead of copying the source into it.
222
+ if meta.is_dir() {
223
+ return Err(
224
+ Error::new(ErrorKind::IsADirectory, "to path should be a file")
225
+ .with_context("to", &to_path),
226
+ );
227
+ }
228
+ // The HDFS copy API does not replace an existing destination,
229
+ // so remove it first to preserve OpenDAL's overwrite semantics.
230
+ self.client
231
+ .remove_file(&to_path)
232
+ .map_err(new_std_io_error)?;
233
+ }
234
+ Err(err) if err.kind() == io::ErrorKind::NotFound => {
235
+ // hdrs copy_file requires the destination parent to already exist.
236
+ self.ensure_parent_dir(&to_path)?;
237
+ }
238
+ Err(err) => return Err(new_std_io_error(err)),
239
+ }
240
+
241
+ let client = self.client.clone();
242
+ let copy_from = from_path.clone();
243
+ let copy_to = to_path.clone();
244
+ tokio::task::spawn_blocking(move || client.copy_file(&copy_from, &copy_to))
245
+ .await
246
+ .map_err(|e| Error::new(ErrorKind::Unexpected, "tokio task join failed").set_source(e))?
247
+ .map_err(new_std_io_error)?;
248
+
249
+ Ok(MetadataBuilder::file(from_meta.len()).build())
250
+ }
251
+
252
+ fn ensure_parent_dir(&self, path: &str) -> Result<()> {
253
+ self.client
254
+ .create_dir(get_parent(path))
255
+ .map_err(new_std_io_error)
256
+ }
218
257
  }
219
258
 
220
259
  #[cfg(test)]
@@ -10,13 +10,21 @@ Depending on its configuration and the backing system, this service can expose:
10
10
  - [x] write
11
11
  - [x] delete
12
12
  - [x] list
13
- - [ ] copy
13
+ - [x] copy
14
14
  - [x] rename
15
15
  - [ ] ~~presign~~
16
16
 
17
17
  Inspect the effective capability set with [`opendal_core::Operator::info`] and
18
18
  [`opendal_core::OperatorInfo::capability`] after building an operator.
19
19
 
20
+ ## Copy behavior
21
+
22
+ The HDFS service copies files to exact destination file paths and rejects
23
+ directory sources and destinations. Copy overwrites an existing destination by
24
+ removing it before copying the source. This replacement is not atomic: if the
25
+ copy fails after removing the destination, the destination can be missing or
26
+ incomplete.
27
+
20
28
  ## Differences with webhdfs
21
29
 
22
30
  The [WebHDFS service](https://docs.rs/opendal-service-webhdfs) uses HDFS's
@@ -36,5 +36,5 @@ bytes = { workspace = true }
36
36
  futures = { workspace = true }
37
37
  hdfs-native = { version = "0.14" }
38
38
  log = { workspace = true }
39
- opendal-core = { path = "../../core", version = "0.59.2", default-features = false }
39
+ opendal-core = { path = "../../core", version = "0.59.4", default-features = false }
40
40
  serde = { workspace = true, features = ["derive"] }
@@ -37,7 +37,7 @@ bytes = { workspace = true }
37
37
  hf-xet = "1.6.0"
38
38
  http = { workspace = true }
39
39
  log = { workspace = true }
40
- opendal-core = { path = "../../core", version = "0.59.2", default-features = false }
40
+ opendal-core = { path = "../../core", version = "0.59.4", default-features = false }
41
41
  percent-encoding = "2"
42
42
  serde = { workspace = true, features = ["derive"] }
43
43
  serde_json = { workspace = true }
@@ -45,6 +45,9 @@ serde_json = { workspace = true }
45
45
  [dev-dependencies]
46
46
  base64 = { workspace = true }
47
47
  futures = { workspace = true }
48
- opendal-http-transport-reqwest = { path = "../../http-transports/reqwest", version = "0.59.2" }
48
+ opendal-core = { path = "../../core", version = "0.59.4", features = [
49
+ "executors-tokio",
50
+ ] }
51
+ opendal-http-transport-reqwest = { path = "../../http-transports/reqwest", version = "0.59.4" }
49
52
  serde_json = { workspace = true }
50
53
  tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
@@ -135,9 +135,14 @@ impl HfBuilder {
135
135
 
136
136
  /// Enable caching of resolved HTTP download addresses and XET file metadata.
137
137
  ///
138
- /// Defaults to `false`: every range resolves through the Hub in either
139
- /// download mode. Set to `true` to share resolve results across readers on
140
- /// the same backend. HTTP addresses refresh near expiry.
138
+ /// Defaults to `false`: each new reader resolves through the Hub. An XET-mode
139
+ /// reader retains the XET metadata returned by its first read for its lifetime,
140
+ /// even when this option is disabled. Its subsequent ranges use that file
141
+ /// version. Create a new reader to resolve the path again. HTTP reads resolve
142
+ /// each range.
143
+ ///
144
+ /// Set to `true` to share resolve results across readers on the same backend.
145
+ /// HTTP addresses refresh near expiry.
141
146
  ///
142
147
  /// Enable this only when previously written files are not modified. Changed
143
148
  /// files can remain invisible while cached results are reused, including
@@ -64,8 +64,10 @@ pub struct HfConfig {
64
64
  ///
65
65
  /// Defaults to `false`. Set to `true` to share resolve results across readers
66
66
  /// on the same backend. Changed files may remain invisible while cached
67
- /// results are reused. See [`HfBuilder::enable_resolve_cache`] for freshness
68
- /// semantics.
67
+ /// results are reused. A reader retains XET metadata from its first read for
68
+ /// its lifetime. Create a new reader to resolve the path again when this
69
+ /// option is disabled.
70
+ /// See [`HfBuilder::enable_resolve_cache`] for freshness semantics.
69
71
  pub enable_resolve_cache: bool,
70
72
  }
71
73
 
@@ -34,8 +34,12 @@ accepted values, defaults, and environment interaction.
34
34
 
35
35
  ### Caching resolve results
36
36
 
37
- By default, every range resolves through the Hugging Face Hub in both HTTP and
38
- XET download modes. Enable
37
+ By default, each new reader resolves through the Hugging Face Hub. An XET-mode
38
+ reader retains the XET metadata returned by its first read for its lifetime.
39
+ Subsequent ranges, including concurrent reads, use that file version. Create a
40
+ new reader to resolve an updated path. HTTP reads resolve each range.
41
+
42
+ Enable
39
43
  [`enable_resolve_cache`](crate::Hf::enable_resolve_cache) to share resolved HTTP
40
44
  download addresses and XET file metadata across readers and batches on the same
41
45
  backend: