diff --git a/.gitignore b/.gitignore index 1e553c7..be66dcf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ /target **/*.rs.bk /mrl +/bar +/tests/foo +/tests/bar +/foo diff --git a/README.md b/README.md index 5995e44..314fa19 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,6 @@ Status](https://travis-ci.com/theimpossibleastronaut/rmwrs.svg?branch=trunk)](ht ## Current state *rmwrs* is in a very early development state and may change rapidly. -Presently files can be removed to a WASTE directory but there's no -`restore` feature. Running `cargo run -- ` will ReMove a file to a waste folder (which is specified in the test configuration file) and create a @@ -34,9 +32,20 @@ and corresponding .trashinfo files were created in ~/.rmwrs-Trash-test/info. The .trashinfo file uses the [FreeDesktop.org Trash specification](https://specifications.freedesktop.org/trash-spec/trashspec-latest.html). -If you use *rmwrs* on a file that has the same name in a destination -waste folder, it will be renamed by adding a time string formatted -suffix (i.e. "_%H%M%S-%y%m%d"). +When rmw'ing an item, if a file or directory with the same name already +exists in the waste (or trash) directory, it will not be overwritten; +instead, the current file being rmw'ed will have a time/date string +(formatted as "_%H%M%S-%y%m%d") appended to it (e.g. +'foo_164353-210508'). + +## -z, --restore FILE(s) +To restore items, specify the path to them in the /files +directory (wildcards ok). + +When restoring an item, if a file or directory with the same name +already exists at the destination, the item being restored will +have a time/date string (formatted as "_%H%M%S-%y%m%d") appended +to it (e.g. 'foo_164353-210508'). ## Configuration file diff --git a/src/lib.rs b/src/lib.rs index 1de2129..74faccd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,10 @@ pub mod cli_options { #[structopt(short = "c", long = "config")] pub custom_config_file: Option, + /// Restore files + #[structopt(short = "z", long = "restore")] + pub restore: bool, + /// Files to process #[structopt(name = "FILE", parse(from_os_str))] pub files: Vec, diff --git a/src/main.rs b/src/main.rs index 2f3b329..c94778a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ -use std::fs::rename; -use std::io; +use std::{fs::rename, io, path::Path}; use structopt::StructOpt; mod trashinfo; use rmwrs::cli_options; @@ -27,13 +26,12 @@ fn main() -> Result<(), io::Error> { let deletion_date = date_now.format("%Y-%m-%dT%H:%M:%S").to_string(); let noclobber_suffix = date_now.format("_%H%M%S-%y%m%d").to_string(); - let mut renamed_list: Vec = Vec::new(); - + // This will be changed later; the subscript number for waste_list depends on whether or not // the file being rmw'ed is // on the same filesystem as the WASTE folder. let waste = &waste_list[0]; - + for file in &opt.files { let file_absolute: Option = file.canonicalize().map_or_else( |e| { @@ -45,37 +43,57 @@ fn main() -> Result<(), io::Error> { if file_absolute == None { continue; } - + let mut basename = rmwrs::libgen::get_basename(&file) .to_str() .unwrap() .to_owned(); - - let mut destination = format!("{}/{}", &waste.file, basename).to_owned(); - if std::path::Path::new(&destination).exists() { - basename.push_str(&noclobber_suffix); - destination.push_str(&noclobber_suffix); + + if opt.restore { + let info_path = trashinfo::info_path(&file_absolute.unwrap()); + let trash_info = trashinfo::Trashinfo::from_file(&info_path)?; + let mut destination = trash_info.1.clone(); + if Path::new(&destination).exists() { + destination.push_str(&noclobber_suffix); + } + + match rename(file, &destination) { + Ok(_val) => { + println!("'{}' -> '{}'", file.display(), destination); + std::fs::remove_file(info_path)?; + } + Err(e) => println!("Error {} renaming {}", e, file.display()), + } } + else { + let mut renamed_list: Vec = Vec::new(); + let mut destination = format!("{}/{}", &waste.file, basename).to_owned(); - match rename(&file, &destination) { - Ok(_val) => { - println!("'{}' -> '{}'", file.display(), destination); - renamed_list.push(destination.clone()); - let trashinfo_file_contents = - trashinfo::Trashinfo::new(&file_absolute.unwrap(), &deletion_date) - .to_contents(); - - trashinfo::create(&basename, &waste.info, trashinfo_file_contents) - .expect("Error writing trashinfo file"); + if Path::new(&destination).exists() { + basename.push_str(&noclobber_suffix); + destination.push_str(&noclobber_suffix); + } + + match rename(&file, &destination) { + Ok(_val) => { + println!("'{}' -> '{}'", file.display(), destination); + renamed_list.push(destination.clone()); + let trashinfo_file_contents = + trashinfo::Trashinfo::new(&file_absolute.unwrap(), &deletion_date) + .to_contents(); + + trashinfo::create(&basename, &waste.info, trashinfo_file_contents) + .expect("Error writing trashinfo file"); + } + Err(e) => println!("Error {} renaming {}", e, file.display()), } - Err(e) => println!("Error {} renaming {}", e, file.display()), + // I don't think we need a unit test for mrl file creation; when there's a restore + // and undo function, + // it can be tested easily using the bin script test. + rmwrs::mrl::create(&datadir, &renamed_list)?; } } - // I don't think we need a unit test for mrl file creation; when there's a restore - // and undo function, - // it can be tested easily using the bin script test. - rmwrs::mrl::create(&datadir, &renamed_list)?; Ok(()) } diff --git a/src/trashinfo.rs b/src/trashinfo.rs index 9e4ee70..6a8a285 100644 --- a/src/trashinfo.rs +++ b/src/trashinfo.rs @@ -1,8 +1,8 @@ use std::fs; use std::io; -use rmwrs::utils::{percent_encode, percent_decode}; +use rmwrs::utils::{percent_encode, percent_decode, read_lines}; -pub struct Trashinfo(String, String, String); +pub struct Trashinfo(String, pub String, pub String); /// The format of the trashinfo file corresponds to that of the FreeDesktop.org /// Trash specification. @@ -14,10 +14,21 @@ impl Trashinfo { 2: deletion_date.to_string(), } } + pub fn to_contents(&self) -> String { let pct_string = percent_encode(&self.1); format!("{}\nPath={}\nDeletionDate={}\n", self.0, pct_string, self.2) } + + pub fn from_file(path: &str) -> io::Result { + let mut lines = read_lines(path)?.skip(1); + let mut path_and_filename = lines.next().expect("Failed to read the path in trashinfo.")?; + path_and_filename.replace_range(..5, ""); + path_and_filename = percent_decode(&path_and_filename).expect("Could not decode path in trashinfo."); + let mut deletion_date = lines.next().expect("Failed to read deletion date in trashinfo.")?; + deletion_date.replace_range(..13, ""); + Ok(Trashinfo::new(&path_and_filename, &deletion_date)) + } } #[test] @@ -30,8 +41,18 @@ fn check_create_trashinfo_contents() { ); } -pub fn create(basename: &str, waste_info: &str, contents: String) -> Result<(), io::Error> { +pub fn create(basename: &str, waste_info: &str, contents: String) -> io::Result<()> { let trashinfo_filename = format!("{}{}", basename, ".trashinfo"); let trashinfo_dest = format!("{}/{}", &waste_info, trashinfo_filename); fs::write(trashinfo_dest, contents) } + +pub fn info_path(file_to_restore_path: &str) -> String { + // This is a bit lazy and doesn't bother reading the waste_info. + format!("{}.trashinfo", file_to_restore_path).replace("/files/", "/info/") +} + +#[test] +fn test_info_path() { + assert_eq!("~/.hello&there/info/🐢👀🍻.trashinfo", info_path("~/.hello&there/files/🐢👀🍻")) +} diff --git a/src/utils.rs b/src/utils.rs index 006be2c..0e4ebe7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::{io::{self, BufRead}, fmt, path::Path, fs::File}; #[derive(Debug, Clone, PartialEq)] pub struct PercentDecodeError(&'static str); @@ -58,6 +58,16 @@ pub fn is_unreserved(check_reserved: char) -> bool { } } +/// See the original [source here](https://doc.rust-lang.org/rust-by-example/std_misc/file/read_lines.html). +/// +/// The output is wrapped in a Result to allow matching on errors +/// Returns an Iterator to the Reader of the lines of the file. +pub fn read_lines

(filename: P) -> io::Result>> +where P: AsRef, { + let file = File::open(filename)?; + Ok(io::BufReader::new(file).lines()) +} + #[test] fn test_escape_url() { assert_eq!(percent_encode("~/foo/bar&/baz boom"),"~/foo/bar%26/baz%20boom"); @@ -74,4 +84,4 @@ fn test_unescape_url() { fn test_invalid_unescape() { assert_eq!(percent_decode("~/foo/bar%26/baz%20boom%"), Err(PercentDecodeError("Char missing in encoding."))); assert_eq!(percent_decode("~/foo/bar%26/baz%2Zboom%"), Err(PercentDecodeError("Error parsing hex to char."))); -} \ No newline at end of file +} diff --git a/tests/test.sh b/tests/test.sh index 043381d..00b39c9 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -1,5 +1,8 @@ #!/bin/sh +# echo commands, exit on an error +set -ev + # Used by rmwrs to determine the home directory. If this is set, # rmwrs will use this "fake" home directory. export RMWRS_TEST_HOME="$(dirname "$(readlink -f "$0")")/rmw_test_home" @@ -8,37 +11,92 @@ if test -r ${RMWRS_TEST_HOME}; then rm -rf ${RMWRS_TEST_HOME} fi -test_result_want_fail() { - set +x - if [ $1 = 0 ]; then - echo "\n --:Test failure:--" - exit 1 - fi +BASENAME_TEST_WASTE_FOLDER=".rmwrs-Trash-test" +TEST_WASTE_FOLDER="${RMWRS_TEST_HOME}/${BASENAME_TEST_WASTE_FOLDER}" + +CFG_FILE="${CARGO_MANIFEST_DIR}/tests/bin_test.conf" +OLD_PWD="${PWD}" + +mkdir -p "${RMWRS_TEST_HOME}" +cd "${RMWRS_TEST_HOME}" + +TEST_CMD="${CARGO_MANIFEST_DIR}/target/debug/${CARGO_PKG_NAME}" + +touch foo bar + +$TEST_CMD foo bar + +test -e ${TEST_WASTE_FOLDER}/files/foo +test -e ${TEST_WASTE_FOLDER}/info/foo.trashinfo + +test -e ${TEST_WASTE_FOLDER}/files/bar +test -e ${TEST_WASTE_FOLDER}/info/bar.trashinfo - echo " -:Test passed:-" - set -x -} +# +# Test restore +# +# Make sure foo doesn't exist before restoring +test ! -e foo -TEST_WASTE_FOLDER="${RMWRS_TEST_HOME}/.rmwrs-Trash-test" +# https://doc.rust-lang.org/cargo/reference/environment-variables.html +# CARGO_BUILD_TARGET_DIR was an empty variable when I tried this +# ${CARGO_BUILD_TARGET_DIR}/${CARGO_PKG_NAME} -c "${CFG_FILE}" -v -z ${TEST_WASTE_FOLDER}/files/foo +# +# It would be best if we didn't need to use the literal string "/target/debug/" +# in the command below. +$TEST_CMD -c "${CFG_FILE}" -v -z ${TEST_WASTE_FOLDER}/files/foo +test -e foo +test ! -e ${TEST_WASTE_FOLDER}/files/foo +test ! -e ${TEST_WASTE_FOLDER}/info/foo.trashinfo -touch foo bar || exit $? +# restore 'bar' for next test +$TEST_CMD -c "${CFG_FILE}" -v -z ${TEST_WASTE_FOLDER}/files/bar -cargo run -- -c tests/bin_test.conf foo bar || exit $? +$TEST_CMD -c "${CFG_FILE}" foo bar +echo -e "\n\nTest restore using absolute path" -test -e ${TEST_WASTE_FOLDER}/files/foo || exit $? -test -e ${TEST_WASTE_FOLDER}/info/foo.trashinfo || exit $? +$TEST_CMD -c "${CFG_FILE}" -z "${TEST_WASTE_FOLDER}/files/"* +test -e foo +test -e bar -test -e ${TEST_WASTE_FOLDER}/files/bar || exit $? -test -e ${TEST_WASTE_FOLDER}/info/bar.trashinfo || exit $? +$TEST_CMD -c "${CFG_FILE}" foo bar + +echo -e "\n\ntest restore using relative path" +echo -e "pwd is ${PWD}\n\n" + +$TEST_CMD -c "${CFG_FILE}" -z "${BASENAME_TEST_WASTE_FOLDER}/files/foo" +test -e "foo" + +$TEST_CMD -c "${CFG_FILE}" foo +# test restore using wildcard + +$TEST_CMD -c "${CFG_FILE}" -z "${BASENAME_TEST_WASTE_FOLDER}/files/"* +test -e "foo" +test -e "bar" + +# filenames with spaces +touch "any bar" +$TEST_CMD -c "${CFG_FILE}" "any bar" +# The space should have been converted to "%20" +substring="any%20bar" +output=$(cat "${BASENAME_TEST_WASTE_FOLDER}/info/any bar.trashinfo") +test "${output#*$substring}" != "$output" +$TEST_CMD -c "${CFG_FILE}" -z "${BASENAME_TEST_WASTE_FOLDER}/files/any bar" +test -e "any bar" + +# When restoring, file should have time/date string (formatted as +# "_%H%M%S-%y%m%d") appended to it (e.g. 'foo_164353-210508'). +$TEST_CMD -c "${CFG_FILE}" foo +touch foo +$TEST_CMD -c "${CFG_FILE}" -z "${BASENAME_TEST_WASTE_FOLDER}/files/foo" +ls foo_* if test -r ${RMWRS_TEST_HOME}; then rm -rf ${RMWRS_TEST_HOME} fi - # Check for invalid attribute in the Waste file (want fail) -cargo run -- -c tests/bin_test_invalid.conf -test_result_want_fail $? +$TEST_CMD -c tests/bin_test_invalid.conf && exit 1 if test -r ${RMWRS_TEST_HOME}; then rm -rf ${RMWRS_TEST_HOME}