|
|
@@ -0,0 +1,135 @@ |
|
|
|
//! Utilities for interacting with RAR archives. |
|
|
|
//! |
|
|
|
//! This module relies on the `unrar` utility being present on the system, but |
|
|
|
//! it could also work with the non-free `rar` program. The rar crate isn't |
|
|
|
//! mature enough so we just call out to another program to get the dirty work |
|
|
|
//! done. |
|
|
|
|
|
|
|
#![allow(unused)] |
|
|
|
|
|
|
|
use std::{ |
|
|
|
ffi::OsStr, |
|
|
|
fs, io, |
|
|
|
path::{Path, PathBuf}, |
|
|
|
process, |
|
|
|
}; |
|
|
|
|
|
|
|
#[derive(Debug)] |
|
|
|
pub enum RarError { |
|
|
|
/// If a target file is not found. |
|
|
|
NotFound, |
|
|
|
|
|
|
|
/// An argument (path) was malformed. |
|
|
|
Malformed, |
|
|
|
|
|
|
|
/// If we'd be overwriting something, including if a target dir is a file. |
|
|
|
Overwrite, |
|
|
|
|
|
|
|
/// If there was an error with the `unrar` command. |
|
|
|
Unrar(Option<i32>), |
|
|
|
|
|
|
|
/// If parsing the output from the command failed, this shouldn't happen. |
|
|
|
Parse, |
|
|
|
|
|
|
|
/// Some IO error, could be permissions or that `unrar` isn't installed. |
|
|
|
Io(io::Error), |
|
|
|
|
|
|
|
/// An extraction succeeded but somehow we can't find the file. Maybe an |
|
|
|
/// argument wasn't set properly, but here's where we expected it. |
|
|
|
OutputLost(PathBuf), |
|
|
|
} |
|
|
|
|
|
|
|
/// List files in a RAR archive. |
|
|
|
pub fn list_archive_files(path: &Path) -> Result<Vec<String>, RarError> { |
|
|
|
// Check that the path even exists. |
|
|
|
if !path.exists() { |
|
|
|
return Err(RarError::NotFound); |
|
|
|
} |
|
|
|
|
|
|
|
// Spawn the program and collect the output. If we don't have read perms |
|
|
|
// this is where we'd find out. |
|
|
|
let output = process::Command::new("unrar") |
|
|
|
.arg("lb") // "l"list contents, "b"are |
|
|
|
.arg("-r") // "r"ecurse into subdirectories (maybe not needed?) |
|
|
|
.arg("--") // stop switch scanning |
|
|
|
.arg(path.as_os_str()) |
|
|
|
.output() |
|
|
|
.map_err(|e| RarError::Io(e))?; |
|
|
|
|
|
|
|
// If the program failed, just return that. |
|
|
|
if !output.status.success() { |
|
|
|
return Err(RarError::Unrar(output.status.code())); |
|
|
|
} |
|
|
|
|
|
|
|
// Convert the output to a string so that we can use .lines() on it. |
|
|
|
let outstr = String::from_utf8(output.stdout).map_err(|_| RarError::Parse)?; |
|
|
|
|
|
|
|
// Split the output into lines and then just return |
|
|
|
Ok(outstr |
|
|
|
.lines() |
|
|
|
.filter(|s| !s.is_empty()) |
|
|
|
.map(String::from) |
|
|
|
.collect()) |
|
|
|
} |
|
|
|
|
|
|
|
/// Extracts a file from the specified RAR archive, putting it into the |
|
|
|
/// specified directory and returning the new file's path. |
|
|
|
pub fn extract_archive_file( |
|
|
|
arc: &Path, |
|
|
|
name: &String, |
|
|
|
dest_dir: &Path, |
|
|
|
) -> Result<PathBuf, RarError> { |
|
|
|
// Check that the archive exists. |
|
|
|
if !arc.exists() { |
|
|
|
return Err(RarError::NotFound); |
|
|
|
} |
|
|
|
|
|
|
|
// Make sure that the destination exists and is a directory. |
|
|
|
if !dest_dir.exists() { |
|
|
|
fs::create_dir_all(dest_dir).map_err(|e| RarError::Io(e))?; |
|
|
|
} else { |
|
|
|
if dest_dir.is_file() { |
|
|
|
return Err(RarError::Overwrite); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Figure out where the output file should be. |
|
|
|
let dest_file: PathBuf = match PathBuf::from(name).file_name() { |
|
|
|
Some(p) => { |
|
|
|
let mut buf = PathBuf::from(dest_dir); |
|
|
|
buf.push(p); |
|
|
|
buf |
|
|
|
} |
|
|
|
None => return Err(RarError::NotFound), |
|
|
|
}; |
|
|
|
|
|
|
|
// Actually extract the file from the archive. |
|
|
|
let mut sub = process::Command::new("unrar") |
|
|
|
.arg("e") // "e"xtract (not using archive paths) |
|
|
|
.arg("-y") // assume "y"es for all prompts |
|
|
|
.arg("-o-") // don't "o"verwrite things |
|
|
|
.arg("--") // stop switch scanning |
|
|
|
.arg(arc.as_os_str()) |
|
|
|
.arg(name) |
|
|
|
.arg(dest_dir.as_os_str()) |
|
|
|
.spawn() |
|
|
|
.map_err(|e| RarError::Io(e))?; |
|
|
|
let exit = sub.wait().map_err(|e| RarError::Io(e))?; |
|
|
|
|
|
|
|
// If we failed, return that we failed. |
|
|
|
if !exit.success() { |
|
|
|
return Err(RarError::Unrar(exit.code())); |
|
|
|
} |
|
|
|
|
|
|
|
// TODO Make this also report not found if "name" isn't a real file in the |
|
|
|
// archive by looking at the output of the program. |
|
|
|
|
|
|
|
// Now, make sure that the output we expected is where it is, if not then |
|
|
|
// report that it's messed up. |
|
|
|
if dest_file.exists() { |
|
|
|
Ok(dest_file) |
|
|
|
} else { |
|
|
|
// TODO Maybe instead make it find the path from the `unrar` output? |
|
|
|
Err(RarError::OutputLost(dest_file)) |
|
|
|
} |
|
|
|
} |