Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add restore function #12

Merged
merged 7 commits into from
Jan 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/target
**/*.rs.bk
/mrl
/bar
/tests/foo
/tests/bar
/foo
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 -- <file>` will ReMove a file to a waste folder
(which is specified in the test configuration file) and create a
Expand All @@ -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 <WASTE>/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

Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ pub mod cli_options {
#[structopt(short = "c", long = "config")]
pub custom_config_file: Option<String>,

/// Restore files
#[structopt(short = "z", long = "restore")]
pub restore: bool,

/// Files to process
#[structopt(name = "FILE", parse(from_os_str))]
pub files: Vec<PathBuf>,
Expand Down
70 changes: 44 additions & 26 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<String> = 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<String> = file.canonicalize().map_or_else(
|e| {
Expand All @@ -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<String> = 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(())
}
Expand Down
27 changes: 24 additions & 3 deletions src/trashinfo.rs
Original file line number Diff line number Diff line change
@@ -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<https://specifications.freedesktop.org/trash-spec/trashspec-latest.html>.
Expand All @@ -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<Trashinfo> {
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, "");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make things more robust, I think there should be a condition here to make sure that the first 5 chars of the string match "Path=".

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]
Expand All @@ -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/🐢👀🍻"))
}
14 changes: 12 additions & 2 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where P: AsRef<Path>, {
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");
Expand All @@ -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.")));
}
}
96 changes: 77 additions & 19 deletions tests/test.sh
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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}
Expand Down