noosphere_cli/native/
workspace.rs1use anyhow::{anyhow, Result};
4use cid::Cid;
5use directories::ProjectDirs;
6use noosphere::{platform::PlatformStorage, sphere::SphereContextBuilder};
7use noosphere_core::authority::Author;
8use noosphere_core::context::{
9 SphereContentRead, SphereContext, SphereCursor, COUNTERPART, GATEWAY_URL,
10};
11use noosphere_core::data::{Did, Link, LinkRecord, MemoIpld};
12use noosphere_storage::{KeyValueStore, SphereDb, StorageConfig};
13use serde_json::Value;
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16use tokio::io::AsyncReadExt;
17use ucan::crypto::KeyMaterial;
18use url::Url;
19
20use noosphere::key::InsecureKeyStorage;
21use tokio::sync::{Mutex, OnceCell};
22
23use crate::native::paths::{IDENTITY_FILE, LINK_RECORD_FILE, VERSION_FILE};
24
25use super::paths::SpherePaths;
26use super::render::SphereRenderer;
27
28pub type CliSphereContext = SphereContext<PlatformStorage>;
30
31pub type SphereDetails = (Did, Link<MemoIpld>, Option<LinkRecord>);
34
35pub struct Workspace {
38 sphere_paths: Option<Arc<SpherePaths>>,
39 key_storage: InsecureKeyStorage,
40 sphere_context: OnceCell<Arc<Mutex<CliSphereContext>>>,
41 working_directory: PathBuf,
42 storage_config: Option<StorageConfig>,
43}
44
45impl Workspace {
46 pub fn working_directory(&self) -> &Path {
49 &self.working_directory
50 }
51
52 pub async fn sphere_context(&self) -> Result<Arc<Mutex<CliSphereContext>>> {
54 Ok(self
55 .sphere_context
56 .get_or_try_init(|| async {
57 let mut builder = SphereContextBuilder::default()
58 .open_sphere(None)
59 .at_storage_path(self.require_sphere_paths()?.root())
60 .reading_keys_from(self.key_storage.clone());
61
62 if let Some(storage_config) = self.storage_config.as_ref() {
63 builder = builder.with_storage_config(storage_config);
64 }
65
66 Ok(Arc::new(Mutex::new(builder.build().await?.into())))
67 as Result<Arc<Mutex<CliSphereContext>>, anyhow::Error>
68 })
69 .await?
70 .clone())
71 }
72
73 pub fn release_sphere_context(&mut self) {
77 self.sphere_context = OnceCell::new();
78 }
79
80 pub async fn db(&self) -> Result<SphereDb<PlatformStorage>> {
84 let context = self.sphere_context().await?;
85 let context = context.lock().await;
86 Ok(context.db().clone())
87 }
88
89 pub fn key_storage(&self) -> &InsecureKeyStorage {
91 &self.key_storage
92 }
93
94 pub async fn author(&self) -> Result<Author<impl KeyMaterial + Clone>> {
96 Ok(self.sphere_context().await?.lock().await.author().clone())
97 }
98
99 pub fn require_sphere_paths(&self) -> Result<&Arc<SpherePaths>> {
102 self.sphere_paths
103 .as_ref()
104 .ok_or_else(|| anyhow!("Sphere paths not discovered for this location"))
105 }
106
107 pub fn sphere_paths(&self) -> Option<&Arc<SpherePaths>> {
110 self.sphere_paths.as_ref()
111 }
112
113 pub async fn sphere_identity(&self) -> Result<Did> {
115 let context = self.sphere_context().await?;
116 let context = context.lock().await;
117
118 Ok(context.identity().clone())
119 }
120
121 pub async fn counterpart_identity(&self) -> Result<Did> {
124 self.db().await?.require_key(COUNTERPART).await
125 }
126
127 pub async fn gateway_url(&self) -> Result<Url> {
129 self.db().await?.require_key(GATEWAY_URL).await
130 }
131
132 pub fn is_sphere_initialized(&self) -> bool {
134 if let Some(sphere_paths) = self.sphere_paths() {
135 sphere_paths.sphere().exists()
136 } else {
137 false
138 }
139 }
140
141 pub fn ensure_sphere_initialized(&self) -> Result<()> {
143 let sphere_paths = self.require_sphere_paths()?;
144 if !sphere_paths.sphere().exists() {
145 return Err(anyhow!(
146 "Expected {} to exist!",
147 sphere_paths.sphere().display()
148 ));
149 }
150 Ok(())
151 }
152
153 pub fn ensure_sphere_uninitialized(&self) -> Result<()> {
155 if let Some(sphere_paths) = self.sphere_paths() {
156 match sphere_paths.sphere().exists() {
157 true => {
158 return Err(anyhow!(
159 "A sphere is already initialized in {}",
160 sphere_paths.root().display()
161 ))
162 }
163 false => (),
164 }
165 }
166
167 Ok(())
168 }
169
170 #[instrument(level = "trace", skip(self))]
174 pub async fn describe_closest_sphere(
175 &self,
176 starting_from: Option<&Path>,
177 ) -> Result<Option<SphereDetails>> {
178 trace!("Looking for closest sphere...");
179
180 let sphere_paths = self.require_sphere_paths()?;
181
182 let canonical =
183 tokio::fs::canonicalize(starting_from.unwrap_or_else(|| self.working_directory()))
184 .await?;
185
186 let peers = sphere_paths.peers();
187 let root = sphere_paths.root();
188
189 let mut sphere_base: &Path = &canonical;
190
191 while let Some(parent) = sphere_base.parent() {
192 trace!("Looking in {}...", parent.display());
193
194 if parent == peers || parent == root {
195 trace!("Found!");
196
197 let (identity, version, link_record) = tokio::join!(
198 tokio::fs::read_to_string(sphere_base.join(IDENTITY_FILE)),
199 tokio::fs::read_to_string(sphere_base.join(VERSION_FILE)),
200 tokio::fs::read_to_string(sphere_base.join(LINK_RECORD_FILE)),
201 );
202 let identity = identity?;
203 let version = version?;
204 let link_record = if let Ok(link_record) = link_record {
205 LinkRecord::try_from(link_record).ok()
206 } else {
207 None
208 };
209
210 return Ok(Some((
211 identity.into(),
212 Cid::try_from(version)?.into(),
213 link_record,
214 )));
215 } else {
216 sphere_base = parent;
217 }
218 }
219
220 Ok(None)
221 }
222
223 #[instrument(level = "trace", skip(self))]
227 pub async fn read_subconscious_flavor_profile_nickname(
228 &self,
229 identity: &Did,
230 version: &Link<MemoIpld>,
231 ) -> Result<Option<String>> {
232 trace!("Looking for profile nickname");
233 let sphere_context = self.sphere_context().await?;
234 let peer_sphere_context = Arc::new(sphere_context.lock().await.to_visitor(identity).await?);
235 let cursor = SphereCursor::mounted_at(peer_sphere_context, version);
236
237 if let Some(mut profile) = cursor.read("_profile_").await? {
238 let mut profile_json = String::new();
239 profile.contents.read_to_string(&mut profile_json).await?;
240 match serde_json::from_str(&profile_json)? {
241 Value::Object(object) => match object.get("nickname") {
242 Some(Value::String(nickname)) => Ok(Some(nickname.to_owned())),
243 _ => Ok(None),
244 },
245 _ => Ok(None),
246 }
247 } else {
248 Ok(None)
249 }
250 }
251
252 #[instrument(level = "trace", skip(self))]
255 fn find_petname_in_path(&self, path: &Path) -> Result<Option<(String, PathBuf)>> {
256 let mut current_path: Option<&Path> = Some(path);
257
258 debug!("Looking for the petname of the local sphere...");
259 while let Some(path) = current_path {
260 trace!("Looking for petname in {}", path.display());
261 if let Some(tail) = path.components().last() {
262 if let Some(str) = tail.as_os_str().to_str() {
263 if str.starts_with('@') {
264 let petname = str.split('@').last().unwrap_or_default().to_owned();
265 debug!("Found petname @{}", petname);
266 return Ok(Some((petname, path.to_owned())));
267 }
268 }
269 }
270
271 current_path = path.parent();
272 }
273
274 debug!("No petname found");
275 Ok(None)
276 }
277
278 #[instrument(level = "debug", skip(self))]
282 pub async fn render(&self, depth: Option<u32>, force_full: bool) -> Result<()> {
283 let renderer = SphereRenderer::new(
284 self.sphere_context().await?,
285 self.require_sphere_paths()?.clone(),
286 );
287
288 renderer.render(depth, force_full).await?;
289
290 Ok(())
291 }
292
293 pub fn initialize(&mut self, sphere_paths: SpherePaths) -> Result<()> {
295 self.ensure_sphere_uninitialized()?;
296
297 self.sphere_paths = Some(Arc::new(sphere_paths));
298
299 Ok(())
300 }
301
302 pub fn new(
322 working_directory: &Path,
323 custom_noosphere_directory: Option<&Path>,
324 storage_config: Option<StorageConfig>,
325 ) -> Result<Self> {
326 let sphere_paths = SpherePaths::discover(Some(working_directory)).map(Arc::new);
327
328 let noosphere_directory = match custom_noosphere_directory {
329 Some(path) => path.to_owned(),
330 None => {
331 let project_dirs = ProjectDirs::from("network", "subconscious", "noosphere")
333 .ok_or_else(|| anyhow!("Unable to determine noosphere config directory"))?;
334 project_dirs.config_dir().to_owned()
335 }
336 };
337
338 debug!(
339 "Initializing key storage from {}",
340 noosphere_directory.display()
341 );
342
343 let key_storage = InsecureKeyStorage::new(&noosphere_directory)?;
344
345 let workspace = Workspace {
346 sphere_paths,
347 key_storage,
348 sphere_context: OnceCell::new(),
349 working_directory: working_directory.to_owned(),
350 storage_config,
351 };
352
353 Ok(workspace)
354 }
355}