libroast/
utils.rs

1// SPDX-License-Identifier: MPL-2.0
2
3// Copyright (C) 2025 Soc Virnyl Estela and contributors
4
5// This Source Code Form is subject to the terms of the Mozilla Public
6// License, v. 2.0. If a copy of the MPL was not distributed with this
7// file, You can obtain one at https://mozilla.org/MPL/2.0/.
8use crate::{
9    common::{
10        Compression,
11        SupportedFormat,
12        UnsupportedFormat,
13    },
14    consts::{
15        BZ2_MIME,
16        GZ_MIME,
17        SUPPORTED_MIME_TYPES,
18        TAR_MIME,
19        XZ_MIME,
20        ZST_MIME,
21    },
22};
23use glob::glob;
24use rayon::prelude::*;
25use std::{
26    fs,
27    io,
28    path::{
29        Path,
30        PathBuf,
31    },
32};
33use terminfo::{
34    Database,
35    capability as cap,
36};
37#[allow(unused_imports)]
38use tracing::{
39    debug,
40    error,
41    info,
42    trace,
43    warn,
44};
45use tracing_subscriber::EnvFilter;
46
47/// Utility function to start tracing subscriber in the environment for logging.
48/// Supports coloured and no coloured outputs using `terminfo::capability`.
49pub fn start_tracing()
50{
51    let terminfodb = Database::from_env().map_err(|e| {
52        error!(err = ?e, "Unable to access terminfo db. This is a bug!");
53        io::Error::other(
54            "Unable to access terminfo db. This is a bug! Setting color option to false!",
55        )
56    });
57
58    let is_termcolorsupported = match terminfodb
59    {
60        Ok(hasterminfodb) => hasterminfodb.get::<cap::MaxColors>().is_some(),
61        Err(_) => false,
62    };
63    let filter_layer = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
64
65    let builder = tracing_subscriber::fmt()
66        .with_level(true)
67        .with_ansi(is_termcolorsupported)
68        .with_env_filter(filter_layer)
69        .with_level(true);
70
71    let builder = if cfg!(debug_assertions)
72    {
73        builder.with_file(true).with_line_number(true)
74    }
75    else
76    {
77        builder
78    };
79
80    builder.init();
81}
82
83/// Checks if a valid file has a supported filetype regardless of extension
84/// using `infer::get_from_path()`. Fallback uses the file extension if a file
85/// extension is known to be supported and the usual mime-type.
86pub fn is_supported_format(src: &Path) -> Result<SupportedFormat, UnsupportedFormat>
87{
88    if let Ok(identified_src) = infer::get_from_path(src)
89    {
90        if let Some(known) = identified_src
91        {
92            debug!(?known);
93            if SUPPORTED_MIME_TYPES.contains(&known.mime_type())
94            {
95                return if known.mime_type().eq(GZ_MIME)
96                {
97                    Ok(SupportedFormat::Compressed(Compression::Gz, src.to_path_buf()))
98                }
99                else if known.mime_type().eq(XZ_MIME)
100                {
101                    Ok(SupportedFormat::Compressed(Compression::Xz, src.to_path_buf()))
102                }
103                else if known.mime_type().eq(ZST_MIME)
104                {
105                    Ok(SupportedFormat::Compressed(Compression::Zst, src.to_path_buf()))
106                }
107                else if known.mime_type().eq(BZ2_MIME)
108                {
109                    Ok(SupportedFormat::Compressed(Compression::Bz2, src.to_path_buf()))
110                }
111                else if known.mime_type().eq(TAR_MIME)
112                {
113                    Ok(SupportedFormat::Compressed(Compression::Not, src.to_path_buf()))
114                }
115                else
116                {
117                    error!("Should not be able to reach here!");
118                    unreachable!()
119                };
120            }
121        }
122        else
123        {
124            let get_ext = match src.extension()
125            {
126                Some(ext) => ext.to_string_lossy().to_string(),
127                None => "unknown format".to_string(),
128            };
129            return Err(UnsupportedFormat { ext: get_ext });
130        }
131    }
132    Err(UnsupportedFormat { ext: "unknown format".to_string() })
133}
134
135pub fn copy_dir_all(src: impl AsRef<Path>, dst: &Path) -> Result<(), io::Error>
136{
137    debug!("Copying sources");
138    debug!(?dst);
139    fs::create_dir_all(dst)?;
140    let custom_walker = fs::read_dir(src)?;
141    custom_walker.par_bridge().into_par_iter().try_for_each(|entry| {
142        let entry = entry?;
143        let ty = entry.file_type()?;
144        trace!(?entry);
145        trace!(?ty);
146        if ty.is_dir()
147        {
148            trace!(?ty, "Is directory?");
149            copy_dir_all(entry.path(), &dst.join(entry.file_name()))
150
151        // Should we respect symlinks?
152        // } else if ty.is_symlink() {
153        //     debug!("Is symlink");
154        //     let path = fs::read_link(&entry.path())?;
155        //     let path = fs::canonicalize(&path).unwrap();
156        //     debug!(?path);
157        //     let pathfilename = path.file_name().unwrap_or(OsStr::new("."));
158        //     if path.is_dir() {
159        //         copy_dir_all(&path, &dst.join(pathfilename))?;
160        //     } else {
161        //         fs::copy(&path, &mut dst.join(pathfilename))?;
162        //     }
163
164        // Be pedantic or you get symlink error
165        }
166        else if ty.is_file()
167        {
168            trace!(?ty, "Is file?");
169            fs::copy(entry.path(), dst.join(entry.file_name()))?;
170            Ok(())
171        }
172        else
173        {
174            Ok(())
175        }
176    })?;
177    Ok(())
178}
179
180/// Taken from firstyear's code in obs-service-cargo
181/// for libroast adoption/migration.
182///
183/// This function processes globs e.g. "*firstyear", "*.tar.gz" to match any
184/// possible file. We only take the last element of the sorted list using the
185/// `core::slice::sort_unstable()` from the `std::core`.
186pub fn process_globs(src: &Path) -> io::Result<PathBuf>
187{
188    let glob_iter = match glob(&src.as_os_str().to_string_lossy())
189    {
190        Ok(gi) =>
191        {
192            trace!(?gi);
193            gi
194        }
195        Err(e) =>
196        {
197            error!(err = ?e, "Invalid glob input");
198            return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid glob input"));
199        }
200    };
201
202    let mut globs = glob_iter.into_iter().collect::<Result<Vec<_>, _>>().map_err(|e| {
203        error!(?e, "glob error");
204        io::Error::new(io::ErrorKind::InvalidInput, "Glob error")
205    })?;
206
207    // There can legitimately be multiple matching files. Generally this happens
208    // with tar_scm where you have name-v1.tar and the service reruns and
209    // creates name-v2.tar. In this case, we would error if we demand a single
210    // match, when what we really need is to take the *latest*. Thankfully for
211    // us, versions in rpm tar names tend to sort lexicographically, so we can
212    // just sort this list and the last element is the newest. (ie v2 sorts
213    // after v1).
214
215    globs.sort_unstable();
216
217    if globs.len() > 1
218    {
219        warn!("⚠️  Multiple files matched glob");
220        for item in &globs
221        {
222            warn!("- {}", item.display());
223        }
224    }
225
226    // Take the last item.
227    globs.pop().inspect(|item| info!("✅ Matched an item: {}", item.display())).ok_or_else(|| {
228        error!("No files/directories matched src glob input");
229        io::Error::new(io::ErrorKind::InvalidInput, "No files/directories matched src glob input")
230    })
231}