use std::fs;
use std::sync::Arc;
use std::fs::File;
use std::io::BufWriter;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use anyhow::bail;
use crossbeam::channel::Receiver;
use crossbeam::channel::Sender;
use log::info;
use zip::ZipWriter;
use crate::command::threadcount::determine_thread_counts_mapcell;
use crate::fileformat::tirp::TirpBascetShardReaderFactory;
use crate::fileformat::zip::ZipBascetShardReaderFactory;
use crate::fileformat::TirpStreamingShardReaderFactory;
use crate::utils;
use crate::fileformat;
use crate::fileformat::ShardRandomFileExtractor;
use crate::fileformat::ShardStreamingFileExtractor;
use crate::fileformat::ConstructFromPath;
use crate::fileformat::detect_shard_format;
use crate::fileformat::DetectedFileformat;
use crate::mapcell::CompressionMode;
use crate::mapcell::MissingFileMode;
use crate::mapcell::MapCellFunction;
use crate::{command::mapcell, mapcell::MapCellFunctionShellScript};
pub const DEFAULT_PATH_TEMP: &str = "temp";
#[derive(Args)]
pub struct MapCellCMD {
#[arg(short = 'i', value_parser= clap::value_parser!(PathBuf))]
pub path_in: Option<PathBuf>,
#[arg(short = 't', value_parser= clap::value_parser!(PathBuf), default_value = DEFAULT_PATH_TEMP)]
pub path_tmp: PathBuf,
#[arg(short = 'o', value_parser = clap::value_parser!(PathBuf))]
pub path_out: Option<PathBuf>,
#[arg(short = 's', value_parser = clap::value_parser!(PathBuf))]
pub path_script: PathBuf,
#[arg(long = "show-script-output")]
pub show_script_output: bool,
#[arg(long = "show-presets")]
pub show_presets: bool,
#[arg(long = "keep-files")]
pub keep_files: bool,
#[arg(short = '@', value_parser = clap::value_parser!(usize))]
num_threads_total: Option<usize>,
#[arg(long, value_parser = clap::value_parser!(usize))]
num_threads_read: Option<usize>,
#[arg(long, value_parser = clap::value_parser!(usize))]
num_threads_write: Option<usize>,
#[arg(long, value_parser = clap::value_parser!(usize))]
num_threads_mapcell: Option<usize>,
}
impl MapCellCMD {
pub fn try_execute(&mut self) -> Result<()> {
if self.show_presets {
let names = crate::mapcell_scripts::get_preset_script_names();
println!("Available preset scripts: {:?}", names);
return Ok(());
}
let preset_name = self.path_script.to_str().expect("argument conversion error");
let script: Arc<Box<dyn MapCellFunction>> = if preset_name.starts_with("_") {
println!("Using preset script: {:?}", self.path_script);
let preset_name=&preset_name[1..]; crate::mapcell_scripts::get_preset_script(preset_name).expect("Unable to load preset script")
} else {
println!("Using user provided script: {:?}", self.path_script);
let s = MapCellFunctionShellScript::new_from_file(&self.path_script).expect("Failed to load user defined script");
Arc::new(Box::new(s))
};
println!("Script info: {:?}", script);
let (num_threads_read, num_threads_write, num_threads_mapcell) = determine_thread_counts_mapcell(
self.num_threads_total,
self.num_threads_read,
self.num_threads_write,
self.num_threads_mapcell,
script.get_recommend_threads()
)?;
println!("Using threads, readers: {}, writers: {}, mapcell: {}",num_threads_read, num_threads_write, num_threads_mapcell);
let params = mapcell::MapCell {
path_in: self.path_in.as_ref().expect("Input file was not provided").clone(),
path_tmp: self.path_tmp.clone(),
path_out: self.path_out.as_ref().expect("Output file was not provided").clone(),
script: script,
threads_read: num_threads_read,
threads_write: num_threads_write,
threads_mapcell: num_threads_mapcell,
show_script_output: self.show_script_output,
keep_files: self.keep_files
};
let _ = mapcell::MapCell::run(params).expect("mapcell failed");
log::info!("Mapcell has finished!");
Ok(())
}
}
#[derive(Clone)]
pub struct MapCell {
pub path_in: std::path::PathBuf,
pub path_tmp: std::path::PathBuf,
pub path_out: std::path::PathBuf,
pub script: Arc<Box<dyn MapCellFunction>>,
pub threads_read: usize,
pub threads_write: usize,
pub threads_mapcell: usize,
pub show_script_output: bool,
pub keep_files: bool
}
impl MapCell {
pub fn run(
params: MapCell
) -> anyhow::Result<()> {
if params.path_tmp.exists() {
bail!("Temporary directory '{}' exists already. For safety reasons, this is not allowed. Specify as a subdirectory of an existing directory", params.path_tmp.display());
} else {
println!("Using tempdir {}", params.path_tmp.display());
if fs::create_dir_all(¶ms.path_tmp).is_err() {
panic!("Failed to create temporary directory");
};
}
let params = Arc::new(params);
let read_queue_size = params.threads_write*2;
println!("Queue of cells is of size: {}",read_queue_size);
let (tx_loaded_cell, rx_loaded_cell) = crossbeam::channel::bounded::<Option<String>>(read_queue_size);
let thread_pool_writers = threadpool::ThreadPool::new(params.threads_write);
let mut list_out_zipfiles: Vec<PathBuf> = Vec::new();
for tidx in 0..params.threads_write {
let file_zip = params.path_tmp.join(format!("out-{}.zip", tidx));
println!("Temporary zip file {}", file_zip.display());
_ = create_writer(
¶ms,
&file_zip,
&Arc::clone(¶ms.script),
&thread_pool_writers,
&rx_loaded_cell
);
list_out_zipfiles.push(file_zip);
}
let input_shard_type = detect_shard_format(¶ms.path_in);
println!("Input file: {:?}",params.path_in);
let perform_streaming=input_shard_type == DetectedFileformat::TIRP;
if perform_streaming {
println!("Reading will be streamed");
let thread_pool_readers = threadpool::ThreadPool::new(params.threads_read);
if input_shard_type == DetectedFileformat::TIRP {
println!("Detected input as TIRP");
for _tidx in 0..params.threads_read { _ = create_streaming_shard_reader(
¶ms,
&thread_pool_readers,
&Arc::clone(¶ms.script),
&tx_loaded_cell,
&Arc::new(TirpStreamingShardReaderFactory::new())
);
}
} else {
bail!("Cannot tell the type of the input format"); }
thread_pool_readers.join();
println!("Streaming readers have finished")
} else {
println!("Reading will be random (this can be slow depending on file format)");
let thread_pool_readers = threadpool::ThreadPool::new(params.threads_read);
let input_shard_type = detect_shard_format(¶ms.path_in);
if input_shard_type == DetectedFileformat::TIRP {
println!("Detected input as TIRP");
for _tidx in 0..params.threads_read { _ = create_random_shard_reader(
¶ms,
&thread_pool_readers,
&Arc::clone(¶ms.script),
&tx_loaded_cell,
&Arc::new(TirpBascetShardReaderFactory::new())
);
}
} else if input_shard_type == DetectedFileformat::ZIP {
println!("Detected input as ZIP");
for _tidx in 0..params.threads_read {
_ = create_random_shard_reader(
¶ms,
&thread_pool_readers,
&Arc::clone(¶ms.script),
&tx_loaded_cell,
&Arc::new(ZipBascetShardReaderFactory::new())
);
}
} else {
bail!("Cannot tell the type of the input format");
}
thread_pool_readers.join();
println!("Random I/O readers have finished")
}
println!("Waiting for writers to finish");
for i in 0..params.threads_write {
println!("Sending termination signal to writer {i}");
_ = tx_loaded_cell.send(None).unwrap();
}
thread_pool_writers.join();
println!("Writers have finished");
println!("Merging zip from writers");
utils::merge_archives_and_delete(¶ms.path_out, &list_out_zipfiles).unwrap();
if !params.keep_files {
let _ = fs::remove_dir_all(¶ms.path_tmp);
}
Ok(())
}
}
fn create_random_shard_reader<R>(
params_io: &Arc<MapCell>,
thread_pool: &threadpool::ThreadPool,
mapcell_script: &Arc<Box<dyn MapCellFunction>>,
tx: &Sender<Option<String>>,
constructor: &Arc<impl ConstructFromPath<R>+Send+ 'static+Sync>
) -> anyhow::Result<()> where R:ShardRandomFileExtractor {
let tx = tx.clone();
let params_io = Arc::clone(¶ms_io);
let mapcell_script = Arc::clone(mapcell_script);
let constructor = Arc::clone(constructor);
thread_pool.execute(move || {
println!("Reader started");
let mut shard = constructor.new_from_path(¶ms_io.path_in).expect("Failed to create bascet reader");
let list_cells = fileformat::try_get_cells_in_file(¶ms_io.path_in).expect("Could not get list of cells from input file");
let list_cells = if let Some(list_cells) = list_cells {
list_cells
} else {
panic!("unable to figure out a list of cells ahead of time; this has not yet been implemented (provide suitable input file format, or manually specify cells)");
};
let mut num_cells_processed = 0;
for cell_id in list_cells { info!("request to read {}",cell_id);
if num_cells_processed%10 ==0 {
println!("processed {} cells, now at {}",num_cells_processed, cell_id);
}
let path_cell_dir = params_io.path_tmp.join(format!("cell-{}", cell_id));
fs::create_dir(&path_cell_dir).unwrap();
let fail_if_missing = mapcell_script.get_missing_file_mode() != MissingFileMode::Ignore;
let success = shard.extract_to_outdir(
&cell_id,
&mapcell_script.get_expect_files(),
fail_if_missing,
&path_cell_dir
).expect("error during extraction");
if success {
_ = tx.send(Some(cell_id));
} else {
let missing_file_mode = mapcell_script.get_missing_file_mode();
if missing_file_mode==MissingFileMode::Fail {
panic!("Failed extraction of {}; shutting down process, keeping temp files for inspection", cell_id);
}
if missing_file_mode==MissingFileMode::Ignore {
println!("Did not find all expected files for '{}', ignoring. Files present: {:?}", cell_id, shard.get_files_for_cell(&cell_id));
}
}
num_cells_processed += 1;
}
println!("Reader ended; read a total of {} cells", num_cells_processed);
});
Ok(())
}
fn create_streaming_shard_reader<R>(
params_io: &Arc<MapCell>,
thread_pool: &threadpool::ThreadPool,
mapcell_script: &Arc<Box<dyn MapCellFunction>>,
tx: &Sender<Option<String>>,
constructor: &Arc<impl ConstructFromPath<R>+Send+ 'static+Sync>
) -> anyhow::Result<()> where R:ShardStreamingFileExtractor {
let tx = tx.clone();
let params_io = Arc::clone(¶ms_io);
let mapcell_script = Arc::clone(mapcell_script);
let constructor = Arc::clone(constructor);
thread_pool.execute(move || {
println!("Reader started");
let mut shard = constructor.new_from_path(¶ms_io.path_in).expect("Failed to create bascet reader");
let mut num_cells_processed = 0;
while let Ok(Some(cell_id)) = shard.next_cell() {
if num_cells_processed%10 ==0 {
println!("processed {} cells, now at {}",num_cells_processed, cell_id);
}
let path_cell_dir = params_io.path_tmp.join(format!("cell-{}", cell_id));
let _ = fs::create_dir(&path_cell_dir);
let fail_if_missing = mapcell_script.get_missing_file_mode() != MissingFileMode::Ignore;
let success = shard.extract_to_outdir(
&mapcell_script.get_expect_files(),
fail_if_missing,
&path_cell_dir
).expect("error during extraction");
if success {
_ = tx.send(Some(cell_id));
} else {
let missing_file_mode = mapcell_script.get_missing_file_mode();
if missing_file_mode==MissingFileMode::Fail {
panic!("Failed extraction of {}; shutting down process, keeping temp files for inspection", cell_id);
}
if missing_file_mode==MissingFileMode::Ignore {
println!("Did not find all expected files for '{}', ignoring. Files present: {:?}", cell_id, shard.get_files_for_cell());
}
}
num_cells_processed+=1;
}
println!("Reader ended; read a total of {} cells", num_cells_processed);
});
Ok(())
}
fn create_writer(
params_io: &Arc<MapCell>,
zip_file: &PathBuf,
mapcell_script: &Arc<Box<dyn MapCellFunction>>,
thread_pool: &threadpool::ThreadPool,
rx: &Receiver<Option<String>>
) -> anyhow::Result<()> {
let params_io = Arc::clone(¶ms_io);
let mapcell_script = Arc::clone(mapcell_script);
let rx = rx.clone();
let zip_file = zip_file.clone();
thread_pool.execute(move || {
println!("Writer started");
let zip_file = File::create(zip_file).unwrap(); let buf_writer = BufWriter::new(zip_file);
let mut zip_writer = ZipWriter::new(buf_writer);
while let Ok(Some(cell_id)) = rx.recv() {
let path_input_dir = params_io.path_tmp.join(format!("cell-{}", cell_id));
let _ = fs::create_dir(&path_input_dir);
let path_output_dir = params_io.path_tmp.join(format!("output-{}", cell_id));
let _ = fs::create_dir(&path_output_dir);
println!("Writer for '{}', running script", cell_id);
let (success, script_output) = mapcell_script.invoke(
&path_input_dir,
&path_output_dir,
params_io.threads_mapcell
).expect("Failed to invoke script");
println!("Writer for '{}', done running script", cell_id);
if !success {
if mapcell_script.get_missing_file_mode()==MissingFileMode::Fail {
panic!("Failed to process a cell, and this script is set to fail in such a scenario");
}
}
if params_io.show_script_output {
println!("{}",&script_output);
}
println!("Writer for '{}', adding log file to zip", cell_id);
{
let path_logfile = path_output_dir.join("_mapcell.log");
let log_file = File::create(&path_logfile).unwrap();
let mut buf_writer = BufWriter::new(log_file);
let _ = std::io::copy(&mut script_output.as_bytes(), &mut buf_writer).unwrap();
}
let list_output_files = get_list_files_recursively(&path_output_dir).expect("failed to list output files");
let basepath_len = params_io.path_tmp.display().to_string().len() + 1 + "output-".len();
let fname_as_string: Vec<String> = list_output_files.iter().map(|f| f.display().to_string() ).collect();
let names_in_zip: Vec<&str> = fname_as_string.iter().map(|f| &f[basepath_len..] ).collect();
println!("Writer for '{}', got files {:?}", cell_id, list_output_files);
println!("Writer for '{}', got names {:?}", cell_id, names_in_zip);
for (file_path, &file_name) in list_output_files.iter().zip(names_in_zip.iter()) {
println!("Writer for '{}', adding to zip: {}",cell_id, file_path.display());
let mut file_input = File::open(&file_path).unwrap();
let compression_mode = match mapcell_script.get_compression_mode(file_name) {
CompressionMode::Default => zip::CompressionMethod::Zstd, CompressionMode::Uncompressed => zip::CompressionMethod::Stored,
};
let opts_zipwriter: zip::write::FileOptions<()> = zip::write::FileOptions::default().compression_method(compression_mode);
let _ = zip_writer.start_file(file_name, opts_zipwriter);
let _ = std::io::copy(&mut file_input, &mut zip_writer).unwrap();
}
if !params_io.keep_files {
let _ = fs::remove_dir_all(&path_input_dir);
let _ = fs::remove_dir_all(&path_output_dir);
}
}
println!("Writer got stop signal, now finishing zip");
let _ = zip_writer.finish();
println!("Writer exiting");
});
Ok(())
}
fn get_list_files_recursively(
path: impl AsRef<Path>
) -> std::io::Result<Vec<PathBuf>> {
let mut buf = vec![];
let entries = fs::read_dir(path)?;
for entry in entries {
let entry = entry?;
let meta = entry.metadata()?;
if meta.is_dir() {
let mut subdir = get_list_files_recursively(entry.path())?;
buf.append(&mut subdir);
}
if meta.is_file() {
buf.push(entry.path());
}
}
Ok(buf)
}