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