Skip to content

Commit

Permalink
Merge pull request #4 from rushsteve1/orphans
Browse files Browse the repository at this point in the history
--orphans command for finding lost files
  • Loading branch information
rushsteve1 authored Sep 20, 2021
2 parents c286aa1 + 71a64bb commit 22fe8d7
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 47 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ A near drop-in replacement for `rm` that uses the
Written in the [D programming language](https://dlang.org/)
using only D's Phobos standard library.

**ONLY LINUX AND BSD ARE SUPPORTED!**
**ONLY LINUX AND BSD ARE CURRENTLY SUPPORTED!**

Windows won't work at all, and MacOS probably won't either.
Any POSIX and Freedesktop compliant system should work fine.
Expand Down Expand Up @@ -65,17 +65,18 @@ additional long options for the `trash-d` specific features.
- `-i`, `--interactive` Ask before each deletion.
- `-f`, `--force` Don't prompt and ignore errors.
- `--version` Output the version and exit.
- `--empty` Empty the trash bin.
- `--delete FILE` Delete a file from the trash.
- `--list` List out the files in the trash.
- `--orphans` List orphaned files in the trash.
- `--delete FILE` Delete a file from the trash.
- `--restore FILE` Restore a file from the trash.
- `--empty` Empty the trash bin.
- `--rm` Escape hatch to permanently delete a file.
- `-h`, `--help` This help information.


#### Option Precedence

The `--help`, `--version`, `--list`, `--restore`, `--delete`, and
The `--help`, `--version`, `--list`, `--orphans`, `--restore`, `--delete`, and
`--empty` flags all cause the program to short-circuit and perform only that
operation and no others. Their precedence is in that order exactly, and is
intended to cause the least destruction.
Expand Down
8 changes: 4 additions & 4 deletions source/app.d
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
The two files are separated for testing purposes
*/

import run : OPTS, runCommands;
import cli : parseOpts;
import run : runCommands;
import cli : OPTS, parseOpts;
import util : err;

import core.memory;
Expand All @@ -27,8 +27,8 @@ int main(string[] args) {
switch (res) {
case 0:
break;
// This is a special case where the options parsing wants to stop
// execution but there was no error
// This is a special case where the options parsing wants to stop
// execution but there was no error
case -1:
return 0;
default:
Expand Down
70 changes: 42 additions & 28 deletions source/cli.d
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
CLI flag parsing and structure using D's getopt implementation
*/

import ver : COPY_TEXT, VER, VER_NAME, VER_TEXT;
import run : OPTS;
import ver : COPY_TEXT, VER_TEXT;
import util : log, err;

import std.getopt;
Expand Down Expand Up @@ -40,6 +39,8 @@ struct Opts {
// ===== Custom Options ====
/// List out the files in the trash bin
bool list;
/// List out the orphaned files in the trash bin
bool orphans;
/// Empty the trash bin
bool empty;
/// Actually delete instead of moving to trash
Expand All @@ -63,8 +64,28 @@ struct Opts {
@property @safe string dirsize_file() const nothrow {
return trash_dir.buildPath("directorysizes");
}

/// Reset the global OPTS to default values
@safe void reset() {
OPTS.dir = false;
OPTS.recursive = false;
OPTS.verbose = false;
OPTS.interactive = false;
OPTS.force = false;
OPTS.ver = false;

OPTS.list = false;
OPTS.orphans = false;
OPTS.empty = false;
OPTS.rm = false;
OPTS.restore = null;
OPTS.del = null;
}
}

/// The parsed CLI options are stored here on a global `Opts` struct
static Opts OPTS;

/**
Parses the command line options into the `OPTS` global struct using D's built-in `getopt` parser
*/
Expand All @@ -74,9 +95,20 @@ int parseOpts(ref string[] args) {

// Always reset the global options at each call
// This is mostly useful for testing
// TODO this is funky, clean it up a bit
const Opts o;
OPTS = o;
OPTS.reset();

// Figure out where the trash dir is based on env variables
OPTS.trash_dir = environment.get("TRASH_D_DIR");
if (OPTS.trash_dir is null) {
// Get the correct XDG directory
string data_home = environment.get("XDG_DATA_HOME");
if (data_home is null) {
data_home = expandTilde("~/.local/share");
}
// Set the trash dir option
OPTS.trash_dir = data_home.buildPath("Trash").absolutePath();
log("trash directory: %s", OPTS.trash_dir);
}

// Parse CLI options using D's getopt
GetoptResult helpInfo;
Expand All @@ -95,10 +127,11 @@ int parseOpts(ref string[] args) {
"force|f", "Don't prompt and ignore errors.", &OPTS.force,
"version", "Output the version and exit.", &OPTS.ver,

"empty", "Empty the trash bin.", &OPTS.empty,
"delete", "Delete a file from the trash.", &OPTS.del,
"list", "List out the files in the trash.", &OPTS.list,
"orphans", "List orphaned files in the trash.", &OPTS.orphans,
"restore", "Restore a file from the trash.", &OPTS.restore,
"delete", "Delete a file from the trash.", &OPTS.del,
"empty", "Empty the trash bin.", &OPTS.empty,
"rm", "Escape hatch to permanently delete a file.", &OPTS.rm,
);
// dfmt on
Expand All @@ -117,30 +150,11 @@ int parseOpts(ref string[] args) {

if (arglen < 2)
return 1;
return -1;
}

// Print the version number and return
// This is here so that the program quits out on --version quickly
if (OPTS.ver) {
writefln("\033[1m%s\033[0m\n\n%s", VER_TEXT, COPY_TEXT);
// This function is a little special because it has a unique return code
// -1 means "stop program and with status code 0"
return -1;
}

OPTS.trash_dir = environment.get("TRASH_D_DIR");
if (OPTS.trash_dir is null) {
// Get the correct XDG directory
string data_home = environment.get("XDG_DATA_HOME");
if (data_home is null) {
data_home = expandTilde("~/.local/share");
} else {
// Set the trash dir option
OPTS.trash_dir = data_home.buildPath("Trash").absolutePath();
log("trash directory: %s", OPTS.trash_dir);
}
}

// This function is a little special because it has a unique return code
// -1 means "stop program and with status code 0"
return 0;
}
28 changes: 26 additions & 2 deletions source/operations.d
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
The various file operations that trash-d can perform
*/

import run : OPTS;
import cli : OPTS;
import trashfile : TrashFile;
import util;

Expand All @@ -11,7 +11,7 @@ import std.algorithm;
import std.datetime.systime : Clock;
import std.file;
import std.format : format;
import std.path : baseName, stripExtension;
import std.path : baseName, buildNormalizedPath, stripExtension;
import std.range : array;
import std.stdio;
import std.string;
Expand Down Expand Up @@ -115,6 +115,7 @@ void list() {
}

// Map the `DirEntry`s to `TrashFile`s
// MUST call .array() to ensure this doesn't get altered by subsequent algos
auto tf = entries.map!(e => TrashFile(e.name.baseName().stripExtension())).array();

// Calculate the maximum length of the name and path for formatting
Expand All @@ -131,6 +132,29 @@ void list() {
}
}

/**
List out the files that are in the trash bin but do not have matching
.trashinfo files so would not show up in --list.
These can be secretly lurking files that are wasting space
*/
void orphans() {
const auto files = OPTS.files_dir.dirEntries(SpanMode.shallow).array();

// If the trash is empty then say so
if (files.length <= 0) {
writeln("No orphaned files");
return;
}

auto tf = files.map!(f => buildNormalizedPath(OPTS.info_dir, f) ~ ".trashinfo")
.filter!(p => !p.exists());

foreach (TrashFile file; tf) {
writefln("%s", buildNormalizedPath(OPTS.files_dir, file.file_name));
}
writefln("\nUse %s --empty to delete these permanently", OPTS.prog_name);
}

/**
Depending on the value of the `del` paramater this function either deletes a
single file from the trash bin, or restores a file from the trash bin to its
Expand Down
19 changes: 15 additions & 4 deletions source/run.d
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,26 @@
- https://specifications.freedesktop.org/trash-spec/trashspec-latest.html
*/

import cli : Opts, parseOpts;
import cli : OPTS, parseOpts;
import operations;
import util : createMissingFolders, err, log;
import ver : COPY_TEXT, VER_TEXT;

import std.stdio : writefln;
import std.string : startsWith;

/// The parsed CLI options are stored here on a global `Opts` struct
static Opts OPTS;

/**
Given the remaining string arguments and the global `OPTS` struct, runs the
given commands. In other words, this function wraps around all the real
operations and acts as a secondary entrypoint that `main()` can `try`.
*/
int runCommands(string[] args) {
// Print the version number and return
if (OPTS.ver) {
writefln("\033[1m%s\033[0m\n\n%s", VER_TEXT, COPY_TEXT);
return -1;
}

// Create missing folders if needed
createMissingFolders();

Expand All @@ -33,6 +38,12 @@ int runCommands(string[] args) {
return 0;
}

// Handle listing out orphans
if (OPTS.orphans) {
orphans();
return 0;
}

// Handle restoring trash files
if (OPTS.restore)
return restoreOrDel(OPTS.restore, false);
Expand Down
34 changes: 32 additions & 2 deletions source/tests.d
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
Integration tests for trash-d
*/

import run : OPTS, runCommands;
import cli : parseOpts;
import run : runCommands;
import cli : OPTS, parseOpts;
import trashfile : TrashFile;

import std.file;
Expand All @@ -27,6 +27,9 @@ int mini(string[] args) {
if (res != 0)
return res;

// Test for a nasty bug that came up
assert(!(OPTS.trash_dir is null));

// Enable verbosity and change the trash directory for testing
OPTS.verbose = true;
OPTS.trash_dir = test_trash_dir;
Expand Down Expand Up @@ -63,6 +66,10 @@ unittest {
parseOpts(args);
assert(OPTS.list);

args = t ~ ["--orphans"];
parseOpts(args);
assert(OPTS.orphans);

args = t ~ ["-f", "--empty"];
parseOpts(args);
assert(OPTS.force);
Expand Down Expand Up @@ -331,6 +338,28 @@ unittest {
test_trash_dir.rmdirRecurse();
}

/**
Test the orpahans command which lists files without trashinfos
*/
unittest {
// Run a command so that the trash directory is created
assert(mini(["--list"]) == 0);

const string testname = "test.file";
string testfile = OPTS.files_dir ~ "/" ~ testname;
testfile.write("hello");
scope (failure)
testfile.remove();
assert(testfile.exists());

assert(mini(["--orphans"]) == 0);
assert(testfile.exists());

// Cleanup
scope (success)
test_trash_dir.rmdirRecurse();
}

/**
Intentionally failing cases to ensure that these are properly handled and for
code coverage
Expand All @@ -350,6 +379,7 @@ unittest {
// Restoring a file that doesn't exist
assert(mini(["--restore", ne]) == 1);
// Deleting a file that doesn't exist
assert(mini(["--rm", ne]) == 1);

// Unknown options should just be ignored
assert(mini(["--unknown"]) == 0);
Expand Down
2 changes: 1 addition & 1 deletion source/trashfile.d
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
TrashFile structure which handles information related to a file in the trash
*/

import run : OPTS;
import cli : OPTS;
import util : log;

import core.sys.posix.sys.stat : S_IWUSR;
Expand Down
2 changes: 1 addition & 1 deletion source/util.d
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Helper utility functions to perform various common operations
*/

import run : OPTS;
import cli : OPTS;

import core.stdc.errno : EXDEV;
import std.stdio : stderr, stdin, writef;
Expand Down
2 changes: 1 addition & 1 deletion source/ver.d
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import std.format : format;

/// trash-d is versioned sequentially starting at 1
const int VER = 12;
const int VER = 13;

/// Ever major release is given a new name
/// Names are based on video game bosses
Expand Down

0 comments on commit 22fe8d7

Please sign in to comment.