opendal 0.1.9 → 0.1.11

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