askar_storage/backend/
mod.rs

1//! Storage backends supported by askar
2
3use std::fmt::Debug;
4
5use crate::{
6    entry::{Entry, EntryKind, EntryOperation, EntryTag, Scan, TagFilter},
7    error::{Error, ErrorKind},
8    future::BoxFuture,
9    protect::{PassKey, StoreKeyMethod},
10};
11
12#[cfg(any(feature = "postgres", feature = "sqlite"))]
13pub(crate) mod db_utils;
14
15#[cfg(feature = "postgres")]
16#[cfg_attr(docsrs, doc(cfg(feature = "postgres")))]
17/// Postgres database support
18pub mod postgres;
19
20#[cfg(feature = "sqlite")]
21#[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))]
22/// Sqlite database support
23pub mod sqlite;
24
25/// Enum to support custom ordering in record queries
26#[derive(Debug, Default)]
27pub enum OrderBy {
28    /// Order by ID field
29    #[default]
30    Id,
31}
32
33/// Represents a generic backend implementation
34pub trait Backend: Debug + Send + Sync {
35    /// The type of session managed by this backend
36    type Session: BackendSession + 'static;
37
38    /// Create a new profile
39    fn create_profile(&self, name: Option<String>) -> BoxFuture<'_, Result<String, Error>>;
40
41    /// Get the name of the active profile
42    fn get_active_profile(&self) -> String;
43
44    /// Get the name of the default profile
45    fn get_default_profile(&self) -> BoxFuture<'_, Result<String, Error>>;
46
47    /// Set the the default profile
48    fn set_default_profile(&self, profile: String) -> BoxFuture<'_, Result<(), Error>>;
49
50    /// Get the details of all store profiles
51    fn list_profiles(&self) -> BoxFuture<'_, Result<Vec<String>, Error>>;
52
53    /// Remove an existing profile
54    fn remove_profile(&self, name: String) -> BoxFuture<'_, Result<bool, Error>>;
55
56    /// Change the name of an existing profile
57    fn rename_profile(
58        &self,
59        from_name: String,
60        to_name: String,
61    ) -> BoxFuture<'_, Result<bool, Error>>;
62
63    /// Create a [`Scan`] against the store
64    #[allow(clippy::too_many_arguments)]
65    fn scan(
66        &self,
67        profile: Option<String>,
68        kind: Option<EntryKind>,
69        category: Option<String>,
70        tag_filter: Option<TagFilter>,
71        offset: Option<i64>,
72        limit: Option<i64>,
73        order_by: Option<OrderBy>,
74        descending: bool,
75    ) -> BoxFuture<'_, Result<Scan<'static, Entry>, Error>>;
76
77    /// Create a new session against the store
78    fn session(&self, profile: Option<String>, transaction: bool) -> Result<Self::Session, Error>;
79
80    /// Replace the wrapping key of the store
81    fn rekey(
82        &mut self,
83        method: StoreKeyMethod,
84        key: PassKey<'_>,
85    ) -> BoxFuture<'_, Result<(), Error>>;
86
87    /// Close the store instance
88    fn close(&self) -> BoxFuture<'_, Result<(), Error>>;
89}
90
91/// Create, open, or remove a generic backend implementation
92pub trait ManageBackend<'a> {
93    /// The type of backend being managed
94    type Backend: Backend;
95
96    /// Open an existing store
97    fn open_backend(
98        self,
99        method: Option<StoreKeyMethod>,
100        pass_key: PassKey<'a>,
101        profile: Option<String>,
102    ) -> BoxFuture<'a, Result<Self::Backend, Error>>;
103
104    /// Provision a new store
105    fn provision_backend(
106        self,
107        method: StoreKeyMethod,
108        pass_key: PassKey<'a>,
109        profile: Option<String>,
110        recreate: bool,
111    ) -> BoxFuture<'a, Result<Self::Backend, Error>>;
112
113    /// Remove an existing store
114    fn remove_backend(self) -> BoxFuture<'a, Result<bool, Error>>;
115}
116
117/// Query from a generic backend implementation
118pub trait BackendSession: Debug + Send {
119    /// Count the number of matching records in the store
120    fn count<'q>(
121        &'q mut self,
122        kind: Option<EntryKind>,
123        category: Option<&'q str>,
124        tag_filter: Option<TagFilter>,
125    ) -> BoxFuture<'q, Result<i64, Error>>;
126
127    /// Fetch a single record from the store by category and name
128    fn fetch<'q>(
129        &'q mut self,
130        kind: EntryKind,
131        category: &'q str,
132        name: &'q str,
133        for_update: bool,
134    ) -> BoxFuture<'q, Result<Option<Entry>, Error>>;
135
136    /// Fetch all matching records from the store
137    #[allow(clippy::too_many_arguments)]
138    fn fetch_all<'q>(
139        &'q mut self,
140        kind: Option<EntryKind>,
141        category: Option<&'q str>,
142        tag_filter: Option<TagFilter>,
143        limit: Option<i64>,
144        order_by: Option<OrderBy>,
145        descending: bool,
146        for_update: bool,
147    ) -> BoxFuture<'q, Result<Vec<Entry>, Error>>;
148
149    /// Insert scan results from another profile or store
150    fn import_scan<'q>(
151        &'q mut self,
152        mut scan: Scan<'q, Entry>,
153    ) -> BoxFuture<'q, Result<(), Error>> {
154        Box::pin(async move {
155            while let Some(rows) = scan.fetch_next().await? {
156                for entry in rows {
157                    self.update(
158                        entry.kind,
159                        EntryOperation::Insert,
160                        entry.category.as_str(),
161                        entry.name.as_str(),
162                        Some(entry.value.as_ref()),
163                        Some(entry.tags.as_ref()),
164                        None,
165                    )
166                    .await?;
167                }
168            }
169            Ok(())
170        })
171    }
172
173    /// Remove all matching records from the store
174    fn remove_all<'q>(
175        &'q mut self,
176        kind: Option<EntryKind>,
177        category: Option<&'q str>,
178        tag_filter: Option<TagFilter>,
179    ) -> BoxFuture<'q, Result<i64, Error>>;
180
181    /// Insert or replace a record in the store
182    #[allow(clippy::too_many_arguments)]
183    fn update<'q>(
184        &'q mut self,
185        kind: EntryKind,
186        operation: EntryOperation,
187        category: &'q str,
188        name: &'q str,
189        value: Option<&'q [u8]>,
190        tags: Option<&'q [EntryTag]>,
191        expiry_ms: Option<i64>,
192    ) -> BoxFuture<'q, Result<(), Error>>;
193
194    /// Test the connection to the store
195    fn ping(&mut self) -> BoxFuture<'_, Result<(), Error>>;
196
197    /// Close the current store session
198    fn close(&mut self, commit: bool) -> BoxFuture<'_, Result<(), Error>>;
199}
200
201/// Insert all records from a given profile
202pub async fn copy_profile<A: Backend, B: Backend>(
203    from_backend: &A,
204    to_backend: &B,
205    from_profile: &str,
206    to_profile: &str,
207) -> Result<(), Error> {
208    let scan = from_backend
209        .scan(
210            Some(from_profile.into()),
211            None,
212            None,
213            None,
214            None,
215            None,
216            None,
217            false,
218        )
219        .await?;
220    if let Err(e) = to_backend.create_profile(Some(to_profile.into())).await {
221        if e.kind() != ErrorKind::Duplicate {
222            return Err(e);
223        }
224    }
225    let mut txn = to_backend.session(Some(to_profile.into()), true)?;
226    let count = txn.count(None, None, None).await?;
227    if count > 0 {
228        return Err(err_msg!(Input, "Profile targeted for import is not empty"));
229    }
230    txn.import_scan(scan).await?;
231    txn.close(true).await?;
232    Ok(())
233}
234
235/// Export an entire Store to another location
236pub async fn copy_store<'m, B: Backend, M: ManageBackend<'m>>(
237    source: &B,
238    target: M,
239    key_method: StoreKeyMethod,
240    pass_key: PassKey<'m>,
241    recreate: bool,
242) -> Result<<M as ManageBackend<'m>>::Backend, Error> {
243    let default_profile = source.get_default_profile().await?;
244    let profile_ids = source.list_profiles().await?;
245    let target = target
246        .provision_backend(key_method, pass_key, Some(default_profile), recreate)
247        .await?;
248    for profile in profile_ids {
249        copy_profile(source, &target, &profile, &profile).await?;
250    }
251    Ok(target)
252}