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 16 import isfreedesktop; 17 18 /** 19 * Flags to rule the trashing behavior. 20 * 21 * $(BLUE Valid only for freedesktop environments). 22 * 23 * See_Also: $(LINK2 https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html, Trash specification). 24 */ 25 enum TrashOptions : int 26 { 27 /** 28 * No options. Just move file to user home trash directory 29 * not paying attention to partition where file resides. 30 */ 31 none = 0, 32 /** 33 * If file that needs to be deleted resides on non-home partition 34 * and top trash directory ($topdir/.Trash/$uid) failed some check, 35 * fallback to user top trash directory ($topdir/.Trash-$uid). 36 * 37 * Makes sense only in conjunction with $(D useTopDirs). 38 */ 39 fallbackToUserDir = 1, 40 /** 41 * If file that needs to be deleted resides on non-home partition 42 * and checks for top trash directories failed, 43 * fallback to home trash directory. 44 * 45 * Makes sense only in conjunction with $(D useTopDirs). 46 */ 47 fallbackToHomeDir = 2, 48 49 /** 50 * Whether to use top trash directories at all. 51 * 52 * If no $(D fallbackToUserDir) nor $(D fallbackToHomeDir) flags are set, 53 * and file that needs to be deleted resides on non-home partition, 54 * and top trash directory ($topdir/.Trash/$uid) failed some check, 55 * exception will be thrown. This can be used to report errors to administrator or user. 56 */ 57 useTopDirs = 4, 58 59 /** 60 * Whether to check presence of 'sticky bit' on $topdir/.Trash directory. 61 * 62 * Makes sense only in conjunction with $(D useTopDirs). 63 */ 64 checkStickyBit = 8, 65 66 /** 67 * All flags set. 68 */ 69 all = (TrashOptions.fallbackToUserDir | TrashOptions.fallbackToHomeDir | TrashOptions.checkStickyBit | TrashOptions.useTopDirs) 70 } 71 72 static if (isFreedesktop) 73 { 74 private: 75 import std.format : format; 76 77 @trusted string numberedBaseName(string path, uint number) { 78 return format("%s %s%s", path.baseName.stripExtension, number, path.extension); 79 } 80 81 unittest 82 { 83 assert(numberedBaseName("/root/file.ext", 1) == "file 1.ext"); 84 assert(numberedBaseName("/root/file", 2) == "file 2"); 85 } 86 87 @trusted string escapeValue(string value) pure { 88 return value.replace("\\", `\\`).replace("\n", `\n`).replace("\r", `\r`).replace("\t", `\t`); 89 } 90 91 unittest 92 { 93 assert("a\\next\nline\top".escapeValue() == `a\\next\nline\top`); 94 } 95 96 @trusted string ensureDirExists(string dir) { 97 std.file.mkdirRecurse(dir); 98 return dir; 99 } 100 101 import core.sys.posix.sys.types; 102 import core.sys.posix.sys.stat; 103 import core.sys.posix.unistd; 104 import core.sys.posix.fcntl; 105 106 @trusted string topDir(string path) 107 in { 108 assert(path.isAbsolute); 109 } 110 out(result) { 111 if (result.length) { 112 assert(result.isAbsolute); 113 } 114 } 115 body { 116 auto current = path; 117 stat_t currentStat; 118 if (stat(current.toStringz, ¤tStat) != 0) { 119 return null; 120 } 121 stat_t parentStat; 122 while(current != "/") { 123 string parent = current.dirName; 124 if (lstat(parent.toStringz, &parentStat) != 0) { 125 return null; 126 } 127 if (currentStat.st_dev != parentStat.st_dev) { 128 return current; 129 } 130 current = parent; 131 } 132 return current; 133 } 134 135 void checkDiskTrashMode(mode_t mode, const bool checkStickyBit = true) 136 { 137 enforce(!S_ISLNK(mode), "Top trash directory is a symbolic link"); 138 enforce(S_ISDIR(mode), "Top trash path is not a directory"); 139 if (checkStickyBit) { 140 enforce((mode & S_ISVTX) != 0, "Top trash directory does not have sticky bit"); 141 } 142 } 143 144 unittest 145 { 146 assertThrown(checkDiskTrashMode(S_IFLNK|S_ISVTX)); 147 assertThrown(checkDiskTrashMode(S_IFDIR)); 148 assertNotThrown(checkDiskTrashMode(S_IFDIR|S_ISVTX)); 149 assertNotThrown(checkDiskTrashMode(S_IFDIR, false)); 150 } 151 152 @trusted string checkDiskTrash(string topdir, const bool checkStickyBit = true) 153 in { 154 assert(topdir.length); 155 } 156 body { 157 string trashDir = buildPath(topdir, ".Trash"); 158 stat_t trashStat; 159 enforce(lstat(trashDir.toStringz, &trashStat) == 0, "Top trash directory does not exist"); 160 checkDiskTrashMode(trashStat.st_mode); 161 return trashDir; 162 } 163 164 string userTrashSubdir(string trashDir, uid_t uid) { 165 return buildPath(trashDir, format("%s", uid)); 166 } 167 168 unittest 169 { 170 assert(userTrashSubdir("/.Trash", 600) == buildPath("/.Trash", "600")); 171 } 172 173 @trusted string ensureUserTrashSubdir(string trashDir) 174 { 175 return userTrashSubdir(trashDir, getuid()).ensureDirExists(); 176 } 177 178 string userTrashDir(string topdir, uid_t uid) { 179 return buildPath(topdir, format(".Trash-%s", uid)); 180 } 181 182 unittest 183 { 184 assert(userTrashDir("/topdir", 700) == buildPath("/topdir", ".Trash-700")); 185 } 186 187 @trusted string ensureUserTrashDir(string topdir) 188 { 189 return userTrashDir(topdir, getuid()).ensureDirExists(); 190 } 191 } 192 193 version(OSX) 194 { 195 private: 196 import core.sys.posix.dlfcn; 197 198 struct FSRef { 199 char[80] hidden; 200 }; 201 202 alias ubyte Boolean; 203 alias int OSStatus; 204 alias uint OptionBits; 205 206 extern(C) @nogc @system OSStatus _dummy_FSPathMakeRefWithOptions(const(char)* path, OptionBits, FSRef*, Boolean*) nothrow {return 0;} 207 extern(C) @nogc @system OSStatus _dummy_FSMoveObjectToTrashSync(const(FSRef)*, FSRef*, OptionBits) nothrow {return 0;} 208 } 209 210 /** 211 * Move file or directory to trash can. 212 * Params: 213 * path = Path of item to remove. Must be absolute. 214 * options = Control behavior of trashing on freedesktop environments. 215 * Throws: 216 * Exception when given path is not absolute or does not exist or some error occured during operation. 217 */ 218 @trusted void moveToTrash(string path, TrashOptions options = TrashOptions.all) 219 { 220 if (!path.isAbsolute) { 221 throw new Exception("Path must be absolute"); 222 } 223 if (!path.exists) { 224 throw new Exception("Path does not exist"); 225 } 226 227 version(Windows) { 228 import core.sys.windows.shellapi; 229 import core.sys.windows.winbase; 230 import core.stdc.wchar_; 231 import std.windows.syserror; 232 import std.utf; 233 234 SHFILEOPSTRUCTW fileOp; 235 fileOp.wFunc = FO_DELETE; 236 fileOp.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NOCONFIRMMKDIR | FOF_ALLOWUNDO; 237 auto wFileName = (path ~ "\0\0").toUTF16(); 238 fileOp.pFrom = wFileName.ptr; 239 int r = SHFileOperation(&fileOp); 240 if (r != 0) { 241 wchar[1024] msg; 242 auto len = FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, r, 0, msg.ptr, msg.length - 1, null); 243 244 if (len) { 245 throw new Exception(msg[0..len].toUTF8().stripRight); 246 } else { 247 throw new Exception("File deletion error"); 248 } 249 } 250 } else version(OSX) { 251 void* handle = dlopen("CoreServices.framework/Versions/A/CoreServices", RTLD_NOW | RTLD_LOCAL); 252 if (handle !is null) { 253 scope(exit) dlclose(handle); 254 255 auto ptrFSPathMakeRefWithOptions = cast(typeof(&_dummy_FSPathMakeRefWithOptions))dlsym(handle, "FSPathMakeRefWithOptions"); 256 if (ptrFSPathMakeRefWithOptions is null) { 257 throw new Exception(fromStringz(dlerror()).idup); 258 } 259 260 auto ptrFSMoveObjectToTrashSync = cast(typeof(&_dummy_FSMoveObjectToTrashSync))dlsym(handle, "FSMoveObjectToTrashSync"); 261 if (ptrFSMoveObjectToTrashSync is null) { 262 throw new Exception(fromStringz(dlerror()).idup); 263 } 264 265 FSRef source; 266 enforce(ptrFSPathMakeRefWithOptions(toStringz(path), 1, &source, null) == 0, "Could not make FSRef from path"); 267 FSRef target; 268 enforce(ptrFSMoveObjectToTrashSync(&source, &target, 0) == 0, "Could not move path to trash"); 269 } else { 270 throw new Exception(fromStringz(dlerror()).idup); 271 } 272 } else { 273 static if (isFreedesktop) { 274 import xdgpaths; 275 276 string dataPath = xdgDataHome(null, true); 277 if (!dataPath.length) { 278 throw new Exception("Could not access data folder"); 279 } 280 dataPath = dataPath.absolutePath; 281 282 string trashBasePath; 283 284 if ((options & TrashOptions.useTopDirs) != 0) { 285 string dataTopDir = topDir(dataPath); 286 string fileTopDir = topDir(path); 287 288 enforce(fileTopDir.length, "Could not get topdir of file being trashed"); 289 enforce(dataTopDir.length, "Could not get topdir of home data directory"); 290 291 if (dataTopDir != fileTopDir) { 292 try { 293 string diskTrash = checkDiskTrash(fileTopDir, (options & TrashOptions.checkStickyBit) != 0); 294 trashBasePath = ensureUserTrashSubdir(diskTrash); 295 } catch(Exception e) { 296 try { 297 if ((options & TrashOptions.fallbackToUserDir) != 0) { 298 trashBasePath = ensureUserTrashDir(fileTopDir); 299 } else { 300 throw e; 301 } 302 } catch(Exception e) { 303 if (!(options & TrashOptions.fallbackToHomeDir)) { 304 throw e; 305 } 306 } 307 } 308 } 309 } 310 311 if (trashBasePath is null) { 312 trashBasePath = ensureDirExists(buildPath(dataPath, "Trash")); 313 } 314 enforce(trashBasePath.length, "Could not access base trash folder"); 315 316 string trashInfoDir = ensureDirExists(buildPath(trashBasePath, "info")); 317 string trashFilePathsDir = ensureDirExists(buildPath(trashBasePath, "files")); 318 319 string trashInfoPath = buildPath(trashInfoDir, path.baseName ~ ".trashinfo"); 320 string trashFilePath = buildPath(trashFilePathsDir, path.baseName); 321 uint number = 1; 322 323 while(trashInfoPath.exists || trashFilePath.exists) { 324 string baseName = numberedBaseName(path, number); 325 trashInfoPath = buildPath(trashInfoDir, baseName ~ ".trashinfo"); 326 trashFilePath = buildPath(trashFilePathsDir, baseName); 327 number++; 328 } 329 330 import std.datetime; 331 import std.conv : octal; 332 333 auto currentTime = Clock.currTime; 334 currentTime.fracSecs = Duration.zero; 335 string timeString = currentTime.toISOExtString(); 336 string contents = format("[Trash Info]\nPath=%s\nDeletionDate=%s\n", path.escapeValue(), timeString); 337 338 const mode = O_CREAT | O_WRONLY | O_EXCL; 339 auto fd = .open(toStringz(trashInfoPath), mode, octal!666); 340 errnoEnforce(fd != -1); 341 scope(exit) .close(fd); 342 errnoEnforce(write(fd, contents.ptr, contents.length) == contents.length); 343 344 path.rename(trashFilePath); 345 } else { 346 static assert("Unsupported platform"); 347 } 348 } 349 } 350 351 unittest 352 { 353 assertThrown(moveToTrash("notabsolute")); 354 }