opendal 0.1.11 → 0.1.13

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 (248) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +31 -36
  3. data/Cargo.toml +1 -1
  4. data/DEPENDENCIES.rust.tsv +25 -35
  5. data/README.md +1 -1
  6. data/core/CHANGELOG.md +22 -0
  7. data/core/Cargo.lock +145 -189
  8. data/core/Cargo.toml +95 -95
  9. data/core/DEPENDENCIES.rust.tsv +259 -290
  10. data/core/LICENSE +16 -0
  11. data/core/benches/vs_fs/Cargo.toml +1 -1
  12. data/core/benches/vs_s3/Cargo.toml +1 -1
  13. data/core/core/CHANGELOG.md +6031 -0
  14. data/core/core/Cargo.toml +1 -1
  15. data/core/core/LICENSE +16 -0
  16. data/core/core/src/docs/mod.rs +1 -1
  17. data/core/core/src/docs/upgrade.md +8 -0
  18. data/core/core/src/lib.rs +1 -3
  19. data/core/core/src/raw/futures_util.rs +26 -21
  20. data/core/core/src/raw/http_util/error.rs +9 -0
  21. data/core/core/src/raw/ops.rs +1 -0
  22. data/core/core/src/types/http_transport/mod.rs +30 -2
  23. data/core/core/src/types/http_transport/uri.rs +166 -0
  24. data/core/core/src/types/metadata.rs +1 -0
  25. data/core/core/src/types/mod.rs +2 -0
  26. data/core/http-transports/reqwest/Cargo.toml +1 -1
  27. data/core/http-transports/reqwest/LICENSE +16 -0
  28. data/core/http-transports/reqwest/src/lib.rs +46 -4
  29. data/core/layers/async-backtrace/Cargo.toml +2 -2
  30. data/core/layers/async-backtrace/LICENSE +16 -0
  31. data/core/layers/await-tree/Cargo.toml +2 -2
  32. data/core/layers/await-tree/LICENSE +16 -0
  33. data/core/layers/capability-check/Cargo.toml +2 -2
  34. data/core/layers/capability-check/LICENSE +16 -0
  35. data/core/layers/chaos/Cargo.toml +2 -2
  36. data/core/layers/chaos/LICENSE +16 -0
  37. data/core/layers/concurrent-limit/Cargo.toml +3 -3
  38. data/core/layers/concurrent-limit/LICENSE +16 -0
  39. data/core/layers/dtrace/Cargo.toml +2 -2
  40. data/core/layers/dtrace/LICENSE +16 -0
  41. data/core/layers/fastmetrics/Cargo.toml +3 -3
  42. data/core/layers/fastmetrics/LICENSE +16 -0
  43. data/core/layers/fastrace/Cargo.toml +2 -2
  44. data/core/layers/fastrace/LICENSE +16 -0
  45. data/core/layers/foyer/Cargo.toml +2 -2
  46. data/core/layers/foyer/LICENSE +16 -0
  47. data/core/layers/hotpath/Cargo.toml +2 -2
  48. data/core/layers/hotpath/LICENSE +16 -0
  49. data/core/layers/immutable-index/Cargo.toml +2 -2
  50. data/core/layers/immutable-index/LICENSE +16 -0
  51. data/core/layers/logging/Cargo.toml +2 -2
  52. data/core/layers/logging/LICENSE +16 -0
  53. data/core/layers/metrics/Cargo.toml +3 -3
  54. data/core/layers/metrics/LICENSE +16 -0
  55. data/core/layers/mime-guess/Cargo.toml +2 -2
  56. data/core/layers/mime-guess/LICENSE +16 -0
  57. data/core/layers/observe-metrics-common/Cargo.toml +1 -1
  58. data/core/layers/observe-metrics-common/LICENSE +16 -0
  59. data/core/layers/otelmetrics/Cargo.toml +3 -3
  60. data/core/layers/otelmetrics/LICENSE +16 -0
  61. data/core/layers/oteltrace/Cargo.toml +2 -2
  62. data/core/layers/oteltrace/LICENSE +16 -0
  63. data/core/layers/prometheus/Cargo.toml +3 -3
  64. data/core/layers/prometheus/LICENSE +16 -0
  65. data/core/layers/prometheus-client/Cargo.toml +3 -3
  66. data/core/layers/prometheus-client/LICENSE +16 -0
  67. data/core/layers/retry/Cargo.toml +4 -4
  68. data/core/layers/retry/LICENSE +16 -0
  69. data/core/layers/route/Cargo.toml +3 -3
  70. data/core/layers/route/LICENSE +16 -0
  71. data/core/layers/tail-cut/Cargo.toml +2 -2
  72. data/core/layers/tail-cut/LICENSE +16 -0
  73. data/core/layers/throttle/Cargo.toml +2 -2
  74. data/core/layers/throttle/LICENSE +16 -0
  75. data/core/layers/timeout/Cargo.toml +3 -3
  76. data/core/layers/timeout/LICENSE +16 -0
  77. data/core/layers/tracing/Cargo.toml +2 -2
  78. data/core/layers/tracing/LICENSE +16 -0
  79. data/core/services/aliyun-drive/Cargo.toml +2 -2
  80. data/core/services/aliyun-drive/LICENSE +16 -0
  81. data/core/services/alluxio/Cargo.toml +1 -1
  82. data/core/services/alluxio/LICENSE +16 -0
  83. data/core/services/azblob/Cargo.toml +2 -2
  84. data/core/services/azblob/LICENSE +16 -0
  85. data/core/services/azblob/src/backend.rs +21 -7
  86. data/core/services/azblob/src/config.rs +4 -1
  87. data/core/services/azdls/Cargo.toml +3 -3
  88. data/core/services/azdls/LICENSE +16 -0
  89. data/core/services/azfile/Cargo.toml +2 -2
  90. data/core/services/azfile/LICENSE +16 -0
  91. data/core/services/azure-common/Cargo.toml +1 -1
  92. data/core/services/azure-common/LICENSE +16 -0
  93. data/core/services/b2/Cargo.toml +2 -2
  94. data/core/services/b2/LICENSE +16 -0
  95. data/core/services/cacache/Cargo.toml +1 -1
  96. data/core/services/cacache/LICENSE +16 -0
  97. data/core/services/cloudflare-kv/Cargo.toml +1 -1
  98. data/core/services/cloudflare-kv/LICENSE +16 -0
  99. data/core/services/compfs/Cargo.toml +1 -1
  100. data/core/services/compfs/LICENSE +16 -0
  101. data/core/services/compfs/src/core.rs +27 -11
  102. data/core/services/cos/Cargo.toml +1 -1
  103. data/core/services/cos/LICENSE +16 -0
  104. data/core/services/d1/Cargo.toml +1 -1
  105. data/core/services/d1/LICENSE +16 -0
  106. data/core/services/dashmap/Cargo.toml +1 -1
  107. data/core/services/dashmap/LICENSE +16 -0
  108. data/core/services/dbfs/Cargo.toml +1 -1
  109. data/core/services/dbfs/LICENSE +16 -0
  110. data/core/services/dropbox/Cargo.toml +2 -2
  111. data/core/services/dropbox/LICENSE +16 -0
  112. data/core/services/etcd/Cargo.toml +2 -2
  113. data/core/services/etcd/LICENSE +16 -0
  114. data/core/services/etcd/src/core.rs +3 -3
  115. data/core/services/foundationdb/Cargo.toml +1 -1
  116. data/core/services/foundationdb/LICENSE +16 -0
  117. data/core/services/foyer/Cargo.toml +3 -3
  118. data/core/services/foyer/LICENSE +16 -0
  119. data/core/services/fs/Cargo.toml +1 -1
  120. data/core/services/fs/LICENSE +16 -0
  121. data/core/services/fs/src/core.rs +69 -10
  122. data/core/services/ftp/Cargo.toml +2 -2
  123. data/core/services/ftp/LICENSE +16 -0
  124. data/core/services/ftp/src/core.rs +1 -1
  125. data/core/services/ftp/src/reader.rs +1 -1
  126. data/core/services/ftp/src/writer.rs +1 -1
  127. data/core/services/gcs/Cargo.toml +3 -2
  128. data/core/services/gcs/LICENSE +16 -0
  129. data/core/services/gcs/src/composer.rs +11 -21
  130. data/core/services/gcs/src/core.rs +116 -2
  131. data/core/services/gcs-grpc/Cargo.toml +1 -1
  132. data/core/services/gcs-grpc/proto/google/storage/v2/storage.proto +17 -18
  133. data/core/services/gdrive/Cargo.toml +3 -2
  134. data/core/services/gdrive/LICENSE +16 -0
  135. data/core/services/gdrive/src/docs.md +2 -0
  136. data/core/services/gdrive/src/path_index.rs +109 -18
  137. data/core/services/ghac/Cargo.toml +2 -2
  138. data/core/services/ghac/LICENSE +16 -0
  139. data/core/services/github/Cargo.toml +1 -1
  140. data/core/services/github/LICENSE +16 -0
  141. data/core/services/goosefs/Cargo.toml +13 -4
  142. data/core/services/goosefs/LICENSE +16 -0
  143. data/core/services/goosefs/README.md +11 -0
  144. data/core/services/goosefs/src/backend.rs +376 -49
  145. data/core/services/goosefs/src/config.rs +18 -10
  146. data/core/services/goosefs/src/core.rs +1 -1
  147. data/core/services/goosefs/src/docs.md +72 -3
  148. data/core/services/goosefs/src/reader.rs +22 -89
  149. data/core/services/goosefs/tests/master_addr_resolution.rs +103 -0
  150. data/core/services/gridfs/Cargo.toml +2 -2
  151. data/core/services/gridfs/LICENSE +16 -0
  152. data/core/services/hdfs/Cargo.toml +1 -1
  153. data/core/services/hdfs/LICENSE +16 -0
  154. data/core/services/hdfs-native/Cargo.toml +1 -1
  155. data/core/services/hdfs-native/LICENSE +16 -0
  156. data/core/services/hf/Cargo.toml +3 -3
  157. data/core/services/hf/LICENSE +16 -0
  158. data/core/services/hf/src/backend.rs +77 -9
  159. data/core/services/hf/src/config.rs +58 -0
  160. data/core/services/hf/src/core.rs +695 -318
  161. data/core/services/hf/src/docs.md +29 -0
  162. data/core/services/hf/src/lister.rs +3 -3
  163. data/core/services/hf/src/reader.rs +128 -143
  164. data/core/services/http/Cargo.toml +3 -3
  165. data/core/services/http/LICENSE +16 -0
  166. data/core/services/ipfs/Cargo.toml +1 -1
  167. data/core/services/ipfs/LICENSE +16 -0
  168. data/core/services/ipmfs/Cargo.toml +1 -1
  169. data/core/services/ipmfs/LICENSE +16 -0
  170. data/core/services/koofr/Cargo.toml +2 -2
  171. data/core/services/koofr/LICENSE +16 -0
  172. data/core/services/lakefs/Cargo.toml +1 -1
  173. data/core/services/lakefs/LICENSE +16 -0
  174. data/core/services/memcached/Cargo.toml +2 -2
  175. data/core/services/memcached/LICENSE +16 -0
  176. data/core/services/memcached/src/core.rs +3 -3
  177. data/core/services/mini_moka/Cargo.toml +1 -1
  178. data/core/services/mini_moka/LICENSE +16 -0
  179. data/core/services/moka/Cargo.toml +1 -1
  180. data/core/services/moka/LICENSE +16 -0
  181. data/core/services/mongodb/Cargo.toml +2 -2
  182. data/core/services/mongodb/LICENSE +16 -0
  183. data/core/services/monoiofs/Cargo.toml +2 -2
  184. data/core/services/monoiofs/LICENSE +16 -0
  185. data/core/services/monoiofs/src/core.rs +27 -11
  186. data/core/services/mysql/Cargo.toml +2 -2
  187. data/core/services/mysql/LICENSE +16 -0
  188. data/core/services/obs/Cargo.toml +1 -1
  189. data/core/services/obs/LICENSE +16 -0
  190. data/core/services/onedrive/Cargo.toml +2 -2
  191. data/core/services/onedrive/LICENSE +16 -0
  192. data/core/services/opfs/Cargo.toml +1 -1
  193. data/core/services/opfs/LICENSE +16 -0
  194. data/core/services/oss/Cargo.toml +1 -1
  195. data/core/services/oss/LICENSE +16 -0
  196. data/core/services/pcloud/Cargo.toml +1 -1
  197. data/core/services/pcloud/LICENSE +16 -0
  198. data/core/services/persy/Cargo.toml +1 -1
  199. data/core/services/persy/LICENSE +16 -0
  200. data/core/services/postgresql/Cargo.toml +2 -2
  201. data/core/services/postgresql/LICENSE +16 -0
  202. data/core/services/redb/Cargo.toml +1 -1
  203. data/core/services/redb/LICENSE +16 -0
  204. data/core/services/redis/Cargo.toml +2 -2
  205. data/core/services/redis/LICENSE +16 -0
  206. data/core/services/redis/src/core.rs +1 -1
  207. data/core/services/rocksdb/Cargo.toml +1 -1
  208. data/core/services/rocksdb/LICENSE +16 -0
  209. data/core/services/s3/Cargo.toml +1 -1
  210. data/core/services/s3/LICENSE +16 -0
  211. data/core/services/seafile/Cargo.toml +4 -2
  212. data/core/services/seafile/LICENSE +16 -0
  213. data/core/services/seafile/src/backend.rs +2 -3
  214. data/core/services/seafile/src/core.rs +189 -86
  215. data/core/services/seafile/src/docs.md +2 -0
  216. data/core/services/sftp/Cargo.toml +2 -2
  217. data/core/services/sftp/LICENSE +16 -0
  218. data/core/services/sftp/src/core.rs +1 -1
  219. data/core/services/sftp/src/reader.rs +1 -1
  220. data/core/services/sled/Cargo.toml +1 -1
  221. data/core/services/sled/LICENSE +16 -0
  222. data/core/services/sqlite/Cargo.toml +2 -2
  223. data/core/services/sqlite/LICENSE +16 -0
  224. data/core/services/surrealdb/Cargo.toml +2 -2
  225. data/core/services/surrealdb/LICENSE +16 -0
  226. data/core/services/swift/Cargo.toml +1 -1
  227. data/core/services/swift/LICENSE +16 -0
  228. data/core/services/tikv/Cargo.toml +2 -2
  229. data/core/services/tikv/LICENSE +16 -0
  230. data/core/services/tos/Cargo.toml +1 -1
  231. data/core/services/tos/LICENSE +16 -0
  232. data/core/services/upyun/Cargo.toml +1 -1
  233. data/core/services/upyun/LICENSE +16 -0
  234. data/core/services/vercel-artifacts/Cargo.toml +1 -1
  235. data/core/services/vercel-artifacts/LICENSE +16 -0
  236. data/core/services/vercel-blob/Cargo.toml +1 -1
  237. data/core/services/vercel-blob/LICENSE +16 -0
  238. data/core/services/webdav/Cargo.toml +1 -2
  239. data/core/services/webdav/LICENSE +16 -0
  240. data/core/services/webhdfs/Cargo.toml +2 -2
  241. data/core/services/webhdfs/LICENSE +16 -0
  242. data/core/services/yandex-disk/Cargo.toml +1 -1
  243. data/core/services/yandex-disk/LICENSE +16 -0
  244. data/core/src/lib.rs +1 -3
  245. data/core/testkit/Cargo.toml +4 -4
  246. data/core/testkit/LICENSE +16 -0
  247. data/core/upgrade.md +8 -0
  248. metadata +5 -2
@@ -16,94 +16,25 @@
16
16
  // under the License.
17
17
 
18
18
  use super::backend::*;
19
- use super::core::GoosefsCore;
20
19
  use super::core::{ErrorContext, parse_error};
21
20
  use goosefs_sdk::io::GoosefsFileReader as SdkReader;
22
21
  use opendal_core::raw::*;
23
22
  use opendal_core::*;
24
- use std::sync::Arc;
25
23
 
26
24
  pub struct GoosefsReadStream {
27
- core: Arc<GoosefsCore>,
28
- path: String,
29
- range: BytesRange,
30
- content_length: Option<u64>,
31
- /// Lazily-opened SDK reader. `None` until the first `read()` call.
32
- inner: Option<SdkReader>,
33
- /// Terminal flag: once the underlying stream has returned `None`,
34
- /// subsequent `read()` calls must keep returning an empty `Buffer`
35
- /// without re-entering the SDK (which would try to open a fresh
36
- /// stream and re-read the file).
25
+ inner: SdkReader,
26
+ // The SDK may reopen its stream after EOF; keep OpenDAL's EOF terminal.
37
27
  done: bool,
38
28
  }
39
29
 
40
- impl GoosefsReadStream {
41
- pub fn new(
42
- core: Arc<GoosefsCore>,
43
- path: String,
44
- range: BytesRange,
45
- content_length: Option<u64>,
46
- ) -> Self {
47
- GoosefsReadStream {
48
- core,
49
- path,
50
- range,
51
- content_length,
52
- inner: None,
53
- done: false,
54
- }
55
- }
56
-
57
- /// Open the underlying SDK reader, picking between the full-file
58
- /// and ranged variants based on `self.range`.
59
- async fn open(&self) -> Result<SdkReader> {
60
- let offset = self.range.offset();
61
- let size = self.range.size();
62
-
63
- // Three cases:
64
- // 1. No offset and no size → full-file stream.
65
- // 2. Offset+size both set → ranged stream.
66
- // 3. Offset set, size unbounded → resolve tail length via
67
- // get_status and fall through to the ranged opener, so we
68
- // still get per-block streaming (rather than buffering the
69
- // whole tail in one call).
70
- match (offset, size) {
71
- (0, None) => self.core.open_reader(&self.path).await,
72
- (off, Some(len)) => self.core.open_range_reader(&self.path, off, len).await,
73
- (off, None) => {
74
- let content_length = self.content_length.ok_or_else(|| {
75
- Error::new(
76
- ErrorKind::Unexpected,
77
- "content length must be known for offset reads",
78
- )
79
- })?;
80
- let len = content_length.saturating_sub(off);
81
- if len == 0 {
82
- // Empty tail — short-circuit with a zero-length
83
- // ranged open so the very next `read_next_block`
84
- // call returns `None` and we EOF cleanly.
85
- self.core.open_range_reader(&self.path, off, 0).await
86
- } else {
87
- self.core.open_range_reader(&self.path, off, len).await
88
- }
89
- }
90
- }
91
- }
92
- }
93
-
94
30
  impl oio::ReadStream for GoosefsReadStream {
95
31
  async fn read(&mut self) -> Result<Buffer> {
96
32
  if self.done {
97
33
  return Ok(Buffer::new());
98
34
  }
99
35
 
100
- // Lazy open on first call.
101
- if self.inner.is_none() {
102
- self.inner = Some(self.open().await?);
103
- }
104
- let reader = self.inner.as_mut().expect("inner was just set");
105
-
106
- match reader
36
+ match self
37
+ .inner
107
38
  .read_next_block()
108
39
  .await
109
40
  .map_err(|err| parse_error(ErrorContext::new(ServiceOperation("ReadNextBlock")), err))?
@@ -137,25 +68,27 @@ impl oio::StreamRead for GoosefsReader {
137
68
  let backend = &self.backend;
138
69
  let path = self.path.as_str();
139
70
 
140
- let content_length = if range.offset() != 0 && range.size().is_none() {
141
- let file_info = backend.core.get_status(path).await?;
142
- Some(
143
- backend
71
+ let inner = match (range.offset(), range.size()) {
72
+ (0, None) => backend.core.open_reader(path).await?,
73
+ (offset, Some(length)) => backend.core.open_range_reader(path, offset, length).await?,
74
+ (offset, None) => {
75
+ let file_info = backend.core.get_status(path).await?;
76
+ let content_length = backend
144
77
  .core
145
78
  .file_info_to_metadata(&file_info)?
146
- .content_length(),
147
- )
148
- } else {
149
- None
79
+ .content_length();
80
+ backend
81
+ .core
82
+ .open_range_reader(path, offset, content_length.saturating_sub(offset))
83
+ .await?
84
+ }
150
85
  };
151
- let rp = RpRead::default();
152
- let stream = GoosefsReadStream::new(
153
- backend.core.clone(),
154
- path.to_string(),
155
- range,
156
- content_length,
157
- );
86
+ let metadata = backend.core.file_info_to_metadata(inner.file_info())?;
87
+ let stream = GoosefsReadStream { inner, done: false };
158
88
 
159
- Ok((rp, Box::new(stream) as Box<dyn oio::ReadStreamDyn>))
89
+ Ok((
90
+ RpRead::new(metadata),
91
+ Box::new(stream) as Box<dyn oio::ReadStreamDyn>,
92
+ ))
160
93
  }
161
94
  }
@@ -0,0 +1,103 @@
1
+ // Licensed to the Apache Software Foundation (ASF) under one
2
+ // or more contributor license agreements. See the NOTICE file
3
+ // distributed with this work for additional information
4
+ // regarding copyright ownership. The ASF licenses this file
5
+ // to you under the Apache License, Version 2.0 (the
6
+ // "License"); you may not use this file except in compliance
7
+ // with the License. You may obtain a copy of the License at
8
+ //
9
+ // http://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing,
12
+ // software distributed under the License is distributed on an
13
+ // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ // KIND, either express or implied. See the License for the
15
+ // specific language governing permissions and limitations
16
+ // under the License.
17
+
18
+ //! End-to-end master-address resolution against a live GooseFS master.
19
+ //!
20
+ //! Requires a live GooseFS master. Skip unless `OPENDAL_GOOSEFS_MASTER_ADDR`
21
+ //! is set (same env the behavior CI fixture exports).
22
+ //!
23
+ //! The whole file is one test because it mutates process-global `GOOSEFS_*`
24
+ //! environment variables.
25
+
26
+ use std::time::Duration;
27
+
28
+ use opendal::Operator;
29
+
30
+ /// A dummy address from TEST-NET-1 (RFC 5737): guaranteed not to host a
31
+ /// GooseFS master.
32
+ const DUMMY_AUTHORITY: &str = "192.0.2.5:9999";
33
+
34
+ /// Bound for an operation expected to fail against [`DUMMY_AUTHORITY`]. The
35
+ /// SDK retries master inquiry for far longer than a test should wait, so a
36
+ /// timeout counts as "did not reach a master".
37
+ const DIAL_FAILURE_TIMEOUT: Duration = Duration::from_secs(20);
38
+
39
+ /// `goosefs-site.properties` supplies the master address even when the URI
40
+ /// authority points somewhere unreachable.
41
+ ///
42
+ /// Reported failure: a client configured through `$GOOSEFS_CONFIG_FILE` still
43
+ /// dialed the URI authority and failed with `gRPC transport error`, so a
44
+ /// deployment's HA master list never took effect.
45
+ #[tokio::test]
46
+ async fn site_properties_supply_the_master_address() {
47
+ let Ok(master) = std::env::var("OPENDAL_GOOSEFS_MASTER_ADDR") else {
48
+ eprintln!("skip: OPENDAL_GOOSEFS_MASTER_ADDR unset");
49
+ return;
50
+ };
51
+ if master.trim().is_empty() {
52
+ eprintln!("skip: OPENDAL_GOOSEFS_MASTER_ADDR is empty");
53
+ return;
54
+ }
55
+ let root = std::env::var("OPENDAL_GOOSEFS_ROOT").unwrap_or_else(|_| "/".to_string());
56
+ let uri = format!("goosefs://{DUMMY_AUTHORITY}{root}");
57
+
58
+ // `GOOSEFS_MASTER_ADDR` outranks both sources under test, and a site file
59
+ // deployed on the host would decide the control case below.
60
+ unsafe {
61
+ std::env::remove_var("GOOSEFS_MASTER_ADDR");
62
+ std::env::set_var(
63
+ "GOOSEFS_CONFIG_FILE",
64
+ "/nonexistent/goosefs-site.properties",
65
+ );
66
+ }
67
+
68
+ // Control: without a site file the URI authority is used, and writing
69
+ // through it must not reach any master.
70
+ let op = Operator::from_uri(uri.as_str()).expect("build operator from dummy URI");
71
+ let control = tokio::time::timeout(DIAL_FAILURE_TIMEOUT, op.write("resolution-control", "x"))
72
+ .await
73
+ .map_err(|_| "timed out");
74
+ assert!(
75
+ !matches!(control, Ok(Ok(_))),
76
+ "{DUMMY_AUTHORITY} must not serve a master, otherwise this test proves nothing"
77
+ );
78
+
79
+ // A site file that lists the live master must win over the URI authority.
80
+ let site_file = std::env::temp_dir().join(format!(
81
+ "opendal_goosefs_resolution_{}_site.properties",
82
+ std::process::id()
83
+ ));
84
+ std::fs::write(
85
+ &site_file,
86
+ format!("goosefs.master.rpc.addresses={master}\n"),
87
+ )
88
+ .expect("write goosefs-site.properties");
89
+ unsafe { std::env::set_var("GOOSEFS_CONFIG_FILE", &site_file) };
90
+
91
+ let op =
92
+ Operator::from_uri(uri.as_str()).expect("build operator from dummy URI with a site file");
93
+ let path = format!("resolution-{}", std::process::id());
94
+ op.write(&path, "site-properties-win")
95
+ .await
96
+ .expect("write must reach the master listed in goosefs-site.properties");
97
+ let body = op.read(&path).await.expect("read back").to_bytes();
98
+ assert_eq!(&body[..], b"site-properties-win");
99
+
100
+ let _ = op.delete(&path).await;
101
+ let _ = std::fs::remove_file(&site_file);
102
+ unsafe { std::env::remove_var("GOOSEFS_CONFIG_FILE") };
103
+ }
@@ -32,10 +32,10 @@ version = { workspace = true }
32
32
  all-features = true
33
33
 
34
34
  [dependencies]
35
- asyncband = { workspace = true }
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.0", default-features = false }
38
+ opendal-core = { path = "../../core", version = "0.59.2", default-features = false }
39
39
  serde = { workspace = true, features = ["derive"] }
40
40
 
41
41
  [dev-dependencies]
@@ -199,3 +199,19 @@
199
199
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
200
  See the License for the specific language governing permissions and
201
201
  limitations under the License.
202
+
203
+
204
+ ------------------------------------------------------------------------------------
205
+ This product bundles third-party components under other open source licenses.
206
+ This section summarizes those components and their licenses. See the referenced
207
+ license files for text of these licenses.
208
+
209
+
210
+ Creative Commons Zero v1.0 Universal
211
+ ------------------------------------
212
+
213
+ bindings/dotnet/.gitignore
214
+
215
+ The above file is derived from VisualStudio.gitignore in
216
+ https://github.com/github/gitignore. Its license text is in
217
+ bindings/dotnet/LICENSE-CC0-1.0.txt.
@@ -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.0", default-features = false }
35
+ opendal-core = { path = "../../core", version = "0.59.2", default-features = false }
36
36
 
37
37
  bytes = { workspace = true }
38
38
  futures = { workspace = true }
@@ -199,3 +199,19 @@
199
199
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
200
  See the License for the specific language governing permissions and
201
201
  limitations under the License.
202
+
203
+
204
+ ------------------------------------------------------------------------------------
205
+ This product bundles third-party components under other open source licenses.
206
+ This section summarizes those components and their licenses. See the referenced
207
+ license files for text of these licenses.
208
+
209
+
210
+ Creative Commons Zero v1.0 Universal
211
+ ------------------------------------
212
+
213
+ bindings/dotnet/.gitignore
214
+
215
+ The above file is derived from VisualStudio.gitignore in
216
+ https://github.com/github/gitignore. Its license text is in
217
+ bindings/dotnet/LICENSE-CC0-1.0.txt.
@@ -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.0", default-features = false }
39
+ opendal-core = { path = "../../core", version = "0.59.2", default-features = false }
40
40
  serde = { workspace = true, features = ["derive"] }
@@ -199,3 +199,19 @@
199
199
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
200
  See the License for the specific language governing permissions and
201
201
  limitations under the License.
202
+
203
+
204
+ ------------------------------------------------------------------------------------
205
+ This product bundles third-party components under other open source licenses.
206
+ This section summarizes those components and their licenses. See the referenced
207
+ license files for text of these licenses.
208
+
209
+
210
+ Creative Commons Zero v1.0 Universal
211
+ ------------------------------------
212
+
213
+ bindings/dotnet/.gitignore
214
+
215
+ The above file is derived from VisualStudio.gitignore in
216
+ https://github.com/github/gitignore. Its license text is in
217
+ bindings/dotnet/LICENSE-CC0-1.0.txt.
@@ -32,12 +32,12 @@ version = { workspace = true }
32
32
  all-features = true
33
33
 
34
34
  [dependencies]
35
- asyncband = { workspace = true }
35
+ asyncband = { workspace = true, features = ["mutex", "once-cell"] }
36
36
  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.0", default-features = false }
40
+ opendal-core = { path = "../../core", version = "0.59.2", default-features = false }
41
41
  percent-encoding = "2"
42
42
  serde = { workspace = true, features = ["derive"] }
43
43
  serde_json = { workspace = true }
@@ -45,6 +45,6 @@ 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.0" }
48
+ opendal-http-transport-reqwest = { path = "../../http-transports/reqwest", version = "0.59.2" }
49
49
  serde_json = { workspace = true }
50
50
  tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
@@ -199,3 +199,19 @@
199
199
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
200
  See the License for the specific language governing permissions and
201
201
  limitations under the License.
202
+
203
+
204
+ ------------------------------------------------------------------------------------
205
+ This product bundles third-party components under other open source licenses.
206
+ This section summarizes those components and their licenses. See the referenced
207
+ license files for text of these licenses.
208
+
209
+
210
+ Creative Commons Zero v1.0 Universal
211
+ ------------------------------------
212
+
213
+ bindings/dotnet/.gitignore
214
+
215
+ The above file is derived from VisualStudio.gitignore in
216
+ https://github.com/github/gitignore. Its license text is in
217
+ bindings/dotnet/LICENSE-CC0-1.0.txt.
@@ -133,6 +133,22 @@ impl HfBuilder {
133
133
  self
134
134
  }
135
135
 
136
+ /// Enable caching of resolved HTTP download addresses and XET file metadata.
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.
141
+ ///
142
+ /// Enable this only when previously written files are not modified. Changed
143
+ /// files can remain invisible while cached results are reused, including
144
+ /// changes from other clients or a floating repository revision. Issued
145
+ /// download URLs can remain usable until expiry after Hub permissions change.
146
+ /// Separate authorization identities must use separately constructed backends.
147
+ pub fn enable_resolve_cache(mut self, enabled: bool) -> Self {
148
+ self.config.enable_resolve_cache = enabled;
149
+ self
150
+ }
151
+
136
152
  /// Set the Hub base URL.
137
153
  ///
138
154
  /// Configure this when your organization uses a
@@ -146,12 +162,18 @@ impl HfBuilder {
146
162
  self
147
163
  }
148
164
 
165
+ /// Resolve the Hub base URL: an explicit config value wins, then
166
+ /// `HF_ENDPOINT`, then the public Hub. A trailing slash is trimmed
167
+ /// because every URL is built by appending `/api/...` to this, and HF
168
+ /// answers the resulting `//api/...` with a 404.
149
169
  fn hf_endpoint(&self) -> String {
150
170
  self.config
151
171
  .endpoint
152
172
  .clone()
153
173
  .or_else(|| std::env::var("HF_ENDPOINT").ok())
154
174
  .unwrap_or_else(|| "https://huggingface.co".to_string())
175
+ .trim_end_matches('/')
176
+ .to_string()
155
177
  }
156
178
 
157
179
  /// Resolve the download mode: an explicit config value wins; otherwise a set,
@@ -260,16 +282,10 @@ impl Builder for HfBuilder {
260
282
  let repo = HfRepo::new(repo_type, repo_id, Some(revision.clone()));
261
283
  debug!("backend repo uri: {:?}", repo.uri(&root, ""));
262
284
 
285
+ let mut core = HfCore::build(info, capability, repo, root, token, endpoint, download_mode)?;
286
+ core.enable_resolve_cache = self.config.enable_resolve_cache;
263
287
  Ok(HfBackend {
264
- core: Arc::new(HfCore::build(
265
- info,
266
- capability,
267
- repo,
268
- root,
269
- token,
270
- endpoint,
271
- download_mode,
272
- )?),
288
+ core: Arc::new(core),
273
289
  })
274
290
  }
275
291
  }
@@ -435,6 +451,19 @@ pub(super) mod test_utils {
435
451
  finish_operator(op)
436
452
  }
437
453
 
454
+ /// Same public dataset as [`mbpp_operator`], but with the repo id cased
455
+ /// differently from the canonical `google-research-datasets/mbpp`. HF
456
+ /// answers every request for it with a `307` to the canonical URL.
457
+ pub fn miscased_mbpp_operator() -> Operator {
458
+ let op = Operator::new(
459
+ HfBuilder::default()
460
+ .repo_type("dataset")
461
+ .repo_id("Google-Research-Datasets/MBPP"),
462
+ )
463
+ .unwrap();
464
+ finish_operator(op)
465
+ }
466
+
438
467
  pub fn testing_dataset_core() -> Arc<HfCore> {
439
468
  let repo_id = std::env::var("HF_OPENDAL_DATASET").expect("HF_OPENDAL_DATASET must be set");
440
469
  let token = std::env::var("HF_OPENDAL_TOKEN").expect("HF_OPENDAL_TOKEN must be set");
@@ -538,6 +567,18 @@ mod tests {
538
567
  assert_eq!(result.as_deref(), Some("file-token"));
539
568
  }
540
569
 
570
+ #[test]
571
+ fn hf_endpoint_trims_trailing_slash() {
572
+ let _guard = ENV_LOCK.lock().unwrap();
573
+ unsafe { std::env::remove_var("HF_ENDPOINT") };
574
+ assert_eq!(
575
+ HfBuilder::default()
576
+ .endpoint("https://hub.example.com/")
577
+ .hf_endpoint(),
578
+ "https://hub.example.com"
579
+ );
580
+ }
581
+
541
582
  #[test]
542
583
  fn hf_endpoint_returns_default() {
543
584
  let _guard = ENV_LOCK.lock().unwrap();
@@ -600,6 +641,33 @@ mod tests {
600
641
  assert_eq!(mode, HfDownloadMode::Xet);
601
642
  }
602
643
 
644
+ #[tokio::test]
645
+ async fn build_resolve_cache_requires_opt_in() -> Result<()> {
646
+ use super::super::core::test_utils::MockHttpTransport;
647
+
648
+ for enabled in [None, Some(false), Some(true)] {
649
+ let mut builder = HfBuilder::default()
650
+ .repo_type("model")
651
+ .repo_id("org/repo")
652
+ .download_mode("xet");
653
+ if let Some(enabled) = enabled {
654
+ builder = builder.enable_resolve_cache(enabled);
655
+ }
656
+ let transport = MockHttpTransport::new();
657
+ let ctx = OperationContext::new()
658
+ .with_http_transport(HttpTransporter::new(transport.clone()));
659
+ let op = Operator::new(builder)?.with_context(ctx);
660
+ for reads in 1..=2 {
661
+ assert_eq!(op.read("plain.txt").await?.to_vec(), b"hello");
662
+ assert_eq!(
663
+ transport.request_count(),
664
+ reads + usize::from(enabled == Some(true))
665
+ );
666
+ }
667
+ }
668
+ Ok(())
669
+ }
670
+
603
671
  #[test]
604
672
  fn build_accepts_datasets_alias() {
605
673
  HfBuilder::default()
@@ -60,6 +60,13 @@ pub struct HfConfig {
60
60
  ///
61
61
  /// See <https://huggingface.co/docs/huggingface_hub/package_reference/environment_variables#hfhubdisablexet>.
62
62
  pub download_mode: Option<HfDownloadMode>,
63
+ /// Enable caching of resolved HTTP download addresses and XET file metadata.
64
+ ///
65
+ /// Defaults to `false`. Set to `true` to share resolve results across readers
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.
69
+ pub enable_resolve_cache: bool,
63
70
  }
64
71
 
65
72
  impl Debug for HfConfig {
@@ -73,6 +80,7 @@ impl Debug for HfConfig {
73
80
  .field("revision", &self.revision)
74
81
  .field("root", &self.root)
75
82
  .field("download_mode", &self.download_mode)
83
+ .field("enable_resolve_cache", &self.enable_resolve_cache)
76
84
  .finish_non_exhaustive()
77
85
  }
78
86
  }
@@ -106,6 +114,19 @@ impl opendal_core::Configurator for HfConfig {
106
114
  .map(|s| HfDownloadMode::parse(s))
107
115
  .transpose()?;
108
116
 
117
+ let enable_resolve_cache = opts
118
+ .get("enable_resolve_cache")
119
+ .map(|value| value.parse::<bool>())
120
+ .transpose()
121
+ .map_err(|err| {
122
+ opendal_core::Error::new(
123
+ opendal_core::ErrorKind::ConfigInvalid,
124
+ "enable_resolve_cache must be true or false",
125
+ )
126
+ .set_source(err)
127
+ })?
128
+ .unwrap_or_default();
129
+
109
130
  if !path.is_empty() {
110
131
  // Full URI like "hf://datasets/user/repo@rev/path"
111
132
  let parsed = HfUri::parse(&path)?;
@@ -117,6 +138,7 @@ impl opendal_core::Configurator for HfConfig {
117
138
  token: opts.get("token").cloned(),
118
139
  endpoint: opts.get("endpoint").cloned(),
119
140
  download_mode,
141
+ enable_resolve_cache,
120
142
  })
121
143
  } else {
122
144
  // Bare scheme from via_iter, all config is in options.
@@ -138,6 +160,7 @@ impl opendal_core::Configurator for HfConfig {
138
160
  token: opts.get("token").cloned(),
139
161
  endpoint: opts.get("endpoint").cloned(),
140
162
  download_mode,
163
+ enable_resolve_cache,
141
164
  })
142
165
  }
143
166
  }
@@ -222,4 +245,39 @@ mod tests {
222
245
  let cfg = HfConfig::from_uri(&uri).unwrap();
223
246
  assert_eq!(cfg.download_mode.unwrap_or_default(), HfDownloadMode::Xet);
224
247
  }
248
+
249
+ #[test]
250
+ fn from_uri_resolve_cache_options() {
251
+ for scheme in ["hf://datasets/user/repo", "huggingface"] {
252
+ for (value, expected) in [(None, false), (Some("false"), false), (Some("true"), true)] {
253
+ let mut options = vec![("repo_type", "dataset"), ("repo_id", "user/repo")];
254
+ if let Some(value) = value {
255
+ options.push(("enable_resolve_cache", value));
256
+ }
257
+ let uri = OperatorUri::new(
258
+ scheme,
259
+ options
260
+ .into_iter()
261
+ .map(|(key, value)| (key.to_string(), value.to_string())),
262
+ )
263
+ .unwrap();
264
+ assert_eq!(
265
+ HfConfig::from_uri(&uri).unwrap().enable_resolve_cache,
266
+ expected
267
+ );
268
+ }
269
+
270
+ let uri = OperatorUri::new(
271
+ scheme,
272
+ [("enable_resolve_cache".to_string(), "invalid".to_string())],
273
+ )
274
+ .unwrap();
275
+ let err = HfConfig::from_uri(&uri).unwrap_err();
276
+ assert_eq!(err.kind(), opendal_core::ErrorKind::ConfigInvalid);
277
+ assert!(
278
+ err.to_string()
279
+ .contains("enable_resolve_cache must be true or false")
280
+ );
281
+ }
282
+ }
225
283
  }