Skip to content

Commit

Permalink
feat: Move files to .trash folder if they are in use (#953)
Browse files Browse the repository at this point in the history
  • Loading branch information
adament authored Nov 21, 2024
1 parent 32eefc8 commit 08c395c
Show file tree
Hide file tree
Showing 2 changed files with 246 additions and 11 deletions.
2 changes: 1 addition & 1 deletion crates/rattler/src/install/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ use simple_spawn_blocking::Cancelled;
use tokio::task::JoinError;
use tracing::instrument;
pub use transaction::{Transaction, TransactionError, TransactionOperation};
pub use unlink::unlink_package;
pub use unlink::{empty_trash, unlink_package};

use crate::install::entry_point::{
create_unix_python_entry_point, create_windows_python_entry_point,
Expand Down
255 changes: 245 additions & 10 deletions crates/rattler/src/install/unlink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
use std::{
collections::HashSet,
ffi::OsString,
io::ErrorKind,
path::{Path, PathBuf},
};
use uuid::Uuid;

use rattler_conda_types::PrefixRecord;

Expand All @@ -22,6 +24,18 @@ pub enum UnlinkError {
/// Failed to read a directory.
#[error("failed to read directory: {0}")]
FailedToReadDirectory(String, std::io::Error),

/// Failed to read a directory.
#[error("failed to test existence: {0}")]
FailedToTestExistence(String, std::io::Error),

/// Failed to create a directory
#[error("failed to create directory: {0}")]
FailedToCreateDirectory(String, std::io::Error),

/// Failed to move a file to the trash
#[error("failed to move file: {0} to {1}")]
FailedToMoveFile(String, String, std::io::Error),
}

pub(crate) fn recursively_remove_empty_directories(
Expand Down Expand Up @@ -98,24 +112,99 @@ pub(crate) fn recursively_remove_empty_directories(
}
}

/// Remove files in trash folder that are not currently in use.
pub async fn empty_trash(target_prefix: &Path) -> Result<(), UnlinkError> {
let trash_dir = target_prefix.join(".trash");
match tokio::fs::read_dir(&trash_dir).await {
Ok(mut read_dir) => {
let mut files_left_in_trash = false;
while let Some(entry) = read_dir.next_entry().await.map_err(|e| {
UnlinkError::FailedToReadDirectory(trash_dir.to_string_lossy().to_string(), e)
})? {
tokio::fs::remove_file(entry.path())
.await
.or_else(|e| match e.kind() {
ErrorKind::NotFound => Ok(()),
ErrorKind::PermissionDenied => {
files_left_in_trash = true;
Ok(())
}
_ => Err(UnlinkError::FailedToDeleteFile(
entry.path().to_string_lossy().to_string(),
e,
)),
})?;
}
if !files_left_in_trash {
tokio::fs::remove_dir(&trash_dir).await.map_err(|e| {
UnlinkError::FailedToDeleteDirectory(trash_dir.to_string_lossy().to_string(), e)
})?;
}
}
Err(e) if e.kind() == ErrorKind::NotFound => {}
Err(e) => {
return Err(UnlinkError::FailedToReadDirectory(
trash_dir.to_string_lossy().to_string(),
e,
))
}
}

Ok(())
}

async fn move_to_trash(target_prefix: &Path, path: &Path) -> Result<(), UnlinkError> {
let mut trash_dest = target_prefix.join(".trash");
match tokio::fs::try_exists(&trash_dest).await {
Ok(true) => {}
Ok(false) => tokio::fs::create_dir(&trash_dest).await.map_err(|e| {
UnlinkError::FailedToCreateDirectory(trash_dest.to_string_lossy().to_string(), e)
})?,
Err(e) => {
return Err(UnlinkError::FailedToTestExistence(
trash_dest.to_string_lossy().to_string(),
e,
))
}
}
let mut new_filename = OsString::new();
if let Some(file_name) = path.file_name() {
new_filename.push(file_name);
new_filename.push(".");
}
new_filename.push(format!("{}.trash", Uuid::new_v4().simple()));
trash_dest.push(new_filename);
match tokio::fs::rename(path, &trash_dest).await {
Ok(_) => Ok(()),
Err(e) => Err(UnlinkError::FailedToMoveFile(
path.to_string_lossy().to_string(),
trash_dest.to_string_lossy().to_string(),
e,
)),
}
}

/// Completely remove the specified package from the environment.
pub async fn unlink_package(
target_prefix: &Path,
prefix_record: &PrefixRecord,
) -> Result<(), UnlinkError> {
// Remove all entries
for paths in prefix_record.paths_data.paths.iter() {
match tokio::fs::remove_file(target_prefix.join(&paths.relative_path)).await {
let p = target_prefix.join(&paths.relative_path);
match tokio::fs::remove_file(&p).await {
Ok(_) => {}
Err(e) if e.kind() == ErrorKind::NotFound => {
Err(e) => match e.kind() {
// Simply ignore if the file is already gone.
}
Err(e) => {
return Err(UnlinkError::FailedToDeleteFile(
paths.relative_path.to_string_lossy().to_string(),
e,
))
}
ErrorKind::NotFound => {}
ErrorKind::PermissionDenied => move_to_trash(target_prefix, &p).await?,
_ => {
return Err(UnlinkError::FailedToDeleteFile(
paths.relative_path.to_string_lossy().to_string(),
e,
))
}
},
}
}

Expand Down Expand Up @@ -148,7 +237,8 @@ mod tests {
use crate::{
get_repodata_record,
install::{
link_package, unlink_package, InstallDriver, InstallOptions, PythonInfo, Transaction,
empty_trash, link_package, unlink_package, InstallDriver, InstallOptions, PythonInfo,
Transaction,
},
};

Expand Down Expand Up @@ -297,4 +387,149 @@ mod tests {
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].as_ref().unwrap().file_name(), "conda-meta");
}

fn count_trash(trash_dir: &Path) -> usize {
if !trash_dir.exists() {
return 0;
}
let mut count = 0;
for entry in std::fs::read_dir(trash_dir).unwrap() {
let entry = entry.unwrap();
if entry.path().extension().unwrap() == "trash" {
count += 1;
}
}
count
}

#[cfg(windows)]
#[tokio::test]
async fn test_unlink_package_in_use() {
use itertools::chain;
use std::env::{join_paths, split_paths, var_os};
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};

let environment_dir = tempfile::TempDir::new().unwrap();
let target_prefix = environment_dir.path();
let trash_dir = target_prefix.join(".trash");
let files = [
("https://conda.anaconda.org/conda-forge/win-64/bat-0.24.0-ha073cba_1.conda", "65a125b7a6e7fd7e5d4588ee537b5db2c984ed71e4832f7041f691c2cfd73504"),
("https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda", "db8dead3dd30fb1a032737554ce91e2819b43496a0db09927edf01c32b577450"),
("https://conda.anaconda.org/conda-forge/win-64/vc-14.3-ha32ba9b_23.conda", "986ddaf8feec2904eac9535a7ddb7acda1a1dfb9482088fdb8129f1595181663"),
("https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.42.34433-he29a5d6_23.conda", "c483b090c4251a260aba6ff3e83a307bcfb5fb24ad7ced872ab5d02971bd3a49"),
];
let conda_meta_path = target_prefix.join("conda-meta");
std::fs::create_dir_all(&conda_meta_path).unwrap();
let install_driver = InstallDriver::default();
let mut prefix_records = Vec::new();
for (package_url, expected_sha256) in files {
let package_path =
tools::download_and_cache_file_async(package_url.parse().unwrap(), expected_sha256)
.await
.unwrap();

let package_dir = tempfile::TempDir::new().unwrap();

// Create package cache
rattler_package_streaming::fs::extract(&package_path, package_dir.path()).unwrap();

// Link the package
let paths = link_package(
package_dir.path(),
target_prefix,
&install_driver,
InstallOptions::default(),
)
.await
.unwrap();

let repodata_record = get_repodata_record(&package_path);
// Construct a PrefixRecord for the package

let prefix_record =
PrefixRecord::from_repodata_record(repodata_record, None, None, paths, None, None);
prefix_record
.write_to_path(conda_meta_path.join(prefix_record.file_name()), true)
.unwrap();
prefix_records.push(prefix_record);
}

// Start bat to block deletion of the bat package
let cmd_path = target_prefix.join("bin").join("bat.exe");
let mut cmd = Command::new(&cmd_path);
cmd.arg("-p");
cmd.stdout(Stdio::piped());
cmd.stdin(Stdio::piped());
cmd.stderr(Stdio::null());
cmd.env(
"PATH",
join_paths(chain(
chain(
[target_prefix.to_path_buf()],
[
"Library/mingw-w64/bin",
"Library/usr/bin",
"Library/bin",
"Scripts",
"bin",
]
.iter()
.map(|x| target_prefix.join(x)),
),
split_paths(&var_os("PATH").unwrap()),
))
.unwrap(),
);
let mut child = cmd.spawn().expect("failed to spawn bat.exe");
let mut stdin = child.stdin.take().expect("failed to open stdin");
let mut stdout = BufReader::new(child.stdout.take().expect("failed to open stdout"));
// Ensure program has started by waiting for it to repeat back to us.
let mut line = String::new();
stdin.write("abc\n".as_bytes()).unwrap();
stdout.read_line(&mut line).unwrap();

// Unlink the package
assert!(!trash_dir.exists());
let prefix_record = prefix_records.first().unwrap();
unlink_package(target_prefix, prefix_record).await.unwrap();
// Check if the conda-meta file is gone
assert!(!conda_meta_path.join(prefix_record.file_name()).exists());
assert!(trash_dir.exists());
assert!(count_trash(&trash_dir) > 0);
assert!(!cmd_path.exists());
empty_trash(target_prefix).await.unwrap();
assert!(count_trash(&trash_dir) > 0);

drop(stdin);
drop(stdout);
child.wait().expect("bat failed");
empty_trash(target_prefix).await.unwrap();
assert!(count_trash(&trash_dir) == 0);
assert!(!trash_dir.exists());
}

#[tokio::test]
async fn test_empty_trash() {
use uuid::Uuid;

let environment_dir = tempfile::TempDir::new().unwrap();
let trash_path = environment_dir.path().join(".trash");
std::fs::create_dir_all(&trash_path).unwrap();
{
let mut file =
File::create(trash_path.join(format!("{}.trash", Uuid::new_v4().simple())))
.unwrap();
write!(file, "some data").unwrap();
}
{
let mut file =
File::create(trash_path.join(format!("{}.trash", Uuid::new_v4().simple())))
.unwrap();
write!(file, "some other data").unwrap();
}
assert!(count_trash(&trash_path) == 2);
empty_trash(environment_dir.path()).await.unwrap();
assert!(!trash_path.exists());
}
}

0 comments on commit 08c395c

Please sign in to comment.