silicon_apps_client/
state.rs1use crate::install::Installed;
3use anyhow::{Context, Result, ensure};
4use fs2::FileExt;
5use serde::{Deserialize, Serialize};
6use std::{
7 collections::BTreeMap,
8 fs,
9 path::{Path, PathBuf},
10};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(default)]
14pub struct Config {
15 pub server: String,
16 pub accounts_url: String,
17 pub telemetry: bool,
18 pub install_script_timeout_seconds: u64,
19 pub update_interval_seconds: u64,
20}
21impl Default for Config {
22 fn default() -> Self {
23 Self {
24 server: crate::DEFAULT_URL.into(),
25 accounts_url: "https://accounts.teamofsilicons.com".into(),
26 telemetry: true,
27 install_script_timeout_seconds: 120,
28 update_interval_seconds: 60,
29 }
30 }
31}
32#[derive(Debug, Clone)]
33pub struct LocalState {
34 pub home: PathBuf,
35 pub root: PathBuf,
36}
37impl LocalState {
38 pub fn new(home: impl Into<PathBuf>) -> Result<Self> {
39 let home = home.into();
40 ensure!(home.is_dir(), "{}: not a directory", home.display());
41 let home = home.canonicalize()?;
42 let root = home.join(".apps");
43 if root.exists() {
44 ensure!(
45 !fs::symlink_metadata(&root)?.file_type().is_symlink(),
46 "{}: state directory must not be a symlink",
47 root.display()
48 );
49 }
50 Ok(Self { home, root })
51 }
52 pub fn discover(explicit: Option<&Path>) -> Result<Self> {
54 if let Some(home) = explicit {
55 return Self::new(home);
56 }
57 if let Some(home) = std::env::var_os("SILICON_HOME") {
58 return Self::new(PathBuf::from(home));
59 }
60 let default = dirs::home_dir()
61 .context("could not locate home; set SILICON_HOME to an existing directory")?;
62 let pointer = default.join(".apps/home");
63 if pointer.is_file() {
64 return Self::new(PathBuf::from(fs::read_to_string(pointer)?.trim()));
65 }
66 Self::new(default)
67 }
68 pub fn set_home(location: &Path) -> Result<Self> {
69 let state = Self::new(location)?;
70 let default = dirs::home_dir().context("could not locate default home")?;
71 let root = default.join(".apps");
72 fs::create_dir_all(&root)?;
73 atomic_write(&root.join("home"), state.home.to_string_lossy().as_bytes())?;
74 Ok(state)
75 }
76 pub fn initialize(&self) -> Result<()> {
77 fs::create_dir_all(&self.root)?;
78 #[cfg(unix)]
79 {
80 use std::os::unix::fs::PermissionsExt;
81 fs::set_permissions(&self.root, fs::Permissions::from_mode(0o700))?;
82 }
83 for p in ["bin", "installed", "locks"] {
84 fs::create_dir_all(self.root.join(p))?;
85 }
86 Ok(())
87 }
88 pub fn config(&self) -> Result<Config> {
89 read_json(&self.root.join("config.json"))
90 }
91 pub fn save_config(&self, config: &Config) -> Result<()> {
92 self.initialize()?;
93 atomic_json(&self.root.join("config.json"), config)
94 }
95 pub fn installed(&self) -> Result<BTreeMap<String, Installed>> {
96 read_json(&self.root.join("installed.json"))
97 }
98 pub fn save_installed(&self, items: &BTreeMap<String, Installed>) -> Result<()> {
99 self.initialize()?;
100 atomic_json(&self.root.join("installed.json"), items)
101 }
102 pub fn lock(&self, name: &str) -> Result<fs::File> {
103 ensure!(
104 name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-'),
105 "invalid lock name"
106 );
107 self.initialize()?;
108 let lock = fs::OpenOptions::new()
109 .read(true)
110 .write(true)
111 .create(true)
112 .truncate(false)
113 .open(self.root.join("locks").join(name))?;
114 lock.try_lock_exclusive().with_context(|| {
115 format!("another Apps process holds the {name} lock; wait for it to finish")
116 })?;
117 Ok(lock)
118 }
119}
120pub fn read_json<T: serde::de::DeserializeOwned + Default>(path: &Path) -> Result<T> {
121 match fs::read(path) {
122 Ok(b) => serde_json::from_slice(&b).with_context(|| {
123 format!(
124 "{} contains invalid JSON; preserve it and repair the reported syntax",
125 path.display()
126 )
127 }),
128 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(T::default()),
129 Err(e) => Err(e.into()),
130 }
131}
132pub fn atomic_json<T: Serialize>(path: &Path, value: &T) -> Result<()> {
133 atomic_write(path, &serde_json::to_vec_pretty(value)?)
134}
135pub fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
136 use std::io::Write;
137 let parent = path.parent().context("state path must have a parent")?;
138 fs::create_dir_all(parent)?;
139 let mut temp = tempfile::NamedTempFile::new_in(parent)?;
140 #[cfg(unix)]
141 {
142 use std::os::unix::fs::PermissionsExt;
143 temp.as_file()
144 .set_permissions(fs::Permissions::from_mode(0o600))?;
145 }
146 temp.write_all(bytes)?;
147 temp.as_file().sync_all()?;
148 temp.persist(path).map_err(|e| e.error)?;
149 Ok(())
150}