Skip to content

Commit a3e2784

Browse files
committed
add conditional deletes
1 parent a673ed6 commit a3e2784

File tree

14 files changed

+395
-34
lines changed

14 files changed

+395
-34
lines changed

src/aws/mod.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
use async_trait::async_trait;
3232
use futures::stream::BoxStream;
3333
use futures::{StreamExt, TryStreamExt};
34-
use reqwest::header::{HeaderName, IF_MATCH, IF_NONE_MATCH};
34+
use reqwest::header::{HeaderName, IF_MATCH, IF_NONE_MATCH, IF_UNMODIFIED_SINCE};
3535
use reqwest::{Method, StatusCode};
3636
use std::{sync::Arc, time::Duration};
3737
use url::Url;
@@ -44,9 +44,9 @@ use crate::multipart::{MultipartStore, PartId};
4444
use crate::signer::Signer;
4545
use crate::util::STRICT_ENCODE_SET;
4646
use crate::{
47-
Error, GetOptions, GetResult, ListResult, MultipartId, MultipartUpload, ObjectMeta,
48-
ObjectStore, Path, PutMode, PutMultipartOptions, PutOptions, PutPayload, PutResult, Result,
49-
UploadPart,
47+
DeleteOptions, Error, GetOptions, GetResult, ListResult, MultipartId, MultipartUpload,
48+
ObjectMeta, ObjectStore, Path, PutMode, PutMultipartOptions, PutOptions, PutPayload, PutResult,
49+
Result, UploadPart,
5050
};
5151

5252
static TAGS_HEADER: HeaderName = HeaderName::from_static("x-amz-tagging");
@@ -255,6 +255,33 @@ impl ObjectStore for AmazonS3 {
255255
Ok(())
256256
}
257257

258+
async fn delete_opts(&self, location: &Path, opts: DeleteOptions) -> Result<()> {
259+
let request = self.client.request(Method::DELETE, location);
260+
261+
// Add conditional headers if specified
262+
let request = if let Some(if_match) = &opts.if_match {
263+
request.header(&IF_MATCH, if_match)
264+
} else {
265+
request
266+
};
267+
268+
let request = if let Some(if_unmodified_since) = opts.if_unmodified_since {
269+
request.header(&IF_UNMODIFIED_SINCE, &if_unmodified_since.to_rfc2822())
270+
} else {
271+
request
272+
};
273+
274+
// AWS S3 supports versioned deletes
275+
let request = if let Some(version) = &opts.version {
276+
request.query(&[("versionId", version)])
277+
} else {
278+
request
279+
};
280+
281+
request.with_extensions(opts.extensions).send().await?;
282+
Ok(())
283+
}
284+
258285
fn delete_stream<'a>(
259286
&'a self,
260287
locations: BoxStream<'a, Result<Path>>,

src/azure/client.rs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,19 @@ use crate::list::{PaginatedListOptions, PaginatedListResult};
2828
use crate::multipart::PartId;
2929
use crate::util::{deserialize_rfc1123, GetRange};
3030
use crate::{
31-
Attribute, Attributes, ClientOptions, GetOptions, ListResult, ObjectMeta, Path, PutMode,
32-
PutMultipartOptions, PutOptions, PutPayload, PutResult, Result, RetryConfig, TagSet,
31+
Attribute, Attributes, ClientOptions, DeleteOptions, GetOptions, ListResult, ObjectMeta, Path,
32+
PutMode, PutMultipartOptions, PutOptions, PutPayload, PutResult, Result, RetryConfig, TagSet,
3333
};
3434
use async_trait::async_trait;
3535
use base64::prelude::{BASE64_STANDARD, BASE64_STANDARD_NO_PAD};
3636
use base64::Engine;
3737
use bytes::{Buf, Bytes};
3838
use chrono::{DateTime, Utc};
3939
use http::{
40-
header::{HeaderMap, HeaderValue, CONTENT_LENGTH, CONTENT_TYPE, IF_MATCH, IF_NONE_MATCH},
40+
header::{
41+
HeaderMap, HeaderValue, CONTENT_LENGTH, CONTENT_TYPE, IF_MATCH, IF_NONE_MATCH,
42+
IF_UNMODIFIED_SINCE,
43+
},
4144
HeaderName, Method,
4245
};
4346
use rand::Rng as _;
@@ -655,6 +658,54 @@ impl AzureClient {
655658
Ok(())
656659
}
657660

661+
/// Make an Azure Delete request with conditional options
662+
pub(crate) async fn delete_request_with_opts(
663+
&self,
664+
path: &Path,
665+
opts: DeleteOptions,
666+
) -> Result<()> {
667+
let credential = self.get_credential().await?;
668+
let url = self.config.path_url(path);
669+
670+
let sensitive = credential
671+
.as_deref()
672+
.map(|c| c.sensitive_request())
673+
.unwrap_or_default();
674+
675+
let mut builder = self
676+
.client
677+
.delete(url.as_str())
678+
.header(&DELETE_SNAPSHOTS, "include");
679+
680+
// Add conditional headers if specified
681+
if let Some(if_match) = &opts.if_match {
682+
builder = builder.header(IF_MATCH, if_match);
683+
}
684+
685+
if let Some(if_unmodified_since) = opts.if_unmodified_since {
686+
builder = builder.header(IF_UNMODIFIED_SINCE, &if_unmodified_since.to_rfc2822());
687+
}
688+
689+
// Azure supports versioned deletes via x-ms-version-id header
690+
if let Some(version) = &opts.version {
691+
builder = builder.header("x-ms-version-id", version);
692+
}
693+
694+
builder
695+
.extensions(opts.extensions)
696+
.with_azure_authorization(&credential, &self.config.account)
697+
.retryable(&self.config.retry_config)
698+
.sensitive(sensitive)
699+
.send()
700+
.await
701+
.map_err(|source| {
702+
let path = path.as_ref().into();
703+
Error::DeleteRequest { source, path }
704+
})?;
705+
706+
Ok(())
707+
}
708+
658709
fn build_bulk_delete_body(
659710
&self,
660711
boundary: &str,

src/azure/mod.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ use crate::{
2626
multipart::{MultipartStore, PartId},
2727
path::Path,
2828
signer::Signer,
29-
GetOptions, GetResult, ListResult, MultipartId, MultipartUpload, ObjectMeta, ObjectStore,
30-
PutMultipartOptions, PutOptions, PutPayload, PutResult, Result, UploadPart,
29+
DeleteOptions, GetOptions, GetResult, ListResult, MultipartId, MultipartUpload, ObjectMeta,
30+
ObjectStore, PutMultipartOptions, PutOptions, PutPayload, PutResult, Result, UploadPart,
3131
};
3232
use async_trait::async_trait;
3333
use futures::stream::{BoxStream, StreamExt, TryStreamExt};
@@ -120,6 +120,12 @@ impl ObjectStore for MicrosoftAzure {
120120
self.client.delete_request(location, &()).await
121121
}
122122

123+
async fn delete_opts(&self, location: &Path, opts: DeleteOptions) -> Result<()> {
124+
// Azure delete_request accepts a query parameter, but we need to handle DeleteOptions differently
125+
// We'll need to update the client to have a specific method for conditional deletes
126+
self.client.delete_request_with_opts(location, opts).await
127+
}
128+
123129
fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, Result<ObjectMeta>> {
124130
self.client.list(prefix)
125131
}

src/chunked.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ use futures::StreamExt;
2828

2929
use crate::path::Path;
3030
use crate::{
31-
GetOptions, GetResult, GetResultPayload, ListResult, MultipartUpload, ObjectMeta, ObjectStore,
32-
PutMultipartOptions, PutOptions, PutResult,
31+
DeleteOptions, GetOptions, GetResult, GetResultPayload, ListResult, MultipartUpload,
32+
ObjectMeta, ObjectStore, PutMultipartOptions, PutOptions, PutResult,
3333
};
3434
use crate::{PutPayload, Result};
3535

@@ -150,6 +150,10 @@ impl ObjectStore for ChunkedStore {
150150
self.inner.delete(location).await
151151
}
152152

153+
async fn delete_opts(&self, location: &Path, opts: DeleteOptions) -> Result<()> {
154+
self.inner.delete_opts(location, opts).await
155+
}
156+
153157
fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, Result<ObjectMeta>> {
154158
self.inner.list(prefix)
155159
}

src/gcp/client.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ use crate::multipart::PartId;
3232
use crate::path::Path;
3333
use crate::util::hex_encode;
3434
use crate::{
35-
Attribute, Attributes, ClientOptions, GetOptions, MultipartId, PutMode, PutMultipartOptions,
36-
PutOptions, PutPayload, PutResult, Result, RetryConfig,
35+
Attribute, Attributes, ClientOptions, DeleteOptions, GetOptions, MultipartId, PutMode,
36+
PutMultipartOptions, PutOptions, PutPayload, PutResult, Result, RetryConfig,
3737
};
3838
use async_trait::async_trait;
3939
use base64::prelude::BASE64_STANDARD;
@@ -563,6 +563,34 @@ impl GoogleCloudStorageClient {
563563
Ok(())
564564
}
565565

566+
pub(crate) async fn delete_request_with_opts(
567+
&self,
568+
path: &Path,
569+
opts: DeleteOptions,
570+
) -> Result<()> {
571+
let mut request = self.request(Method::DELETE, path);
572+
573+
// Add conditional headers if specified
574+
if let Some(if_match) = &opts.if_match {
575+
request = request.header(&HeaderName::from_static("if-match"), if_match);
576+
}
577+
578+
if let Some(if_unmodified_since) = opts.if_unmodified_since {
579+
request = request.header(
580+
&HeaderName::from_static("if-unmodified-since"),
581+
&if_unmodified_since.to_rfc2822(),
582+
);
583+
}
584+
585+
// GCS supports versioned deletes via generation parameter
586+
if let Some(version) = &opts.version {
587+
request = request.query(&[("generation", version)]);
588+
}
589+
590+
request.with_extensions(opts.extensions).send().await?;
591+
Ok(())
592+
}
593+
566594
/// Perform a copy request <https://cloud.google.com/storage/docs/xml-api/put-object-copy>
567595
pub(crate) async fn copy_request(
568596
&self,

src/gcp/mod.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ use crate::client::CredentialProvider;
4141
use crate::gcp::credential::GCSAuthorizer;
4242
use crate::signer::Signer;
4343
use crate::{
44-
multipart::PartId, path::Path, GetOptions, GetResult, ListResult, MultipartId, MultipartUpload,
45-
ObjectMeta, ObjectStore, PutMultipartOptions, PutOptions, PutPayload, PutResult, Result,
46-
UploadPart,
44+
multipart::PartId, path::Path, DeleteOptions, GetOptions, GetResult, ListResult, MultipartId,
45+
MultipartUpload, ObjectMeta, ObjectStore, PutMultipartOptions, PutOptions, PutPayload,
46+
PutResult, Result, UploadPart,
4747
};
4848
use async_trait::async_trait;
4949
use client::GoogleCloudStorageClient;
@@ -184,6 +184,10 @@ impl ObjectStore for GoogleCloudStorage {
184184
self.client.delete_request(location).await
185185
}
186186

187+
async fn delete_opts(&self, location: &Path, opts: DeleteOptions) -> Result<()> {
188+
self.client.delete_request_with_opts(location, opts).await
189+
}
190+
187191
fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, Result<ObjectMeta>> {
188192
self.client.list(prefix)
189193
}

src/http/mod.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ use crate::client::{http_connector, HttpConnector};
4545
use crate::http::client::Client;
4646
use crate::path::Path;
4747
use crate::{
48-
ClientConfigKey, ClientOptions, GetOptions, GetResult, ListResult, MultipartUpload, ObjectMeta,
49-
ObjectStore, PutMode, PutMultipartOptions, PutOptions, PutPayload, PutResult, Result,
50-
RetryConfig,
48+
ClientConfigKey, ClientOptions, DeleteOptions, GetOptions, GetResult, ListResult,
49+
MultipartUpload, ObjectMeta, ObjectStore, PutMode, PutMultipartOptions, PutOptions, PutPayload,
50+
PutResult, Result, RetryConfig,
5151
};
5252

5353
mod client;
@@ -136,6 +136,11 @@ impl ObjectStore for HttpStore {
136136
self.client.delete(location).await
137137
}
138138

139+
async fn delete_opts(&self, _location: &Path, _opts: DeleteOptions) -> Result<()> {
140+
// HTTP/WebDAV protocol doesn't support conditional deletes
141+
Err(crate::Error::NotImplemented)
142+
}
143+
139144
fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, Result<ObjectMeta>> {
140145
let prefix_len = prefix.map(|p| p.as_ref().len()).unwrap_or_default();
141146
let prefix = prefix.cloned();

0 commit comments

Comments
 (0)