use std::collections::BTreeMap;
#[cfg(not(target_family = "wasm"))]
use std::path::{Path, PathBuf};
use futures::{AsyncRead, AsyncSeek, AsyncWrite, Future};
use serde_derive::{Deserialize, Serialize};
pub type OfferSend = Offer<OfferContent>;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(bound(deserialize = "T: Default"))]
pub struct Offer<T = ()> {
pub(super) content: BTreeMap<String, OfferEntry<T>>,
}
impl OfferSend {
#[cfg(not(target_family = "wasm"))]
pub async fn new_file_or_folder(offer_name: String, path: impl AsRef<Path>) -> std::io::Result<Self> {
let path = path.as_ref();
tracing::trace!("OfferSend::new_file_or_folder: {offer_name}, {}", path.display());
let mut content = BTreeMap::new();
content.insert(offer_name, OfferSendEntry::new(path).await?);
Ok(Self { content })
}
#[cfg(not(target_family = "wasm"))]
pub async fn new_paths(paths: impl IntoIterator<Item = PathBuf>) -> std::io::Result<Self> {
let mut content = BTreeMap::new();
for path in paths {
let offer_name = path.file_name().expect("Path must have a name");
let offer_name = offer_name
.to_str()
.ok_or_else(|| std::io::Error::other(format!("{} is not UTF-8 encoded", (offer_name.as_ref() as &Path).display())))?
.to_owned();
let old = content.insert(offer_name, OfferSendEntry::new(path).await?);
assert!(old.is_none(), "Duplicate names found");
}
Ok(Self { content })
}
pub fn new_file_custom(offer_name: String, size: u64, content: OfferContent) -> Self {
let mut content_ = BTreeMap::new();
content_.insert(offer_name, OfferSendEntry::RegularFile { size, content });
Self { content: content_ }
}
}
impl<T> Offer<T> {
pub fn top_level_paths(&self) -> impl Iterator<Item = &String> + '_ {
self.content.keys()
}
pub fn get(&self, path: &[String]) -> Option<&OfferEntry<T>> {
match path {
[] => None,
[start, rest @ ..] => self.content.get(start).and_then(|inner| inner.get(rest)),
}
}
pub fn get_file(&self, path: &[String]) -> Option<(&T, u64)> {
match path {
[] => None,
[start, rest @ ..] => self.content.get(start).and_then(|inner| inner.get_file(rest)),
}
}
pub fn iter_file_paths(&self) -> impl Iterator<Item = Vec<String>> + '_ {
self.iter_files().map(|val| val.0)
}
pub fn iter_files(&self) -> impl Iterator<Item = (Vec<String>, &T, u64)> + '_ {
self.content.iter().flat_map(|(name, offer)| {
let name = name.clone();
offer.iter_files().map(move |mut val| {
val.0.insert(0, name.clone());
val
})
})
}
pub fn total_size(&self) -> u64 {
self.iter_files().map(|v| v.2).sum()
}
#[cfg(not(target_family = "wasm"))]
pub fn accept_all(&self, target_dir: &Path) -> OfferAccept {
self.set_content(|path| {
let full_path: PathBuf = target_dir.join(path.join("/"));
let content = new_accept_content(move |append| {
let full_path = full_path.clone();
async_fs::OpenOptions::new().write(true).create(true).append(append).truncate(!append).open(full_path)
});
AcceptInner { content: Box::new(content) as _, offset: 0, sha256: None }
})
}
#[cfg(not(target_family = "wasm"))]
pub async fn create_directories(&self, target_path: &Path) -> std::io::Result<()> {
for (name, file) in &self.content {
file.create_directories(&target_path.join(name)).await?;
}
Ok(())
}
pub fn offer_name(&self) -> String {
let (name, entry) = self.content.iter().next().unwrap();
if self.is_multiple() {
format!("{name} and {} other files or directories", self.content.len() - 1)
} else if self.is_directory() {
let count = entry.iter_files().count();
format!("{name} with {count} files inside")
} else {
name.clone()
}
}
pub fn is_multiple(&self) -> bool {
self.content.len() > 1
}
pub fn is_directory(&self) -> bool {
self.is_multiple() || self.content.values().any(|f| matches!(f, OfferEntry::Directory { .. }))
}
pub fn set_content<U>(&self, mut f: impl FnMut(&[String]) -> U) -> Offer<U> {
Offer { content: self.content.iter().map(|(k, v)| (k.clone(), v.set_content(&mut vec![k.clone()], &mut f))).collect() }
}
}
impl<T: 'static + Send> Offer<T> {
pub fn into_iter_files(self) -> impl Iterator<Item = (Vec<String>, T, u64)> + Send {
self.content.into_iter().flat_map(|(name, offer)| {
offer.into_iter_files().map(move |mut val| {
val.0.insert(0, name.clone());
val
})
})
}
}
impl<T> From<&Offer<T>> for Offer {
fn from(from: &Offer<T>) -> Self {
from.set_content(|_| ())
}
}
pub trait AsyncReadSeek: AsyncRead + AsyncSeek {}
impl<T> AsyncReadSeek for T where T: AsyncRead + AsyncSeek {}
pub type OfferContent = Box<dyn Fn() -> futures::future::BoxFuture<'static, std::io::Result<Box<dyn AsyncReadSeek + Unpin + Send>>> + Send>;
pub fn new_offer_content<F, G, H>(content_provider: F) -> OfferContent
where
F: Fn() -> G + Send + 'static,
G: Future<Output = std::io::Result<H>> + Send + 'static,
H: AsyncReadSeek + Unpin + Send + 'static,
{
let wrap_fun = move || {
use futures::TryFutureExt;
let fut = content_provider();
let wrap_fut = fut.map_ok(|read| Box::new(read) as Box<dyn AsyncReadSeek + Unpin + Send>);
Box::pin(wrap_fut) as futures::future::BoxFuture<'static, _>
};
Box::new(wrap_fun) as _
}
pub type OfferSendEntry = OfferEntry<OfferContent>;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
#[serde(tag = "type")]
#[serde(bound(deserialize = "T: Default"))]
pub enum OfferEntry<T = ()> {
RegularFile {
size: u64,
#[serde(skip)]
content: T,
},
Directory {
content: BTreeMap<String, Self>,
},
}
impl OfferSendEntry {
#[cfg(not(target_family = "wasm"))]
pub(super) async fn new(path: impl AsRef<Path>) -> std::io::Result<Self> {
#[inline(always)]
fn new_recurse<'a>(path: impl AsRef<Path> + 'a + Send) -> futures::future::BoxFuture<'a, std::io::Result<OfferSendEntry>> {
Box::pin(OfferSendEntry::new(path))
}
let path = path.as_ref();
let metadata = async_fs::metadata(path).await?;
if metadata.is_file() {
tracing::trace!("OfferSendEntry::new {path:?} is file");
let path = path.to_owned();
Ok(Self::RegularFile {
size: metadata.len(),
content: new_offer_content(move || {
let path = path.clone();
async_fs::File::open(path)
}),
})
} else if metadata.is_dir() {
use futures::TryStreamExt;
tracing::trace!("OfferSendEntry::new {path:?} is directory");
let content: BTreeMap<String, Self> = async_fs::read_dir(path)
.await?
.and_then(|file| async move {
let path = file.path();
let name = path
.file_name()
.expect("Internal error: non-root paths should always have a name")
.to_str()
.ok_or_else(|| std::io::Error::other(format!("{} is not UTF-8 encoded", path.display())))?
.to_owned();
let offer = new_recurse(path).await?;
Ok((name, offer))
})
.try_collect()
.await?;
Ok(Self::Directory { content })
} else {
unreachable!()
}
}
}
impl<T> OfferEntry<T> {
fn iter_files(&self) -> impl Iterator<Item = (Vec<String>, &T, u64)> + '_ {
match self {
Self::Directory { content, .. } => {
let iter = content.iter().flat_map(|(name, offer)| {
let name = name.clone();
offer.iter_files().map(move |mut val| {
val.0.insert(0, name.clone());
val
})
});
Box::new(iter) as Box<dyn Iterator<Item = _>>
}
Self::RegularFile { content, size } => Box::new(std::iter::once((vec![], content, *size))) as Box<dyn Iterator<Item = _>>, }
}
fn get(&self, path: &[String]) -> Option<&Self> {
match path {
[] => Some(self),
[start, rest @ ..] => match self {
Self::Directory { content, .. } => content.get(start).and_then(|inner| inner.get(rest)),
_ => None,
},
}
}
fn get_file(&self, path: &[String]) -> Option<(&T, u64)> {
match path {
[] => match self {
Self::RegularFile { content, size } => Some((content, *size)),
_ => None,
},
[start, rest @ ..] => match self {
Self::Directory { content, .. } => content.get(start).and_then(|inner| inner.get_file(rest)),
_ => None,
},
}
}
#[cfg(not(target_family = "wasm"))]
async fn create_directories(&self, target_path: &Path) -> std::io::Result<()> {
#[inline(always)]
fn recurse<'a, T>(this: &'a OfferEntry<T>, path: &'a Path) -> futures::future::LocalBoxFuture<'a, std::io::Result<()>> {
Box::pin(OfferEntry::create_directories(this, path))
}
match self {
Self::Directory { content, .. } => {
async_fs::create_dir(target_path).await?;
for (name, file) in content {
recurse(file, &target_path.join(name)).await?;
}
Ok(())
}
_ => Ok(()),
}
}
fn set_content<U>(&self, base_path: &mut Vec<String>, f: &mut impl FnMut(&[String]) -> U) -> OfferEntry<U> {
match self {
OfferEntry::RegularFile { size, .. } => OfferEntry::RegularFile { size: *size, content: f(base_path) },
OfferEntry::Directory { content } => OfferEntry::Directory {
content: content
.iter()
.map(|(k, v)| {
base_path.push(k.clone());
let v = v.set_content(base_path, f);
base_path.pop();
(k.clone(), v)
})
.collect(),
},
}
}
}
impl<T: 'static + Send> OfferEntry<T> {
fn into_iter_files(self) -> impl Iterator<Item = (Vec<String>, T, u64)> + Send {
match self {
Self::Directory { content, .. } => {
let iter = content.into_iter().flat_map(|(name, offer)| {
offer.into_iter_files().map(move |mut val| {
val.0.insert(0, name.clone());
val
})
});
Box::new(iter) as Box<dyn Iterator<Item = _> + Send>
}
Self::RegularFile { content, size } => Box::new(std::iter::once((vec![], content, size))) as Box<dyn Iterator<Item = _> + Send>, }
}
}
impl<T> From<&OfferEntry<T>> for OfferEntry {
fn from(from: &OfferEntry<T>) -> Self {
from.set_content(&mut vec![], &mut |_| ())
}
}
pub type AcceptContent = Box<dyn FnOnce(bool) -> futures::future::BoxFuture<'static, std::io::Result<Box<dyn AsyncWrite + Unpin + Send>>> + Send>;
pub fn new_accept_content<F, G, H>(content_handler: F) -> AcceptContent
where
F: Fn(bool) -> G + Send + 'static,
G: Future<Output = std::io::Result<H>> + Send + 'static,
H: AsyncWrite + Unpin + Send + 'static,
{
let wrap_fun = move |append| {
use futures::TryFutureExt;
let fut = content_handler(append);
let wrap_fut = fut.map_ok(|write| Box::new(write) as Box<dyn AsyncWrite + Unpin + Send>);
Box::pin(wrap_fut) as futures::future::BoxFuture<'static, _>
};
Box::new(wrap_fun) as _
}
pub type OfferAccept = Offer<AcceptInner>;
pub struct AcceptInner {
pub offset: u64,
pub sha256: Option<[u8; 32]>,
pub content: AcceptContent,
}