Files
catena/crates/booksniff/src/main.rs
T

100 lines
2.9 KiB
Rust

//! Says what a file actually is, whatever its name claims.
use std::io::Write;
use std::path::PathBuf;
use std::process::ExitCode;
use booksniff::bytes::File;
use clap::Parser;
use corpus_core::{Doc, Kind, exit, ndjson};
/// Identify files by what is in them rather than by what they are called.
///
/// Writes one doc record per file. Reads only the few kilobytes it needs —
/// the front of the file, and an archive's directory at the back — so a
/// directory of scans costs the same as a directory of anything else.
#[derive(Parser)]
#[command(
version,
about,
after_help = "\
Exit codes: 0 every file was identified, 1 at least one was not, 74 a file
could not be read.
Example:
booksniff library/*.pdf | jq -r 'select(.kind != \"pdf\") | .path'"
)]
struct Args {
/// Files to identify.
#[arg(required = true)]
paths: Vec<PathBuf>,
/// Print a line a person can read instead of the record.
#[arg(long)]
plain: bool,
}
fn main() -> ExitCode {
let args: Args = match catena_cli::parse() {
Ok(args) => args,
Err(code) => return code,
};
let mut out = std::io::BufWriter::new(std::io::stdout().lock());
let mut unknown = false;
let mut unreadable = false;
for path in &args.paths {
let name = path.display().to_string();
let doc = match File::open(path) {
Ok(file) => booksniff::sniff(&file, &name),
Err(e) => {
eprintln!("booksniff: {name}: {e}");
unreadable = true;
continue;
}
};
unknown |= doc.kind == Kind::Unknown;
let wrote = if args.plain {
plain(&mut out, &doc)
} else {
ndjson::write_line(&mut out, &doc).map_err(|e| match e {
ndjson::Error::Io(e) => e,
other => std::io::Error::other(other.to_string()),
})
};
if let Err(e) = wrote {
// A consumer that stopped reading is not a failure.
if e.kind() == std::io::ErrorKind::BrokenPipe {
return ExitCode::from(exit::OK);
}
eprintln!("booksniff: {e}");
return ExitCode::from(exit::IO);
}
}
if let Err(e) = out.flush() {
if e.kind() == std::io::ErrorKind::BrokenPipe {
return ExitCode::from(exit::OK);
}
eprintln!("booksniff: {e}");
return ExitCode::from(exit::IO);
}
ExitCode::from(match (unreadable, unknown) {
(true, _) => exit::IO,
(false, true) => exit::NO,
(false, false) => exit::OK,
})
}
fn plain(out: &mut impl Write, doc: &Doc) -> std::io::Result<()> {
let kind = serde_json::to_string(&doc.kind).unwrap_or_default();
writeln!(out, "{}\t{}", kind.trim_matches('"'), doc.path)?;
for note in &doc.notes {
writeln!(out, "\t{note}")?;
}
Ok(())
}