1use std::fs::{self, File};
10use std::io::BufReader;
11use std::path::{Path, PathBuf};
12
13use flate2::read::GzDecoder;
14use tar::Archive;
15use xz2::read::XzDecoder;
16
17use crate::Result;
18use crate::errors::Error;
19use crate::paths::Paths;
20use crate::version::Version;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum CompressionFormat {
24 Xz,
25 Gzip,
26}
27
28impl CompressionFormat {
29 pub fn from_path(path: &Path) -> Option<Self> {
30 let name = path.file_name()?.to_str()?;
31 if name.ends_with(".tar.xz") {
32 Some(CompressionFormat::Xz)
33 } else if name.ends_with(".tar.gz") || name.ends_with(".tgz") {
34 Some(CompressionFormat::Gzip)
35 } else {
36 None
37 }
38 }
39}
40
41pub fn extract_version_from_tarball_name(path: &Path) -> Option<Version> {
42 let name = path.file_name()?.to_str()?;
43
44 let name_without_ext = name
45 .strip_suffix(".tar.xz")
46 .or_else(|| name.strip_suffix(".tar.gz"))
47 .or_else(|| name.strip_suffix(".tgz"))?;
48
49 extract_version_from_stem(name_without_ext)
50}
51
52fn extract_version_from_stem(stem: &str) -> Option<Version> {
53 for (i, _) in stem.char_indices() {
54 let suffix = &stem[i..];
55 if let Ok(version) = suffix.parse::<Version>() {
56 return Some(version);
57 }
58 if let Some(stripped) = suffix.strip_prefix('-')
59 && let Ok(version) = stripped.parse::<Version>()
60 {
61 return Some(version);
62 }
63 }
64 None
65}
66
67pub fn extract_tarball(tarball_path: &Path, version: &Version, paths: &Paths) -> Result<()> {
68 let format = CompressionFormat::from_path(tarball_path).ok_or_else(|| {
69 Error::ExtractionFailed(format!(
70 "unsupported archive format: {}",
71 tarball_path.display()
72 ))
73 })?;
74
75 let file = File::open(tarball_path)?;
76 let reader = BufReader::new(file);
77
78 let temp_dir = paths
79 .versions_dir()
80 .join(format!(".{}-extracting", version));
81 if temp_dir.exists() {
82 fs::remove_dir_all(&temp_dir)?;
83 }
84 fs::create_dir_all(&temp_dir)?;
85
86 match format {
87 CompressionFormat::Xz => {
88 let decoder = XzDecoder::new(reader);
89 let mut archive = Archive::new(decoder);
90 archive
91 .unpack(&temp_dir)
92 .map_err(|e| Error::ExtractionFailed(e.to_string()))?;
93 }
94 CompressionFormat::Gzip => {
95 let decoder = GzDecoder::new(reader);
96 let mut archive = Archive::new(decoder);
97 archive
98 .unpack(&temp_dir)
99 .map_err(|e| Error::ExtractionFailed(e.to_string()))?;
100 }
101 }
102
103 let extracted_dir = find_extracted_rabbitmq_dir(&temp_dir)?;
104 let final_path = paths.version_dir(version);
105
106 if final_path.exists() {
107 fs::remove_dir_all(&final_path)?;
108 }
109
110 fs::rename(&extracted_dir, &final_path).map_err(|e| {
111 Error::ExtractionFailed(format!("failed to move extracted directory: {}", e))
112 })?;
113
114 fs::remove_dir_all(&temp_dir)?;
115
116 Ok(())
117}
118
119fn find_extracted_rabbitmq_dir(temp_dir: &Path) -> Result<PathBuf> {
120 let mut fallback = None;
121
122 for entry in fs::read_dir(temp_dir)? {
123 let entry = entry?;
124 let path = entry.path();
125 if path.is_dir() {
126 let name = entry.file_name();
127 let name_str = name.to_string_lossy();
128 if name_str.contains("rabbitmq") {
129 return Ok(path);
130 }
131 if fallback.is_none() {
132 fallback = Some(path);
133 }
134 }
135 }
136
137 fallback
138 .ok_or_else(|| Error::ExtractionFailed("no directory found in extracted archive".into()))
139}
140
141pub fn verify_extracted_version(paths: &Paths, expected: &Version) -> Result<()> {
142 let version_dir = paths.version_dir(expected);
143 if !version_dir.exists() {
144 return Err(Error::ExtractionFailed(format!(
145 "extracted directory not found: {}",
146 version_dir.display()
147 )));
148 }
149
150 let sbin_dir = version_dir.join("sbin");
151 if !sbin_dir.exists() {
152 return Err(Error::ExtractionFailed(
153 "extracted archive does not contain sbin directory".into(),
154 ));
155 }
156
157 Ok(())
158}