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
@@ -58,6 +58,10 @@ pub mod constants {
58
58
  pub const X_GOOG_ACL: &str = "x-goog-acl";
59
59
  pub const X_GOOG_STORAGE_CLASS: &str = "x-goog-storage-class";
60
60
  pub const X_GOOG_META_PREFIX: &str = "x-goog-meta-";
61
+
62
+ pub const RESPONSE_CONTENT_DISPOSITION: &str = "response-content-disposition";
63
+ pub const RESPONSE_CONTENT_TYPE: &str = "response-content-type";
64
+ pub const RESPONSE_CACHE_CONTROL: &str = "response-cache-control";
61
65
  }
62
66
 
63
67
  pub struct GcsCore {
@@ -211,7 +215,33 @@ impl GcsCore {
211
215
  ) -> Result<Request<Buffer>> {
212
216
  let p = build_abs_path(&self.root, path);
213
217
 
214
- let url = format!("{}/{}/{}", self.endpoint, self.bucket, p);
218
+ let mut url = format!("{}/{}/{}", self.endpoint, self.bucket, p);
219
+
220
+ let mut query_args = Vec::new();
221
+ if let Some(override_content_disposition) = args.override_content_disposition() {
222
+ query_args.push(format!(
223
+ "{}={}",
224
+ constants::RESPONSE_CONTENT_DISPOSITION,
225
+ percent_encode_path(override_content_disposition)
226
+ ))
227
+ }
228
+ if let Some(override_content_type) = args.override_content_type() {
229
+ query_args.push(format!(
230
+ "{}={}",
231
+ constants::RESPONSE_CONTENT_TYPE,
232
+ percent_encode_path(override_content_type)
233
+ ))
234
+ }
235
+ if let Some(override_cache_control) = args.override_cache_control() {
236
+ query_args.push(format!(
237
+ "{}={}",
238
+ constants::RESPONSE_CACHE_CONTROL,
239
+ percent_encode_path(override_cache_control)
240
+ ))
241
+ }
242
+ if !query_args.is_empty() {
243
+ url.push_str(&format!("?{}", query_args.join("&")));
244
+ }
215
245
 
216
246
  let mut req = Request::get(&url);
217
247
 
@@ -575,7 +605,33 @@ impl GcsCore {
575
605
  ) -> Result<Request<Buffer>> {
576
606
  let p = build_abs_path(&self.root, path);
577
607
 
578
- let url = format!("{}/{}/{}", self.endpoint, self.bucket, p);
608
+ let mut url = format!("{}/{}/{}", self.endpoint, self.bucket, p);
609
+
610
+ let mut query_args = Vec::new();
611
+ if let Some(override_content_disposition) = args.override_content_disposition() {
612
+ query_args.push(format!(
613
+ "{}={}",
614
+ constants::RESPONSE_CONTENT_DISPOSITION,
615
+ percent_encode_path(override_content_disposition)
616
+ ))
617
+ }
618
+ if let Some(override_content_type) = args.override_content_type() {
619
+ query_args.push(format!(
620
+ "{}={}",
621
+ constants::RESPONSE_CONTENT_TYPE,
622
+ percent_encode_path(override_content_type)
623
+ ))
624
+ }
625
+ if let Some(override_cache_control) = args.override_cache_control() {
626
+ query_args.push(format!(
627
+ "{}={}",
628
+ constants::RESPONSE_CACHE_CONTROL,
629
+ percent_encode_path(override_cache_control)
630
+ ))
631
+ }
632
+ if !query_args.is_empty() {
633
+ url.push_str(&format!("?{}", query_args.join("&")));
634
+ }
579
635
 
580
636
  let mut req = Request::head(&url);
581
637
 
@@ -1618,6 +1674,64 @@ mod tests {
1618
1674
  assert_eq!(create.headers()["x-goog-if-generation-match"], "0");
1619
1675
  }
1620
1676
 
1677
+ #[test]
1678
+ fn test_gcs_get_object_xml_request_with_response_overrides() {
1679
+ let core = test_core();
1680
+
1681
+ let read = core
1682
+ .gcs_get_object_xml_request(
1683
+ "object",
1684
+ BytesRange::default(),
1685
+ &read_args(options::ReadOptions {
1686
+ override_content_disposition: Some(
1687
+ "attachment; filename=\"other.txt\"".to_string(),
1688
+ ),
1689
+ override_content_type: Some("application/pdf".to_string()),
1690
+ override_cache_control: Some("max-age=3600".to_string()),
1691
+ ..Default::default()
1692
+ }),
1693
+ )
1694
+ .expect("read request must build");
1695
+
1696
+ let query = read.uri().query().unwrap();
1697
+ assert!(
1698
+ query.contains(
1699
+ "response-content-disposition=attachment%3B%20filename%3D%22other.txt%22"
1700
+ )
1701
+ );
1702
+ assert!(query.contains("response-content-type=application/pdf"));
1703
+ assert!(query.contains("response-cache-control=max-age%3D3600"));
1704
+ }
1705
+
1706
+ #[test]
1707
+ fn test_gcs_head_object_xml_request_with_response_overrides() {
1708
+ let core = test_core();
1709
+
1710
+ let stat = core
1711
+ .gcs_head_object_xml_request(
1712
+ "object",
1713
+ &options::StatOptions {
1714
+ override_content_disposition: Some(
1715
+ "attachment; filename=\"other.txt\"".to_string(),
1716
+ ),
1717
+ override_content_type: Some("application/pdf".to_string()),
1718
+ override_cache_control: Some("max-age=3600".to_string()),
1719
+ ..Default::default()
1720
+ }
1721
+ .into(),
1722
+ )
1723
+ .expect("stat request must build");
1724
+
1725
+ let query = stat.uri().query().unwrap();
1726
+ assert!(
1727
+ query.contains(
1728
+ "response-content-disposition=attachment%3B%20filename%3D%22other.txt%22"
1729
+ )
1730
+ );
1731
+ assert!(query.contains("response-content-type=application/pdf"));
1732
+ assert!(query.contains("response-cache-control=max-age%3D3600"));
1733
+ }
1734
+
1621
1735
  #[test]
1622
1736
  fn test_parse_missing_target_with_version_condition() {
1623
1737
  let parse_missing = |ctx| {
@@ -36,7 +36,7 @@ bytes = { workspace = true }
36
36
  futures = { workspace = true, features = ["async-await", "std"] }
37
37
  http = { workspace = true }
38
38
  log = { workspace = true }
39
- opendal-core = { path = "../../core", version = "0.59.0", default-features = false, features = [
39
+ opendal-core = { path = "../../core", version = "0.59.2", default-features = false, features = [
40
40
  "reqsign",
41
41
  ] }
42
42
  percent-encoding = "2"
@@ -1,21 +1,20 @@
1
- /*
2
- * Licensed to the Apache Software Foundation (ASF) under one
3
- * or more contributor license agreements. See the NOTICE file
4
- * distributed with this work for additional information
5
- * regarding copyright ownership. The ASF licenses this file
6
- * to you under the Apache License, Version 2.0 (the
7
- * "License"); you may not use this file except in compliance
8
- * with the License. You may obtain a copy of the License at
9
- *
10
- * http://www.apache.org/licenses/LICENSE-2.0
11
- *
12
- * Unless required by applicable law or agreed to in writing,
13
- * software distributed under the License is distributed on an
14
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
- * KIND, either express or implied. See the License for the
16
- * specific language governing permissions and limitations
17
- * under the License.
18
- */
1
+ // Copyright 2026 Google LLC
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ // Modified by the Apache Software Foundation: retain the wire-compatible subset
16
+ // used by OpenDAL, with unused definitions, annotations, and imports removed.
17
+ // Source: https://github.com/googleapis/googleapis/blob/2e9c5681901a2eebf7f547f0b60c895b1732415e/google/storage/v2/storage.proto
19
18
 
20
19
  syntax = "proto3";
21
20
 
@@ -32,9 +32,9 @@ 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
- asyncband = { workspace = true }
37
+ asyncband = { workspace = true, features = ["mutex", "singleflight"] }
38
38
  bytes = { workspace = true }
39
39
  http = { workspace = true }
40
40
  log = { workspace = true }
@@ -42,4 +42,5 @@ serde = { workspace = true, features = ["derive"] }
42
42
  serde_json = { workspace = true }
43
43
 
44
44
  [dev-dependencies]
45
+ asyncband = { workspace = true, features = ["semaphore"] }
45
46
  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.
@@ -21,6 +21,8 @@ Google Drive allows duplicate file or directory names under the same parent.
21
21
  When multiple entries match the same path, OpenDAL resolves the most recently
22
22
  modified match, falling back to the newer creation time if needed.
23
23
 
24
+ Concurrent directory creation through an operator and its clones reuses work for shared ancestors and allows independent paths to progress concurrently.
25
+
24
26
  ## Configuration
25
27
 
26
28
  Use [`crate::GdriveConfig`] for serializable configuration and this builder's
@@ -19,6 +19,7 @@ use std::collections::HashMap;
19
19
  use std::collections::VecDeque;
20
20
 
21
21
  use asyncband::mutex::Mutex;
22
+ use asyncband::singleflight::Group;
22
23
 
23
24
  use super::core::normalize_dir_path;
24
25
  use opendal_core::raw::*;
@@ -52,7 +53,7 @@ struct GdrivePathIndexState {
52
53
  pub struct GdrivePathIndex<Q: GdrivePathQueryer> {
53
54
  query: Q,
54
55
  state: Mutex<GdrivePathIndexState>,
55
- ensure_dir_lock: Mutex<()>,
56
+ ensure_dirs: Group<(String, String), String>,
56
57
  }
57
58
 
58
59
  impl<Q: GdrivePathQueryer> GdrivePathIndex<Q> {
@@ -60,7 +61,7 @@ impl<Q: GdrivePathQueryer> GdrivePathIndex<Q> {
60
61
  Self {
61
62
  query,
62
63
  state: Mutex::new(GdrivePathIndexState::default()),
63
- ensure_dir_lock: Mutex::new(()),
64
+ ensure_dirs: Group::new(),
64
65
  }
65
66
  }
66
67
 
@@ -253,8 +254,6 @@ impl<Q: GdrivePathQueryer> GdrivePathIndex<Q> {
253
254
 
254
255
  /// Ensure input dir exists.
255
256
  pub async fn ensure_dir(&self, ctx: &OperationContext, path: &str) -> Result<String> {
256
- let _guard = self.ensure_dir_lock.lock().await;
257
-
258
257
  let path = Self::canonical_dir_path(path);
259
258
  if path.is_empty() {
260
259
  return self.root_id(ctx).await;
@@ -285,17 +284,28 @@ impl<Q: GdrivePathQueryer> GdrivePathIndex<Q> {
285
284
  }
286
285
 
287
286
  let name = get_basename(parent);
288
- let value = match self.query.query(ctx, &parent_id, name).await {
289
- Ok(Some(value)) => value,
290
- Ok(None) => match self.query.create_dir(ctx, &parent_id, name).await {
291
- Ok(value) => value,
292
- Err(err) if !refreshed && err.kind() == ErrorKind::NotFound => {
293
- refreshed = true;
294
- self.refresh_path(parent).await;
295
- continue 'retry;
287
+ // A refreshed ancestor has a different ID and must not join work for its old ID.
288
+ let result: Result<String> = self
289
+ .ensure_dirs
290
+ .try_work((parent_id.clone(), name.to_string()), async || {
291
+ if let Some(value) = {
292
+ let state = self.state.lock().await;
293
+ Self::lookup_cached_id(&state.entries, parent)
294
+ } {
295
+ return Ok(value);
296
296
  }
297
- Err(err) => return Err(err),
298
- },
297
+
298
+ let value = match self.query.query(ctx, &parent_id, name).await? {
299
+ Some(value) => value,
300
+ None => self.query.create_dir(ctx, &parent_id, name).await?,
301
+ };
302
+ // Publish before completing the flight so a later caller uses the cache.
303
+ self.upsert_dir(parent, &value).await;
304
+ Ok(value)
305
+ })
306
+ .await;
307
+ parent_id = match result {
308
+ Ok(value) => value,
299
309
  Err(err) if !refreshed && err.kind() == ErrorKind::NotFound => {
300
310
  refreshed = true;
301
311
  self.refresh_path(parent).await;
@@ -303,9 +313,6 @@ impl<Q: GdrivePathQueryer> GdrivePathIndex<Q> {
303
313
  }
304
314
  Err(err) => return Err(err),
305
315
  };
306
-
307
- self.upsert_dir(parent, &value).await;
308
- parent_id = value;
309
316
  }
310
317
 
311
318
  return Ok(parent_id);
@@ -317,11 +324,15 @@ impl<Q: GdrivePathQueryer> GdrivePathIndex<Q> {
317
324
  mod tests {
318
325
  use std::collections::HashMap;
319
326
  use std::collections::HashSet;
327
+ use std::future::Future;
320
328
  use std::sync::Arc;
321
329
  use std::sync::atomic::AtomicUsize;
322
330
  use std::sync::atomic::Ordering;
331
+ use std::task::Context;
332
+ use std::task::Waker;
323
333
 
324
334
  use asyncband::mutex::Mutex;
335
+ use asyncband::semaphore::Semaphore;
325
336
 
326
337
  use super::*;
327
338
 
@@ -331,6 +342,7 @@ mod tests {
331
342
  entries: Arc<Mutex<HashMap<(String, String), String>>>,
332
343
  stale_parents: Arc<Mutex<HashSet<String>>>,
333
344
  create_count: Arc<AtomicUsize>,
345
+ create_gate: Option<Arc<Semaphore>>,
334
346
  }
335
347
 
336
348
  impl TestQuery {
@@ -340,6 +352,7 @@ mod tests {
340
352
  entries: Arc::new(Mutex::new(HashMap::new())),
341
353
  stale_parents: Arc::new(Mutex::new(HashSet::new())),
342
354
  create_count: Arc::new(AtomicUsize::new(0)),
355
+ create_gate: None,
343
356
  }
344
357
  }
345
358
 
@@ -390,6 +403,9 @@ mod tests {
390
403
  return Err(Error::new(ErrorKind::NotFound, "stale parent"));
391
404
  }
392
405
  self.create_count.fetch_add(1, Ordering::SeqCst);
406
+ if let Some(gate) = &self.create_gate {
407
+ drop(gate.acquire(1).await);
408
+ }
393
409
  let id = format!("{parent_id}:{name}");
394
410
  self.insert(parent_id, name, &id).await;
395
411
  Ok(id)
@@ -453,7 +469,7 @@ mod tests {
453
469
  }
454
470
 
455
471
  #[tokio::test]
456
- async fn test_ensure_dir_is_serialized() {
472
+ async fn test_ensure_dir_reuses_existing_directory() {
457
473
  let query = TestQuery::new();
458
474
  query.insert("remote-root", "root/", "svc-root").await;
459
475
 
@@ -469,6 +485,81 @@ mod tests {
469
485
  assert_eq!(query.create_count.load(Ordering::SeqCst), 1);
470
486
  }
471
487
 
488
+ #[tokio::test]
489
+ async fn test_ensure_dir_coalesces_shared_ancestors() {
490
+ let gate = Arc::new(Semaphore::new(0));
491
+ let mut query = TestQuery::new();
492
+ query.create_gate = Some(gate.clone());
493
+ query.insert("remote-root", "root/", "svc-root").await;
494
+ let index = GdrivePathIndex::new(query.clone());
495
+ let ctx = OperationContext::new();
496
+ let mut first = Box::pin(index.ensure_dir(&ctx, "root/shared/left"));
497
+ let mut second = Box::pin(index.ensure_dir(&ctx, "root/shared/right"));
498
+ let mut cx = Context::from_waker(Waker::noop());
499
+
500
+ assert!(first.as_mut().poll(&mut cx).is_pending());
501
+ assert!(second.as_mut().poll(&mut cx).is_pending());
502
+ assert_eq!(query.create_count.load(Ordering::SeqCst), 1);
503
+
504
+ gate.release(1);
505
+ let (first, second) = tokio::join!(first, second);
506
+ assert_eq!(first.unwrap(), "svc-root:shared/:left/");
507
+ assert_eq!(second.unwrap(), "svc-root:shared/:right/");
508
+ assert_eq!(query.create_count.load(Ordering::SeqCst), 3);
509
+ }
510
+
511
+ #[tokio::test]
512
+ async fn test_ensure_dir_unrelated_paths_progress_concurrently() {
513
+ for (first_path, second_path) in [("root/left", "root/right"), ("left/dir", "right/dir")] {
514
+ let gate = Arc::new(Semaphore::new(0));
515
+ let mut query = TestQuery::new();
516
+ query.create_gate = Some(gate.clone());
517
+ for name in ["root/", "left/", "right/"] {
518
+ query.insert("remote-root", name, name).await;
519
+ }
520
+ let index = GdrivePathIndex::new(query.clone());
521
+ let ctx = OperationContext::new();
522
+ let mut first = Box::pin(index.ensure_dir(&ctx, first_path));
523
+ let mut second = Box::pin(index.ensure_dir(&ctx, second_path));
524
+ let mut cx = Context::from_waker(Waker::noop());
525
+
526
+ assert!(first.as_mut().poll(&mut cx).is_pending());
527
+ assert!(second.as_mut().poll(&mut cx).is_pending());
528
+ assert_eq!(query.create_count.load(Ordering::SeqCst), 2);
529
+
530
+ gate.release(1);
531
+ let (first, second) = tokio::join!(first, second);
532
+ assert_ne!(first.unwrap(), second.unwrap());
533
+ }
534
+ }
535
+
536
+ #[tokio::test]
537
+ async fn test_ensure_dir_retries_after_creator_is_cancelled() {
538
+ let gate = Arc::new(Semaphore::new(0));
539
+ let mut query = TestQuery::new();
540
+ query.create_gate = Some(gate.clone());
541
+ query.insert("remote-root", "root/", "svc-root").await;
542
+ let index = GdrivePathIndex::new(query.clone());
543
+ let ctx = OperationContext::new();
544
+ let mut first = Box::pin(index.ensure_dir(&ctx, "root/dir"));
545
+ let mut second = Box::pin(index.ensure_dir(&ctx, "root/dir"));
546
+ let mut cx = Context::from_waker(Waker::noop());
547
+
548
+ assert!(first.as_mut().poll(&mut cx).is_pending());
549
+ assert!(second.as_mut().poll(&mut cx).is_pending());
550
+ drop(first);
551
+ assert!(second.as_mut().poll(&mut cx).is_pending());
552
+ assert_eq!(query.create_count.load(Ordering::SeqCst), 2);
553
+
554
+ gate.release(1);
555
+ assert_eq!(second.await.unwrap(), "svc-root:dir/");
556
+ assert_eq!(
557
+ index.ensure_dir(&ctx, "root/dir").await.unwrap(),
558
+ "svc-root:dir/"
559
+ );
560
+ assert_eq!(query.create_count.load(Ordering::SeqCst), 2);
561
+ }
562
+
472
563
  #[tokio::test]
473
564
  async fn test_ensure_dir_refreshes_stale_ancestor_chain() {
474
565
  let query = TestQuery::new();
@@ -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.0", default-features = false }
40
- opendal-service-azblob = { path = "../azblob", version = "0.59.0", default-features = false }
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 }
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 }
@@ -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,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.0", default-features = false }
39
+ opendal-core = { path = "../../core", version = "0.59.2", default-features = false }
40
40
  serde = { workspace = true, features = ["derive"] }
41
41
  serde_json = { workspace = true }
42
42
 
@@ -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.
@@ -31,16 +31,25 @@ version = { workspace = true }
31
31
  [package.metadata.docs.rs]
32
32
  all-features = true
33
33
 
34
+ [features]
35
+ # Keep the default GooseFS client graph to gRPC. Cache backends are opt-in so
36
+ # applications that only need the data plane do not pull `lru`, foyer, or
37
+ # io_uring.
38
+ default = []
39
+ metadata-cache = ["goosefs-sdk/metadata-cache"]
40
+ page-cache = ["goosefs-sdk/page-cache"]
41
+ page-cache-io-uring = ["goosefs-sdk/page-cache-io-uring"]
42
+
34
43
  [dependencies]
44
+ asyncband = { workspace = true, features = ["rwlock"] }
35
45
  bytes = { workspace = true }
36
- goosefs-sdk = "0.1.8"
46
+ goosefs-sdk = { version = "0.2.1", default-features = false }
37
47
  log = { workspace = true }
38
- opendal-core = { path = "../../core", version = "0.59.0", default-features = false }
48
+ opendal-core = { path = "../../core", version = "0.59.2", default-features = false }
39
49
  serde = { workspace = true, features = ["derive"] }
40
- tokio = { workspace = true }
41
50
 
42
51
  [dev-dependencies]
43
- opendal = { path = "../..", version = "0.59.0", features = [
52
+ opendal = { path = "../..", version = "0.59.2", features = [
44
53
  "services-goosefs",
45
54
  ] }
46
55
  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.
@@ -15,12 +15,23 @@ cargo add opendal --features services-goosefs
15
15
  The service is available as `opendal::services::GooseFs`. Configure the
16
16
  service builder, then pass it to `opendal::Operator::new`.
17
17
 
18
+ Optional client caches stay on this crate, not on the `opendal` facade. Add
19
+ this crate as a direct dependency so Cargo unifies the features into the same
20
+ `opendal-service-goosefs` build:
21
+
22
+ ```shell
23
+ cargo add opendal-service-goosefs --features metadata-cache,page-cache
24
+ # Linux io_uring page-cache backend:
25
+ cargo add opendal-service-goosefs --features page-cache-io-uring
26
+ ```
27
+
18
28
  ## Use with `opendal-core`
19
29
 
20
30
  Add the split crates directly:
21
31
 
22
32
  ```shell
23
33
  cargo add opendal-core opendal-service-goosefs
34
+ # Optional caches: cargo add opendal-service-goosefs --features metadata-cache,page-cache
24
35
  ```
25
36
 
26
37
  Pass a configured service builder to `Operator::new`: