Skip to main content

coven_storage/
memory_connector.rs

1//! The in-memory provider as an application's storage connector: settings
2//! saved through the builder's connector hook, S3 detection that conforms to
3//! any settings, and the new-store check of a location that holds one store.
4use super::*;
5
6impl MemoryStorage {
7    /// Rename the store's folder as its owner would in Dropbox or OneDrive,
8    /// without a request: the next folder label read answers `name`.
9    pub async fn rename_folder(&self, name: &str) {
10        self.provider.state.lock().await.folder_name = name.into();
11    }
12    /// Replace or remove the location marker as an external actor, without a request.
13    pub async fn plant_location_marker(&self, bytes: Option<Vec<u8>>) {
14        self.provider.state.lock().await.location_marker = bytes;
15    }
16    /// Bind a simulated installation to the real synchronization gate, preserving
17    /// this provider's credentials, durable objects and request observations.
18    pub fn with_publication_gate(&self, publications: Arc<dyn PublicationGate>) -> Self {
19        Self::with_gate(
20            self.provider.clone(),
21            self.device,
22            publications,
23            self.cooldown.clone(),
24            self.transfers.clone(),
25            self.provider.clock.clone(),
26        )
27    }
28
29    /// Write for `device` on this simulated connection: what it stages sits
30    /// in that device's reserved folder (D10).
31    pub fn with_device(&self, device: coven_foundation::id_source::DeviceId) -> Self {
32        Self::with_gate(
33            self.provider.clone(),
34            device,
35            self.publications.clone(),
36            self.cooldown.clone(),
37            self.transfers.clone(),
38            self.provider.clock.clone(),
39        )
40    }
41
42    /// Bind this connection to the store's shared provider cooldown.
43    pub fn with_cooldown(&self, cooldown: Arc<dyn crate::Cooldown>) -> Self {
44        Self::with_gate(
45            self.provider.clone(),
46            self.device,
47            self.publications.clone(),
48            cooldown,
49            self.transfers.clone(),
50            self.provider.clock.clone(),
51        )
52    }
53
54    /// Use the handle's transfer budget on this simulated connection.
55    pub fn with_transfers(&self, transfers: Arc<crate::TransferBudget>) -> Self {
56        Self::with_gate(
57            self.provider.clone(),
58            self.device,
59            self.publications.clone(),
60            self.cooldown.clone(),
61            transfers,
62            self.provider.clock.clone(),
63        )
64    }
65
66    /// Erase the provider type while retaining this exact connection's
67    /// credentials.
68    pub fn shared_connection(&self) -> Arc<StorageConnection> {
69        let connection = StorageConnection::with_gate(
70            self.provider.clone() as Arc<dyn ProviderOps>,
71            self.device,
72            self.publications.clone(),
73            self.cooldown.clone(),
74            self.transfers.clone(),
75            self.provider.clock.clone(),
76        );
77        Arc::new(connection)
78    }
79}
80
81#[async_trait]
82impl crate::providers::StorageConnector for MemoryStorage {
83    async fn account_label(
84        &self,
85        auth: crate::ProviderAuth,
86        cooldown: Arc<dyn crate::Cooldown>,
87    ) -> Result<String, StorageError> {
88        self.connect(
89            self.config(),
90            auth,
91            self.device,
92            self.publications.clone(),
93            cooldown,
94            self.transfers.clone(),
95        )
96        .await?
97        .account()
98        .await
99    }
100
101    async fn prepare_cloudkit(
102        &self,
103        container: &str,
104        _store: coven_foundation::id_source::StoreId,
105    ) -> Result<StorageConfig, StorageError> {
106        let config = self.config();
107        if !matches!(&config, StorageConfig::CloudKit { container: expected, .. } if expected == container)
108        {
109            return Err(StorageFailure::InvalidConfiguration
110                .with_source("memory connector container mismatch"));
111        }
112        Ok(config)
113    }
114    async fn connect(
115        &self,
116        config: StorageConfig,
117        auth: crate::ProviderAuth,
118        device: coven_foundation::id_source::DeviceId,
119        publications: Arc<dyn crate::PublicationGate>,
120        cooldown: Arc<dyn crate::Cooldown>,
121        transfers: Arc<crate::TransferBudget>,
122    ) -> Result<Arc<StorageConnection>, StorageError> {
123        let connection = self
124            .connect_device(config, auth)
125            .await?
126            .with_device(device)
127            .with_publication_gate(publications)
128            .with_cooldown(cooldown)
129            .with_transfers(transfers);
130        Ok(connection.shared_connection())
131    }
132    /// Memory behaves as any settings demand: the detection conforms to the
133    /// location's own settings, or to plain S3 settings for detection.
134    /// Memory holds no probes, so no run, this one or an earlier one, has
135    /// left anything.
136    async fn detect_s3(
137        &self,
138        config: StorageConfig,
139        _credentials: crate::S3Credentials,
140        _store: coven_foundation::id_source::StoreId,
141        runs: &crate::S3DetectionRuns,
142        _waits: &crate::SetupWaits,
143    ) -> crate::S3DetectionRun {
144        let detection = async {
145            config.validate()?;
146            let StorageConfig::S3 { endpoint, profile, .. } = &config else {
147                return Err(StorageFailure::InvalidConfiguration.with_source("expected S3"));
148            };
149            Ok(conforming_detection(
150                crate::providers::s3_settings(endpoint.as_ref(), profile.as_ref())?
151                    .unwrap_or(DETECTED_S3_SETTINGS),
152            ))
153        }
154        .await;
155        let cleaned = std::iter::once(runs.run).chain(runs.earlier.iter().copied()).collect();
156        crate::S3DetectionRun { detection, cleaned }
157    }
158    /// Memory's objects are one location, which another store's object
159    /// occupies.
160    async fn check_new_store(
161        &self,
162        config: StorageConfig,
163        _auth: crate::ProviderAuth,
164        store: coven_foundation::id_source::StoreId,
165        _waits: &crate::SetupWaits,
166    ) -> Result<(), StorageError> {
167        config.validate()?;
168        if !config.holds_one_store() {
169            return Err(StorageFailure::InvalidConfiguration
170                .with_source("the location holds several stores, so nothing checks it"));
171        }
172        if self.provider.state.lock().await.objects.keys().any(|path| path.store() != store) {
173            return Err(StorageFailure::OwnLocationRequired.into());
174        }
175        Ok(())
176    }
177}
178
179/// What memory's detection reports where the location needs detection.
180pub const DETECTED_S3_SETTINGS: S3Settings = S3Settings {
181    signing: S3Signing::AwsSigV4,
182    single_put: S3SinglePut::IfNoneMatch,
183    copy_publication: S3CopyPublication::Never,
184    multipart_timestamp: S3MultipartTimestamp::Completion,
185    checksums: S3Checksums::Absent,
186    listing: S3Listing::V2,
187    revision: S3Revision::ETag,
188    response_headers: S3ResponseHeaders::S3,
189    request_checksums: false,
190};
191
192/// A detection whose every observation agrees with these settings.
193pub fn conforming_detection(settings: S3Settings) -> S3Detection {
194    let completion = settings.multipart_timestamp == S3MultipartTimestamp::Completion;
195    let results = [
196        (S3Probe::Signing(settings.signing), true),
197        (S3Probe::SinglePut(settings.single_put), true),
198        (S3Probe::PartialUploadHidden, true),
199        (S3Probe::ConditionalCompletion, settings.copy_publication == S3CopyPublication::Never),
200        (S3Probe::CompletionTimestamp, completion),
201        (S3Probe::CopyTimestamp, true),
202        (S3Probe::Listing(S3Listing::V1), true),
203        (S3Probe::Listing(settings.listing), true),
204        (S3Probe::ChecksumMode, settings.request_checksums),
205        (S3Probe::ChecksumReturned, settings.checksums != S3Checksums::Absent),
206        (S3Probe::ChecksumScopeStated, settings.checksums != S3Checksums::UnstatedIgnored),
207        (S3Probe::GoogleGeneration, settings.revision == S3Revision::Generation),
208        (S3Probe::GoogleHeaders, settings.response_headers == S3ResponseHeaders::Google),
209        (S3Probe::BucketList, true),
210    ]
211    .into_iter()
212    .chain(
213        settings
214            .copy_publication
215            .multipart()
216            .map(|condition| (S3Probe::CopyPublication(condition), true)),
217    )
218    .map(|(probe, held)| S3ProbeResult { probe, held, detail: "memory".into() })
219    .collect();
220    S3Detection { versioning: Ok(S3BucketVersioning::Disabled), settings, results }
221}