forge_browser/
profiles.rs1use std::path::{Path, PathBuf};
14
15use forge_primitives::browser::ProfileKey;
16
17const MARKER: &str = "legacy-profiles-deleted";
20
21const CLIENT_BUNDLE: &str = "dev.vedhavyas.forge";
24
25pub struct Store {
27 root: PathBuf,
28}
29
30#[derive(Clone, Debug, Default, PartialEq, Eq)]
33pub struct Legacy {
34 pub removed: Vec<PathBuf>,
35 pub ran: bool,
36}
37
38impl Store {
39 pub fn at(root: &Path) -> Self {
40 Self { root: root.to_owned() }
41 }
42
43 pub fn profiles(&self) -> PathBuf {
45 self.root.join("profiles")
46 }
47
48 pub fn org_profile(&self, key: &ProfileKey) -> PathBuf {
50 self.profiles().join(key.org()).join(key.name())
51 }
52
53 pub fn client_root() -> Option<PathBuf> {
58 let support = forge_sdk::app_support_dir().ok()?.parent()?.to_owned();
59 Some(client_root_at(&support))
60 }
61
62 pub fn delete_legacy(&self) -> Result<Legacy, String> {
70 self.delete_legacy_at(Self::client_root().as_deref())
71 }
72
73 pub fn delete_legacy_at(&self, client: Option<&Path>) -> Result<Legacy, String> {
76 let marker = self.root.join(MARKER);
77 if marker.exists() {
78 return Ok(Legacy { removed: Vec::new(), ran: false });
79 }
80 let mut removed = Vec::new();
81 let shared = self.root.join("shared");
82 if shared.is_dir() {
83 remove_tree(&shared)?;
84 removed.push(shared);
85 }
86 removed.extend(remove_named_profiles(&self.profiles())?);
87 if let Some(client) = client.filter(|client| client.is_dir()) {
88 let shared = client.join("cef-profile");
92 if shared.is_dir() {
93 remove_tree(&shared)?;
94 removed.push(shared);
95 }
96 removed.extend(remove_named_profiles(&client.join("profiles"))?);
97 }
98 std::fs::create_dir_all(&self.root)
99 .map_err(|why| format!("the browser root cannot be made: {why}"))?;
100 std::fs::write(&marker, b"1")
101 .map_err(|why| format!("the marker cannot be written: {why}"))?;
102 Ok(Legacy { removed, ran: true })
103 }
104}
105
106fn remove_named_profiles(profiles: &Path) -> Result<Vec<PathBuf>, String> {
110 let mut removed = Vec::new();
111 match std::fs::read_dir(profiles) {
112 Ok(entries) => {
113 for entry in entries {
114 let entry = entry.map_err(|why| format!("the profiles cannot be read: {why}"))?;
115 let path = entry.path();
116 let cache = path.join("cef");
117 if cache.is_dir() && path.is_dir() && !cache.join("cef").is_dir() {
123 remove_tree(&path)?;
124 removed.push(path);
125 }
126 }
127 }
128 Err(why) if why.kind() == std::io::ErrorKind::NotFound => {}
129 Err(why) => return Err(format!("the profiles cannot be read: {why}")),
130 }
131 Ok(removed)
132}
133
134fn remove_tree(path: &Path) -> Result<(), String> {
135 std::fs::remove_dir_all(path)
136 .map_err(|why| format!("{} could not be deleted: {why}", path.display()))
137}
138
139fn client_root_at(support: &Path) -> PathBuf {
143 support.join(CLIENT_BUNDLE).join("browser")
144}
145
146#[cfg(test)]
147mod tests {
148 use super::Store;
149 use forge_primitives::browser::ProfileKey;
150
151 #[test]
154 fn the_client_root_is_the_clients_bundle_browser_tree() {
155 assert_eq!(
156 super::client_root_at(std::path::Path::new("/support")),
157 std::path::Path::new("/support/dev.vedhavyas.forge/browser"),
158 );
159 }
160
161 #[test]
165 fn the_client_root_is_derived_from_app_supports_parent() {
166 let support = forge_sdk::app_support_dir().expect("this machine has an app-support dir");
167 assert_eq!(
168 Store::client_root(),
169 support.parent().map(|parent| parent.join("dev.vedhavyas.forge/browser")),
170 );
171 }
172
173 #[test]
174 fn an_org_profile_is_one_directory_deeper() {
175 let dir = tempfile::tempdir().expect("a temp dir");
176 let store = Store::at(dir.path());
177 let key = ProfileKey::of("Busytools", "work").expect("a key");
178 assert_eq!(store.org_profile(&key), dir.path().join("profiles/Busytools/work"));
179 }
180
181 #[test]
182 fn the_legacy_server_and_client_trees_go_once() {
183 let dir = tempfile::tempdir().expect("a temp dir");
184 let client = dir.path().join("client/browser");
185 std::fs::create_dir_all(dir.path().join("shared/cef")).expect("the shared tree");
186 std::fs::create_dir_all(dir.path().join("profiles/lead/cef")).expect("a named tree");
187 std::fs::write(dir.path().join("profiles/owner.json"), b"{}").expect("a stray file");
188 std::fs::create_dir_all(dir.path().join("profiles/Busytools/work/cef"))
189 .expect("an org tree");
190 std::fs::create_dir_all(dir.path().join("profiles/Busytools/cef/cef"))
193 .expect("a profile named cef under an org");
194 std::fs::create_dir_all(client.join("cef-profile")).expect("the client's shared cache");
197 std::fs::create_dir_all(client.join("profiles/lead/cef")).expect("a client profile");
198 std::fs::create_dir_all(client.join("profiles/notes")).expect("a client dir, no cache");
199
200 let store = Store::at(dir.path());
201 let first = store.delete_legacy_at(Some(&client)).expect("the first boot deletes");
202 assert_eq!(first.removed.len(), 4, "the server's two legacy trees and the client's two go");
203 assert!(!dir.path().join("shared").exists());
204 assert!(!dir.path().join("profiles/lead").exists());
205 assert!(dir.path().join("profiles/Busytools/work/cef").exists(), "an org tree stays");
206 assert!(
207 dir.path().join("profiles/Busytools/cef/cef").exists(),
208 "an org whose profile is literally named `cef` stays",
209 );
210 assert!(dir.path().join("profiles/owner.json").exists(), "a stray file is not a profile");
211 assert!(!client.join("cef-profile").exists(), "the client's shared cache goes");
212 assert!(!client.join("profiles/lead").exists(), "its per-profile cache goes");
213 assert!(client.join("profiles/notes").exists(), "a directory with no cef cache stays");
214
215 std::fs::create_dir_all(client.join("cef-profile")).expect("a client cache returns");
216 let second = store.delete_legacy_at(Some(&client)).expect("the second boot is a no-op");
217 assert_eq!(second.removed.len(), 0, "the deletion never runs twice");
218 assert!(client.join("cef-profile").exists());
219 }
220
221 #[cfg(unix)]
224 #[test]
225 fn a_legacy_tree_that_cannot_be_removed_fails_the_boot_before_the_marker() {
226 use std::os::unix::fs::PermissionsExt;
227
228 let dir = tempfile::tempdir().expect("a temp dir");
229 let profiles = dir.path().join("profiles");
230 std::fs::create_dir_all(profiles.join("lead/cef")).expect("a legacy tree");
231 std::fs::set_permissions(&profiles, std::fs::Permissions::from_mode(0o500))
232 .expect("profiles made read-only");
233
234 let probe = profiles.join("permission-probe");
236 if std::fs::write(&probe, b"").is_ok() {
237 std::fs::remove_file(&probe).expect("the probe goes");
238 std::fs::set_permissions(&profiles, std::fs::Permissions::from_mode(0o700))
239 .expect("profiles writable again");
240 println!("skipping: the mode bits are not enforced (running as root?)");
241 return;
242 }
243
244 let store = Store::at(dir.path());
245 assert!(
246 store.delete_legacy_at(None).is_err(),
247 "a legacy tree that cannot be removed fails the boot"
248 );
249 assert!(
250 !dir.path().join("legacy-profiles-deleted").exists(),
251 "the marker is not written when the deletion failed"
252 );
253
254 std::fs::set_permissions(&profiles, std::fs::Permissions::from_mode(0o700))
255 .expect("profiles writable again");
256 }
257
258 #[cfg(unix)]
262 #[test]
263 fn a_legacy_tree_that_cannot_be_listed_fails_before_the_marker() {
264 use std::os::unix::fs::PermissionsExt;
265
266 let dir = tempfile::tempdir().expect("a temp dir");
267 let profiles = dir.path().join("profiles");
268 std::fs::create_dir_all(&profiles).expect("the profiles dir");
269 std::fs::set_permissions(&profiles, std::fs::Permissions::from_mode(0o000))
270 .expect("profiles closed");
271
272 let probe = profiles.join("permission-probe");
274 if std::fs::write(&probe, b"").is_ok() {
275 std::fs::remove_file(&probe).expect("the probe goes");
276 std::fs::set_permissions(&profiles, std::fs::Permissions::from_mode(0o700))
277 .expect("profiles readable again");
278 println!("skipping: the mode bits are not enforced (running as root?)");
279 return;
280 }
281
282 let store = Store::at(dir.path());
283 assert!(
284 store.delete_legacy_at(None).is_err(),
285 "a tree that cannot be listed fails the boot"
286 );
287 assert!(
288 !dir.path().join("legacy-profiles-deleted").exists(),
289 "the marker is not written when the listing failed"
290 );
291
292 std::fs::set_permissions(&profiles, std::fs::Permissions::from_mode(0o700))
293 .expect("profiles readable again");
294 }
295}