123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- /*
- * 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), macOS, and Windows.
- *
- * Authors: Mio
- * Date: March 06, 2024
- * Homepage: https://codeberg.org/supercell/mlib
- * License: 0BSD
- * Standards: The FreeDesktop.org Trash Specification 1.0
- * Version: 0.4.0
- *
- * History:
- * 0.4.0 add support for macOS
- * 0.3.0 fix XDG naming convention bug
- * 0.2.0 added support for Windows
- * 0.1.0 is the initial version
- *
- * Macros:
- * DREF = <a href="https://dlang.org/phobos/$1.html#$2">$2</a>
- * LREF = <a href="#$1">$1</a>
- */
- 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 macOS or Windows.
- *
- * Throws:
- * - $(DREF std_file, FileException) if the file cannot be trashed.
- */
- void trash(string path, out string pathInTrash)
- {
- version (OSX) {
- _macos_trash(path);
- } else 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 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". */
- const attrs = getAttributes(path);
- if (false == ((S_IRUSR & attrs) && (S_IWUSR & attrs))) {
- throw new FileException(path, EACCES);
- }
- const pathDev = _posix_getDevice(path);
- const trashDev = _posix_getDevice(environment["HOME"]);
- // $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.");
- }
- const basename = baseName(path);
- const filename = stripExtension(basename);
- const 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");
- }
- }
- string getInfo(string src, string topdir) {
- import std.uri : encode;
- import std.datetime.systime : Clock;
- if (false == topdir.isParentOf(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;
- }
- bool isParentOf(string parent, string path) {
- import std.string : startsWith;
- path = path.absolutePath;
- parent = parent.absolutePath;
- return startsWith(path, parent);
- }
- } // 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)
- /*
- * Another disclaimer!
- *
- * I also don't partiularly use macOS, though I did manage
- * to purchase a third-hand macbook recently so I can test
- * my programs. Hopefully, it should work for a couple
- * more years.
- */
- version(OSX) {
- import core.attribute : selector;
- // For the macOS version we need to use the functions provided by
- // the Foundation framework as this will ask for permission to
- // access the trash directory (this would not work otherwise).
- //
- // Unforunately, this isn't just a simple function that one can
- // call.
- extern(Objective-C)
- extern class NSObject
- {
- void release() @selector("release");
- }
- extern(Objective-C)
- extern class NSError : NSObject
- {
- }
- extern(Objective-C)
- extern class NSString : NSObject
- {
- static NSString stringWith(const char*) @selector("stringWithUTF8String:");
- NSString stringByExpandingTilde() @selector("stringByExpandingTilde");
- }
- extern(Objective-C)
- extern class NSURL : NSObject
- {
- static NSURL fileURLWithPath(NSString path) @selector("fileURLWithPath:");
- }
- extern(Objective-C)
- extern class NSFileManager : NSObject
- {
- static NSFileManager defaultManager() @selector("defaultManager");
- bool trashItemAtURL(NSURL url, NSURL resultingItemURL, NSError error) @selector("trashItemAtURL:resultingItemURL:error:");
- }
- void _macos_trash(string path)
- {
- // We need to expand the tilde first, in D.
- string expandedPath = expandTilde(path);
- NSString nsPath = NSString.stringWith(expandedPath.ptr);
- scope(exit) nsPath.release();
- NSURL baseURL = NSURL.fileURLWithPath(nsPath);
- scope(exit) baseURL.release();
- NSFileManager manager = NSFileManager.defaultManager();
- scope(exit) manager.release();
- // While it's possible to use the resultingItemURL argument,
- // it doesn't seem to work correctly in D.
- // Changing the NSURL to NSURL* as the type works, but creates
- // some cruft after the path as there is (presumably) no
- // terminating zero.
- manager.trashItemAtURL(baseURL, null, null);
- }
- }
|