use {
crate::{
errors::*,
preview::PreviewMode,
},
serde::Deserialize,
std::{
fs,
hash::{
DefaultHasher,
Hash,
Hasher,
},
path::{
Path,
PathBuf,
},
process::Command,
},
tempfile::TempDir,
};
#[derive(Debug, Clone, Copy)]
pub struct TransformerId {
idx: usize,
}
pub struct PreviewTransformers {
transformers: Vec<PreviewTransformer>,
temp_dir: TempDir,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PreviewTransformerConf {
pub input_extensions: Vec<String>,
pub output_extension: String,
pub command: Vec<String>,
pub mode: PreviewMode,
}
#[derive(Debug, Clone)]
pub struct PreviewTransformer {
pub input_extensions: Vec<String>,
pub output_extension: String,
pub command: Vec<String>,
pub mode: PreviewMode,
pub input_kind: ProcessInputKind,
pub output_kind: ProcessOutputKind,
}
#[derive(Debug, Clone, Copy)]
pub enum ProcessInputKind {
File,
Stdin,
}
#[derive(Debug, Clone, Copy)]
pub enum ProcessOutputKind {
File,
Dir,
Stdout,
}
pub struct PreviewTransform {
pub transformer_id: TransformerId,
pub output_path: PathBuf,
}
impl PreviewTransformers {
pub fn new(transformer_confs: &[PreviewTransformerConf]) -> Result<Self, ConfError> {
let mut transformers = Vec::with_capacity(transformer_confs.len());
for transformer_conf in transformer_confs {
transformers.push(PreviewTransformer::from_conf(transformer_conf)?);
}
let temp_dir = tempfile::Builder::new()
.prefix("broot-conversions")
.tempdir()?;
Ok(Self {
transformers,
temp_dir,
})
}
pub fn transformer(
&self,
id: TransformerId,
) -> &PreviewTransformer {
&self.transformers[id.idx]
}
pub fn transform(
&self,
input_path: &Path,
mode: Option<PreviewMode>,
) -> Option<PreviewTransform> {
let transformer_id = self.find_transformer_for(input_path, mode)?;
let temp_dir = self.temp_dir.path();
match self.transformers[transformer_id.idx].transform(input_path, temp_dir) {
Ok(output_path) => Some(PreviewTransform {
transformer_id,
output_path,
}),
Err(e) => {
error!(
"conversion failed using {:?}",
self.transformers[transformer_id.idx].command
);
error!("conversion error: {:?}", e);
None
}
}
}
pub fn find_transformer_for(
&self,
path: &Path,
mode: Option<PreviewMode>,
) -> Option<TransformerId> {
let extension = path.extension().and_then(|ext| ext.to_str())?;
for (idx, transformer) in self.transformers.iter().enumerate() {
if !transformer
.input_extensions
.iter()
.any(|ext| ext.eq_ignore_ascii_case(extension))
{
continue;
}
if let Some(mode) = mode {
if transformer.mode != mode {
continue;
}
}
return Some(TransformerId { idx });
}
None
}
}
impl PreviewTransformer {
pub fn from_conf(conf: &PreviewTransformerConf) -> Result<Self, ConfError> {
if conf.command.is_empty() {
return Err(ConfError::MissingField {
txt: "empty command in preview transformer".to_string(),
});
}
let has_input_path = conf.command.iter().any(|c| c.contains("{input-path}"));
let has_output_path = conf.command.iter().any(|c| c.contains("{output-path}"));
let has_output_dir = conf.command.iter().any(|c| c.contains("{output-dir}"));
let input_kind = if has_input_path {
ProcessInputKind::File
} else {
ProcessInputKind::Stdin
};
let output_kind = if has_output_path {
ProcessOutputKind::File
} else if has_output_dir {
ProcessOutputKind::Dir
} else {
ProcessOutputKind::Stdout
};
Ok(Self {
input_extensions: conf.input_extensions.clone(),
output_extension: conf.output_extension.clone(),
command: conf.command.clone(),
mode: conf.mode,
input_kind,
output_kind,
})
}
pub fn transform(
&self,
input_path: &Path,
temp_dir: &Path,
) -> Result<PathBuf, PreviewTransformerError> {
let hash = {
let mut hasher = DefaultHasher::new();
input_path.hash(&mut hasher);
hasher.finish()
};
let input_stem = input_path
.file_stem()
.ok_or(PreviewTransformerError::InvalidInput)?
.to_string_lossy();
let output_dir = temp_dir.join(format!("{:x}", hash));
if output_dir.exists() {
if let Some(path) = first_file_in_dir(&output_dir)? {
let input_modified = input_path.metadata().and_then(|m| m.modified());
let transformed_modified = path.metadata().and_then(|m| m.modified());
match (input_modified, transformed_modified) {
(Ok(input_date), Ok(transformed_date)) if input_date <= transformed_date => {
debug!("preview transform {:?} up to date", path);
return Ok(path);
}
_ => {
debug!("preview transform {:?} obsolete", path);
fs::remove_file(&path)?;
}
}
}
} else {
fs::create_dir(&output_dir)?;
}
let mut output_path = output_dir.join(format!("{}.{}", input_stem, self.output_extension));
let mut command = self.command.iter().map(|part| {
part.replace("{input-path}", &input_path.to_string_lossy())
.replace("{output-dir}", &output_dir.to_string_lossy())
.replace("{output-path}", &output_path.to_string_lossy())
});
info!("transforming {:?} to {:?}", input_path, output_path);
let executable = command.next().unwrap();
let mut process = Command::new(executable);
process.stderr(std::process::Stdio::null());
process.args(command);
match self.input_kind {
ProcessInputKind::File => {
process.stdin(std::process::Stdio::null());
}
ProcessInputKind::Stdin => {
process.stdin(std::fs::File::open(input_path)?);
}
}
match self.output_kind {
ProcessOutputKind::File | ProcessOutputKind::Dir => {
process.stdout(std::process::Stdio::null());
}
ProcessOutputKind::Stdout => {
process.stdout(std::fs::File::create(&output_path)?);
}
}
let exit_status = process.spawn().and_then(|mut p| p.wait())?;
output_path = first_file_in_dir(&output_dir)?.ok_or(PreviewTransformerError::NoOutput)?;
if exit_status.success() {
Ok(output_path)
} else {
let _ = std::fs::remove_file(&output_path);
match exit_status.code() {
Some(code) => Err(PreviewTransformerError::ProcessFailed { code }),
None => Err(PreviewTransformerError::ProcessInterrupted),
}
}
}
}
fn first_file_in_dir(dir: &Path) -> Result<Option<PathBuf>, PreviewTransformerError> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
return Ok(Some(path));
}
}
Ok(None)
}