noosphere_cli/native/render/
writer.rs1use anyhow::{anyhow, Result};
2use cid::Cid;
3use noosphere_core::context::{AsyncFileBody, SphereFile};
4use noosphere_core::data::Did;
5use pathdiff::diff_paths;
6use std::{
7 path::{Path, PathBuf},
8 sync::{Arc, OnceLock},
9};
10use symlink::{remove_symlink_dir, remove_symlink_file, symlink_dir, symlink_file};
11use tokio::{fs::File, io::copy};
12
13use crate::native::paths::{
14 SpherePaths, IDENTITY_FILE, LINK_RECORD_FILE, MOUNT_DIRECTORY, VERSION_FILE,
15};
16
17use super::JobKind;
18
19#[derive(Debug, Clone)]
25pub struct SphereWriter {
26 kind: JobKind,
27 paths: Arc<SpherePaths>,
28 base: OnceLock<PathBuf>,
29 mount: OnceLock<PathBuf>,
30 private: OnceLock<PathBuf>,
31}
32
33impl SphereWriter {
34 pub fn new(kind: JobKind, paths: Arc<SpherePaths>) -> Self {
37 SphereWriter {
38 kind,
39 paths,
40 base: Default::default(),
41 mount: Default::default(),
42 private: Default::default(),
43 }
44 }
45
46 fn is_root_writer(&self) -> bool {
47 matches!(self.kind, JobKind::Root { .. })
48 }
49
50 fn petname(&self, name: &str) -> PathBuf {
51 self.mount().join(format!("@{}", name))
52 }
53
54 pub fn mount(&self) -> &Path {
56 self.mount.get_or_init(|| match &self.kind {
57 JobKind::Root { .. } | JobKind::RefreshPeers => self.base().to_owned(),
58 JobKind::Peer(_, _, _) => self.base().join(MOUNT_DIRECTORY),
59 })
60 }
61
62 pub fn base(&self) -> &Path {
65 self.base.get_or_init(|| match &self.kind {
66 JobKind::Root { .. } | JobKind::RefreshPeers => self.paths.root().to_owned(),
67 JobKind::Peer(did, cid, _) => self.paths.peer(did, cid),
68 })
69 }
70
71 pub fn private(&self) -> &Path {
73 self.private.get_or_init(|| match &self.kind {
74 JobKind::Root { .. } | JobKind::RefreshPeers => self.paths().sphere().to_owned(),
75 JobKind::Peer(_, _, _) => self.base().to_owned(),
76 })
77 }
78
79 pub fn paths(&self) -> &SpherePaths {
81 &self.paths
82 }
83
84 pub async fn write_link_record(&self) -> Result<()> {
87 if let JobKind::Peer(_, _, link_record) = &self.kind {
88 tokio::fs::write(self.private().join(LINK_RECORD_FILE), link_record.encode()?).await?;
89 }
90 Ok(())
91 }
92
93 pub async fn write_identity_and_version(&self, identity: &Did, version: &Cid) -> Result<()> {
95 let private = self.private();
96
97 tokio::try_join!(
98 tokio::fs::write(private.join(IDENTITY_FILE), identity.to_string()),
99 tokio::fs::write(private.join(VERSION_FILE), version.to_string())
100 )?;
101
102 Ok(())
103 }
104
105 pub fn content_hard_link<R>(&self, slug: &str, file: &SphereFile<R>) -> Result<PathBuf> {
111 if self.is_root_writer() {
112 self.paths.root_hard_link(slug, &file.memo)
113 } else {
114 Ok(self.paths.peer_hard_link(&file.memo_version))
115 }
116 }
117
118 #[instrument(level = "trace")]
120 pub async fn remove_content(&self, slug: &str) -> Result<()> {
121 if self.is_root_writer() {
122 let slug_path = self.paths.slug(slug)?;
123
124 if !slug_path.exists() {
125 trace!(
126 "No slug link found at '{}', skipping removal of '{slug}'...",
127 slug_path.display()
128 );
129 }
130
131 let file_path = tokio::fs::read_link(&slug_path).await?;
132
133 let _ = remove_symlink_file(slug_path);
134
135 if file_path.exists() {
136 trace!("Removing '{}'", file_path.display());
137 tokio::fs::remove_file(&file_path).await?;
138 }
139
140 Ok(())
141 } else {
142 Err(anyhow!("Cannot 'remove' individual peer content"))
143 }
144 }
145
146 #[instrument(level = "trace", skip(file))]
148 pub async fn write_content<R>(&self, slug: &str, file: &mut SphereFile<R>) -> Result<()>
149 where
150 R: AsyncFileBody,
151 {
152 let file_path = self.content_hard_link(slug, file)?;
153
154 trace!("Final file path will be '{}'", file_path.display());
155
156 let file_directory = file_path
157 .parent()
158 .ok_or_else(|| anyhow!("Unable to determine base directory for '{}'", slug))?;
159
160 tokio::fs::create_dir_all(file_directory).await?;
161
162 match tokio::fs::try_exists(&file_path).await {
163 Ok(true) => {
164 trace!("'{}' content already exists, not re-rendering...", slug);
165 }
166 Err(error) => {
167 warn!("Error checking for existing file: {}", error);
168 }
169 _ => {
170 debug!("Rendering content for '{}'...", slug);
171 let mut fs_file = File::create(&file_path).await?;
172 copy(&mut file.contents, &mut fs_file).await?;
173 }
174 };
175
176 if self.is_root_writer() {
183 self.symlink_slug(slug, &file_path).await?;
184 } else {
185 self.symlink_content(
186 &file.memo_version,
187 &self.paths.file(self.mount(), slug, &file.memo)?,
188 )
189 .await?;
190 }
191
192 Ok(())
193 }
194
195 pub async fn unlink_peer(&self, petname: &str) -> Result<()> {
197 let absolute_peer_destination = self.petname(petname);
198 if absolute_peer_destination.is_symlink() {
199 trace!(?petname, ?absolute_peer_destination, "Unlinking peer");
200 remove_symlink_dir(absolute_peer_destination)?;
201 }
202 Ok(())
203 }
204
205 pub async fn symlink_peer(&self, peer: &Did, version: &Cid, petname: &str) -> Result<()> {
207 let absolute_peer_destination = self.petname(petname);
208 let peer_directory_path = absolute_peer_destination.parent().ok_or_else(|| {
209 anyhow!(
210 "Unable to determine base directory for '{}'",
211 absolute_peer_destination.display()
212 )
213 })?;
214
215 tokio::fs::create_dir_all(peer_directory_path).await?;
216
217 let relative_peer_source = diff_paths(
218 self.paths.peer(peer, version).join(MOUNT_DIRECTORY),
219 self.mount(),
220 )
221 .ok_or_else(|| anyhow!("Could not resolve relative path for to '@{petname}'",))?;
222
223 self.unlink_peer(petname).await?;
224
225 trace!(?petname, ?absolute_peer_destination, "Symlinking peer");
226
227 symlink_dir(relative_peer_source, absolute_peer_destination)?;
228
229 Ok(())
230 }
231
232 async fn symlink_content(&self, memo_cid: &Cid, file_path: &PathBuf) -> Result<()> {
233 let file_directory_path = file_path.parent().ok_or_else(|| {
234 anyhow!(
235 "Unable to determine base directory for '{}'",
236 file_path.display()
237 )
238 })?;
239
240 tokio::fs::create_dir_all(file_directory_path).await?;
241
242 let relative_peer_content_path =
243 diff_paths(self.paths.peer_hard_link(memo_cid), file_directory_path).ok_or_else(
244 || {
245 anyhow!(
246 "Could not resolve relative path for '{}'",
247 file_path.display()
248 )
249 },
250 )?;
251
252 trace!(
253 "Symlinking from '{}' to '{}'...",
254 relative_peer_content_path.display(),
255 file_path.display()
256 );
257
258 if file_path.exists() {
259 remove_symlink_file(file_path)?;
260 }
261
262 symlink_file(relative_peer_content_path, file_path)?;
263
264 Ok(())
265 }
266
267 async fn symlink_slug(&self, slug: &str, file_path: &PathBuf) -> Result<()> {
268 let slug_path = self.paths.slug(slug)?;
269 let slug_base = slug_path
270 .parent()
271 .ok_or_else(|| anyhow!("Can't resolve parent directory of {}", slug_path.display()))?;
272
273 let relative_file_path = diff_paths(file_path, slug_base).ok_or_else(|| {
274 anyhow!(
275 "Could not resolve relative path for '{}'",
276 file_path.display()
277 )
278 })?;
279
280 if slug_path.exists() {
281 remove_symlink_file(&slug_path)?;
282 }
283
284 symlink_file(relative_file_path, slug_path)?;
285
286 Ok(())
287 }
288}