use bytes::Bytes;
use failure::{err_msg, Error};
use libflate::gzip;
use sha3::{Digest, Sha3_256};
use std::collections::LinkedList;
use std::convert::TryInto;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::{Component, Path, PathBuf};
use walkdir::WalkDir;
struct FileDescriptor {
pack_path: String,
content_type: String,
etag: String,
content: Bytes,
content_gzip: Option<Bytes>,
}
impl FileDescriptor {
fn serialize_into<W: Write>(&mut self, write: &mut W) -> Result<(), Error> {
let pack_path_bytes = self.pack_path.as_bytes();
let pack_path_bytes_length: u16 = pack_path_bytes.len().try_into()?;
write.write_all(&pack_path_bytes_length.to_ne_bytes())?;
write.write_all(pack_path_bytes)?;
let content_type_bytes = self.content_type.as_bytes();
let content_type_bytes_length: u8 = content_type_bytes.len().try_into()?;
write.write_all(&content_type_bytes_length.to_ne_bytes())?;
write.write_all(content_type_bytes)?;
let etag_bytes = self.etag.as_bytes();
let etag_bytes_length: u8 = etag_bytes.len().try_into()?;
write.write_all(&etag_bytes_length.to_ne_bytes())?;
write.write_all(etag_bytes)?;
let content_bytes_length: u32 = self.content.len().try_into()?;
write.write_all(&content_bytes_length.to_ne_bytes())?;
write.write_all(&self.content)?;
let content_bytes_gzip_length: u32 = match self.content_gzip.as_ref() {
Some(content_gzip) => content_gzip.len().try_into()?,
None => 0,
};
write.write_all(&content_bytes_gzip_length.to_ne_bytes())?;
if let Some(ref content_gzip) = self.content_gzip {
write.write_all(content_gzip)?;
}
Ok(())
}
}
pub struct Pack {
file_descriptors: LinkedList<FileDescriptor>,
}
impl Pack {
pub fn new() -> Self {
Pack {
file_descriptors: LinkedList::new(),
}
}
pub fn file_add(&mut self, fs_path: PathBuf, pack_path: String) -> Result<(), Error> {
log::info!(
"Packing file {} -> {}",
fs_path.as_path().to_string_lossy(),
pack_path
);
let content = Bytes::from(fs::read(&fs_path)?);
let mut content_gzip = gzip::Encoder::new(Vec::new())?;
content_gzip.write_all(&content)?;
let content_gzip = Bytes::from(content_gzip.finish().into_result()?);
let content_gzip = if content_gzip.len() < content.len() {
Some(content_gzip)
} else {
None
};
let mut content_type = mime_guess::from_path(&fs_path)
.first_or_octet_stream()
.as_ref()
.to_owned();
if content_type.starts_with("text/") {
content_type.push_str("; charset=UTF-8");
}
let mut etag = Sha3_256::new();
etag.update(&content);
let etag = etag.finalize();
let etag = format!("\"{:x}\"", &etag);
log::info!(
"Packed {}: content_type={}, etag={}, content.len={}, content_gzip.len={:?}",
pack_path,
content_type,
etag,
content.len(),
content_gzip.as_ref().map(|content_gzip| content_gzip.len())
);
self.file_descriptors.push_back(FileDescriptor {
pack_path,
content_type,
etag,
content,
content_gzip,
});
Ok(())
}
pub fn directory_add(&mut self, fs_path: &Path, pack_path_prefix: &Path) -> Result<(), Error> {
let walk_dir = WalkDir::new(fs_path).follow_links(true);
for entry in walk_dir {
let entry = entry?;
if entry.file_type().is_dir() {
continue;
}
let entry_path = entry.into_path();
let relative_path = pack_path_prefix.join(entry_path.strip_prefix(fs_path)?);
let pack_path_components = relative_path
.components()
.filter(|component| match component {
Component::RootDir => false,
_ => true,
})
.map(|component| {
component
.as_os_str()
.to_str()
.ok_or(err_msg("Cannot convert path component to string"))
})
.collect::<Result<Vec<_>, _>>()?;
let pack_path = itertools::join([""].iter().chain(pack_path_components.iter()), "/");
self.file_add(entry_path, pack_path)?;
}
Ok(())
}
pub fn store(&mut self, path: &Path) -> Result<(), Error> {
let mut file = File::create(&path)?;
for file_descriptor in self.file_descriptors.iter_mut() {
file_descriptor.serialize_into(&mut file)?;
}
Ok(())
}
}