use crate::{data_model::Post, find_ghost_db_in, log_progress, try_archive, Error};
use log;
use path_absolutize::Absolutize;
use rusqlite::Connection;
use std::io::Write;
use std::path::{Path, PathBuf};
use tempfile::NamedTempFile;
struct PartialExtraction {
database: NamedTempFile,
images: Vec<PathBuf>,
}
impl PartialExtraction {
fn new() -> Result<PartialExtraction, Error> {
Ok(PartialExtraction {
database: NamedTempFile::new()?,
images: Vec::new(),
})
}
}
macro_rules! contextualize {
($e:expr) => {
contextualize!($e; stringify!($e))
};
($e:expr; $($c:expr),+) => {
($e).map_err(|e| {log::error!($($c),+); e})
};
}
fn extract_images_and_db<AP>(
archive_path: AP,
prefix: Option<PathBuf>,
extract_path: &Path,
) -> Result<PartialExtraction, Error>
where
AP: AsRef<Path>,
{
let archive_path = archive_path.as_ref();
let extract_path = contextualize!(extract_path.canonicalize())?;
let db_path = contextualize!(find_ghost_db_in(archive_path, prefix))?;
let images_base = db_path
.parent()
.and_then(|parent| parent.parent())
.map(|grandparent| grandparent.join("images"));
log::info!("processing archive");
let mut archive = contextualize!(try_archive(archive_path))?;
let mut out = contextualize!(PartialExtraction::new())?;
for (idx, entry) in contextualize!(archive.entries())?.enumerate() {
log_progress(idx, "processed");
let mut entry = contextualize!(entry)?;
let path = contextualize!(entry.path())?;
if path == db_path {
contextualize!(std::io::copy(&mut entry, &mut out.database))?;
log::info!("extracted database at entry {}", idx);
} else if entry.header().entry_type() == tar::EntryType::Directory
|| path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
== Some(String::from("md"))
{
continue;
} else if let Some(images_base) = &images_base {
if path.starts_with(images_base) {
let subpath = contextualize!(path.strip_prefix(images_base))?;
let extract_to =
contextualize!((&extract_path).join(subpath).absolutize())?.to_path_buf();
if !extract_to.starts_with(&extract_path) {
log::warn!(
"malicious file in tar attempted to extract past extraction root: {}",
subpath.display(),
);
continue;
}
if let Some(parent) = extract_to.parent() {
contextualize!(std::fs::create_dir_all(parent))?;
}
log::trace!("extracting image: {}", extract_to.display());
contextualize!(entry.unpack(&extract_to))?;
out.images.push(extract_to);
}
}
}
log::info!("extracted {} images", out.images.len());
Ok(out)
}
pub fn extract_archive<AP, EP>(
archive_path: AP,
prefix: Option<PathBuf>,
extract_path: EP,
) -> Result<usize, Error>
where
AP: AsRef<Path>,
EP: AsRef<Path>,
{
let extract_path = extract_path.as_ref();
extract_images_and_db(archive_path, prefix, extract_path)?.extract_database(extract_path)
}
impl PartialExtraction {
fn extract_database(self, extract_path: &Path) -> Result<usize, Error> {
let conn = Connection::open_with_flags(
self.database.path(),
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
)?;
let posts = Post::query(&conn)?;
for post in posts.iter() {
let relative_path = post.relative_path();
let path = extract_path.join(&relative_path);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.open(path)?;
let mut writer = std::io::BufWriter::new(file);
post.render_to(&mut writer)?;
log::trace!("generated {}", relative_path.display());
}
log::info!("extracted {} posts", posts.len());
let n_indices = ensure_indices(extract_path)?;
log::info!("added {} indices", n_indices);
Ok(posts.len())
}
}
const ROOT_INDEX_DATA: &[u8] = include_bytes!("../templates/root._index.md");
const BRANCH_INDEX_DATA: &[u8] = include_bytes!("../templates/branch._index.md");
fn ensure_indices(extract_path: &Path) -> Result<u32, Error> {
let mut n = 0;
let index = extract_path.join("_index.md");
if !index.exists() {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.open(index)?;
file.write_all(ROOT_INDEX_DATA)?;
n += 1;
}
for subdir in extract_path.read_dir()?.filter(|maybe_dir_entry| {
maybe_dir_entry
.as_ref()
.map(|dir_entry| {
dir_entry
.file_type()
.map(|file_type| file_type.is_dir())
.unwrap_or_default()
})
.unwrap_or_default()
}) {
let subdir = match subdir {
Ok(subdir) => subdir,
Err(e) => {
log::error!(
"failed to read subdirectory of {}: {:#?}",
extract_path.display(),
e
);
continue;
}
};
n += ensure_indices_recursive(&subdir.path())?;
}
fn ensure_indices_recursive(path: &Path) -> Result<u32, Error> {
let mut n = 0;
let index = path.join("_index.md");
if !index.exists() {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.open(index)?;
file.write_all(BRANCH_INDEX_DATA)?;
n += 1;
}
for subdir in path.read_dir()?.filter(|maybe_dir_entry| {
maybe_dir_entry
.as_ref()
.map(|dir_entry| {
dir_entry
.file_type()
.map(|file_type| file_type.is_dir())
.unwrap_or_default()
})
.unwrap_or_default()
}) {
let subdir = match subdir {
Ok(subdir) => subdir,
Err(e) => {
log::error!(
"failed to read subdirectory of {}: {:#?}",
path.display(),
e
);
continue;
}
};
n += ensure_indices_recursive(&subdir.path())?;
}
Ok(n)
}
Ok(n)
}