this repo has no description
1extern crate tar;
2
3use anyhow::bail;
4use clap::Args;
5use std::fs::File;
6use std::io::{self, Seek, SeekFrom, Write};
7use tar::{Archive, Builder};
8use tempfile::tempfile;
9
10use crate::utils::*;
11
12#[derive(Args, Debug)]
13pub struct TarArgs {
14 #[clap(flatten)]
15 pub common_args: CommonArgs,
16}
17
18#[derive(Default)]
19pub struct Tar {}
20
21impl Tar {
22 pub fn new(_args: &TarArgs) -> Tar {
23 Tar {}
24 }
25}
26
27impl Compressor for Tar {
28 /// Full name for tar, also used for extension
29 fn name(&self) -> &str {
30 "tar"
31 }
32
33 /// Tar extracts to a directory by default
34 fn default_extracted_target(&self) -> ExtractedTarget {
35 ExtractedTarget::DIRECTORY
36 }
37
38 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result {
39 match output {
40 CmprssOutput::Path(path) => {
41 let file = File::create(path)?;
42 self.compress_internal(input, Builder::new(file))
43 }
44 CmprssOutput::Pipe(mut pipe) => {
45 // Create a temporary file to write the tar to
46 let mut temp_file = tempfile()?;
47 self.compress_internal(input, Builder::new(&mut temp_file))?;
48
49 // Reset the file position to the beginning
50 temp_file.seek(SeekFrom::Start(0))?;
51
52 // Copy the temporary file to the pipe
53 io::copy(&mut temp_file, &mut pipe)?;
54 Ok(())
55 }
56 CmprssOutput::Writer(mut writer) => {
57 let mut temp_file = tempfile()?;
58 self.compress_internal(input, Builder::new(&mut temp_file))?;
59 temp_file.seek(SeekFrom::Start(0))?;
60 io::copy(&mut temp_file, &mut writer)?;
61 Ok(())
62 }
63 }
64 }
65
66 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result {
67 match output {
68 CmprssOutput::Path(ref out_dir) => {
69 // Create the output directory if it doesn't exist
70 if !out_dir.exists() {
71 std::fs::create_dir_all(out_dir)?;
72 } else if !out_dir.is_dir() {
73 bail!("tar extraction output must be a directory");
74 }
75
76 match input {
77 CmprssInput::Path(paths) => {
78 if paths.len() != 1 {
79 bail!("tar extraction expects a single archive file");
80 }
81 let file = File::open(&paths[0])?;
82 let mut archive = Archive::new(file);
83 Ok(archive.unpack(out_dir)?)
84 }
85 CmprssInput::Pipe(mut pipe) => {
86 // Create a temporary file to store the tar content
87 let mut temp_file = tempfile()?;
88
89 // Copy from pipe to temporary file
90 io::copy(&mut pipe, &mut temp_file)?;
91
92 // Reset the file position to the beginning
93 temp_file.seek(SeekFrom::Start(0))?;
94
95 // Extract from the temporary file
96 let mut archive = Archive::new(temp_file);
97 Ok(archive.unpack(out_dir)?)
98 }
99 CmprssInput::Reader(reader) => {
100 let mut archive = Archive::new(reader.0);
101 archive.unpack(out_dir)?;
102 Ok(())
103 }
104 }
105 }
106 CmprssOutput::Pipe(_) => bail!("tar extraction to stdout is not supported"),
107 CmprssOutput::Writer(mut writer) => match input {
108 CmprssInput::Path(paths) => {
109 if paths.len() != 1 {
110 bail!("tar extraction expects a single archive file");
111 }
112 let mut file = File::open(&paths[0])?;
113 io::copy(&mut file, &mut writer)?;
114 Ok(())
115 }
116 CmprssInput::Pipe(mut pipe) => {
117 io::copy(&mut pipe, &mut writer)?;
118 Ok(())
119 }
120 CmprssInput::Reader(mut reader) => {
121 io::copy(&mut reader, &mut writer)?;
122 Ok(())
123 }
124 },
125 }
126 }
127}
128
129impl Tar {
130 /// Internal compress helper
131 fn compress_internal<W: Write>(&self, input: CmprssInput, mut archive: Builder<W>) -> Result {
132 match input {
133 CmprssInput::Path(paths) => {
134 for path in paths {
135 if path.is_file() {
136 archive.append_file(
137 path.file_name().unwrap(),
138 &mut File::open(path.as_path())?,
139 )?;
140 } else if path.is_dir() {
141 archive.append_dir_all(path.file_name().unwrap(), path.as_path())?;
142 } else {
143 bail!("unsupported file type for tar compression");
144 }
145 }
146 }
147 CmprssInput::Pipe(mut pipe) => {
148 // For pipe input, we'll create a single file named "archive"
149 let mut temp_file = tempfile()?;
150 io::copy(&mut pipe, &mut temp_file)?;
151 temp_file.seek(SeekFrom::Start(0))?;
152 archive.append_file("archive", &mut temp_file)?;
153 }
154 CmprssInput::Reader(_) => {
155 bail!("Cannot tar a reader input directly");
156 }
157 }
158 Ok(archive.finish()?)
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::test_utils::*;
166 use assert_fs::prelude::*;
167 use predicates::prelude::*;
168 use std::path::PathBuf;
169
170 /// Test the basic interface of the Tar compressor
171 #[test]
172 fn test_tar_interface() {
173 let compressor = Tar::default();
174 test_compressor_interface(&compressor, "tar", Some("tar"));
175 }
176
177 /// Test the default compression level
178 #[test]
179 fn test_tar_default_compression() -> Result {
180 let compressor = Tar::default();
181 test_compression(&compressor)
182 }
183
184 /// Test tar-specific functionality: directory handling
185 #[test]
186 fn test_directory_handling() -> Result {
187 let compressor = Tar::default();
188 let dir = assert_fs::TempDir::new()?;
189 let file_path = dir.child("file.txt");
190 file_path.write_str("garbage data for testing")?;
191 let working_dir = assert_fs::TempDir::new()?;
192 let archive = working_dir.child("dir_archive.tar");
193 archive.assert(predicate::path::missing());
194
195 compressor.compress(
196 CmprssInput::Path(vec![dir.path().to_path_buf()]),
197 CmprssOutput::Path(archive.path().to_path_buf()),
198 )?;
199 archive.assert(predicate::path::is_file());
200
201 let extract_dir = working_dir.child("extracted");
202 std::fs::create_dir_all(extract_dir.path())?;
203 compressor.extract(
204 CmprssInput::Path(vec![archive.path().to_path_buf()]),
205 CmprssOutput::Path(extract_dir.path().to_path_buf()),
206 )?;
207
208 let dir_name: PathBuf = dir.path().file_name().unwrap().into();
209 extract_dir
210 .child(dir_name)
211 .child("file.txt")
212 .assert(predicate::path::eq_file(file_path.path()));
213 Ok(())
214 }
215}