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 ensureDirExists(string dir) { 88 std.file.mkdirRecurse(dir); 89 return dir; 90 } 91 92 import core.sys.posix.sys.types; 93 import core.sys.posix.sys.stat; 94 import core.sys.posix.unistd; 95 import core.sys.posix.fcntl; 96 97 @trusted string topDir(string path) 98 in { 99 assert(path.isAbsolute); 100 } 101 out(result) { 102 if (result.length) { 103 assert(result.isAbsolute); 104 } 105 } 106 body { 107 auto current = path; 108 stat_t currentStat; 109 if (stat(current.toStringz, ¤tStat) != 0) { 110 return null; 111 } 112 stat_t parentStat; 113 while(current != "/") { 114 string parent = current.dirName; 115 if (lstat(parent.toStringz, &parentStat) != 0) { 116 return null; 117 } 118 if (currentStat.st_dev != parentStat.st_dev) { 119 return current; 120 } 121 current = parent; 122 } 123 return current; 124 } 125 126 void checkDiskTrashMode(mode_t mode, const bool checkStickyBit = true) 127 { 128 enforce(!S_ISLNK(mode), "Top trash directory is a symbolic link"); 129 enforce(S_ISDIR(mode), "Top trash path is not a directory"); 130 if (checkStickyBit) { 131 enforce((mode & S_ISVTX) != 0, "Top trash directory does not have sticky bit"); 132 } 133 } 134 135 unittest 136 { 137 assertThrown(checkDiskTrashMode(S_IFLNK|S_ISVTX)); 138 assertThrown(checkDiskTrashMode(S_IFDIR)); 139 assertNotThrown(checkDiskTrashMode(S_IFDIR|S_ISVTX)); 140 assertNotThrown(checkDiskTrashMode(S_IFDIR, false)); 141 } 142 143 @trusted string checkDiskTrash(string topdir, const bool checkStickyBit = true) 144 in { 145 assert(topdir.length); 146 } 147 body { 148 string trashDir = buildPath(topdir, ".Trash"); 149 stat_t trashStat; 150 enforce(lstat(trashDir.toStringz, &trashStat) == 0, "Top trash directory does not exist"); 151 checkDiskTrashMode(trashStat.st_mode, checkStickyBit); 152 return trashDir; 153 } 154 155 string userTrashSubdir(string trashDir, uid_t uid) { 156 return buildPath(trashDir, format("%s", uid)); 157 } 158 159 unittest 160 { 161 assert(userTrashSubdir("/.Trash", 600) == buildPath("/.Trash", "600")); 162 } 163 164 @trusted string ensureUserTrashSubdir(string trashDir) 165 { 166 return userTrashSubdir(trashDir, getuid()).ensureDirExists(); 167 } 168 169 string userTrashDir(string topdir, uid_t uid) { 170 return buildPath(topdir, format(".Trash-%s", uid)); 171 } 172 173 unittest 174 { 175 assert(userTrashDir("/topdir", 700) == buildPath("/topdir", ".Trash-700")); 176 } 177 178 @trusted string ensureUserTrashDir(string topdir) 179 { 180 return userTrashDir(topdir, getuid()).ensureDirExists(); 181 } 182 } 183 184 version(OSX) 185 { 186 private: 187 import core.sys.posix.dlfcn; 188 189 struct FSRef { 190 char[80] hidden; 191 }; 192 193 alias ubyte Boolean; 194 alias int OSStatus; 195 alias uint OptionBits; 196 197 extern(C) @nogc @system OSStatus _dummy_FSPathMakeRefWithOptions(const(char)* path, OptionBits, FSRef*, Boolean*) nothrow {return 0;} 198 extern(C) @nogc @system OSStatus _dummy_FSMoveObjectToTrashSync(const(FSRef)*, FSRef*, OptionBits) nothrow {return 0;} 199 } 200 201 /** 202 * Move file or directory to trash can. 203 * Params: 204 * path = Path of item to remove. Must be absolute. 205 * options = Control behavior of trashing on freedesktop environments. 206 * Throws: 207 * Exception when given path is not absolute or does not exist or some error occured during operation. 208 */ 209 @trusted void moveToTrash(string path, TrashOptions options = TrashOptions.all) 210 { 211 if (!path.isAbsolute) { 212 throw new Exception("Path must be absolute"); 213 } 214 if (!path.exists) { 215 throw new Exception("Path does not exist"); 216 } 217 218 version(Windows) { 219 import core.sys.windows.shellapi; 220 import core.sys.windows.winbase; 221 import core.stdc.wchar_; 222 import std.windows.syserror; 223 import std.utf; 224 225 SHFILEOPSTRUCTW fileOp; 226 fileOp.wFunc = FO_DELETE; 227 fileOp.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NOCONFIRMMKDIR | FOF_ALLOWUNDO; 228 auto wFileName = (path ~ "\0\0").toUTF16(); 229 fileOp.pFrom = wFileName.ptr; 230 int r = SHFileOperation(&fileOp); 231 if (r != 0) { 232 import std.format; 233 throw new Exception(format("SHFileOperation failed with error code %d", r)); 234 } 235 } else version(OSX) { 236 void* handle = dlopen("CoreServices.framework/Versions/A/CoreServices", RTLD_NOW | RTLD_LOCAL); 237 if (handle !is null) { 238 scope(exit) dlclose(handle); 239 240 auto ptrFSPathMakeRefWithOptions = cast(typeof(&_dummy_FSPathMakeRefWithOptions))dlsym(handle, "FSPathMakeRefWithOptions"); 241 if (ptrFSPathMakeRefWithOptions is null) { 242 throw new Exception(fromStringz(dlerror()).idup); 243 } 244 245 auto ptrFSMoveObjectToTrashSync = cast(typeof(&_dummy_FSMoveObjectToTrashSync))dlsym(handle, "FSMoveObjectToTrashSync"); 246 if (ptrFSMoveObjectToTrashSync is null) { 247 throw new Exception(fromStringz(dlerror()).idup); 248 } 249 250 FSRef source; 251 enforce(ptrFSPathMakeRefWithOptions(toStringz(path), 1, &source, null) == 0, "Could not make FSRef from path"); 252 FSRef target; 253 enforce(ptrFSMoveObjectToTrashSync(&source, &target, 0) == 0, "Could not move path to trash"); 254 } else { 255 throw new Exception(fromStringz(dlerror()).idup); 256 } 257 } else { 258 static if (isFreedesktop) { 259 import xdgpaths; 260 261 string dataPath = xdgDataHome(null, true); 262 if (!dataPath.length) { 263 throw new Exception("Could not access data folder"); 264 } 265 dataPath = dataPath.absolutePath; 266 267 string trashBasePath; 268 bool usingTopdir = false; 269 string fileTopDir; 270 271 if ((options & TrashOptions.useTopDirs) != 0) { 272 string dataTopDir = topDir(dataPath); 273 fileTopDir = topDir(path); 274 275 enforce(fileTopDir.length, "Could not get topdir of file being trashed"); 276 enforce(dataTopDir.length, "Could not get topdir of home data directory"); 277 278 if (dataTopDir != fileTopDir) { 279 try { 280 string diskTrash = checkDiskTrash(fileTopDir, (options & TrashOptions.checkStickyBit) != 0); 281 trashBasePath = ensureUserTrashSubdir(diskTrash); 282 usingTopdir = true; 283 } catch(Exception e) { 284 try { 285 if ((options & TrashOptions.fallbackToUserDir) != 0) { 286 trashBasePath = ensureUserTrashDir(fileTopDir); 287 usingTopdir = true; 288 } else { 289 throw e; 290 } 291 } catch(Exception e) { 292 if (!(options & TrashOptions.fallbackToHomeDir)) { 293 throw e; 294 } 295 } 296 } 297 } 298 } 299 300 if (trashBasePath is null) { 301 trashBasePath = ensureDirExists(buildPath(dataPath, "Trash")); 302 } 303 enforce(trashBasePath.length, "Could not access base trash folder"); 304 305 string trashInfoDir = ensureDirExists(buildPath(trashBasePath, "info")); 306 string trashFilePathsDir = ensureDirExists(buildPath(trashBasePath, "files")); 307 308 string trashInfoPath = buildPath(trashInfoDir, path.baseName ~ ".trashinfo"); 309 string trashFilePath = buildPath(trashFilePathsDir, path.baseName); 310 311 import std.datetime; 312 import std.conv : octal; 313 import std.uri; 314 import core.stdc.errno; 315 316 auto currentTime = Clock.currTime; 317 currentTime.fracSecs = Duration.zero; 318 string timeString = currentTime.toISOExtString(); 319 string contents = format("[Trash Info]\nPath=%s\nDeletionDate=%s\n", (usingTopdir ? path.relativePath(fileTopDir) : path).encode(), timeString); 320 321 const mode = O_CREAT | O_WRONLY | O_EXCL; 322 int fd; 323 uint number = 1; 324 while(trashFilePath.exists || ((fd = .open(toStringz(trashInfoPath), mode, octal!666)) == -1 && errno == EEXIST)) { 325 string baseName = numberedBaseName(path, number); 326 trashFilePath = buildPath(trashFilePathsDir, baseName); 327 trashInfoPath = buildPath(trashInfoDir, baseName ~ ".trashinfo"); 328 number++; 329 } 330 errnoEnforce(fd != -1); 331 scope(exit) .close(fd); 332 errnoEnforce(write(fd, contents.ptr, contents.length) == contents.length); 333 334 path.rename(trashFilePath); 335 } else { 336 static assert("Unsupported platform"); 337 } 338 } 339 } 340 341 unittest 342 { 343 assertThrown(moveToTrash("notabsolute")); 344 }