trash.d 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. /*
  2. * Permission to use, copy, modify, and/or distribute this software for
  3. * any purpose with or without fee is hereby granted.
  4. *
  5. * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
  6. * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
  7. * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
  8. * FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
  9. * DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
  10. * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
  11. * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  12. */
  13. /**
  14. * Common 'Trash' operations for the OS's Recycle Bin.
  15. *
  16. * Supports POSIX (XDG Specification), macOS, and Windows.
  17. *
  18. * Authors: Mio
  19. * Date: March 06, 2024
  20. * Homepage: https://codeberg.org/supercell/mlib
  21. * License: 0BSD
  22. * Standards: The FreeDesktop.org Trash Specification 1.0
  23. * Version: 0.4.0
  24. *
  25. * History:
  26. * 0.4.0 add support for macOS
  27. * 0.3.0 fix XDG naming convention bug
  28. * 0.2.0 added support for Windows
  29. * 0.1.0 is the initial version
  30. *
  31. * Macros:
  32. * DREF = <a href="https://dlang.org/phobos/$1.html#$2">$2</a>
  33. * LREF = <a href="#$1">$1</a>
  34. */
  35. module mlib.trash;
  36. import core.stdc.errno;
  37. import std.file;
  38. import std.path;
  39. import std.process : environment;
  40. import std.stdio;
  41. /*
  42. * Permanetely delete all trashed records.
  43. *
  44. * This currently throws an Exception as it's not yet implemented.
  45. */
  46. // void emptyTrash()
  47. // {
  48. // throw new Exception(__PRETTY_FUNCTION__ ~ " not implemented");
  49. // }
  50. /*
  51. * Restore one (or all: "") trashed records
  52. *
  53. * Params:
  54. * pathInTrash = The unique filename in the trash directory to
  55. * restore. By not providing an argument (or by
  56. * passing `""`) this will restore _all_ files.
  57. *
  58. * Note: This currently throws an Exception as it's not yet
  59. * implemented.
  60. */
  61. // void restoreTrash(string pathInTrash = "")
  62. // {
  63. // throw new Exception(__PRETTY_FUNCTION__ ~ " not implemented");
  64. // }
  65. /*
  66. * List all the files and directories currently inside the trash.
  67. *
  68. * Returns: A list of strings containing every filename in the trash.
  69. *
  70. * Note: This currently throws an Exception as it's not yet
  71. * implemented.
  72. */
  73. // string[] listTrash()
  74. // {
  75. // throw new Exception(__PRETTY_FUNCTION__ ~ " not implemented");
  76. // }
  77. /**
  78. * Trash the file or directory at *path*.
  79. *
  80. * Params:
  81. * path = The path to move to the trash.
  82. *
  83. * Throws:
  84. * - $(DREF std_file, FileException) if the file cannot be trashed.
  85. */
  86. void trash(string path)
  87. {
  88. scope string pathInTrash;
  89. trash(path, pathInTrash);
  90. }
  91. ///
  92. unittest
  93. {
  94. import std.stdio : File;
  95. import std.exception : assertNotThrown;
  96. // Create a file with some basic text
  97. auto file = File("hello.txt", "w+");
  98. file.writeln("hello, world!");
  99. file.close();
  100. assertNotThrown!Exception(trash("hello.txt"));
  101. }
  102. /**
  103. * Trash the file or directory at *path*, and sets *pathInTrash* to the
  104. * path at which the file can be found within the trash.
  105. *
  106. * Params:
  107. * path = The path to move to the trash.
  108. * pathInTrash = The path at which the newly trashed item can be found.
  109. *
  110. * Bugs: The *pathInTrash* parameter isn't supported on macOS or Windows.
  111. *
  112. * Throws:
  113. * - $(DREF std_file, FileException) if the file cannot be trashed.
  114. */
  115. void trash(string path, out string pathInTrash)
  116. {
  117. version (OSX) {
  118. _macos_trash(path);
  119. } else version (Posix) {
  120. _posix_trash(path, pathInTrash);
  121. } else version (Windows) {
  122. _windows_trash(path);
  123. } else {
  124. throw new Exception(__PRETTY_FUNCTION__ ~ " is not supported on your OS");
  125. }
  126. }
  127. /**
  128. * Erase the file from the operating system.
  129. *
  130. * This skips the "trashing" operation and unlinks the file from the
  131. * system and recovers the space. Files which have been erased are
  132. * not recoverable.
  133. *
  134. * Throws:
  135. * - $(DREF std_file, FileException) if the file cannot be removed.
  136. */
  137. void erase(string path)
  138. {
  139. // Really just a convenience function.
  140. remove(path);
  141. }
  142. private:
  143. /*
  144. * System specific implementation of the above functions.
  145. */
  146. version(Posix) {
  147. import core.sys.posix.sys.stat;
  148. import std.conv : to;
  149. import std.string : toStringz;
  150. void _posix_trash(string path, out string pathInTrash) {
  151. if (false == exists(path)) {
  152. throw new FileException(path, ENOENT);
  153. }
  154. /* "When trashing a file or directory, the implementation SHOULD check
  155. * whether the user has the necessary permissions to delete it, before
  156. * starting the trashing operation itself". */
  157. const attrs = getAttributes(path);
  158. if (false == ((S_IRUSR & attrs) && (S_IWUSR & attrs))) {
  159. throw new FileException(path, EACCES);
  160. }
  161. const pathDev = _posix_getDevice(path);
  162. const trashDev = _posix_getDevice(environment["HOME"]);
  163. // $topdir
  164. string topdir;
  165. // $trash
  166. string trash;
  167. /* w.r.t. homeTrash:
  168. * "Files that the user trashes from the same file system (device/partition) SHOULD
  169. * be stored here ... If this directory is needed for a trashing operation but does
  170. * not exist, the implementation SHOULD automatically create it, without warnings
  171. * or delays. */
  172. if (pathDev == trashDev) {
  173. topdir = _xdg_datahome();
  174. trash = buildPath(topdir, "Trash");
  175. } else {
  176. /* "The implementation MAY also support trashing files from the rest of the
  177. * system (including other partitions, shared network resources, and removable
  178. * devices) into the "home trash" directory."
  179. *
  180. * I can only really test the partitions and removable devices, but I don't
  181. * have my desktop setup with multiple partitions. Will check with removable
  182. * devices, but want to see same file system usage work first. */
  183. throw new Exception("The device for the Trash directory and the device for the path are different.");
  184. }
  185. const basename = baseName(path);
  186. const filename = stripExtension(basename);
  187. const ext = extension(basename);
  188. // $trash/files
  189. string filesDir = buildPath(trash, "files");
  190. if (false == exists(filesDir)) {
  191. mkdirRecurse(filesDir);
  192. }
  193. // $trash/info
  194. string infoDir = buildPath(trash, "info");
  195. if (false == exists(infoDir)) {
  196. mkdirRecurse(infoDir);
  197. }
  198. /* "The names in [$trash/files and $trash/info] are to be determined by the
  199. * implementation; the only limitation is that they must be unique within the
  200. * directory. Even if a file with the same name and location gets trashed many times,
  201. * each subsequent trashing must not overwrite a previous copy." */
  202. size_t counter = 0;
  203. string filesFilename = basename;
  204. string infoFilename = filesFilename ~ ".trashinfo";
  205. while (exists(buildPath(filesDir, filesFilename)) || exists(buildPath(infoDir, infoFilename))) {
  206. counter += 1;
  207. filesFilename = basename ~ "_" ~ to!string(counter) ~ ext;
  208. infoFilename = filesFilename ~ ".trashinfo";
  209. }
  210. {
  211. /* "When trashing a file or directory, the implementation MUST create the
  212. * corresponding file in $trash/info first." */
  213. auto infoFile = File(buildPath(infoDir, infoFilename), "w");
  214. infoFile.write(getInfo(path, topdir));
  215. }
  216. {
  217. string filesPath = buildPath(filesDir, filesFilename);
  218. rename(path, filesPath);
  219. pathInTrash = filesPath;
  220. }
  221. /* TODO: Directory size cache */
  222. }
  223. ulong _posix_getDevice(string path) {
  224. stat_t statbuf;
  225. lstat(toStringz(path), &statbuf);
  226. return statbuf.st_dev;
  227. }
  228. string _xdg_datahome() {
  229. if ("XDG_DATA_HOME" in environment) {
  230. return environment["XDG_DATA_HOME"];
  231. } else {
  232. return buildPath(environment["HOME"], ".local", "share");
  233. }
  234. }
  235. string getInfo(string src, string topdir) {
  236. import std.uri : encode;
  237. import std.datetime.systime : Clock;
  238. if (false == topdir.isParentOf(src)) {
  239. src = src.absolutePath;
  240. } else {
  241. src = relativePath(src, topdir);
  242. }
  243. string info = "[Trash Info]\n";
  244. info ~= "Path=" ~ encode(src) ~ "\n";
  245. /*
  246. * Prior to D 2.099.0, the toISOExtString method didn't
  247. * have a precision argument, which means it includes
  248. * fractional seconds by default. So to accommodate
  249. * for earlier versions, just trim it off.
  250. */
  251. static if (__VERSION__ < 2099L) {
  252. import std.string : split;
  253. string dateTime = Clock.currTime.toISOExtString().split(".")[0];
  254. info ~= "DeletionDate=" ~ dateTime ~ "\n";
  255. } else {
  256. info ~= "DeletionDate=" ~ Clock.currTime.toISOExtString(0) ~ "\n";
  257. }
  258. return info;
  259. }
  260. bool isParentOf(string parent, string path) {
  261. import std.string : startsWith;
  262. path = path.absolutePath;
  263. parent = parent.absolutePath;
  264. return startsWith(path, parent);
  265. }
  266. } // End of version(Posix)
  267. /*
  268. * Disclaimer:
  269. *
  270. * I don't use Windows. As such, this may not be the _best_ way
  271. * to send a file to the recycle bin. In theory it shouldn't
  272. * break (given Windows' tendency for backwards support), but
  273. * if there is an error, you'll either have to let me know
  274. * or send a patch yourself.
  275. */
  276. version(Windows) {
  277. import core.sys.windows.windows;
  278. import std.utf : toUTF16z;
  279. // There doesn't seem to be a way to determine the path of a
  280. // file in the Recycle Bin.
  281. void _windows_trash(string path) {
  282. // If the path is not absolute, then it won't be recycled.
  283. string absPath = absolutePath(path);
  284. SHFILEOPSTRUCT fileOp = SHFILEOPSTRUCTW(null, FO_DELETE);
  285. /*
  286. * NOTE:
  287. * While toUTF16z appends a null character to the input string,
  288. * SHFILEOPSTRUCT treats pFrom (and pTo) as a list of strings
  289. * separated by a single '\0'. To specify the end of the list,
  290. * the string must end with double null terminator.
  291. *
  292. * See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shfileopstructa#remarks
  293. */
  294. fileOp.pFrom = toUTF16z(absPath ~ '\0');
  295. fileOp.pTo = null;
  296. fileOp.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT;
  297. fileOp.fAnyOperationsAborted = FALSE;
  298. fileOp.lpszProgressTitle = null;
  299. if (0 != SHFileOperation(&fileOp)) {
  300. throw new FileException(path, "File could not be deleted");
  301. }
  302. }
  303. } // End of version(Windows)
  304. /*
  305. * Another disclaimer!
  306. *
  307. * I also don't partiularly use macOS, though I did manage
  308. * to purchase a third-hand macbook recently so I can test
  309. * my programs. Hopefully, it should work for a couple
  310. * more years.
  311. */
  312. version(OSX) {
  313. import core.attribute : selector;
  314. // For the macOS version we need to use the functions provided by
  315. // the Foundation framework as this will ask for permission to
  316. // access the trash directory (this would not work otherwise).
  317. //
  318. // Unforunately, this isn't just a simple function that one can
  319. // call.
  320. extern(Objective-C)
  321. extern class NSObject
  322. {
  323. void release() @selector("release");
  324. }
  325. extern(Objective-C)
  326. extern class NSError : NSObject
  327. {
  328. }
  329. extern(Objective-C)
  330. extern class NSString : NSObject
  331. {
  332. static NSString stringWith(const char*) @selector("stringWithUTF8String:");
  333. NSString stringByExpandingTilde() @selector("stringByExpandingTilde");
  334. }
  335. extern(Objective-C)
  336. extern class NSURL : NSObject
  337. {
  338. static NSURL fileURLWithPath(NSString path) @selector("fileURLWithPath:");
  339. }
  340. extern(Objective-C)
  341. extern class NSFileManager : NSObject
  342. {
  343. static NSFileManager defaultManager() @selector("defaultManager");
  344. bool trashItemAtURL(NSURL url, NSURL resultingItemURL, NSError error) @selector("trashItemAtURL:resultingItemURL:error:");
  345. }
  346. void _macos_trash(string path)
  347. {
  348. // We need to expand the tilde first, in D.
  349. string expandedPath = expandTilde(path);
  350. NSString nsPath = NSString.stringWith(expandedPath.ptr);
  351. scope(exit) nsPath.release();
  352. NSURL baseURL = NSURL.fileURLWithPath(nsPath);
  353. scope(exit) baseURL.release();
  354. NSFileManager manager = NSFileManager.defaultManager();
  355. scope(exit) manager.release();
  356. // While it's possible to use the resultingItemURL argument,
  357. // it doesn't seem to work correctly in D.
  358. // Changing the NSURL to NSURL* as the type works, but creates
  359. // some cruft after the path as there is (presumably) no
  360. // terminating zero.
  361. manager.trashItemAtURL(baseURL, null, null);
  362. }
  363. }