1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
//! Implementation related to the file system layout of a sphere workspace
use anyhow::{anyhow, Result};
use cid::{multihash::Code, multihash::MultihashDigest, Cid};
use libipld_core::raw::RawCodec;
use noosphere_core::data::{Did, MemoIpld};
use noosphere_storage::base64_encode;
use std::{
fmt::Debug,
path::{Path, PathBuf},
};
use super::extension::infer_file_extension;
/// The name of the "private" sphere folder, similar to a .git folder, that is
/// used to record and update the structure of a sphere over time
pub const SPHERE_DIRECTORY: &str = ".sphere";
pub(crate) const STORAGE_DIRECTORY: &str = "storage";
pub(crate) const CONTENT_DIRECTORY: &str = "content";
pub(crate) const PEERS_DIRECTORY: &str = "peers";
pub(crate) const SLUGS_DIRECTORY: &str = "slugs";
pub(crate) const MOUNT_DIRECTORY: &str = "mount";
pub(crate) const VERSION_FILE: &str = "version";
pub(crate) const IDENTITY_FILE: &str = "identity";
pub(crate) const DEPTH_FILE: &str = "depth";
pub(crate) const LINK_RECORD_FILE: &str = "link_record";
/// [SpherePaths] record the critical paths within a sphere workspace as
/// rendered to a typical file system. It is used to ensure that we read from
/// and write to consistent locations when rendering and updating a sphere as
/// files on disk.
///
/// NOTE: We use hashes to represent internal paths for a couple of reasons,
/// both related to Windows filesystem limitations:
///
/// 1. Windows filesystem, in the worst case, only allows 260 character-long
/// paths
/// 2. Windows does not allow various characters (e.g., ':') in file paths, and
/// there is no option to escape those characters
///
/// Hashing eliminates problem 2 and improves conditions so that we are more
/// likely to avoid problem 1.
///
/// See:
/// https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry
/// See also:
/// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
#[derive(Clone)]
pub struct SpherePaths {
root: PathBuf,
sphere: PathBuf,
storage: PathBuf,
slugs: PathBuf,
content: PathBuf,
peers: PathBuf,
version: PathBuf,
identity: PathBuf,
depth: PathBuf,
}
impl Debug for SpherePaths {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SpherePaths")
.field("root", &self.root)
.finish()
}
}
impl SpherePaths {
/// Returns true if the given path has a .sphere folder
fn has_sphere_directory(path: &Path) -> bool {
path.is_absolute() && path.join(SPHERE_DIRECTORY).is_dir()
}
// Root is the path that contains the .sphere folder
fn new(root: &Path) -> Self {
let sphere = root.join(SPHERE_DIRECTORY);
Self {
root: root.into(),
storage: sphere.join(STORAGE_DIRECTORY),
content: sphere.join(CONTENT_DIRECTORY),
peers: sphere.join(PEERS_DIRECTORY),
slugs: sphere.join(SLUGS_DIRECTORY),
version: sphere.join(VERSION_FILE),
identity: sphere.join(IDENTITY_FILE),
depth: sphere.join(DEPTH_FILE),
sphere,
}
}
/// Initialize [SpherePaths] for a given root path. This has the effect of
/// creating the "private" directory hierarchy (starting from
/// [SPHERE_DIRECTORY] inside the root).
pub async fn initialize(root: &Path) -> Result<Self> {
if !root.is_absolute() {
return Err(anyhow!(
"Must use an absolute path to initialize sphere directories; got {:?}",
root
));
}
let paths = Self::new(root);
std::fs::create_dir_all(&paths.storage)?;
std::fs::create_dir_all(&paths.content)?;
std::fs::create_dir_all(&paths.peers)?;
std::fs::create_dir_all(&paths.slugs)?;
Ok(paths)
}
/// Attempt to discover an existing workspace root by traversing ancestor
/// directories until one is found that contains a [SPHERE_DIRECTORY].
#[instrument(level = "trace")]
pub fn discover(from: Option<&Path>) -> Option<Self> {
trace!("Looking in {:?}", from);
match from {
Some(directory) => {
if Self::has_sphere_directory(directory) {
trace!("Found in {:?}!", directory);
Some(Self::new(directory))
} else {
Self::discover(directory.parent())
}
}
None => None,
}
}
/// The path to the root version file within the local [SPHERE_DIRECTORY]
pub fn version(&self) -> &Path {
&self.version
}
/// The path to the root identity file within the local [SPHERE_DIRECTORY]
pub fn identity(&self) -> &Path {
&self.identity
}
/// The path to the root depth file within the local [SPHERE_DIRECTORY]
pub fn depth(&self) -> &Path {
&self.depth
}
/// The path to the workspace root directory, which contains a
/// [SPHERE_DIRECTORY]
pub fn root(&self) -> &Path {
&self.root
}
/// The path to the [SPHERE_DIRECTORY] within the workspace root
pub fn sphere(&self) -> &Path {
&self.sphere
}
/// The path the directory within the [SPHERE_DIRECTORY] that contains
/// rendered peer spheres
pub fn peers(&self) -> &Path {
&self.peers
}
/// Given a slug, get a path where we may write a reverse-symlink to a file
/// system file that is a rendered equivalent of the content that can be
/// found at that slug. The slug's UTF-8 bytes are base64-encoded so that
/// certain characters that are allowed in slugs (e.g., '/') do not prevent
/// us from creating the symlink.
pub fn slug(&self, slug: &str) -> Result<PathBuf> {
Ok(self.slugs.join(base64_encode(slug.as_bytes())?))
}
/// Given a peer [Did] and sphere version [Cid], get a path where the that
/// peer's sphere at the given version ought to be rendered. The path will
/// be unique and deterministic far a given combination of [Did] and [Cid].
pub fn peer(&self, peer: &Did, version: &Cid) -> PathBuf {
let cid = Cid::new_v1(
RawCodec.into(),
Code::Blake3_256.digest(&[peer.as_bytes(), &version.to_bytes()].concat()),
);
self.peers.join(cid.to_string())
}
/// Given a [Cid] for a peer's memo, get a path to a file where the content
/// referred to by that memo ought to be written.
pub fn peer_hard_link(&self, memo_cid: &Cid) -> PathBuf {
self.content.join(memo_cid.to_string())
}
/// Given a slug and a [MemoIpld] referring to some content in the local
/// sphere, get a path to a file where the content referred to by the
/// [MemoIpld] ought to be rendered (including file extension).
pub fn root_hard_link(&self, slug: &str, memo: &MemoIpld) -> Result<PathBuf> {
self.file(&self.root, slug, memo)
}
/// Similar to [SpherePaths::root_hard_link] but for a peer given by [Did]
/// and sphere version [Cid].
pub fn peer_soft_link(
&self,
peer: &Did,
version: &Cid,
slug: &str,
memo: &MemoIpld,
) -> Result<PathBuf> {
self.file(&self.peer(peer, version).join(MOUNT_DIRECTORY), slug, memo)
}
/// Given a base path, a slug and a [MemoIpld], get the full file path
/// (including inferred file extension) for a file that corresponds to the
/// given [MemoIpld].
pub fn file(&self, base: &Path, slug: &str, memo: &MemoIpld) -> Result<PathBuf> {
let extension = infer_file_extension(memo)?;
let file_fragment = match extension {
Some(extension) => [slug, &extension].join("."),
None => slug.into(),
};
Ok(base.join(file_fragment))
}
}