use crate::{
files::{self, normalize_filename},
rollsum::SeaSplit,
splitter::FileSplitter,
Files,
};
use anyhow::Context;
use flume as mpsc;
use futures::future::join_all;
use ignore::{DirEntry, WalkBuilder};
use infinitree::{
object::{Pool, Writer},
Infinitree,
};
use memmap2::{Mmap, MmapOptions};
use std::{
fs,
io::{Cursor, Read},
num::NonZeroUsize,
path::PathBuf,
};
use tokio::task;
use tracing::{debug, debug_span, error, trace, warn, Instrument};
type Sender = mpsc::Sender<(PathBuf, files::Entry)>;
type Receiver = mpsc::Receiver<(PathBuf, files::Entry)>;
const MAX_FILE_SIZE: usize = 4 * 1024 * 1024;
#[derive(clap::Args, Debug, Default, Clone)]
pub struct Options {
pub paths: Vec<PathBuf>,
#[clap(flatten)]
pub preserve: files::PreserveMetadata,
#[clap(short = 'f', long)]
pub force: bool,
#[clap(short = 'M', long = "max-size")]
pub max_size: Option<u64>,
#[clap(short = 'x', long = "same-file-system")]
pub same_fs: bool,
#[clap(short = 'd', long = "ignore-hidden")]
pub hidden: bool,
#[clap(short = 'i', long = "ignore-case-insensitive")]
pub case_insensitive: bool,
#[clap(short = 'P', long = "inherit-parent-ignore", default_value = "true")]
pub parents: bool,
#[clap(short = 'G', long = "git-global-ignore")]
pub git_global: bool,
#[clap(short = 'E', long = "git-exclude")]
pub git_exclude: bool,
#[clap(short = 'g', long = "git-gitignore")]
pub git_ignore: bool,
#[clap(short = 'I', long = "dot-ignore")]
pub ignore: bool,
#[clap(short = 'l', long = "follow-links")]
pub follow_links: bool,
}
impl Options {
pub async fn add_recursive(
&self,
stash: &Infinitree<Files>,
threads: usize,
) -> anyhow::Result<()> {
let (sender, workers) = start_workers(stash, threads, self.force)?;
let dir_walk = self.dir_walk()?;
let mut current_file_list = vec![];
for dir_entry in dir_walk {
let (metadata, path) = match dir_entry {
Ok(de) => (de.metadata(), de.path().to_owned()),
Err(error) => {
warn!(%error, "failed to process file; skipping");
continue;
}
};
current_file_list.push(normalize_filename(&path)?);
let metadata = match metadata {
Ok(md) if md.is_file() || md.is_symlink() => md,
Ok(md) if md.is_dir() => {
let path_str = path.to_str().unwrap();
stash.index().tree.insert_directory(path_str).unwrap();
continue;
}
Err(error) => {
warn!(%error, ?path, "failed to get file metadata; skipping");
continue;
}
_ => continue,
};
let entry = match files::Entry::from_metadata(metadata, &path, &self.preserve) {
Ok(e) => e,
Err(error) => {
error!(%error, ?path, "failed to ingest file; aborting");
break;
}
};
trace!(?path, "queued");
sender.send((path, entry)).unwrap();
}
drop(sender);
join_all(workers).await;
let source_paths = self
.paths
.iter()
.map(normalize_filename)
.collect::<Result<Vec<_>, _>>()?;
stash.index().tree.retain(|p, _| {
for sp in source_paths.iter() {
if p.starts_with(sp) {
let status = current_file_list.contains(&p.to_string());
return status;
}
}
true
});
Ok(())
}
fn dir_walk(&self) -> anyhow::Result<impl Iterator<Item = Result<DirEntry, ignore::Error>>> {
let mut paths = self.paths.iter();
let mut builder = WalkBuilder::new(paths.next().context("no path available")?);
for path in paths {
builder.add(path);
}
builder.standard_filters(false);
builder.max_filesize(self.max_size);
builder.same_file_system(self.same_fs);
builder.hidden(self.hidden);
builder.ignore_case_insensitive(self.case_insensitive);
builder.parents(self.parents);
builder.git_exclude(self.git_exclude);
builder.git_ignore(self.git_ignore);
builder.git_global(self.git_global);
builder.ignore(self.ignore);
builder.follow_links(self.follow_links);
Ok(builder.build())
}
}
fn start_workers(
stash: &Infinitree<Files>,
threads: usize,
force: bool,
) -> anyhow::Result<(Sender, Vec<task::JoinHandle<()>>)> {
let (sender, receiver) = mpsc::bounded(threads * 2);
let balancer = Pool::new(NonZeroUsize::new(threads).unwrap(), stash.storage_writer()?)?;
let hasher = stash.hasher()?;
let workers = (0..threads)
.map(|_| {
task::spawn(process_file_loop(
force,
receiver.clone(),
stash.index().clone(),
hasher.clone(),
balancer.clone(),
))
})
.collect::<Vec<_>>();
Ok((sender, workers))
}
async fn process_file_loop(
force: bool,
r: Receiver,
index: crate::Files,
hasher: infinitree::Hasher,
writer: Pool<impl Writer + Clone + 'static>,
) {
let mut buf = Vec::with_capacity(MAX_FILE_SIZE);
while let Ok((path, entry)) = r.recv_async().await {
buf.clear();
let path_str = path.to_string_lossy();
if !force {
let tree = &index.tree;
if let Ok(Some(node)) = tree.node_by_path(&path_str) {
match node.as_ref() {
crate::Node::File { refs: _, entry: e } if *e.as_ref() == entry => {
debug!(?path, "already indexed, skipping");
continue;
}
crate::Node::File { refs: _, entry: _ } => {
debug!(?path, "adding new file");
}
crate::Node::Directory { .. } => {}
}
}
}
let size = entry.size;
if size == 0 || entry.file_type.is_symlink() {
index.tree.insert_file(&path_str, entry).unwrap();
continue;
}
let osfile = match fs::File::open(&path) {
Ok(f) => f,
Err(error) => {
warn!(%error, ?path, "failed to open file; skipping");
continue;
}
};
index_file(
entry,
osfile,
&mut buf,
path.clone(),
&index,
hasher.clone(),
&writer,
)
.instrument(debug_span!("indexing", ?path, size))
.await;
}
}
async fn index_file(
mut entry: files::Entry,
mut osfile: fs::File,
buf: &mut Vec<u8>,
path: PathBuf,
index: &crate::Files,
hasher: infinitree::Hasher,
writer: &Pool<impl Writer + Clone + 'static>,
) {
let size = entry.size as usize;
if size < MAX_FILE_SIZE {
osfile.read_to_end(buf).unwrap();
}
let mut mmap = MmappedFile::new(size, osfile);
let (_, chunks) = async_scoped::TokioScope::scope_and_block(|s| {
let splitter = if size < MAX_FILE_SIZE {
FileSplitter::<SeaSplit>::new(&buf[0..size], hasher)
} else {
FileSplitter::<SeaSplit>::new(mmap.open(), hasher)
};
for (start, hash, data) in splitter {
let mut writer = writer.clone();
s.spawn(async move {
let store = || writer.write_chunk(&hash, data).unwrap();
let ptr = index.chunks.insert_with(hash, store);
(start, ptr)
})
}
});
_ = std::mem::replace(
&mut entry.chunks,
chunks.into_iter().collect::<Result<Vec<_>, _>>().unwrap(),
);
debug!(?path, chunks = entry.chunks.len(), "indexed");
let path_str = path.to_str().unwrap();
index.tree.insert_file(path_str, entry).unwrap();
}
pub fn index_buf(
mut file: Cursor<Vec<u8>>,
mut entry: files::Entry,
hasher: infinitree::Hasher,
index: &mut crate::Files,
writer: &Pool<impl Writer + Clone + 'static>,
path: String,
) {
let mut buf = Vec::with_capacity(entry.size as usize);
file.read_to_end(&mut buf).unwrap();
let splitter = FileSplitter::<SeaSplit>::new(&buf, hasher);
let mut chunks: Vec<Result<(u64, std::sync::Arc<infinitree::ChunkPointer>), anyhow::Error>> =
Vec::default();
for (start, hash, data) in splitter {
let mut writer = writer.clone();
let store = || writer.write_chunk(&hash, data).unwrap();
let ptr = index.chunks.insert_with(hash, store);
chunks.push(Ok((start, ptr)))
}
_ = std::mem::replace(
&mut entry.chunks,
chunks.into_iter().collect::<Result<Vec<_>, _>>().unwrap(),
);
let index_tree = &mut index.tree;
index_tree.update_file(&path, entry.clone()).unwrap();
}
struct MmappedFile {
mmap: Option<Mmap>,
len: usize,
_file: std::fs::File,
}
impl MmappedFile {
fn new(len: usize, _file: std::fs::File) -> Self {
Self {
mmap: None,
len,
_file,
}
}
fn open(&mut self) -> &[u8] {
self.mmap.get_or_insert(unsafe {
MmapOptions::new()
.len(self.len)
.populate()
.map(&self._file)
.unwrap()
})
}
}