1use std::path::Path;
2
3use crate::{AsAttributes, Result, Secret, dbus::Service, file::UnlockedKeyring};
4
5pub async fn migrate(attributes: Vec<impl AsAttributes>, replace: bool) -> Result<()> {
11 let service = Service::new().await?;
12 let secret = match Secret::sandboxed().await {
13 Ok(secret) => Ok(secret),
14 Err(super::file::Error::Portal(ashpd::Error::PortalNotFound(_))) => {
15 #[cfg(feature = "tracing")]
16 tracing::debug!("Portal not available, no migration to do");
17 return Ok(());
18 }
19 Err(err) => Err(err),
20 }?;
21 let keyring_path = crate::file::api::Keyring::default_path()?;
22
23 migrate_inner(&service, secret, &keyring_path, attributes, replace).await
24}
25
26async fn migrate_inner(
28 service: &Service,
29 secret: Secret,
30 keyring_path: &Path,
31 attributes: Vec<impl AsAttributes>,
32 replace: bool,
33) -> Result<()> {
34 let file_backend = UnlockedKeyring::load(keyring_path, secret).await?;
35
36 let collection = service.default_collection().await?;
37 let mut all_items = Vec::default();
38
39 for attrs in attributes {
40 let items = collection.search_items(&attrs).await?;
41 all_items.extend(items);
42 }
43 let mut new_items = Vec::with_capacity(all_items.capacity());
44
45 for item in all_items.iter() {
46 let attributes = item.attributes().await?;
47 let label = item.label().await?;
48 let secret = item.secret().await?;
49
50 new_items.push((label, attributes, secret, replace));
51 }
52
53 file_backend.create_items(new_items).await?;
54
55 let mut deletion_errors = Vec::new();
57 for item in all_items.iter() {
58 if let Err(e) = item.delete(None).await {
59 deletion_errors.push(e);
60 }
61 }
62
63 if !deletion_errors.is_empty() {
65 #[cfg(feature = "tracing")]
66 tracing::error!(
67 "Migration partially failed: {} items could not be deleted from source",
68 deletion_errors.len()
69 );
70 return Err(deletion_errors.into_iter().next().unwrap().into());
71 }
72
73 Ok(())
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use crate::{Secret, dbus::Service, file::UnlockedKeyring};
80
81 #[tokio::test]
82 #[cfg(feature = "tokio")]
83 async fn test_migrate_from_dbus_to_file() {
84 let temp_dir = tempfile::tempdir().unwrap();
85 let setup = oo7_daemon::tests::TestServiceSetup::plain_session(true)
86 .await
87 .unwrap();
88
89 let service = Service::new_with_connection(&setup.client_conn)
91 .await
92 .unwrap();
93
94 let collection = service.default_collection().await.unwrap();
96
97 collection
98 .create_item(
99 "Migration Test 1",
100 &[("app", "test-migration"), ("user", "alice")],
101 "secret1",
102 false,
103 None,
104 )
105 .await
106 .unwrap();
107
108 collection
109 .create_item(
110 "Migration Test 2",
111 &[("app", "test-migration"), ("user", "bob")],
112 "secret2",
113 false,
114 None,
115 )
116 .await
117 .unwrap();
118
119 let items_before = collection
121 .search_items(&[("app", "test-migration")])
122 .await
123 .unwrap();
124 assert_eq!(items_before.len(), 2);
125
126 let keyring_path = temp_dir.path().join("migrated.keyring");
128 let secret = Secret::from([1, 2].into_iter().cycle().take(64).collect::<Vec<_>>());
129
130 migrate_inner(
132 &service,
133 secret.clone(),
134 &keyring_path,
135 vec![&[("app", "test-migration")]],
136 false,
137 )
138 .await
139 .unwrap();
140
141 let items_after = collection
143 .search_items(&[("app", "test-migration")])
144 .await
145 .unwrap();
146 assert_eq!(items_after.len(), 0);
147
148 let file_backend = UnlockedKeyring::load(&keyring_path, secret).await.unwrap();
150 let migrated_items = file_backend
151 .search_items(&[("app", "test-migration")])
152 .await
153 .unwrap();
154
155 assert_eq!(migrated_items.len(), 2);
156
157 let alice_item = migrated_items
159 .iter()
160 .find(|item| {
161 item.attributes()
162 .get("user")
163 .map(|u| u == "alice")
164 .unwrap_or(false)
165 })
166 .expect("Alice's item should exist");
167
168 assert_eq!(alice_item.label(), "Migration Test 1");
169 assert_eq!(alice_item.secret(), Secret::text("secret1"));
170 assert_eq!(
171 alice_item.attributes().get("app").unwrap(),
172 "test-migration"
173 );
174
175 let bob_item = migrated_items
176 .iter()
177 .find(|item| {
178 item.attributes()
179 .get("user")
180 .map(|u| u == "bob")
181 .unwrap_or(false)
182 })
183 .expect("Bob's item should exist");
184
185 assert_eq!(bob_item.label(), "Migration Test 2");
186 assert_eq!(bob_item.secret(), Secret::text("secret2"));
187 assert_eq!(bob_item.attributes().get("app").unwrap(), "test-migration");
188 }
189}