1 /** 2 * Moving files and directories to trash can. 3 * Copyright: 4 * Roman Chistokhodov, 2016 5 * License: 6 * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 7 */ 8 9 module trashcan; 10 11 import std.path; 12 import std.string; 13 import std.file; 14 import std.exception; 15 import std.range : InputRange, inputRangeObject; 16 import std.datetime; 17 18 import isfreedesktop; 19 20 static if (isFreedesktop) { 21 import std.uri : encode, decode; 22 import volumeinfo; 23 import xdgpaths : xdgDataHome, xdgAllDataDirs; 24 } 25 26 /** 27 * Flags to rule the trashing behavior. 28 * 29 * $(BLUE Valid only for freedesktop environments). 30 * 31 * See_Also: $(LINK2 https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html, Trash specification). 32 */ 33 enum TrashOptions : int 34 { 35 /** 36 * No options. Just move file to user home trash directory 37 * not paying attention to partition where file resides. 38 */ 39 none = 0, 40 /** 41 * If file that needs to be deleted resides on non-home partition 42 * and top trash directory ($topdir/.Trash/$uid) failed some check, 43 * fallback to user top trash directory ($topdir/.Trash-$uid). 44 * 45 * Makes sense only in conjunction with $(D useTopDirs). 46 */ 47 fallbackToUserDir = 1, 48 /** 49 * If file that needs to be deleted resides on non-home partition 50 * and checks for top trash directories failed, 51 * fallback to home trash directory. 52 * 53 * Makes sense only in conjunction with $(D useTopDirs). 54 */ 55 fallbackToHomeDir = 2, 56 57 /** 58 * Whether to use top trash directories at all. 59 * 60 * If no $(D fallbackToUserDir) nor $(D fallbackToHomeDir) flags are set, 61 * and file that needs to be deleted resides on non-home partition, 62 * and top trash directory ($topdir/.Trash/$uid) failed some check, 63 * exception will be thrown. This can be used to report errors to administrator or user. 64 */ 65 useTopDirs = 4, 66 67 /** 68 * Whether to check presence of 'sticky bit' on $topdir/.Trash directory. 69 * 70 * Makes sense only in conjunction with $(D useTopDirs). 71 */ 72 checkStickyBit = 8, 73 74 /** 75 * All flags set. 76 */ 77 all = (TrashOptions.fallbackToUserDir | TrashOptions.fallbackToHomeDir | TrashOptions.checkStickyBit | TrashOptions.useTopDirs) 78 } 79 80 static if (isFreedesktop) 81 { 82 private: 83 import std.format : format; 84 85 @trusted string numberedBaseName(scope string path, uint number) { 86 return format("%s %s%s", path.baseName.stripExtension, number, path.extension); 87 } 88 89 unittest 90 { 91 assert(numberedBaseName("/root/file.ext", 1) == "file 1.ext"); 92 assert(numberedBaseName("/root/file", 2) == "file 2"); 93 } 94 95 @trusted string ensureDirExists(scope string dir) { 96 std.file.mkdirRecurse(dir); 97 return dir; 98 } 99 100 import core.sys.posix.sys.types; 101 import core.sys.posix.sys.stat; 102 import core.sys.posix.unistd; 103 import core.sys.posix.fcntl; 104 105 @trusted string topDir(string path) 106 in { 107 assert(path.isAbsolute); 108 } 109 do { 110 return volumePath(path); 111 } 112 113 void checkDiskTrashMode(mode_t mode, const bool checkStickyBit = true) 114 { 115 enforce(!S_ISLNK(mode), "Top trash directory is a symbolic link"); 116 enforce(S_ISDIR(mode), "Top trash path is not a directory"); 117 if (checkStickyBit) { 118 enforce((mode & S_ISVTX) != 0, "Top trash directory does not have sticky bit"); 119 } 120 } 121 122 unittest 123 { 124 assertThrown(checkDiskTrashMode(S_IFLNK|S_ISVTX)); 125 assertThrown(checkDiskTrashMode(S_IFDIR)); 126 assertNotThrown(checkDiskTrashMode(S_IFDIR|S_ISVTX)); 127 assertNotThrown(checkDiskTrashMode(S_IFDIR, false)); 128 } 129 130 @trusted string checkDiskTrash(scope string topdir, const bool checkStickyBit = true) 131 in { 132 assert(topdir.length); 133 } 134 body { 135 string trashDir = buildPath(topdir, ".Trash"); 136 stat_t trashStat; 137 enforce(lstat(trashDir.toStringz, &trashStat) == 0, "Top trash directory does not exist"); 138 checkDiskTrashMode(trashStat.st_mode, checkStickyBit); 139 return trashDir; 140 } 141 142 @safe string userTrashSubdir(scope string trashDir, uid_t uid) { 143 import std.conv : to; 144 return buildPath(trashDir, uid.to!string); 145 } 146 147 unittest 148 { 149 assert(userTrashSubdir("/.Trash", 600) == buildPath("/.Trash", "600")); 150 } 151 152 @trusted string ensureUserTrashSubdir(scope string trashDir) 153 { 154 return userTrashSubdir(trashDir, getuid()).ensureDirExists(); 155 } 156 157 @safe string userTrashDir(string topdir, uid_t uid) { 158 return buildPath(topdir, format(".Trash-%s", uid)); 159 } 160 161 unittest 162 { 163 assert(userTrashDir("/topdir", 700) == buildPath("/topdir", ".Trash-700")); 164 } 165 166 @trusted string ensureUserTrashDir(scope string topdir) 167 { 168 return userTrashDir(topdir, getuid()).ensureDirExists(); 169 } 170 } 171 172 version(OSX) 173 { 174 private: 175 import core.sys.posix.dlfcn; 176 177 struct FSRef { 178 char[80] hidden; 179 }; 180 181 alias ubyte Boolean; 182 alias int OSStatus; 183 alias uint OptionBits; 184 185 extern(C) @nogc @system OSStatus _dummy_FSPathMakeRefWithOptions(const(char)* path, OptionBits, FSRef*, Boolean*) nothrow {return 0;} 186 extern(C) @nogc @system OSStatus _dummy_FSMoveObjectToTrashSync(const(FSRef)*, FSRef*, OptionBits) nothrow {return 0;} 187 } 188 189 /** 190 * Move file or directory to trash can. 191 * Params: 192 * path = Path of item to remove. Must be absolute. 193 * options = Control behavior of trashing on freedesktop environments. 194 * Throws: 195 * $(B Exception) when given path is not absolute or does not exist, or some error occured during operation, 196 * or the operation is not supported on the current platform. 197 */ 198 @trusted void moveToTrash(scope string path, TrashOptions options = TrashOptions.all) 199 { 200 if (!path.isAbsolute) { 201 throw new Exception("Path must be absolute"); 202 } 203 if (!path.exists) { 204 throw new Exception("Path does not exist"); 205 } 206 207 version(Windows) { 208 import core.sys.windows.shellapi; 209 import core.sys.windows.winbase; 210 import core.stdc.wchar_; 211 import std.windows.syserror; 212 import std.utf; 213 214 SHFILEOPSTRUCTW fileOp; 215 fileOp.wFunc = FO_DELETE; 216 fileOp.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NOCONFIRMMKDIR | FOF_ALLOWUNDO; 217 auto wFileName = (path ~ "\0\0").toUTF16(); 218 fileOp.pFrom = wFileName.ptr; 219 int r = SHFileOperation(&fileOp); 220 if (r != 0) { 221 import std.format; 222 throw new Exception(format("SHFileOperation failed with error code %d", r)); 223 } 224 } else version(OSX) { 225 void* handle = dlopen("CoreServices.framework/Versions/A/CoreServices", RTLD_NOW | RTLD_LOCAL); 226 if (handle !is null) { 227 scope(exit) dlclose(handle); 228 229 auto ptrFSPathMakeRefWithOptions = cast(typeof(&_dummy_FSPathMakeRefWithOptions))dlsym(handle, "FSPathMakeRefWithOptions"); 230 if (ptrFSPathMakeRefWithOptions is null) { 231 throw new Exception(fromStringz(dlerror()).idup); 232 } 233 234 auto ptrFSMoveObjectToTrashSync = cast(typeof(&_dummy_FSMoveObjectToTrashSync))dlsym(handle, "FSMoveObjectToTrashSync"); 235 if (ptrFSMoveObjectToTrashSync is null) { 236 throw new Exception(fromStringz(dlerror()).idup); 237 } 238 239 FSRef source; 240 enforce(ptrFSPathMakeRefWithOptions(toStringz(path), 1, &source, null) == 0, "Could not make FSRef from path"); 241 FSRef target; 242 enforce(ptrFSMoveObjectToTrashSync(&source, &target, 0) == 0, "Could not move path to trash"); 243 } else { 244 throw new Exception(fromStringz(dlerror()).idup); 245 } 246 } else { 247 static if (isFreedesktop) { 248 string dataPath = xdgDataHome(null, true); 249 if (!dataPath.length) { 250 throw new Exception("Could not access data folder"); 251 } 252 dataPath = dataPath.absolutePath; 253 254 string trashBasePath; 255 bool usingTopdir = false; 256 string fileTopDir; 257 258 if ((options & TrashOptions.useTopDirs) != 0) { 259 string dataTopDir = topDir(dataPath); 260 fileTopDir = topDir(path); 261 262 enforce(fileTopDir.length, "Could not get topdir of file being trashed"); 263 enforce(dataTopDir.length, "Could not get topdir of home data directory"); 264 265 if (dataTopDir != fileTopDir) { 266 try { 267 string diskTrash = checkDiskTrash(fileTopDir, (options & TrashOptions.checkStickyBit) != 0); 268 trashBasePath = ensureUserTrashSubdir(diskTrash); 269 usingTopdir = true; 270 } catch(Exception e) { 271 try { 272 if ((options & TrashOptions.fallbackToUserDir) != 0) { 273 trashBasePath = ensureUserTrashDir(fileTopDir); 274 usingTopdir = true; 275 } else { 276 throw e; 277 } 278 } catch(Exception e) { 279 if (!(options & TrashOptions.fallbackToHomeDir)) { 280 throw e; 281 } 282 } 283 } 284 } 285 } 286 287 if (trashBasePath is null) { 288 trashBasePath = ensureDirExists(buildPath(dataPath, "Trash")); 289 } 290 enforce(trashBasePath.length, "Could not access base trash folder"); 291 292 string trashInfoDir = ensureDirExists(buildPath(trashBasePath, "info")); 293 string trashFilePathsDir = ensureDirExists(buildPath(trashBasePath, "files")); 294 295 string trashInfoPath = buildPath(trashInfoDir, path.baseName ~ ".trashinfo"); 296 string trashFilePath = buildPath(trashFilePathsDir, path.baseName); 297 298 import std.conv : octal; 299 import core.stdc.errno; 300 301 auto currentTime = Clock.currTime; 302 currentTime.fracSecs = Duration.zero; 303 string timeString = currentTime.toISOExtString(); 304 string contents = format("[Trash Info]\nPath=%s\nDeletionDate=%s\n", (usingTopdir ? path.relativePath(fileTopDir) : path).encode(), timeString); 305 306 const mode = O_CREAT | O_WRONLY | O_EXCL; 307 int fd; 308 uint number = 1; 309 while(trashFilePath.exists || ((fd = .open(toStringz(trashInfoPath), mode, octal!666)) == -1 && errno == EEXIST)) { 310 string baseName = numberedBaseName(path, number); 311 trashFilePath = buildPath(trashFilePathsDir, baseName); 312 trashInfoPath = buildPath(trashInfoDir, baseName ~ ".trashinfo"); 313 number++; 314 } 315 errnoEnforce(fd != -1); 316 scope(exit) .close(fd); 317 errnoEnforce(write(fd, contents.ptr, contents.length) == contents.length); 318 319 path.rename(trashFilePath); 320 } else { 321 throw new Exception("Trashing operation is not implemented on this platform"); 322 } 323 } 324 } 325 326 unittest 327 { 328 assertThrown(moveToTrash("notabsolute")); 329 } 330 331 version(Windows) 332 { 333 import std.typecons : RefCounted, refCounted, RefCountedAutoInitialize; 334 import std.windows.syserror : WindowsException; 335 336 import core.sys.windows.windows; 337 import core.sys.windows.shlobj; 338 import core.sys.windows.shlwapi; 339 import core.sys.windows.wtypes; 340 import core.sys.windows.oaidl; 341 import core.sys.windows.objidl; 342 343 pragma(lib, "Ole32"); 344 pragma(lib, "OleAut32"); 345 } 346 347 version(Windows) private struct ItemIdList 348 { 349 @disable this(this); 350 this(LPITEMIDLIST pidl) { 351 this.pidl = pidl; 352 } 353 alias pidl this; 354 LPITEMIDLIST pidl; 355 ~this() { 356 if (pidl) { 357 CoTaskMemFree(pidl); 358 pidl = null; 359 } 360 } 361 } 362 363 /// Item (file or folder) stored in the trashcan. 364 struct TrashcanItem 365 { 366 version(Windows) private @trusted this(string restorePath, bool isDir, ref scope const SysTime deletionTime, LPITEMIDLIST pidl) { 367 _restorePath = restorePath; 368 _isDir = isDir; 369 _deletionTime = deletionTime; 370 this.pidl = refCounted(ItemIdList(pidl)); 371 } 372 static if (isFreedesktop) { 373 private @trusted this(string restorePath, bool isDir, ref scope const SysTime deletionTime, string trashInfoPath, string trashedPath) { 374 assert(trashInfoPath.length != 0); 375 assert(trashedPath.length != 0); 376 _restorePath = restorePath; 377 _isDir = isDir; 378 _deletionTime = deletionTime; 379 _trashInfoPath = trashInfoPath; 380 _trashedPath = trashedPath; 381 } 382 } 383 /// Original location of the item (absolute path) before it was moved to trashcan. 384 @safe @property @nogc nothrow pure string restorePath() const { 385 return _restorePath; 386 } 387 /// Whether the item is directory. 388 @safe @property @nogc nothrow pure bool isDir() const { 389 return _isDir; 390 } 391 /// The time when the item was moved to trashcan. 392 @safe @property @nogc nothrow pure SysTime deletionTime() const { 393 return _deletionTime; 394 } 395 version(D_Ddoc) { 396 static if (!is(typeof(LPITEMIDLIST.init))) 397 { 398 static struct LPITEMIDLIST {} 399 } 400 /** 401 * Windows-specific function to get LPITEMIDLIST associated with item. 402 * 403 * Note: 404 * The returned object must not outlive this TrashcanItem (or its copies). If you want to keep this object around use $(LINK2 https://msdn.microsoft.com/en-us/library/windows/desktop/bb776433(v=vs.85).aspx, ILClone). Don't forget to call ILFree or CoTaskMemFree, when it's no longer needed. 405 */ 406 @system @property @nogc nothrow LPITEMIDLIST itemIdList() {return LPITEMIDLIST.init;} 407 /** 408 * Freedesktop-specific function to get .trashinfo file path. 409 */ 410 @property @nogc nothrow string trashInfoPath() const {return string.init;} 411 /** 412 * Freedesktop-specific function to get the path where the trashed file or directory is located. 413 */ 414 @property @nogc nothrow string trashedPath() const {return string.init;} 415 } else version(Windows) { 416 @system @property @nogc nothrow LPITEMIDLIST itemIdList() { 417 if (pidl.refCountedStore.isInitialized) 418 return pidl.refCountedPayload.pidl; 419 return null; 420 } 421 } else static if (isFreedesktop) { 422 @safe @property @nogc nothrow string trashInfoPath() const { 423 return _trashInfoPath; 424 } 425 @safe @property @nogc nothrow string trashedPath() const { 426 return _trashedPath; 427 } 428 } 429 private: 430 string _restorePath; 431 bool _isDir; 432 SysTime _deletionTime; 433 version(Windows) RefCounted!(ItemIdList, RefCountedAutoInitialize.no) pidl; 434 static if (isFreedesktop) { 435 string _trashInfoPath; 436 string _trashedPath; 437 } 438 } 439 440 version(Windows) private 441 { 442 // Redefine IShellFolder2 since it's bugged in druntime 443 interface IShellFolder2 : IShellFolder 444 { 445 HRESULT GetDefaultSearchGUID(GUID*); 446 HRESULT EnumSearches(IEnumExtraSearch*); 447 HRESULT GetDefaultColumn(DWORD, ULONG*, ULONG*); 448 HRESULT GetDefaultColumnState(UINT, SHCOLSTATEF*); 449 HRESULT GetDetailsEx(LPCITEMIDLIST, const(SHCOLUMNID)*, VARIANT*); 450 HRESULT GetDetailsOf(LPCITEMIDLIST, UINT, SHELLDETAILS*); 451 HRESULT MapColumnToSCID(UINT, SHCOLUMNID*); 452 } 453 454 // Define missing declarations 455 alias SICHINTF = DWORD; 456 457 enum SIGDN { 458 SIGDN_NORMALDISPLAY, 459 SIGDN_PARENTRELATIVEPARSING, 460 SIGDN_DESKTOPABSOLUTEPARSING, 461 SIGDN_PARENTRELATIVEEDITING, 462 SIGDN_DESKTOPABSOLUTEEDITING, 463 SIGDN_FILESYSPATH, 464 SIGDN_URL, 465 SIGDN_PARENTRELATIVEFORADDRESSBAR, 466 SIGDN_PARENTRELATIVE, 467 SIGDN_PARENTRELATIVEFORUI 468 }; 469 470 interface IShellItem : IUnknown { 471 HRESULT BindToHandler(IBindCtx pbc, REFGUID bhid, REFIID riid, void **ppv); 472 HRESULT GetParent(IShellItem *ppsi); 473 HRESULT GetDisplayName(SIGDN sigdnName, LPWSTR *ppszName); 474 HRESULT GetAttributes(SFGAOF sfgaoMask, SFGAOF *psfgaoAttribs); 475 HRESULT Compare(IShellItem psi, SICHINTF hint, int *piOrder); 476 } 477 478 extern(Windows) HRESULT SHCreateShellItem(LPCITEMIDLIST pidlParent, IShellFolder psfParent, LPCITEMIDLIST pidl, IShellItem *ppsi) nothrow @nogc; 479 extern(Windows) LPITEMIDLIST ILCreateFromPath(PCTSTR pszPath); 480 481 alias IFileOperationProgressSink = IUnknown; 482 alias IOperationsProgressDialog = IUnknown; 483 alias IPropertyChangeArray = IUnknown; 484 485 immutable CLSID CLSID_FileOperation = {0x3ad05575,0x8857,0x4850,[0x92,0x77,0x11,0xb8,0x5b,0xdb,0x8e,0x9]}; 486 immutable IID IID_IFileOperation = {0x947aab5f,0xa5c,0x4c13,[0xb4,0xd6,0x4b,0xf7,0x83,0x6f,0xc9,0xf8]}; 487 488 interface IFileOperation : IUnknown 489 { 490 HRESULT Advise(IFileOperationProgressSink pfops, DWORD *pdwCookie); 491 HRESULT Unadvise(DWORD dwCookie); 492 HRESULT SetOperationFlags(DWORD dwOperationFlags); 493 HRESULT SetProgressMessage(LPCWSTR pszMessage); 494 HRESULT SetProgressDialog(IOperationsProgressDialog popd); 495 HRESULT SetProperties (IPropertyChangeArray pproparray); 496 HRESULT SetOwnerWindow(HWND hwndOwner); 497 HRESULT ApplyPropertiesToItem(IShellItem psiItem); 498 HRESULT ApplyPropertiesToItems (IUnknown punkItems); 499 HRESULT RenameItem(IShellItem psiItem, LPCWSTR pszNewName, IFileOperationProgressSink pfopsItem); 500 HRESULT RenameItems(IUnknown pUnkItems, LPCWSTR pszNewName); 501 HRESULT MoveItem(IShellItem psiItem, IShellItem psiDestinationFolder, LPCWSTR pszNewName, IFileOperationProgressSink pfopsItem); 502 HRESULT MoveItems(IUnknown punkItems, IShellItem psiDestinationFolder); 503 HRESULT CopyItem(IShellItem psiItem, IShellItem psiDestinationFolder, LPCWSTR pszCopyName, IFileOperationProgressSink pfopsItem); 504 HRESULT CopyItems(IUnknown punkItems, IShellItem psiDestinationFolder); 505 HRESULT DeleteItem(IShellItem psiItem, IFileOperationProgressSink pfopsItem); 506 HRESULT DeleteItems(IUnknown punkItems); 507 HRESULT NewItem(IShellItem psiDestinationFolder, DWORD dwFileAttributes, LPCWSTR pszName, LPCWSTR pszTemplateName, IFileOperationProgressSink pfopsItem); 508 HRESULT PerformOperations(); 509 HRESULT GetAnyOperationsAborted(BOOL *pfAnyOperationsAborted); 510 } 511 512 static @trusted string StrRetToString(ref scope STRRET strRet, LPITEMIDLIST pidl) 513 { 514 import std.string : fromStringz; 515 switch (strRet.uType) 516 { 517 case STRRET_CSTR: 518 return fromStringz(strRet.cStr.ptr).idup; 519 case STRRET_OFFSET: 520 return string.init; 521 case STRRET_WSTR: 522 char[MAX_PATH] szTemp; 523 auto len = WideCharToMultiByte (CP_UTF8, 0, strRet.pOleStr, -1, szTemp.ptr, szTemp.sizeof, null, null); 524 scope(exit) CoTaskMemFree(strRet.pOleStr); 525 if (len) 526 return szTemp[0..len-1].idup; 527 else 528 return string.init; 529 default: 530 return string.init; 531 } 532 } 533 534 static @trusted wstring StrRetToWString(ref scope STRRET strRet, LPITEMIDLIST pidl) 535 { 536 switch (strRet.uType) 537 { 538 case STRRET_CSTR: 539 { 540 char[] cstr = fromStringz(strRet.cStr.ptr); 541 wchar[] toReturn; 542 toReturn.reserve(cstr.length); 543 foreach(char c; cstr) 544 toReturn ~= cast(wchar)c; 545 return assumeUnique(toReturn); 546 } 547 case STRRET_WSTR: 548 scope(exit) CoTaskMemFree(strRet.pOleStr); 549 return strRet.pOleStr[0..lstrlenW(strRet.pOleStr)].idup; 550 default: 551 return wstring.init; 552 } 553 } 554 555 static @trusted SysTime StrRetToSysTime(ref scope STRRET strRet, LPITEMIDLIST pidl) 556 { 557 auto str = StrRetToWString(strRet, pidl); 558 if (str.length) { 559 wchar[] temp; 560 temp.reserve(str.length + 1); 561 foreach(wchar c; str) { 562 if (c != '\u200E' && c != '\u200F') 563 temp ~= c; 564 } 565 temp ~= '\0'; 566 DATE date; 567 if(SUCCEEDED(VarDateFromStr(temp.ptr, LOCALE_USER_DEFAULT, 0, &date))) 568 { 569 SYSTEMTIME sysTime; 570 if (VariantTimeToSystemTime(date, &sysTime)) 571 return SYSTEMTIMEToSysTime(&sysTime); 572 } 573 } 574 return SysTime.init; 575 } 576 577 @trusted static void henforce(HRESULT hres, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) 578 { 579 if (FAILED(hres)) 580 throw new WindowsException(hres, msg, file, line); 581 } 582 583 @trusted static string getDisplayNameOf(IShellFolder folder, LPITEMIDLIST pidl) 584 in { 585 assert(folder); 586 assert(pidl); 587 } 588 do { 589 STRRET strRet; 590 if (SUCCEEDED(folder.GetDisplayNameOf(pidl, SHGNO.SHGDN_NORMAL, &strRet))) 591 return StrRetToString(strRet, pidl); 592 return string.init; 593 } 594 595 @trusted static string getStringDetailOf(IShellFolder2 folder, LPITEMIDLIST pidl, uint index) 596 in { 597 assert(folder); 598 assert(pidl); 599 } 600 do { 601 SHELLDETAILS details; 602 if(SUCCEEDED(folder.GetDetailsOf(pidl, index, &details))) 603 return StrRetToString(details.str, pidl); 604 return string.init; 605 } 606 607 @trusted static wstring getWStringDetailOf(IShellFolder2 folder, LPITEMIDLIST pidl, uint index) 608 in { 609 assert(folder); 610 assert(pidl); 611 } 612 do { 613 SHELLDETAILS details; 614 if(SUCCEEDED(folder.GetDetailsOf(pidl, index, &details))) 615 return StrRetToWString(details.str, pidl); 616 return wstring.init; 617 } 618 619 @trusted static SysTime getSysTimeDetailOf(IShellFolder2 folder, LPITEMIDLIST pidl, uint index) 620 in { 621 assert(folder); 622 assert(pidl); 623 } 624 do { 625 SHELLDETAILS details; 626 if(SUCCEEDED(folder.GetDetailsOf(pidl, index, &details))) 627 return StrRetToSysTime(details.str, pidl); 628 return SysTime.init; 629 } 630 631 @trusted static void RunVerb(string verb)(IShellFolder folder, LPITEMIDLIST pidl) 632 in { 633 assert(folder); 634 } 635 do { 636 enforce(pidl !is null, "Empty trashcan item, can't run an operation"); 637 IContextMenu contextMenu; 638 henforce(folder.GetUIObjectOf(null, 1, cast(LPCITEMIDLIST*)(&pidl), &IID_IContextMenu, null, cast(LPVOID *)&contextMenu), "Failed to get context menu ui object"); 639 assert(pidl); 640 assert(contextMenu); 641 scope(exit) contextMenu.Release(); 642 CMINVOKECOMMANDINFO ci; 643 ci.fMask = CMIC_MASK_FLAG_NO_UI; 644 ci.cbSize = CMINVOKECOMMANDINFO.sizeof; 645 ci.lpVerb = verb; 646 henforce(contextMenu.InvokeCommand(&ci), "Failed to " ~ verb ~ " item"); 647 } 648 649 @trusted static IFileOperation CreateFileOperation() 650 { 651 IFileOperation op; 652 henforce(CoCreateInstance(&CLSID_FileOperation, null, CLSCTX_ALL, &IID_IFileOperation, cast(void**)&op), "Failed to create instance of IFileOperation"); 653 assert(op); 654 return op; 655 } 656 657 @trusted static IShellItem CreateShellItem(IShellFolder folder, LPITEMIDLIST pidl) 658 { 659 IShellItem item; 660 henforce(SHCreateShellItem(null, folder, pidl, &item), "Failed to get IShellItem"); 661 assert(item); 662 return item; 663 } 664 665 @trusted static void RunDeleteOperation(IShellFolder folder, LPITEMIDLIST pidl) 666 in { 667 assert(folder); 668 } 669 body { 670 enforce(pidl !is null, "Empty trashcan item, can't run a delete operation"); 671 IShellItem item = CreateShellItem(folder, pidl); 672 scope(exit) item.Release(); 673 674 IFileOperation op = CreateFileOperation(); 675 scope(exit) op.Release(); 676 677 op.SetOperationFlags(FOF_NOCONFIRMATION|FOF_NOERRORUI|FOF_SILENT); 678 op.DeleteItem(item, null); 679 henforce(op.PerformOperations(), "Failed to perform file deletion operation"); 680 } 681 682 @trusted static void RunRestoreOperation(IShellFolder2 folder, LPITEMIDLIST pidl) 683 in { 684 assert(folder); 685 } 686 body { 687 enforce(pidl !is null, "Empty trashcan item, can't run a restore operation"); 688 689 import std.utf; 690 wstring originalLocation = getWStringDetailOf(folder, pidl, 1); 691 auto originalLocationZ = originalLocation.toUTF16z; 692 693 auto originalLocationPidl = ILCreateFromPath(originalLocationZ); 694 scope(exit) ILFree(originalLocationPidl); 695 696 IShellItem originalLocationItem = CreateShellItem(null, originalLocationPidl); 697 698 IShellItem item = CreateShellItem(folder, pidl); 699 scope(exit) item.Release(); 700 701 IFileOperation op = CreateFileOperation(); 702 scope(exit) op.Release(); 703 704 op.SetOperationFlags(FOF_NOCONFIRMATION|FOF_NOERRORUI|FOF_SILENT); 705 op.MoveItem(item, originalLocationItem, null, null); 706 henforce(op.PerformOperations(), "Failed to perform file deletion operation"); 707 } 708 } 709 710 /// Interface to trashcan. 711 interface ITrashcan 712 { 713 /// List items stored in trashcan. 714 @trusted InputRange!TrashcanItem byItem(); 715 /// Restore item to its original location. 716 @safe void restore(ref scope TrashcanItem item); 717 /// Ditto 718 @trusted final void restore(TrashcanItem item) { 719 restore(item); 720 } 721 /// Erase item from trashcan. 722 @safe void erase(ref scope TrashcanItem item); 723 /// Ditto 724 @trusted final erase(TrashcanItem item) { 725 erase(item); 726 } 727 /// The name of trashcan (possibly localized). 728 @property @safe string displayName() nothrow; 729 } 730 731 version(D_Ddoc) 732 { 733 /** 734 * Implementation of $(D ITrashcan). This class may have additional platform-dependent functions and different constructors. 735 * This class is currently available only for $(BLUE Windows) and $(BLUE Freedesktop) (GNU/Linux, FreeBSD, etc.) platforms. 736 */ 737 final class Trashcan : ITrashcan 738 { 739 /// 740 @trusted this() {} 741 /// Lazily list items stored in trashcan. 742 @trusted InputRange!TrashcanItem byItem() {return null;} 743 /** 744 * Restore item to its original location. 745 * Throws: 746 * $(B WindowsException) on Windows when the operation failed.$(BR) 747 * $(B FileException) on Posix when could not move the item to its original location or could not recreate original location directory.$(BR) 748 * $(B Exception) on other errors. 749 */ 750 @safe void restore(ref scope TrashcanItem item) {} 751 /** 752 * Erase item from trashcan. 753 * Throws: 754 * $(B WindowsException) on Windows when the operation failed.$(BR) 755 * $(B FileException) on Posix when could not delete the item.$(BR) 756 * $(B Exception) on other errors. 757 */ 758 @safe void erase(ref scope TrashcanItem item) {} 759 /** 760 * The name of trashcan (possibly localized). Currently implemented only for Windows and KDE, and returns empty string on other platforms. 761 * Returns: 762 * Name of trashcan as defined by system for the current user. Empty string if the name is unknown. 763 */ 764 @property @safe string displayName() nothrow {return string.init;} 765 766 static if (!is(typeof(IShellFolder2.init))) 767 { 768 static struct IShellFolder2 {} 769 } 770 /** 771 * Windows-only function to get $(LINK2 https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ishellfolder2, IShellFolder2) object associated with recycle bin. 772 * 773 * Note: 774 * If you want a returned object to outlive $(D Trashcan), you must call AddRef on it (and then Release when it's no longer needed). 775 */ 776 @system @property @nogc IShellFolder2 recycleBin() nothrow {return IShellFolder2.init;} 777 } 778 } 779 else version(Windows) final class Trashcan : ITrashcan 780 { 781 @trusted this() { 782 CoInitializeEx(null, COINIT.COINIT_APARTMENTTHREADED); 783 IShellFolder desktop; 784 LPITEMIDLIST pidlRecycleBin; 785 786 henforce(SHGetDesktopFolder(&desktop), "Failed to get desktop shell folder"); 787 assert(desktop); 788 scope(exit) desktop.Release(); 789 henforce(SHGetSpecialFolderLocation(null, CSIDL_BITBUCKET, &pidlRecycleBin), "Failed to get recycle bin location"); 790 assert(pidlRecycleBin); 791 scope(exit) ILFree(pidlRecycleBin); 792 793 henforce(desktop.BindToObject(pidlRecycleBin, null, &IID_IShellFolder2, cast(LPVOID *)&_recycleBin), "Failed to get recycle bin shell folder"); 794 assert(_recycleBin); 795 collectException(getDisplayNameOf(desktop, pidlRecycleBin), _displayName); 796 } 797 798 @trusted ~this() { 799 assert(_recycleBin); 800 _recycleBin.Release(); 801 CoUninitialize(); 802 } 803 804 private static struct ByItem 805 { 806 this(IShellFolder2 folder) { 807 this.folder = folder; 808 folder.AddRef(); 809 with(SHCONTF) henforce(folder.EnumObjects(null, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN, &enumFiles), "Failed to enumerate objects in recycle bin"); 810 popFront(); 811 } 812 this(this) { 813 if (enumFiles) 814 enumFiles.AddRef(); 815 if (folder) 816 folder.AddRef(); 817 } 818 ~this() { 819 if (enumFiles) 820 enumFiles.Release(); 821 if (folder) 822 folder.Release(); 823 } 824 TrashcanItem front() { 825 return current; 826 } 827 TrashcanItem moveFront() { 828 import std.algorithm.mutation : move; 829 return move(current); 830 } 831 void popFront() { 832 LPITEMIDLIST pidl; 833 if (enumFiles.Next(1, &pidl, null) == S_FALSE) { 834 atTheEnd = true; 835 } else { 836 assert(pidl); 837 ULONG attributes = SFGAOF.SFGAO_FOLDER; 838 folder.GetAttributesOf(1,cast(LPCITEMIDLIST *)&pidl,&attributes); 839 string fileName = getDisplayNameOf(folder, pidl); 840 string extension = getStringDetailOf(folder, pidl, 166); 841 SysTime deletionTime = getSysTimeDetailOf(folder, pidl, 2); 842 // The returned name may or may not contain the extension depending on the view parameters of the recycle bin folder 843 if (fileName.extension != extension) 844 fileName ~= extension; 845 current = TrashcanItem(fileName, !!(attributes & SFGAOF.SFGAO_FOLDER), deletionTime, pidl); 846 } 847 } 848 bool empty() { 849 return atTheEnd; 850 } 851 private: 852 IShellFolder2 folder; 853 IEnumIDList enumFiles; 854 TrashcanItem current; 855 bool atTheEnd; 856 } 857 858 @trusted InputRange!TrashcanItem byItem() { 859 return inputRangeObject(ByItem(_recycleBin)); 860 } 861 862 private @trusted void trustedRestore(ref scope TrashcanItem item) { 863 //RunVerb!"undelete"(_recycleBin, item.itemIdList); 864 RunRestoreOperation(_recycleBin, item.itemIdList); 865 } 866 @safe void restore(ref scope TrashcanItem item) { 867 trustedRestore(item); 868 } 869 private @trusted void trustedErase(ref scope TrashcanItem item) { 870 //RunVerb!"delete"(_recycleBin, item.itemIdList); 871 RunDeleteOperation(_recycleBin, item.itemIdList); 872 } 873 @safe void erase(ref scope TrashcanItem item) { 874 trustedErase(item); 875 } 876 @property @safe string displayName() nothrow { 877 return _displayName; 878 } 879 @property @system @nogc IShellFolder2 recycleBin() nothrow { 880 return _recycleBin; 881 } 882 private: 883 string _displayName; 884 IShellFolder2 _recycleBin; 885 } else static if (isFreedesktop) 886 { 887 final class Trashcan : ITrashcan 888 { 889 private @safe static bool isDirNothrow(string path) nothrow { 890 bool isDirectory; 891 if (collectException(path.isDir, isDirectory) is null) 892 return isDirectory; 893 return false; 894 } 895 896 @safe this() {} 897 898 import std.typecons : Tuple; 899 900 @trusted InputRange!TrashcanItem byItem() { 901 import std.algorithm.iteration : cache, map, joiner, filter; 902 alias Tuple!(string, "base", string, "info", string, "files", string, "root") TrashLocation; 903 return inputRangeObject(standardTrashBasePaths().map!(trashDir => TrashLocation(trashDir.base, buildPath(trashDir.base, "info"), buildPath(trashDir.base, "files"), trashDir.root)).filter!(t => isDirNothrow(t.info) && isDirNothrow(t.files)).map!(delegate(TrashLocation trash) { 904 InputRange!TrashcanItem toReturn; 905 try { 906 toReturn = inputRangeObject(dirEntries(trash.info, SpanMode.shallow, false).filter!(entry => entry.extension == ".trashinfo").map!(delegate(DirEntry entry) { 907 string trashedFile = buildPath(trash.files, entry.baseName.stripExtension); 908 try { 909 if (exists(trashedFile)) { 910 import inilike.read; 911 912 string path; 913 SysTime deletionTime; 914 915 auto onGroup = delegate ActionOnGroup(string groupName) { 916 if (groupName == "Trash Info") 917 return ActionOnGroup.stopAfter; 918 return ActionOnGroup.skip; 919 }; 920 auto onKeyValue = delegate void(string key, string value, string groupName) { 921 if (groupName == "Trash Info") 922 { 923 if (key == "Path") 924 path = value; 925 else if (key == "DeletionDate") 926 collectException(SysTime.fromISOExtString(value), deletionTime); 927 } 928 }; 929 readIniLike(iniLikeFileReader(entry.name), null, onGroup, onKeyValue, null); 930 931 if (path.length) { 932 path = path.decode(); 933 string restorePath; 934 if (path.isAbsolute) 935 restorePath = path; 936 else 937 restorePath = buildPath(trash.root, path); 938 return TrashcanItem(restorePath, trashedFile.isDir, deletionTime, entry.name, trashedFile); 939 } 940 } 941 } catch(Exception e) {} 942 return TrashcanItem.init; 943 }).cache.filter!(item => item.restorePath.length)); 944 } catch(Exception e) { 945 toReturn = inputRangeObject(TrashcanItem[].init); 946 } 947 return toReturn; 948 }).cache.joiner); 949 } 950 951 @safe void restore(ref scope TrashcanItem item) { 952 mkdirRecurse(item.restorePath.dirName); 953 rename(item.trashedPath, item.restorePath); 954 collectException(remove(item.trashInfoPath)); 955 } 956 @safe void erase(ref scope TrashcanItem item) { 957 static @trusted void trustedErase(string path) 958 { 959 if (path.isDir) 960 rmdirRecurse(path); 961 else 962 remove(path); 963 } 964 trustedErase(item.trashedPath); 965 collectException(remove(item.trashInfoPath)); 966 } 967 @property @safe string displayName() nothrow { 968 if (!_triedToRetrieveName) { 969 _triedToRetrieveName = true; 970 971 static @safe string currentLocale() nothrow 972 { 973 import std.process : environment; 974 try { 975 return environment.get("LC_ALL", environment.get("LC_MESSAGES", environment.get("LANG"))); 976 } catch(Exception e) { 977 return null; 978 } 979 } 980 981 static @trusted string readTrashName(scope const(string)[] desktopFiles, scope string locale) nothrow { 982 foreach(path; desktopFiles) { 983 if (!path.exists) 984 continue; 985 try { 986 import inilike.read; 987 import inilike.common; 988 989 string name; 990 string bestLocale; 991 992 auto onGroup = delegate ActionOnGroup(string groupName) { 993 if (groupName == "Desktop Entry") 994 return ActionOnGroup.stopAfter; 995 return ActionOnGroup.skip; 996 }; 997 auto onKeyValue = delegate void(string key, string value, string groupName) { 998 if (groupName == "Desktop Entry") 999 { 1000 auto keyAndLocale = separateFromLocale(key); 1001 if (keyAndLocale[0] == "Name") 1002 { 1003 auto lv = selectLocalizedValue(locale, keyAndLocale[1], value, bestLocale, name); 1004 bestLocale = lv[0]; 1005 name = lv[1].unescapeValue(); 1006 } 1007 } 1008 }; 1009 readIniLike(iniLikeFileReader(path), null, onGroup, onKeyValue, null); 1010 if (name.length) 1011 return name; 1012 } catch(Exception e) {} 1013 } 1014 return string.init; 1015 } 1016 1017 const locale = currentLocale(); 1018 _displayName = readTrashName(xdgAllDataDirs("kio_desktop/directory.trash"), locale); 1019 if (!_displayName.length) { 1020 _displayName = readTrashName(xdgAllDataDirs("kde4/apps/kio_desktop/directory.trash"), locale); 1021 } 1022 } 1023 /+ 1024 On GNOME it can be read from nautilus translation file (.mo). 1025 +/ 1026 return _displayName; 1027 } 1028 private: 1029 alias Tuple!(string, "base", string, "root") TrashRoot; 1030 @trusted static TrashRoot[] standardTrashBasePaths() { 1031 TrashRoot[] trashBasePaths; 1032 import core.sys.posix.unistd; 1033 1034 string homeTrashPath = xdgDataHome("Trash"); 1035 string homeTrashTopDir; 1036 if (homeTrashPath.length && homeTrashPath.isAbsolute && isDirNothrow(homeTrashPath)) { 1037 homeTrashTopDir = homeTrashPath.topDir; 1038 trashBasePaths ~= TrashRoot(homeTrashPath, homeTrashTopDir); 1039 } 1040 1041 auto userId = getuid(); 1042 auto volumes = mountedVolumes(); 1043 foreach(volume; volumes) { 1044 if (!volume.isValid) 1045 continue; 1046 if (homeTrashTopDir == volume.path) 1047 continue; 1048 string diskTrash; 1049 string userTrash; 1050 if (collectException(checkDiskTrash(volume.path), diskTrash) is null) { 1051 userTrash = userTrashSubdir(diskTrash, userId); 1052 if (isDirNothrow(userTrash)) 1053 trashBasePaths ~= TrashRoot(userTrash, volume.path); 1054 } 1055 userTrash = userTrashDir(volume.path, userId); 1056 if (isDirNothrow(userTrash)) 1057 trashBasePaths ~= TrashRoot(userTrash, volume.path); 1058 } 1059 return trashBasePaths; 1060 } 1061 string _displayName; 1062 bool _triedToRetrieveName; 1063 } 1064 }