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 fn rename_profile(
58 &self,
59 from_name: String,
60 to_name: String,
61 ) -> BoxFuture<'_, Result<bool, Error>>;
62
63 #[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 fn session(&self, profile: Option<String>, transaction: bool) -> Result<Self::Session, Error>;
79
80 fn rekey(
82 &mut self,
83 method: StoreKeyMethod,
84 key: PassKey<'_>,
85 ) -> BoxFuture<'_, Result<(), Error>>;
86
87 fn close(&self) -> BoxFuture<'_, Result<(), Error>>;
89}
90
91pub trait ManageBackend<'a> {
93 type Backend: Backend;
95
96 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 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 fn remove_backend(self) -> BoxFuture<'a, Result<bool, Error>>;
115}
116
117pub trait BackendSession: Debug + Send {
119 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 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 #[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 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 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 #[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 fn ping(&mut self) -> BoxFuture<'_, Result<(), Error>>;
196
197 fn close(&mut self, commit: bool) -> BoxFuture<'_, Result<(), Error>>;
199}
200
201pub 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
235pub 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}