Skip to main content

frm/
tanzu.rs

1// Copyright (c) 2025-2026 Michael S. Klishin and Contributors
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use 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}