/* * 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)