1use 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")))]
17pub mod postgres;
19
20#[cfg(feature = "sqlite")]
21#[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))]
22pub mod sqlite;
24
25#[derive(Debug, Default)]
27pub enum OrderBy {
28 #[default]
30 Id,
31}
32
33pub trait Backend: Debug + Send + Sync {
35 type Session: BackendSession + 'static;
37
38 fn create_profile(&self, name: Option<String>) -> BoxFuture<'_, Result<String, Error>>;
40
41 fn get_active_profile(&self) -> String;
43
44 fn get_default_profile(&self) -> BoxFuture<'_, Result<String, Error>>;
46
47 fn set_default_profile(&self, profile: String) -> BoxFuture<'_, Result<(), Error>>;
49
50 fn list_profiles(&self) -> BoxFuture<'_, Result<Vec<String>, Error>>;
52
53 fn remove_profile(&self, name: String) -> BoxFuture<'_, Result<bool, Error>>;
55
56 #[allow(clippy::too_many_arguments)]
58 fn scan(
59 &self,
60 profile: Option<String>,
61 kind: Option<EntryKind>,
62 category: Option<String>,
63 tag_filter: Option<TagFilter>,
64 offset: Option<i64>,
65 limit: Option<i64>,
66 order_by: Option<OrderBy>,
67 descending: bool,
68 ) -> BoxFuture<'_, Result<Scan<'static, Entry>, Error>>;
69
70 fn session(&self, profile: Option<String>, transaction: bool) -> Result<Self::Session, Error>;
72
73 fn rekey(
75 &mut self,
76 method: StoreKeyMethod,
77 key: PassKey<'_>,
78 ) -> BoxFuture<'_, Result<(), Error>>;
79
80 fn close(&self) -> BoxFuture<'_, Result<(), Error>>;
82}
83
84pub trait ManageBackend<'a> {
86 type Backend: Backend;
88
89 fn open_backend(
91 self,
92 method: Option<StoreKeyMethod>,
93 pass_key: PassKey<'a>,
94 profile: Option<String>,
95 ) -> BoxFuture<'a, Result<Self::Backend, Error>>;
96
97 fn provision_backend(
99 self,
100 method: StoreKeyMethod,
101 pass_key: PassKey<'a>,
102 profile: Option<String>,
103 recreate: bool,
104 ) -> BoxFuture<'a, Result<Self::Backend, Error>>;
105
106 fn remove_backend(self) -> BoxFuture<'a, Result<bool, Error>>;
108}
109
110pub trait BackendSession: Debug + Send {
112 fn count<'q>(
114 &'q mut self,
115 kind: Option<EntryKind>,
116 category: Option<&'q str>,
117 tag_filter: Option<TagFilter>,
118 ) -> BoxFuture<'q, Result<i64, Error>>;
119
120 fn fetch<'q>(
122 &'q mut self,
123 kind: EntryKind,
124 category: &'q str,
125 name: &'q str,
126 for_update: bool,
127 ) -> BoxFuture<'q, Result<Option<Entry>, Error>>;
128
129 #[allow(clippy::too_many_arguments)]
131 fn fetch_all<'q>(
132 &'q mut self,
133 kind: Option<EntryKind>,
134 category: Option<&'q str>,
135 tag_filter: Option<TagFilter>,
136 limit: Option<i64>,
137 order_by: Option<OrderBy>,
138 descending: bool,
139 for_update: bool,
140 ) -> BoxFuture<'q, Result<Vec<Entry>, Error>>;
141
142 fn import_scan<'q>(
144 &'q mut self,
145 mut scan: Scan<'q, Entry>,
146 ) -> BoxFuture<'_, Result<(), Error>> {
147 Box::pin(async move {
148 while let Some(rows) = scan.fetch_next().await? {
149 for entry in rows {
150 self.update(
151 entry.kind,
152 EntryOperation::Insert,
153 entry.category.as_str(),
154 entry.name.as_str(),
155 Some(entry.value.as_ref()),
156 Some(entry.tags.as_ref()),
157 None,
158 )
159 .await?;
160 }
161 }
162 Ok(())
163 })
164 }
165
166 fn remove_all<'q>(
168 &'q mut self,
169 kind: Option<EntryKind>,
170 category: Option<&'q str>,
171 tag_filter: Option<TagFilter>,
172 ) -> BoxFuture<'q, Result<i64, Error>>;
173
174 #[allow(clippy::too_many_arguments)]
176 fn update<'q>(
177 &'q mut self,
178 kind: EntryKind,
179 operation: EntryOperation,
180 category: &'q str,
181 name: &'q str,
182 value: Option<&'q [u8]>,
183 tags: Option<&'q [EntryTag]>,
184 expiry_ms: Option<i64>,
185 ) -> BoxFuture<'q, Result<(), Error>>;
186
187 fn ping(&mut self) -> BoxFuture<'_, Result<(), Error>>;
189
190 fn close(&mut self, commit: bool) -> BoxFuture<'_, Result<(), Error>>;
192}
193
194pub async fn copy_profile<A: Backend, B: Backend>(
196 from_backend: &A,
197 to_backend: &B,
198 from_profile: &str,
199 to_profile: &str,
200) -> Result<(), Error> {
201 let scan = from_backend
202 .scan(
203 Some(from_profile.into()),
204 None,
205 None,
206 None,
207 None,
208 None,
209 None,
210 false,
211 )
212 .await?;
213 if let Err(e) = to_backend.create_profile(Some(to_profile.into())).await {
214 if e.kind() != ErrorKind::Duplicate {
215 return Err(e);
216 }
217 }
218 let mut txn = to_backend.session(Some(to_profile.into()), true)?;
219 let count = txn.count(None, None, None).await?;
220 if count > 0 {
221 return Err(err_msg!(Input, "Profile targeted for import is not empty"));
222 }
223 txn.import_scan(scan).await?;
224 txn.close(true).await?;
225 Ok(())
226}
227
228pub async fn copy_store<'m, B: Backend, M: ManageBackend<'m>>(
230 source: &B,
231 target: M,
232 key_method: StoreKeyMethod,
233 pass_key: PassKey<'m>,
234 recreate: bool,
235) -> Result<<M as ManageBackend<'m>>::Backend, Error> {
236 let default_profile = source.get_default_profile().await?;
237 let profile_ids = source.list_profiles().await?;
238 let target = target
239 .provision_backend(key_method, pass_key, Some(default_profile), recreate)
240 .await?;
241 for profile in profile_ids {
242 copy_profile(source, &target, &profile, &profile).await?;
243 }
244 Ok(target)
245}