use anyhow::{anyhow, Result};
use cid::Cid;
use globset::{Glob, GlobSet, GlobSetBuilder};
use noosphere_core::data::{BodyChunkIpld, ContentType};
use noosphere_storage::{BlockStore, MemoryStore};
use pathdiff::diff_paths;
use std::collections::{BTreeMap, BTreeSet};
use subtext::util::to_slug;
use tokio::fs;
use tokio_stream::StreamExt;
use noosphere_core::context::SphereWalker;
use super::{extension::infer_content_type, paths::SpherePaths, workspace::Workspace};
pub struct FileReference {
pub cid: Cid,
pub content_type: ContentType,
pub extension: Option<String>,
}
#[derive(Default)]
pub struct ContentChanges {
pub new: BTreeMap<String, Option<ContentType>>,
pub updated: BTreeMap<String, Option<ContentType>>,
pub removed: BTreeMap<String, Option<ContentType>>,
pub unchanged: BTreeSet<String>,
}
impl ContentChanges {
pub fn is_empty(&self) -> bool {
self.new.is_empty() && self.updated.is_empty() && self.removed.is_empty()
}
}
#[derive(Default)]
pub struct Content {
pub matched: BTreeMap<String, FileReference>,
pub ignored: BTreeSet<String>,
}
impl Content {
pub fn is_empty(&self) -> bool {
self.matched.is_empty()
}
fn get_ignored_patterns() -> Result<GlobSet> {
let ignored_patterns = vec!["@*", ".*"];
let mut builder = GlobSetBuilder::new();
for pattern in ignored_patterns {
builder.add(Glob::new(pattern)?);
}
Ok(builder.build()?)
}
pub async fn read_all<S: BlockStore>(paths: &SpherePaths, store: &mut S) -> Result<Content> {
let root_path = paths.root();
let mut directories = vec![(None, tokio::fs::read_dir(root_path).await?)];
let ignore_patterns = Content::get_ignored_patterns()?;
let mut content = Content::default();
while let Some((slug_prefix, mut directory)) = directories.pop() {
while let Some(entry) = directory.next_entry().await? {
let path = entry.path();
let relative_path = diff_paths(&path, root_path)
.ok_or_else(|| anyhow!("Could not determine relative path to {:?}", path))?;
if ignore_patterns.is_match(&relative_path) {
continue;
}
if path.is_dir() {
let slug_prefix = relative_path.to_string_lossy().to_string();
directories.push((Some(slug_prefix), tokio::fs::read_dir(path).await?));
continue;
}
let ignored = false;
let name = match path.file_stem() {
Some(name) => name.to_string_lossy(),
None => continue,
};
let name = match &slug_prefix {
Some(prefix) => format!("{prefix}/{name}"),
None => name.to_string(),
};
let slug = match to_slug(&name) {
Ok(slug) if slug == name => slug,
_ => continue,
};
if ignored {
content.ignored.insert(slug);
continue;
}
let extension = path
.extension()
.map(|extension| String::from(extension.to_string_lossy()));
let content_type = match &extension {
Some(extension) => infer_content_type(extension).await?,
None => ContentType::Bytes,
};
let file_bytes = fs::read(path).await?;
let body_cid = BodyChunkIpld::store_bytes(&file_bytes, store).await?;
content.matched.insert(
slug,
FileReference {
cid: body_cid,
content_type,
extension,
},
);
}
}
Ok(content)
}
pub async fn read_changes(
workspace: &Workspace,
) -> Result<Option<(Content, ContentChanges, MemoryStore)>> {
let mut new_blocks = MemoryStore::default();
let file_content =
Content::read_all(workspace.require_sphere_paths()?, &mut new_blocks).await?;
let sphere_context = workspace.sphere_context().await?;
let walker = SphereWalker::from(&sphere_context);
let content_stream = walker.content_stream();
tokio::pin!(content_stream);
let mut changes = ContentChanges::default();
while let Some((slug, sphere_file)) = content_stream.try_next().await? {
if file_content.ignored.contains(&slug) {
continue;
}
match file_content.matched.get(&slug) {
Some(FileReference {
cid: body_cid,
content_type,
extension: _,
}) => {
if &sphere_file.memo.body == body_cid {
changes.unchanged.insert(slug.clone());
continue;
}
changes
.updated
.insert(slug.clone(), Some(content_type.clone()));
}
None => {
changes
.removed
.insert(slug.clone(), sphere_file.memo.content_type());
}
}
}
for (slug, FileReference { content_type, .. }) in &file_content.matched {
if changes.updated.contains_key(slug)
|| changes.removed.contains_key(slug)
|| changes.unchanged.contains(slug)
{
continue;
}
changes.new.insert(slug.clone(), Some(content_type.clone()));
}
if changes.is_empty() {
Ok(None)
} else {
Ok(Some((file_content, changes, new_blocks)))
}
}
}