mirror of
https://github.com/ouch-org/ouch.git
synced 2025-07-19 16:10:53 +00:00
feat: add basic compressing support for squashfs
This commit is contained in:
parent
87cf8529f2
commit
0121a0c0df
@ -1,17 +1,23 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs,
|
env, fs,
|
||||||
io::{self, BufWriter, Write},
|
io::{self, BufWriter, Seek, Write},
|
||||||
path::Path,
|
path::{Path, PathBuf},
|
||||||
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
use backhand::{FilesystemReader, InnerNode, SquashfsFileReader};
|
use backhand::{
|
||||||
use filetime_creation::{set_file_handle_times, set_file_mtime, FileTime};
|
compression::Compressor, FilesystemCompressor, FilesystemReader, FilesystemWriter, InnerNode, NodeHeader,
|
||||||
|
SquashfsFileReader,
|
||||||
|
};
|
||||||
|
use filetime_creation::{set_file_handle_times, FileTime};
|
||||||
|
use same_file::Handle;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
error::FinalError,
|
||||||
list::FileInArchive,
|
list::FileInArchive,
|
||||||
utils::{
|
utils::{
|
||||||
logger::{info, warning},
|
logger::{info, warning},
|
||||||
Bytes,
|
Bytes, FileVisibilityPolicy,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -131,3 +137,205 @@ pub fn unpack_archive(archive: FilesystemReader<'_>, output_folder: &Path, quiet
|
|||||||
|
|
||||||
Ok(unpacked_files)
|
Ok(unpacked_files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build_archive_from_paths<W>(
|
||||||
|
input_filenames: &[PathBuf],
|
||||||
|
output_path: &Path,
|
||||||
|
mut writer: W,
|
||||||
|
file_visibility_policy: FileVisibilityPolicy,
|
||||||
|
quiet: bool,
|
||||||
|
follow_symlinks: bool,
|
||||||
|
) -> crate::Result<W>
|
||||||
|
where
|
||||||
|
W: Write + Seek,
|
||||||
|
{
|
||||||
|
let root_dir = match input_filenames {
|
||||||
|
[path] if path.is_dir() => path,
|
||||||
|
_ => {
|
||||||
|
let error = FinalError::with_title("Cannot build squashfs")
|
||||||
|
.detail("Squashfs requires a single directory input for root directory")
|
||||||
|
.detail(if input_filenames.len() != 1 {
|
||||||
|
"Multiple paths are provided".into()
|
||||||
|
} else {
|
||||||
|
format!("Not a directory: {:?}", input_filenames[0])
|
||||||
|
});
|
||||||
|
return Err(error.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let output_handle = Handle::from_path(output_path);
|
||||||
|
|
||||||
|
let mut fs_writer = FilesystemWriter::default();
|
||||||
|
// Set the default compression to Gzip with default level, matching mksquashfs's default.
|
||||||
|
// The default choice of `backhand` is Xz which is not enabled by us.
|
||||||
|
// TODO: We do not support customization argument for archive formats.
|
||||||
|
fs_writer.set_compressor(FilesystemCompressor::new(Compressor::Gzip, None).expect("gzip is supported"));
|
||||||
|
|
||||||
|
// cd *into* the source directory, using it as the archive root.
|
||||||
|
let previous_cwd = env::current_dir()?;
|
||||||
|
env::set_current_dir(root_dir)?;
|
||||||
|
|
||||||
|
for entry in file_visibility_policy.build_walker(".") {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if let Ok(handle) = &output_handle {
|
||||||
|
if matches!(Handle::from_path(path), Ok(x) if &x == handle) {
|
||||||
|
warning(format!(
|
||||||
|
"Cannot compress `{}` into itself, skipping",
|
||||||
|
output_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !quiet {
|
||||||
|
// `fs_writer.push_*` only maintains metadata. We do not want to give
|
||||||
|
// users a false information that we are compressing during the
|
||||||
|
// traversal. So this is not "compressing".
|
||||||
|
// File reading, compression and writing are done in
|
||||||
|
// `fs_writer.write` below, after the hierarchy tree gets finalized.
|
||||||
|
info(format!("Found {path:?}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = entry.metadata()?;
|
||||||
|
let file_type = metadata.file_type();
|
||||||
|
|
||||||
|
let mut header = NodeHeader::default();
|
||||||
|
header.mtime = match metadata.modified() {
|
||||||
|
// Not available.
|
||||||
|
Err(_) => 0,
|
||||||
|
Ok(mtime) => {
|
||||||
|
let mtime = mtime
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.ok()
|
||||||
|
.and_then(|dur| u32::try_from(dur.as_secs()).ok());
|
||||||
|
if mtime.is_none() {
|
||||||
|
warning(format!(
|
||||||
|
"Modification time of {path:?} exceeds the representable range (1970-01-01 ~ 2106-02-07) \
|
||||||
|
of squashfs. Recorded as 1970-01-01."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
mtime.unwrap_or(0)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
header.permissions = 0o777;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||||
|
|
||||||
|
// Only permission bits, not file type bits.
|
||||||
|
header.permissions = metadata.permissions().mode() as u16 & !(libc::S_IFMT as u16);
|
||||||
|
header.uid = metadata.uid();
|
||||||
|
header.gid = metadata.gid();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root directory is special cased.
|
||||||
|
if path == Path::new(".") {
|
||||||
|
fs_writer.set_root_mode(header.permissions);
|
||||||
|
fs_writer.set_root_uid(header.uid);
|
||||||
|
fs_writer.set_root_gid(header.gid);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if file_type.is_dir() {
|
||||||
|
fs_writer.push_dir(path, header)?;
|
||||||
|
} else if !follow_symlinks && file_type.is_symlink() {
|
||||||
|
let target = fs::read_link(path)?;
|
||||||
|
fs_writer.push_symlink(target, path, header)?;
|
||||||
|
} else if maybe_push_unix_special_inode(&mut fs_writer, path, &metadata, header)? {
|
||||||
|
// Already handled.
|
||||||
|
} else {
|
||||||
|
// Fallback case: read as a regular file.
|
||||||
|
// See comments of `LazyFile` for why not `File::open` here.
|
||||||
|
let reader = LazyFile::Path(path.to_path_buf());
|
||||||
|
fs_writer.push_file(reader, path, header)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !quiet {
|
||||||
|
info(format!("Compressing data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize the superblock and write data. This should be done before
|
||||||
|
// resetting current directory, because `LazyFile`s store relative paths.
|
||||||
|
fs_writer.write(&mut writer)?;
|
||||||
|
|
||||||
|
env::set_current_dir(previous_cwd)?;
|
||||||
|
|
||||||
|
Ok(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn maybe_push_unix_special_inode(
|
||||||
|
_writer: &mut FilesystemWriter,
|
||||||
|
_path: &Path,
|
||||||
|
_metadata: &fs::Metadata,
|
||||||
|
_header: NodeHeader,
|
||||||
|
) -> io::Result<bool> {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn maybe_push_unix_special_inode(
|
||||||
|
writer: &mut FilesystemWriter,
|
||||||
|
path: &Path,
|
||||||
|
metadata: &fs::Metadata,
|
||||||
|
header: NodeHeader,
|
||||||
|
) -> io::Result<bool> {
|
||||||
|
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
||||||
|
|
||||||
|
let file_type = metadata.file_type();
|
||||||
|
if file_type.is_fifo() {
|
||||||
|
writer.push_fifo(path, header)?;
|
||||||
|
} else if file_type.is_socket() {
|
||||||
|
writer.push_socket(path, header)?;
|
||||||
|
} else if file_type.is_block_device() {
|
||||||
|
let dev = metadata.rdev() as u32;
|
||||||
|
writer.push_block_device(dev, path, header)?;
|
||||||
|
} else if file_type.is_char_device() {
|
||||||
|
let dev = metadata.rdev() as u32;
|
||||||
|
writer.push_char_device(dev, path, header)?;
|
||||||
|
} else {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delay file opening until the first read and close it as soon as the EOF is encountered.
|
||||||
|
///
|
||||||
|
/// Due to design of `backhand`, we need to store all `impl Read` into the
|
||||||
|
/// builder during traversal and write out the squashfs later. But we cannot
|
||||||
|
/// open and store all file handles during traversal or it will exhaust file
|
||||||
|
/// descriptors on *NIX if there are thousands of files (a pretty low limit!).
|
||||||
|
///
|
||||||
|
/// Upstream discussion: https://github.com/wcampbell0x2a/backhand/discussions/614
|
||||||
|
enum LazyFile {
|
||||||
|
Path(PathBuf),
|
||||||
|
Opened(fs::File),
|
||||||
|
Closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Read for LazyFile {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
match self {
|
||||||
|
LazyFile::Path(path) => {
|
||||||
|
let file = fs::File::open(path)?;
|
||||||
|
*self = Self::Opened(file);
|
||||||
|
self.read(buf)
|
||||||
|
}
|
||||||
|
LazyFile::Opened(file) => {
|
||||||
|
let cnt = file.read(buf)?;
|
||||||
|
if buf.len() != 0 && cnt == 0 {
|
||||||
|
*self = Self::Closed;
|
||||||
|
}
|
||||||
|
Ok(cnt)
|
||||||
|
}
|
||||||
|
LazyFile::Closed => Ok(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -124,21 +124,22 @@ pub fn compress_files(
|
|||||||
)?;
|
)?;
|
||||||
writer.flush()?;
|
writer.flush()?;
|
||||||
}
|
}
|
||||||
Squashfs => todo!(),
|
Zip | Squashfs => {
|
||||||
Zip => {
|
let is_zip = matches!(first_format, Zip);
|
||||||
|
|
||||||
if !formats.is_empty() {
|
if !formats.is_empty() {
|
||||||
// Locking necessary to guarantee that warning and question
|
// Locking necessary to guarantee that warning and question
|
||||||
// messages stay adjacent
|
// messages stay adjacent
|
||||||
let _locks = lock_and_flush_output_stdio();
|
let _locks = lock_and_flush_output_stdio();
|
||||||
|
|
||||||
warn_user_about_loading_in_memory(".zip");
|
warn_user_about_loading_in_memory(if is_zip { ".zip" } else { ".sqfs" });
|
||||||
if !user_wants_to_continue(output_path, question_policy, QuestionAction::Compression)? {
|
if !user_wants_to_continue(output_path, question_policy, QuestionAction::Compression)? {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut vec_buffer = Cursor::new(vec![]);
|
let mut vec_buffer = Cursor::new(vec![]);
|
||||||
|
if is_zip {
|
||||||
archive::zip::build_archive_from_paths(
|
archive::zip::build_archive_from_paths(
|
||||||
&files,
|
&files,
|
||||||
output_path,
|
output_path,
|
||||||
@ -147,6 +148,17 @@ pub fn compress_files(
|
|||||||
quiet,
|
quiet,
|
||||||
follow_symlinks,
|
follow_symlinks,
|
||||||
)?;
|
)?;
|
||||||
|
} else {
|
||||||
|
archive::squashfs::build_archive_from_paths(
|
||||||
|
&files,
|
||||||
|
output_path,
|
||||||
|
&mut vec_buffer,
|
||||||
|
file_visibility_policy,
|
||||||
|
quiet,
|
||||||
|
follow_symlinks,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
vec_buffer.rewind()?;
|
vec_buffer.rewind()?;
|
||||||
io::copy(&mut vec_buffer, &mut writer)?;
|
io::copy(&mut vec_buffer, &mut writer)?;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user