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 
15 import isfreedesktop;
16 
17 static if (isFreedesktop)
18 {
19 private:
20     import std.format : format;
21     @trusted string numberedBaseName(string path, uint number) {
22         return format("%s %s%s", path.baseName.stripExtension, number, path.extension);
23     }
24     
25     unittest
26     {
27         assert(numberedBaseName("/root/file.ext", 1) == "file 1.ext");
28         assert(numberedBaseName("/root/file", 2) == "file 2");
29     }
30     
31     @trusted string escapeValue(string value) pure {
32         return value.replace("\\", `\\`).replace("\n", `\n`).replace("\r", `\r`).replace("\t", `\t`);
33     }
34 }
35 
36 version(OSX)
37 {
38 private:
39     import core.sys.posix.dlfcn;
40     
41     struct FSRef {
42         char[80] hidden;
43     };
44     
45     alias ubyte Boolean;
46     alias int OSStatus;
47     alias uint OptionBits;
48     
49     extern(C) @nogc @system OSStatus _dummy_FSPathMakeRefWithOptions(const(char)* path, OptionBits, FSRef*, Boolean*) nothrow {return 0;}
50     extern(C) @nogc @system OSStatus _dummy_FSMoveObjectToTrashSync(const(FSRef)*, FSRef*, OptionBits) nothrow {return 0;}
51 }
52 
53 /**
54  * Move file or directory to trash can. 
55  * Params:
56  *  path = Path of item to remove. Must be absolute.
57  * Throws:
58  *  Exception when given path is not absolute or does not exist or some error occured during operation.
59  */
60 @trusted void moveToTrash(string path)
61 {
62     if (!path.isAbsolute) {
63         throw new Exception("Path must be absolute");
64     }
65     if (!path.exists) {
66         throw new Exception("Path does not exist");
67     }
68     
69     version(Windows) {
70         import core.sys.windows.shellapi;
71         import core.sys.windows.winbase;
72         import core.stdc.wchar_;
73         import std.windows.syserror;
74         import std.utf;
75         
76         SHFILEOPSTRUCTW fileOp;
77         fileOp.wFunc = FO_DELETE;
78         fileOp.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NOCONFIRMMKDIR | FOF_ALLOWUNDO;
79         auto wFileName = (path ~ "\0\0").toUTF16();
80         fileOp.pFrom = wFileName.ptr;
81         int r = SHFileOperation(&fileOp);
82         if (r != 0) {
83             wchar[1024] msg;
84             auto len = FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, r, 0, msg.ptr, msg.length - 1, null);
85             
86             if (len) {
87                 throw new Exception(msg[0..len].toUTF8().stripRight);
88             } else {
89                 throw new Exception("File deletion error");
90             }
91         }
92     } else version(OSX) {
93         import std.exception;
94         
95         void* handle = dlopen("CoreServices.framework/Versions/A/CoreServices", RTLD_NOW | RTLD_LOCAL);
96         if (handle !is null) {
97             scope(exit) dlclose(handle);
98             
99             auto ptrFSPathMakeRefWithOptions = cast(typeof(&_dummy_FSPathMakeRefWithOptions))dlsym(handle, "FSPathMakeRefWithOptions");
100             if (ptrFSPathMakeRefWithOptions is null) {
101                 throw new Exception(fromStringz(dlerror()).idup);
102             }
103             
104             auto ptrFSMoveObjectToTrashSync = cast(typeof(&_dummy_FSMoveObjectToTrashSync))dlsym(handle, "FSMoveObjectToTrashSync");
105             if (ptrFSMoveObjectToTrashSync is null) {
106                 throw new Exception(fromStringz(dlerror()).idup);
107             }
108             
109             FSRef source;
110             enforce(ptrFSPathMakeRefWithOptions(toStringz(path), 1, &source, null) == 0, "Could not make FSRef from path");
111             FSRef target;
112             enforce(ptrFSMoveObjectToTrashSync(&source, &target, 0) == 0, "Could not move path to trash");
113         } else {
114             throw new Exception(fromStringz(dlerror()).idup);
115         }
116     } else {
117         static if (isFreedesktop) {
118             import xdgpaths;
119             
120             string trashInfoDir = xdgDataHome("Trash/info", true);
121             if (!trashInfoDir.length) {
122                 throw new Exception("Could not access trash info folder");
123             }
124             string trashFilePathsDir = xdgDataHome("Trash/files", true);
125             if (!trashFilePathsDir.length) {
126                 throw new Exception("Could not access trash files folder");
127             }
128             
129             string trashInfoPath = buildPath(trashInfoDir, path.baseName ~ ".trashinfo");
130             string trashFilePath = buildPath(trashFilePathsDir, path.baseName);
131             uint number = 1;
132             
133             while(trashInfoPath.exists || trashFilePath.exists) {
134                 string baseName = numberedBaseName(path, number);
135                 trashInfoPath = buildPath(trashInfoDir, baseName ~ ".trashinfo");
136                 trashFilePath = buildPath(trashFilePathsDir, baseName);
137                 number++;
138             }
139             
140             import std.datetime;
141             auto currentTime = Clock.currTime;
142             currentTime.fracSecs = Duration.zero;
143             string contents = format("[Trash Info]\nPath=%s\nDeletionDate=%s\n", path.escapeValue(), currentTime.toISOExtString());
144             write(trashInfoPath, contents);
145             path.rename(trashFilePath);
146         } else {
147             static assert("Unsupported platform");
148         }
149     }
150 }