Skip to main content

coven_storage/providers/
s3_empty.rs

1//! Emptying an S3 bucket: a development tool outside the sync protocol
2//! (api.md E14), and, under one store's folder, the live certification's
3//! cleanup of its namespace. It removes what is there whatever wrote it, so
4//! no store's layout limits it: current objects, upload temporaries,
5//! detection probes and unfinished multipart uploads alike.
6#[cfg(feature = "dev-tools")]
7use super::own_bucket::RootEntry;
8use super::*;
9
10/// What emptying an S3 bucket removed (api.md E14).
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct EmptiedS3Bucket {
13    /// Objects deleted.
14    pub objects: usize,
15    /// Unfinished multipart uploads aborted.
16    pub uploads: usize,
17}
18
19/// Why an S3 bucket was not emptied (api.md E14).
20#[cfg(feature = "dev-tools")]
21#[derive(Debug, thiserror::Error)]
22pub enum EmptyS3BucketError {
23    /// The bucket's root holds something no store wrote: an object other
24    /// than the location marker, or a folder not named by a store id. Nothing
25    /// was removed.
26    #[error("the S3 bucket holds files no coven store wrote")]
27    OtherFiles,
28    /// The location or key was refused, or a request failed. Whatever was
29    /// removed before the failure stays removed; emptying again finishes.
30    #[error("emptying the S3 bucket: {0}")]
31    Storage(#[source] StorageError),
32}
33
34/// Abort every unfinished upload and delete every object in the location's
35/// bucket, with the location's own settings, once its root shows it holds
36/// only stores' folders and the location marker.
37#[cfg(feature = "dev-tools")]
38pub(crate) async fn empty_bucket(
39    config: StorageConfig,
40    credentials: S3Credentials,
41    clock: ClockRef,
42    ids: IdSourceRef,
43    http: HttpClient,
44) -> Result<EmptiedS3Bucket, EmptyS3BucketError> {
45    crate::ConnectionCredentials {
46        location: config.clone(),
47        credentials: StorageCredentials::S3(credentials.clone()),
48    }
49    .validate()
50    .map_err(EmptyS3BucketError::Storage)?;
51    let storage = S3Storage::new(
52        config,
53        credentials,
54        clock,
55        ids,
56        std::sync::Arc::new(crate::NoCooldown),
57        http,
58    )
59    .map_err(EmptyS3BucketError::Storage)?;
60    let stores_only = storage
61        .root_admits(|entry| match entry {
62            RootEntry::Object(key) => key == crate::location_claim::LOCATION_MARKER,
63            RootEntry::Folder(folder) => folder
64                .strip_suffix('/')
65                .is_some_and(|name| coven_foundation::id_source::canonical_uuid(name).is_ok()),
66        })
67        .await
68        .map_err(EmptyS3BucketError::Storage)?;
69    if !stores_only {
70        return Err(EmptyS3BucketError::OtherFiles);
71    }
72    storage.empty_under("", storage.settings.listing).await.map_err(EmptyS3BucketError::Storage)
73}
74
75impl S3Storage {
76    /// Abort every unfinished upload whose key starts with `prefix`, then
77    /// delete every object whose key does, and confirm that none of either
78    /// remains. Everything is listed before anything is removed, so the
79    /// listings' markers stay valid. The first failure stops it; removal is
80    /// idempotent, so running it again finishes. Retained versions of a
81    /// versioned bucket are left: member keys may not delete them.
82    pub(super) async fn empty_under(
83        &self,
84        prefix: &str,
85        listing: S3Listing,
86    ) -> Result<EmptiedS3Bucket, StorageError> {
87        let uploads = self.unfinished_uploads(prefix, None).await?;
88        for (key, id, _) in &uploads {
89            self.abort_multipart(key, id, None).await?;
90        }
91        let keys = self.keys_under(prefix, listing).await?;
92        for key in &keys {
93            self.delete_key(key).await?;
94        }
95        // Something still writing here, such as another device's library,
96        // would leave the prefix occupied however often it is emptied.
97        let objects = self.list_page(listing, prefix, None, None, Some(1)).await?;
98        if !objects.contents.is_empty() || !self.unfinished_uploads(prefix, None).await?.is_empty()
99        {
100            return Err(StorageFailure::Conflict
101                .with_source("objects or uploads arrived under the prefix while it was emptied"));
102        }
103        Ok(EmptiedS3Bucket { objects: keys.len(), uploads: uploads.len() })
104    }
105}