use std::{
borrow::Cow,
fs::read_dir,
path::{Component, Path, PathBuf},
};
use anyhow::{bail, Context};
use bytes::Bytes;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct DataSource {
name: String,
path: PathBuf,
}
impl DataSource {
pub fn new(path: PathBuf) -> Self {
let name = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
DataSource { path, name }
}
pub fn with_name(path: PathBuf, name: String) -> Self {
DataSource { path, name }
}
pub fn name(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.name)
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl From<PathBuf> for DataSource {
fn from(value: PathBuf) -> Self {
DataSource::new(value)
}
}
impl From<&std::path::Path> for DataSource {
fn from(value: &std::path::Path) -> Self {
DataSource::new(value.to_path_buf())
}
}
#[cfg(feature = "rpc")]
pub fn scan_path(
path: PathBuf,
wrap: crate::rpc::client::blobs::WrapOption,
) -> anyhow::Result<Vec<DataSource>> {
use crate::rpc::client::blobs::WrapOption;
if path.is_dir() {
scan_dir(path, wrap)
} else {
let name = match wrap {
WrapOption::NoWrap => bail!("Cannot scan a file without wrapping"),
WrapOption::Wrap { name: None } => file_name(&path)?,
WrapOption::Wrap { name: Some(name) } => name,
};
Ok(vec![DataSource { name, path }])
}
}
#[cfg(feature = "rpc")]
#[cfg_attr(iroh_docsrs, doc(cfg(feature = "rpc")))]
fn file_name(path: &Path) -> anyhow::Result<String> {
relative_canonicalized_path_to_string(path.file_name().context("path is invalid")?)
}
#[cfg(feature = "rpc")]
#[cfg_attr(iroh_docsrs, doc(cfg(feature = "rpc")))]
pub fn scan_dir(
root: PathBuf,
wrap: crate::rpc::client::blobs::WrapOption,
) -> anyhow::Result<Vec<DataSource>> {
use crate::rpc::client::blobs::WrapOption;
if !root.is_dir() {
bail!("Expected {} to be a file", root.to_string_lossy());
}
let prefix = match wrap {
WrapOption::NoWrap => None,
WrapOption::Wrap { name: None } => Some(file_name(&root)?),
WrapOption::Wrap { name: Some(name) } => Some(name),
};
let files = walkdir::WalkDir::new(&root).into_iter();
let data_sources = files
.map(|entry| {
let entry = entry?;
if !entry.file_type().is_file() {
return Ok(None);
}
let path = entry.into_path();
let mut name = relative_canonicalized_path_to_string(path.strip_prefix(&root)?)?;
if let Some(prefix) = &prefix {
name = format!("{prefix}/{name}");
}
anyhow::Ok(Some(DataSource { name, path }))
})
.filter_map(Result::transpose);
let data_sources: Vec<anyhow::Result<DataSource>> = data_sources.collect::<Vec<_>>();
data_sources.into_iter().collect::<anyhow::Result<Vec<_>>>()
}
pub fn relative_canonicalized_path_to_string(path: impl AsRef<Path>) -> anyhow::Result<String> {
canonicalized_path_to_string(path, true)
}
#[cfg(feature = "rpc")]
#[cfg_attr(iroh_docsrs, doc(cfg(feature = "rpc")))]
pub async fn load_secret_key(key_path: PathBuf) -> anyhow::Result<iroh_net::key::SecretKey> {
use tokio::io::AsyncWriteExt;
if key_path.exists() {
let keystr = tokio::fs::read(key_path).await?;
let secret_key =
iroh_net::key::SecretKey::try_from_openssh(keystr).context("invalid keyfile")?;
Ok(secret_key)
} else {
let secret_key = iroh_net::key::SecretKey::generate();
let ser_key = secret_key.to_openssh()?;
let key_path = key_path.canonicalize().unwrap_or(key_path);
let key_path_parent = key_path.parent().ok_or_else(|| {
anyhow::anyhow!("no parent directory found for '{}'", key_path.display())
})?;
tokio::fs::create_dir_all(&key_path_parent).await?;
let (file, temp_file_path) = tempfile::NamedTempFile::new_in(key_path_parent)
.context("unable to create tempfile")?
.into_parts();
let mut file = tokio::fs::File::from_std(file);
file.write_all(ser_key.as_bytes())
.await
.context("unable to write keyfile")?;
file.flush().await?;
drop(file);
tokio::fs::rename(temp_file_path, key_path)
.await
.context("failed to rename keyfile")?;
Ok(secret_key)
}
}
#[derive(Debug, Clone)]
pub struct PathContent {
pub size: u64,
pub files: u64,
}
pub fn path_content_info(path: impl AsRef<Path>) -> anyhow::Result<PathContent> {
path_content_info0(path)
}
fn path_content_info0(path: impl AsRef<Path>) -> anyhow::Result<PathContent> {
let mut files = 0;
let mut size = 0;
let path = path.as_ref();
if path.is_dir() {
for entry in read_dir(path)? {
let path0 = entry?.path();
match path_content_info0(path0) {
Ok(path_content) => {
size += path_content.size;
files += path_content.files;
}
Err(e) => bail!(e),
}
}
} else {
match path.try_exists() {
Ok(true) => {
size = path
.metadata()
.context(format!("Error reading metadata for {path:?}"))?
.len();
files = 1;
}
Ok(false) => {
tracing::warn!("Not including broking symlink at {path:?}");
}
Err(e) => {
bail!(e);
}
}
}
Ok(PathContent { size, files })
}
pub fn key_to_path(
key: impl AsRef<[u8]>,
prefix: Option<String>,
root: Option<PathBuf>,
) -> anyhow::Result<PathBuf> {
let mut key = key.as_ref();
if key.is_empty() {
return Ok(PathBuf::new());
}
if b'\0' == key[key.len() - 1] {
key = &key[..key.len() - 1]
}
let key = if let Some(prefix) = prefix {
let prefix = prefix.into_bytes();
if prefix[..] == key[..prefix.len()] {
&key[prefix.len()..]
} else {
anyhow::bail!("key {:?} does not begin with prefix {:?}", key, prefix);
}
} else {
key
};
let mut path = if key[0] == b'/' {
PathBuf::from("/")
} else {
PathBuf::new()
};
for component in key
.split(|c| c == &b'/')
.map(|c| String::from_utf8(c.into()).context("key contains invalid data"))
{
let component = component?;
path = path.join(component);
}
let path = if let Some(root) = root {
root.join(path)
} else {
path
};
Ok(path)
}
pub fn path_to_key(
path: impl AsRef<Path>,
prefix: Option<String>,
root: Option<PathBuf>,
) -> anyhow::Result<Bytes> {
let path = path.as_ref();
let path = if let Some(root) = root {
path.strip_prefix(root)?
} else {
path
};
let suffix = canonicalized_path_to_string(path, false)?.into_bytes();
let mut key = if let Some(prefix) = prefix {
prefix.into_bytes().to_vec()
} else {
Vec::new()
};
key.extend(suffix);
key.push(b'\0');
Ok(key.into())
}
pub fn canonicalized_path_to_string(
path: impl AsRef<Path>,
must_be_relative: bool,
) -> anyhow::Result<String> {
let mut path_str = String::new();
let parts = path
.as_ref()
.components()
.filter_map(|c| match c {
Component::Normal(x) => {
let c = match x.to_str() {
Some(c) => c,
None => return Some(Err(anyhow::anyhow!("invalid character in path"))),
};
if !c.contains('/') && !c.contains('\\') {
Some(Ok(c))
} else {
Some(Err(anyhow::anyhow!("invalid path component {:?}", c)))
}
}
Component::RootDir => {
if must_be_relative {
Some(Err(anyhow::anyhow!("invalid path component {:?}", c)))
} else {
path_str.push('/');
None
}
}
_ => Some(Err(anyhow::anyhow!("invalid path component {:?}", c))),
})
.collect::<anyhow::Result<Vec<_>>>()?;
let parts = parts.join("/");
path_str.push_str(&parts);
Ok(path_str)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_to_key_roundtrip() {
let path = PathBuf::from("/foo/bar");
let expect_path = PathBuf::from("/foo/bar");
let key = b"/foo/bar\0";
let expect_key = Bytes::from(&key[..]);
let got_key = path_to_key(path.clone(), None, None).unwrap();
let got_path = key_to_path(got_key.clone(), None, None).unwrap();
assert_eq!(expect_key, got_key);
assert_eq!(expect_path, got_path);
let prefix = String::from("prefix:");
let key = b"prefix:/foo/bar\0";
let expect_key = Bytes::from(&key[..]);
let got_key = path_to_key(path.clone(), Some(prefix.clone()), None).unwrap();
assert_eq!(expect_key, got_key);
let got_path = key_to_path(got_key, Some(prefix.clone()), None).unwrap();
assert_eq!(expect_path, got_path);
let root = PathBuf::from("/foo");
let key = b"prefix:bar\0";
let expect_key = Bytes::from(&key[..]);
let got_key = path_to_key(path, Some(prefix.clone()), Some(root.clone())).unwrap();
assert_eq!(expect_key, got_key);
let got_path = key_to_path(got_key, Some(prefix), Some(root)).unwrap();
assert_eq!(expect_path, got_path);
}
#[test]
fn test_canonicalized_path_to_string() {
assert_eq!(
canonicalized_path_to_string("foo/bar", true).unwrap(),
"foo/bar"
);
assert_eq!(canonicalized_path_to_string("", true).unwrap(), "");
assert_eq!(
canonicalized_path_to_string("foo bar/baz/bat", true).unwrap(),
"foo bar/baz/bat"
);
assert_eq!(
canonicalized_path_to_string("/foo/bar", true).map_err(|e| e.to_string()),
Err("invalid path component RootDir".to_string())
);
assert_eq!(
canonicalized_path_to_string("/foo/bar", false).unwrap(),
"/foo/bar"
);
let path = PathBuf::from("/").join("Ü").join("⁰€™■・�").join("東京");
assert_eq!(
canonicalized_path_to_string(path, false).unwrap(),
"/Ü/⁰€™■・�/東京"
)
}
#[test]
fn test_get_path_content() {
let dir = testdir::testdir!();
let PathContent { size, files } = path_content_info(&dir).unwrap();
assert_eq!(0, size);
assert_eq!(0, files);
let foo = b"hello_world";
let bar = b"ipsum lorem";
let bat = b"happy birthday";
let expect_size = foo.len() + bar.len() + bat.len();
std::fs::write(dir.join("foo.txt"), foo).unwrap();
std::fs::write(dir.join("bar.txt"), bar).unwrap();
std::fs::write(dir.join("bat.txt"), bat).unwrap();
let PathContent { size, files } = path_content_info(&dir).unwrap();
assert_eq!(expect_size as u64, size);
assert_eq!(3, files);
std::fs::create_dir(dir.join("1")).unwrap();
std::fs::create_dir(dir.join("2")).unwrap();
let dir3 = dir.join("3");
std::fs::create_dir(&dir3).unwrap();
let dir4 = dir3.join("4");
std::fs::create_dir(&dir4).unwrap();
std::fs::write(dir4.join("foo.txt"), foo).unwrap();
std::fs::write(dir4.join("bar.txt"), bar).unwrap();
std::fs::write(dir4.join("bat.txt"), bat).unwrap();
let expect_size = expect_size * 2;
let PathContent { size, files } = path_content_info(&dir).unwrap();
assert_eq!(expect_size as u64, size);
assert_eq!(6, files);
}
}