/*
* Permission to use, copy, modify, and/or distribute this software for
* any purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
* WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
* FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
* DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
* OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/**
* Common 'Trash' operations for the OS's Recycle Bin.
*
* Supports POSIX (XDG Specification) and Windows. Proper support for
* macOS will be implemented in a future version.
*
* Authors: nemophila
* Date: April 05, 2023
* Homepage: https://osdn.net/users/nemophila/pf/mlib
* License: 0BSD
* Standards: The FreeDesktop.org Trash Specification 1.0
* Version: 0.3.0
*
* History:
* 0.3.0 fix XDG naming convention bug
* 0.2.0 added support for Windows
* 0.1.0 is the initial version
*
* Macros:
* DREF = $2
* LREF = $1
*/
module mlib.trash;
import core.stdc.errno;
import std.file;
import std.path;
import std.process : environment;
import std.stdio;
/*
* Permanetely delete all trashed records.
*
* This currently throws an Exception as it's not yet implemented.
*/
// void emptyTrash()
// {
// throw new Exception(__PRETTY_FUNCTION__ ~ " not implemented");
// }
/*
* Restore one (or all: "") trashed records
*
* Params:
* pathInTrash = The unique filename in the trash directory to
* restore. By not providing an argument (or by
* passing `""`) this will restore _all_ files.
*
* Note: This currently throws an Exception as it's not yet
* implemented.
*/
// void restoreTrash(string pathInTrash = "")
// {
// throw new Exception(__PRETTY_FUNCTION__ ~ " not implemented");
// }
/*
* List all the files and directories currently inside the trash.
*
* Returns: A list of strings containing every filename in the trash.
*
* Note: This currently throws an Exception as it's not yet
* implemented.
*/
// string[] listTrash()
// {
// throw new Exception(__PRETTY_FUNCTION__ ~ " not implemented");
// }
/**
* Trash the file or directory at *path*.
*
* Params:
* path = The path to move to the trash.
*
* Throws:
* - $(DREF std_file, FileException) if the file cannot be trashed.
*/
void trash(string path)
{
scope string pathInTrash;
trash(path, pathInTrash);
}
///
unittest
{
import std.stdio : File;
import std.exception : assertNotThrown;
// Create a file with some basic text
auto file = File("hello.txt", "w+");
file.writeln("hello, world!");
file.close();
assertNotThrown!Exception(trash("hello.txt"));
}
/**
* Trash the file or directory at *path*, and sets *pathInTrash* to the
* path at which the file can be found within the trash.
*
* Params:
* path = The path to move to the trash.
* pathInTrash = The path at which the newly trashed item can be found.
*
* Bugs: The *pathInTrash* parameter isn't supported on Windows.
*
* Throws:
* - $(DREF std_file, FileException) if the file cannot be trashed.
*/
void trash(string path, out string pathInTrash)
{
version (Posix) {
_posix_trash(path, pathInTrash);
} else version (Windows) {
_windows_trash(path);
} else {
throw new Exception(__PRETTY_FUNCTION__ ~ " is not supported on your OS");
}
}
/**
* Erase the file from the operating system.
*
* This skips the "trashing" operation and unlinks the file from the
* system and recovers the space. Files which have been erased are
* not recoverable.
*
* Throws:
* - $(DREF std_file, FileException) if the file cannot be removed.
*/
void erase(string path)
{
// Really just a convenience function.
remove(path);
}
private:
/*
* System independant functions.
* These will call the system specific function.
*/
ulong getDevice(string path) {
version (Posix) {
return _posix_getDevice(path);
} else {
// Not used on Windows
return 0;
}
}
string getHomeDirectory() {
version (Posix) {
return environment["HOME"];
} else {
// Not used on Windows
return "";
}
}
bool isParent(string parent, string path) {
import std.string : startsWith;
path = path.absolutePath;
parent = parent.absolutePath;
return startsWith(path, parent);
}
string getInfo(string src, string topdir) {
import std.uri : encode;
import std.datetime.systime : Clock;
if (false == isParent(topdir, src)) {
src = src.absolutePath;
} else {
src = relativePath(src, topdir);
}
string info = "[Trash Info]\n";
info ~= "Path=" ~ encode(src) ~ "\n";
/*
* Prior to D 2.099.0, the toISOExtString method didn't
* have a precision argument, which means it includes
* fractional seconds by default. So to accommodate
* for earlier versions, just trim it off.
*/
static if (__VERSION__ < 2099L) {
import std.string : split;
string dateTime = Clock.currTime.toISOExtString().split(".")[0];
info ~= "DeletionDate=" ~ dateTime ~ "\n";
} else {
info ~= "DeletionDate=" ~ Clock.currTime.toISOExtString(0) ~ "\n";
}
return info;
}
/*
* System specific implementation of the above functions.
*/
version(Posix) {
import core.sys.posix.sys.stat;
import std.conv : to;
import std.string : toStringz;
void _posix_trash(string path, out string pathInTrash) {
if (false == exists(path)) {
throw new FileException(path, ENOENT);
}
/* "When trashing a file or directory, the implementation SHOULD check
* whether the user has the necessary permissions to delete it, before
* starting the trashing operation itself". */
uint attrs = getAttributes(path);
if (false == ((S_IRUSR & attrs) && (S_IWUSR & attrs))) {
throw new FileException(path, EACCES);
}
ulong pathDev = getDevice(path);
ulong trashDev = getDevice(getHomeDirectory());
// $topdir
string topdir;
// $trash
string trash;
/* w.r.t. homeTrash:
* "Files that the user trashes from the same file system (device/partition) SHOULD
* be stored here ... If this directory is needed for a trashing operation but does
* not exist, the implementation SHOULD automatically create it, without warnings
* or delays. */
if (pathDev == trashDev) {
topdir = _xdg_datahome();
trash = buildPath(topdir, "Trash");
} else {
/* "The implementation MAY also support trashing files from the rest of the
* system (including other partitions, shared network resources, and removable
* devices) into the "home trash" directory."
*
* I can only really test the partitions and removable devices, but I don't
* have my desktop setup with multiple partitions. Will check with removable
* devices, but want to see same file system usage work first. */
throw new Exception("The device for the Trash directory and the device for the path are different.");
}
string basename = baseName(path);
string filename = stripExtension(basename);
string ext = extension(basename);
// $trash/files
string filesDir = buildPath(trash, "files");
if (false == exists(filesDir)) {
mkdirRecurse(filesDir);
}
// $trash/info
string infoDir = buildPath(trash, "info");
if (false == exists(infoDir)) {
mkdirRecurse(infoDir);
}
/* "The names in [$trash/files and $trash/info] are to be determined by the
* implementation; the only limitation is that they must be unique within the
* directory. Even if a file with the same name and location gets trashed many times,
* each subsequent trashing must not overwrite a previous copy." */
size_t counter = 0;
string filesFilename = basename;
string infoFilename = filesFilename ~ ".trashinfo";
while (exists(buildPath(filesDir, filesFilename)) || exists(buildPath(infoDir, infoFilename))) {
counter += 1;
filesFilename = basename ~ "_" ~ to!string(counter) ~ ext;
infoFilename = filesFilename ~ ".trashinfo";
}
{
/* "When trashing a file or directory, the implementation MUST create the
* corresponding file in $trash/info first." */
auto infoFile = File(buildPath(infoDir, infoFilename), "w");
infoFile.write(getInfo(path, topdir));
}
{
string filesPath = buildPath(filesDir, filesFilename);
rename(path, filesPath);
pathInTrash = filesPath;
}
/* TODO: Directory size cache */
}
ulong _posix_getDevice(string path) {
stat_t statbuf;
lstat(toStringz(path), &statbuf);
return statbuf.st_dev;
}
string _xdg_datahome()
{
if ("XDG_DATA_HOME" in environment) {
return environment["XDG_DATA_HOME"];
} else {
return buildPath(environment["HOME"], ".local", "share");
}
}
} // End of version(Posix)
/*
* Disclaimer:
*
* I don't use Windows. As such, this may not be the _best_ way
* to send a file to the recycle bin. In theory it shouldn't
* break (given Windows' tendency for backwards support), but
* if there is an error, you'll either have to let me know
* or send a patch yourself.
*/
version(Windows) {
import core.sys.windows.windows;
import std.utf : toUTF16z;
// There doesn't seem to be a way to determine the path of a
// file in the Recycle Bin.
void _windows_trash(string path) {
// If the path is not absolute, then it won't be recycled.
string absPath = absolutePath(path);
SHFILEOPSTRUCT fileOp = SHFILEOPSTRUCTW(null, FO_DELETE);
/*
* NOTE:
* While toUTF16z appends a null character to the input string,
* SHFILEOPSTRUCT treats pFrom (and pTo) as a list of strings
* separated by a single '\0'. To specify the end of the list,
* the string must end with double null terminator.
*
* See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shfileopstructa#remarks
*/
fileOp.pFrom = toUTF16z(absPath ~ '\0');
fileOp.pTo = null;
fileOp.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT;
fileOp.fAnyOperationsAborted = FALSE;
fileOp.lpszProgressTitle = null;
if (0 != SHFileOperation(&fileOp)) {
throw new FileException(path, "File could not be deleted");
}
}
} // End of version(Windows)