Skip to content

Commit f4a2a88

Browse files
authored
Add ObjectMeta::version and GetOptions::version (#4925) (#4935)
1 parent 0b9105d commit f4a2a88

File tree

11 files changed

+75
-9
lines changed

11 files changed

+75
-9
lines changed

object_store/src/aws/client.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use crate::aws::credential::{AwsCredential, CredentialExt};
2020
use crate::aws::{AwsCredentialProvider, S3CopyIfNotExists, STORE, STRICT_PATH_ENCODE_SET};
2121
use crate::client::get::GetClient;
2222
use crate::client::header::get_etag;
23+
use crate::client::header::HeaderConfig;
2324
use crate::client::list::ListClient;
2425
use crate::client::list_response::ListResponse;
2526
use crate::client::retry::RetryExt;
@@ -549,6 +550,12 @@ impl S3Client {
549550
impl GetClient for S3Client {
550551
const STORE: &'static str = STORE;
551552

553+
const HEADER_CONFIG: HeaderConfig = HeaderConfig {
554+
etag_required: false,
555+
last_modified_required: false,
556+
version_header: Some("x-amz-version-id"),
557+
};
558+
552559
/// Make an S3 GET request <https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html>
553560
async fn get_request(&self, path: &Path, options: GetOptions) -> Result<Response> {
554561
let credential = self.get_credential().await?;
@@ -558,7 +565,11 @@ impl GetClient for S3Client {
558565
false => Method::GET,
559566
};
560567

561-
let builder = self.client.request(method, url);
568+
let mut builder = self.client.request(method, url);
569+
570+
if let Some(v) = &options.version {
571+
builder = builder.query(&[("versionId", v)])
572+
}
562573

563574
let response = builder
564575
.with_get_options(options)

object_store/src/azure/client.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use super::credential::AzureCredential;
1919
use crate::azure::credential::*;
2020
use crate::azure::{AzureCredentialProvider, STORE};
2121
use crate::client::get::GetClient;
22+
use crate::client::header::HeaderConfig;
2223
use crate::client::list::ListClient;
2324
use crate::client::retry::RetryExt;
2425
use crate::client::GetOptionsExt;
@@ -254,6 +255,12 @@ impl AzureClient {
254255
impl GetClient for AzureClient {
255256
const STORE: &'static str = STORE;
256257

258+
const HEADER_CONFIG: HeaderConfig = HeaderConfig {
259+
etag_required: true,
260+
last_modified_required: true,
261+
version_header: Some("x-ms-version-id"),
262+
};
263+
257264
/// Make an Azure GET request
258265
/// <https://docs.microsoft.com/en-us/rest/api/storageservices/get-blob>
259266
/// <https://docs.microsoft.com/en-us/rest/api/storageservices/get-blob-properties>
@@ -265,12 +272,16 @@ impl GetClient for AzureClient {
265272
false => Method::GET,
266273
};
267274

268-
let builder = self
275+
let mut builder = self
269276
.client
270277
.request(method, url)
271278
.header(CONTENT_LENGTH, HeaderValue::from_static("0"))
272279
.body(Bytes::new());
273280

281+
if let Some(v) = &options.version {
282+
builder = builder.query(&[("versionid", v)])
283+
}
284+
274285
let response = builder
275286
.with_get_options(options)
276287
.with_azure_authorization(&credential, &self.config.account)
@@ -427,6 +438,7 @@ impl TryFrom<Blob> for ObjectMeta {
427438
last_modified: value.properties.last_modified,
428439
size: value.properties.content_length as usize,
429440
e_tag: value.properties.e_tag,
441+
version: None, // For consistency with S3 and GCP which don't include this
430442
})
431443
}
432444
}

object_store/src/client/get.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,7 @@ pub trait GetClient: Send + Sync + 'static {
2929
const STORE: &'static str;
3030

3131
/// Configure the [`HeaderConfig`] for this client
32-
const HEADER_CONFIG: HeaderConfig = HeaderConfig {
33-
etag_required: true,
34-
last_modified_required: true,
35-
};
32+
const HEADER_CONFIG: HeaderConfig;
3633

3734
async fn get_request(&self, path: &Path, options: GetOptions) -> Result<Response>;
3835
}

object_store/src/client/header.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ pub struct HeaderConfig {
3535
///
3636
/// Defaults to `true`
3737
pub last_modified_required: bool,
38+
39+
/// The version header name if any
40+
pub version_header: Option<&'static str>,
3841
}
3942

4043
#[derive(Debug, Snafu)]
@@ -98,14 +101,20 @@ pub fn header_meta(
98101
.context(MissingContentLengthSnafu)?;
99102

100103
let content_length = content_length.to_str().context(BadHeaderSnafu)?;
101-
let content_length = content_length
104+
let size = content_length
102105
.parse()
103106
.context(InvalidContentLengthSnafu { content_length })?;
104107

108+
let version = match cfg.version_header.and_then(|h| headers.get(h)) {
109+
Some(v) => Some(v.to_str().context(BadHeaderSnafu)?.to_string()),
110+
None => None,
111+
};
112+
105113
Ok(ObjectMeta {
106114
location: location.clone(),
107115
last_modified,
108-
size: content_length,
116+
version,
117+
size,
109118
e_tag,
110119
})
111120
}

object_store/src/client/list_response.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ impl TryFrom<ListContents> for ObjectMeta {
8080
last_modified: value.last_modified,
8181
size: value.size,
8282
e_tag: value.e_tag,
83+
version: None,
8384
})
8485
}
8586
}

object_store/src/gcp/client.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
// under the License.
1717

1818
use crate::client::get::GetClient;
19-
use crate::client::header::get_etag;
19+
use crate::client::header::{get_etag, HeaderConfig};
2020
use crate::client::list::ListClient;
2121
use crate::client::list_response::ListResponse;
2222
use crate::client::retry::RetryExt;
@@ -333,6 +333,11 @@ impl GoogleCloudStorageClient {
333333
#[async_trait]
334334
impl GetClient for GoogleCloudStorageClient {
335335
const STORE: &'static str = STORE;
336+
const HEADER_CONFIG: HeaderConfig = HeaderConfig {
337+
etag_required: true,
338+
last_modified_required: true,
339+
version_header: Some("x-goog-generation"),
340+
};
336341

337342
/// Perform a get request <https://cloud.google.com/storage/docs/xml-api/get-object-download>
338343
async fn get_request(&self, path: &Path, options: GetOptions) -> Result<Response> {

object_store/src/http/client.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ impl GetClient for Client {
277277
const HEADER_CONFIG: HeaderConfig = HeaderConfig {
278278
etag_required: false,
279279
last_modified_required: false,
280+
version_header: None,
280281
};
281282

282283
async fn get_request(&self, path: &Path, options: GetOptions) -> Result<Response> {
@@ -375,6 +376,7 @@ impl MultiStatusResponse {
375376
last_modified,
376377
size: self.size()?,
377378
e_tag: self.prop_stat.prop.e_tag.clone(),
379+
version: None,
378380
})
379381
}
380382

object_store/src/lib.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,8 @@ pub struct ObjectMeta {
637637
///
638638
/// <https://datatracker.ietf.org/doc/html/rfc9110#name-etag>
639639
pub e_tag: Option<String>,
640+
/// A version indicator for this object
641+
pub version: Option<String>,
640642
}
641643

642644
/// Options for a get request, such as range
@@ -685,6 +687,8 @@ pub struct GetOptions {
685687
///
686688
/// <https://datatracker.ietf.org/doc/html/rfc9110#name-range>
687689
pub range: Option<Range<usize>>,
690+
/// Request a particular object version
691+
pub version: Option<String>,
688692
/// Request transfer of no content
689693
///
690694
/// <https://datatracker.ietf.org/doc/html/rfc9110#name-head>
@@ -1379,6 +1383,24 @@ mod tests {
13791383
};
13801384
let err = storage.get_opts(&path, options).await.unwrap_err();
13811385
assert!(matches!(err, Error::Precondition { .. }), "{err}");
1386+
1387+
if let Some(version) = meta.version {
1388+
storage.put(&path, "bar".into()).await.unwrap();
1389+
1390+
let options = GetOptions {
1391+
version: Some(version),
1392+
..GetOptions::default()
1393+
};
1394+
1395+
// Can retrieve previous version
1396+
let get_opts = storage.get_opts(&path, options).await.unwrap();
1397+
let old = get_opts.bytes().await.unwrap();
1398+
assert_eq!(old, b"foo".as_slice());
1399+
1400+
// Current version contains the updated data
1401+
let current = storage.get(&path).await.unwrap().bytes().await.unwrap();
1402+
assert_eq!(&current, b"bar".as_slice());
1403+
}
13821404
}
13831405

13841406
/// Returns a chunk of length `chunk_length`
@@ -1691,6 +1713,7 @@ mod tests {
16911713
last_modified: Utc.timestamp_nanos(100),
16921714
size: 100,
16931715
e_tag: Some("123".to_string()),
1716+
version: None,
16941717
};
16951718

16961719
let mut options = GetOptions::default();

object_store/src/local.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,7 @@ fn convert_metadata(metadata: Metadata, location: Path) -> Result<ObjectMeta> {
969969
last_modified,
970970
size,
971971
e_tag: Some(get_etag(&metadata)),
972+
version: None,
972973
})
973974
}
974975

object_store/src/memory.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ impl ObjectStore for InMemory {
166166
last_modified: entry.last_modified,
167167
size: entry.data.len(),
168168
e_tag: Some(e_tag),
169+
version: None,
169170
};
170171
options.check_preconditions(&meta)?;
171172

@@ -212,6 +213,7 @@ impl ObjectStore for InMemory {
212213
last_modified: entry.last_modified,
213214
size: entry.data.len(),
214215
e_tag: Some(entry.e_tag.to_string()),
216+
version: None,
215217
})
216218
}
217219

@@ -241,6 +243,7 @@ impl ObjectStore for InMemory {
241243
last_modified: value.last_modified,
242244
size: value.data.len(),
243245
e_tag: Some(value.e_tag.to_string()),
246+
version: None,
244247
})
245248
})
246249
.collect();
@@ -285,6 +288,7 @@ impl ObjectStore for InMemory {
285288
last_modified: v.last_modified,
286289
size: v.data.len(),
287290
e_tag: Some(v.e_tag.to_string()),
291+
version: None,
288292
};
289293
objects.push(object);
290294
}

object_store/src/prefix.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ impl<T: ObjectStore> PrefixStore<T> {
7373
size: meta.size,
7474
location: self.strip_prefix(meta.location),
7575
e_tag: meta.e_tag,
76+
version: None,
7677
}
7778
}
7879
}

0 commit comments

Comments
 (0)