use content_inspector::ContentType;
use fs_err as fs;
use rattler_conda_types::PrefixRecord;
use std::{
collections::{HashMap, HashSet},
io::{self, Read},
path::{Path, PathBuf},
};
use tempfile::TempDir;
use walkdir::WalkDir;
use crate::metadata::Output;
use super::{PackagingError, file_mapper, normalize_path_for_comparison};
use rattler_build_recipe::stage1::GlobVec;
#[derive(Debug, Clone)]
struct CaseInsensitivePath {
path: String,
}
impl CaseInsensitivePath {
fn new(path: &Path) -> Self {
Self {
path: normalize_path_for_comparison(path, true).unwrap(),
}
}
}
impl std::hash::Hash for CaseInsensitivePath {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.path.hash(state);
}
}
impl PartialEq for CaseInsensitivePath {
fn eq(&self, other: &Self) -> bool {
self.path == other.path
}
}
impl Eq for CaseInsensitivePath {}
#[derive(Debug)]
pub struct Files {
pub new_files: HashSet<PathBuf>,
pub old_files: HashSet<PathBuf>,
pub prefix: PathBuf,
}
#[derive(Debug)]
pub struct TempFiles {
pub files: HashSet<PathBuf>,
pub temp_dir: tempfile::TempDir,
pub encoded_prefix: PathBuf,
content_type_map: HashMap<PathBuf, Option<ContentType>>,
}
pub fn content_type(path: &Path) -> Result<Option<ContentType>, io::Error> {
if path.is_dir() || path.is_symlink() {
return Ok(None);
}
let mut file = fs::File::open(path)?;
let mut buffer = [0; 1024];
let n = file.read(&mut buffer)?;
let buffer = &buffer[..n];
Ok(Some(content_inspector::inspect(buffer)))
}
pub fn record_files(directory: &Path) -> Result<HashSet<PathBuf>, io::Error> {
let mut res = HashSet::new();
for entry in WalkDir::new(directory) {
res.insert(entry?.path().to_owned());
}
Ok(res)
}
fn check_is_case_sensitive() -> Result<bool, io::Error> {
let tempdir = TempDir::new()?;
let file1 = tempdir.path().join("testfile.txt");
let file2 = tempdir.path().join("TESTFILE.txt");
fs::File::create(&file1)?;
Ok(!file2.exists() && file1.exists())
}
fn find_new_files(
current_files: &HashSet<PathBuf>,
previous_files: &HashSet<PathBuf>,
prefix: &Path,
is_case_sensitive: bool,
) -> HashSet<PathBuf> {
if is_case_sensitive {
current_files.difference(previous_files).cloned().collect()
} else {
let previous_case_aware: HashSet<CaseInsensitivePath> = previous_files
.iter()
.map(|p| {
CaseInsensitivePath::new(p.strip_prefix(prefix).expect("File should be in prefix"))
})
.collect();
current_files
.clone()
.into_iter()
.filter(|p| {
!previous_case_aware.contains(&CaseInsensitivePath::new(
p.strip_prefix(prefix).expect("File should be in prefix"),
))
})
.collect::<HashSet<_>>()
}
}
impl Files {
pub fn from_prefix(
prefix: &Path,
always_include: &GlobVec,
files: &GlobVec,
post_install_files: Option<&HashSet<PathBuf>>,
) -> Result<Self, io::Error> {
if !prefix.exists() {
return Ok(Files {
new_files: HashSet::new(),
old_files: HashSet::new(),
prefix: prefix.to_owned(),
});
}
let fs_is_case_sensitive = check_is_case_sensitive()?;
let mut previous_files = if prefix.join("conda-meta").exists() {
let prefix_records: Vec<PrefixRecord> = PrefixRecord::collect_from_prefix(prefix)?;
let mut previous_files =
prefix_records
.into_iter()
.fold(HashSet::new(), |mut acc, record| {
acc.extend(record.files.iter().map(|f| prefix.join(f)));
acc
});
previous_files.extend(record_files(&prefix.join("conda-meta"))?);
previous_files
} else {
HashSet::new()
};
if let Some(extra) = post_install_files {
previous_files.extend(extra.iter().cloned());
}
let current_files = record_files(prefix)?;
let mut difference = find_new_files(
¤t_files,
&previous_files,
prefix,
fs_is_case_sensitive,
);
if !files.is_empty() {
difference.retain(|f| {
files.is_match(f.strip_prefix(prefix).expect("File should be in prefix"))
});
}
if !always_include.is_empty() {
for file in current_files {
let file_without_prefix =
file.strip_prefix(prefix).expect("File should be in prefix");
if always_include.is_match(file_without_prefix) {
tracing::info!("Forcing inclusion of file: {:?}", file_without_prefix);
difference.insert(file);
}
}
}
Ok(Files {
new_files: difference,
old_files: previous_files,
prefix: prefix.to_owned(),
})
}
pub fn to_temp_folder(&self, output: &Output) -> Result<TempFiles, PackagingError> {
let temp_dir = TempDir::with_prefix(output.name().as_normalized())?;
let mut files = HashSet::new();
let mut content_type_map = HashMap::new();
for f in &self.new_files {
if file_mapper::filter_pyc(f, &self.old_files) {
continue;
}
if let Some(dest_file) = output.write_to_dest(f, &self.prefix, temp_dir.path())? {
content_type_map.insert(dest_file.clone(), content_type(f)?);
files.insert(dest_file);
}
}
Ok(TempFiles {
files,
temp_dir,
encoded_prefix: self.prefix.clone(),
content_type_map,
})
}
}
impl TempFiles {
pub fn add_files<I>(&mut self, files: I)
where
I: IntoIterator<Item = PathBuf>,
{
for f in files {
self.content_type_map
.insert(f.clone(), content_type(&f).unwrap_or(None));
self.files.insert(f);
}
}
pub const fn content_type_map(&self) -> &HashMap<PathBuf, Option<ContentType>> {
&self.content_type_map
}
}
#[cfg(test)]
mod test {
use std::{collections::HashSet, path::PathBuf};
use crate::packaging::file_finder::{check_is_case_sensitive, find_new_files};
#[test]
fn test_find_new_files_case_sensitive() {
let current_files: HashSet<PathBuf> = [
PathBuf::from("/test/File.txt"),
PathBuf::from("/test/file.txt"),
PathBuf::from("/test/common.txt"),
]
.into_iter()
.collect();
let previous_files: HashSet<PathBuf> = [
PathBuf::from("/test/File.txt"),
PathBuf::from("/test/common.txt"),
]
.into_iter()
.collect();
let prefix = PathBuf::from("/test");
let new_files = find_new_files(¤t_files, &previous_files, &prefix, true);
assert_eq!(new_files.len(), 1);
assert!(new_files.contains(&PathBuf::from("/test/file.txt")));
}
#[test]
fn test_find_new_files_case_insensitive() {
let current_files: HashSet<PathBuf> = [
PathBuf::from("/test/File.txt"),
PathBuf::from("/test/file.txt"),
PathBuf::from("/test/common.txt"),
PathBuf::from("/test/NEW.txt"),
]
.into_iter()
.collect();
let previous_files: HashSet<PathBuf> = [
PathBuf::from("/test/FILE.TXT"), PathBuf::from("/test/common.txt"),
]
.into_iter()
.collect();
let prefix = PathBuf::from("/test");
let new_files = find_new_files(¤t_files, &previous_files, &prefix, false);
assert_eq!(new_files.len(), 1);
assert!(new_files.contains(&PathBuf::from("/test/NEW.txt")));
assert!(!new_files.contains(&PathBuf::from("/test/File.txt")));
assert!(!new_files.contains(&PathBuf::from("/test/file.txt")));
}
#[test]
fn test_check_is_case_sensitive() {
let result = check_is_case_sensitive();
assert!(result.is_ok());
let _is_case_sensitive = result.unwrap();
}
}