summaryrefslogtreecommitdiff
path: root/zen/recycler.cpp
blob: 5b5e44d495d4f76329c6c383df0dcc29266b493c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
// **************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under *
// * GNU General Public License: http://www.gnu.org/licenses/gpl.html       *
// * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved        *
// **************************************************************************

#include "recycler.h"
#include "file_access.h"

#ifdef ZEN_WIN
#include "thread.h"
#include "dll.h"
#include "win_ver.h"
#include "long_path_prefix.h"
#include "IFileOperation/file_op.h"

#elif defined ZEN_LINUX
#include <sys/stat.h>
#include <gio/gio.h>
#include "scope_guard.h"

#elif defined ZEN_MAC
#include <CoreServices/CoreServices.h>
#endif

using namespace zen;


#ifdef ZEN_WIN
namespace
{
/*
Performance test: delete 1000 files
------------------------------------
SHFileOperation - single file     33s
SHFileOperation - multiple files  2,1s
IFileOperation  - single file     33s
IFileOperation  - multiple files  2,1s

=> SHFileOperation and IFileOperation have nearly IDENTICAL performance characteristics!

Nevertheless, let's use IFileOperation for better error reporting (including details on locked files)!
*/

struct CallbackData
{
    CallbackData(const std::function<void (const Zstring& currentItem)>& notifyDeletionStatus) :
        notifyDeletionStatus_(notifyDeletionStatus) {}

    const std::function<void (const Zstring& currentItem)>& notifyDeletionStatus_; //in, optional
    std::exception_ptr exception; //out
};


bool onRecyclerCallback(const wchar_t* itempath, void* sink)
{
    CallbackData& cbd = *static_cast<CallbackData*>(sink); //sink is NOT optional here

    if (cbd.notifyDeletionStatus_)
        try
        {
            cbd.notifyDeletionStatus_(itempath); //throw ?
        }
        catch (...)
        {
            cbd.exception = std::current_exception();
            return false;
        }
    return true;
}
}


void zen::recycleOrDelete(const std::vector<Zstring>& itempaths, const std::function<void (const Zstring& currentItem)>& notifyDeletionStatus)
{
    if (itempaths.empty())
        return;
    //::SetFileAttributes(applyLongPathPrefix(itempath).c_str(), FILE_ATTRIBUTE_NORMAL);
    //warning: moving long file paths to recycler does not work!
    //both ::SHFileOperation() and ::IFileOperation() cannot delete a folder named "System Volume Information" with normal attributes but shamelessly report success
    //both ::SHFileOperation() and ::IFileOperation() can't handle \\?\-prefix!

    if (vistaOrLater()) //new recycle bin usage: available since Vista
    {
#define DEF_DLL_FUN(name) const DllFun<fileop::FunType_##name> name(fileop::getDllName(), fileop::funName_##name);
        DEF_DLL_FUN(moveToRecycleBin);
        DEF_DLL_FUN(getLastErrorMessage);
#undef DEF_DLL_FUN

        if (!moveToRecycleBin || !getLastErrorMessage)
            throw FileError(replaceCpy(_("Unable to move %x to the recycle bin."), L"%x", fmtFileName(itempaths[0])),
                            replaceCpy(_("Cannot load file %x."), L"%x", fmtFileName(fileop::getDllName())));

        std::vector<const wchar_t*> cNames;
        for (auto it = itempaths.begin(); it != itempaths.end(); ++it) //CAUTION: do not create temporary strings here!!
            cNames.push_back(it->c_str());

        CallbackData cbd(notifyDeletionStatus);
        if (!moveToRecycleBin(&cNames[0], cNames.size(), onRecyclerCallback, &cbd))
        {
            if (cbd.exception)
                std::rethrow_exception(cbd.exception);

            std::wstring itempathFmt = fmtFileName(itempaths[0]); //probably not the correct file name for file lists larger than 1!
            if (itempaths.size() > 1)
                itempathFmt += L", ..."; //give at least some hint that there are multiple files, and the error need not be related to the first one

            throw FileError(replaceCpy(_("Unable to move %x to the recycle bin."), L"%x", itempathFmt), getLastErrorMessage()); //already includes details about locking errors!
        }
    }
    else //regular recycle bin usage: available since XP
    {
        Zstring itempathsDoubleNull;
        for (const Zstring& itempath : itempaths)
        {
            itempathsDoubleNull += itempath;
            itempathsDoubleNull += L'\0';
        }

        SHFILEOPSTRUCT fileOp = {};
        fileOp.hwnd   = nullptr;
        fileOp.wFunc  = FO_DELETE;
        fileOp.pFrom  = itempathsDoubleNull.c_str();
        fileOp.pTo    = nullptr;
        fileOp.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT | FOF_NOERRORUI;
        fileOp.fAnyOperationsAborted = false;
        fileOp.hNameMappings         = nullptr;
        fileOp.lpszProgressTitle     = nullptr;

        //"You should use fully-qualified path names with this function. Using it with relative path names is not thread safe."
        if (::SHFileOperation(&fileOp) != 0 || fileOp.fAnyOperationsAborted)
        {
            throw FileError(replaceCpy(_("Unable to move %x to the recycle bin."), L"%x", fmtFileName(itempaths[0]))); //probably not the correct file name for file list larger than 1!
        }
    }
}
#endif


bool zen::recycleOrDelete(const Zstring& itempath) //throw FileError
{
    if (!somethingExists(itempath)) //[!] do not optimize away, OS X needs this for reliable detection of "recycle bin missing"
        return false; //neither file nor any other object with that name existing: no error situation, manual deletion relies on it!

#ifdef ZEN_WIN
    std::vector<Zstring> itempaths;
    itempaths.push_back(itempath);
    recycleOrDelete(itempaths, nullptr); //throw FileError

#elif defined ZEN_LINUX
    GFile* file = ::g_file_new_for_path(itempath.c_str()); //never fails according to docu
    ZEN_ON_SCOPE_EXIT(g_object_unref(file);)

    GError* error = nullptr;
    ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error););

    if (!::g_file_trash(file, nullptr, &error))
    {
        const std::wstring errorMsg = replaceCpy(_("Unable to move %x to the recycle bin."), L"%x", fmtFileName(itempath));

        if (!error)
            throw FileError(errorMsg, L"Unknown error."); //user should never see this

        //implement same behavior as in Windows: if recycler is not existing, delete permanently
        if (error->code == G_IO_ERROR_NOT_SUPPORTED)
        {
            struct ::stat fileInfo = {};
            if (::lstat(itempath.c_str(), &fileInfo) != 0)
                return false;

            if (S_ISLNK(fileInfo.st_mode) || S_ISREG(fileInfo.st_mode))
                removeFile(itempath); //throw FileError
            else if (S_ISDIR(fileInfo.st_mode))
                removeDirectory(itempath); //throw FileError
            return true;
        }

        throw FileError(errorMsg, replaceCpy<std::wstring>(L"Glib Error Code %x:", L"%x", numberTo<std::wstring>(error->code)) + L" " + utfCvrtTo<std::wstring>(error->message));
        //g_quark_to_string(error->domain)
    }

#elif defined ZEN_MAC
    //we cannot use FSPathMoveObjectToTrashSync directly since it follows symlinks!

    static_assert(sizeof(Zchar) == sizeof(char), "");
    const UInt8* itempathUtf8 = reinterpret_cast<const UInt8*>(itempath.c_str());

    auto throwFileError = [&](OSStatus oss)
    {
        const std::wstring errorMsg = replaceCpy(_("Unable to move %x to the recycle bin."), L"%x", fmtFileName(itempath));
        std::wstring errorDescr = L"OSStatus Code " + numberTo<std::wstring>(oss);

        if (const char* description = ::GetMacOSStatusCommentString(oss)) //found no documentation for proper use of GetMacOSStatusCommentString
            errorDescr += L": " + utfCvrtTo<std::wstring>(description);
        throw FileError(errorMsg, errorDescr);
    };

    FSRef objectRef = {}; //= POD structure not a pointer type!
    OSStatus rv = ::FSPathMakeRefWithOptions(itempathUtf8, //const UInt8 *path,
                                             kFSPathMakeRefDoNotFollowLeafSymlink, //OptionBits options,
                                             &objectRef,  //FSRef *ref,
                                             nullptr);    //Boolean *isDirectory
    if (rv != noErr)
        throwFileError(rv);

    //deprecated since OS X 10.8!!! "trashItemAtURL" should be used instead
    OSStatus rv2 = ::FSMoveObjectToTrashSync(&objectRef, //const FSRef *source,
                                             nullptr,    //FSRef *target,
                                             kFSFileOperationDefaultOptions); //OptionBits options
    if (rv2 != noErr)
    {
        //implement same behavior as in Windows: if recycler is not existing, delete permanently
        if (rv2 == -120) //=="Directory not found or incomplete pathname." but should really be "recycle bin directory not found"!
        {
            struct ::stat fileInfo = {};
            if (::lstat(itempath.c_str(), &fileInfo) != 0)
                return false;

            if (S_ISLNK(fileInfo.st_mode) || S_ISREG(fileInfo.st_mode))
                removeFile(itempath); //throw FileError
            else if (S_ISDIR(fileInfo.st_mode))
                removeDirectory(itempath); //throw FileError
            return true;
        }

        throwFileError(rv2);
    }
#endif
    return true;
}


#ifdef ZEN_WIN
bool zen::recycleBinExists(const Zstring& pathName, const std::function<void ()>& onUpdateGui) //throw FileError
{
    if (vistaOrLater())
    {
        using namespace fileop;
        const DllFun<FunType_getRecycleBinStatus> getRecycleBinStatus(getDllName(), funName_getRecycleBinStatus);
        const DllFun<FunType_getLastErrorMessage> getLastErrorMessage(getDllName(), funName_getLastErrorMessage);

        if (!getRecycleBinStatus || !getLastErrorMessage)
            throw FileError(replaceCpy(_("Checking recycle bin failed for folder %x."), L"%x", fmtFileName(pathName)),
                            replaceCpy(_("Cannot load file %x."), L"%x", fmtFileName(getDllName())));

        bool hasRecycler = false;
        if (!getRecycleBinStatus(pathName.c_str(), hasRecycler))
            throw FileError(replaceCpy(_("Checking recycle bin failed for folder %x."), L"%x", fmtFileName(pathName)), getLastErrorMessage());

        return hasRecycler;
    }
    else
    {
        //excessive runtime if recycle bin exists, is full and drive is slow:
        auto ft = async([pathName]()
        {
            SHQUERYRBINFO recInfo = {};
            recInfo.cbSize = sizeof(recInfo);
            return ::SHQueryRecycleBin(pathName.c_str(), //__in_opt  LPCTSTR pszRootPath,
                                       &recInfo);        //__inout   LPSHQUERYRBINFO pSHQueryRBInfo
        });

        while (!ft.timed_wait(boost::posix_time::milliseconds(50)))
            if (onUpdateGui)
                onUpdateGui(); //may throw!

        return ft.get() == S_OK;
    }

    //1. ::SHQueryRecycleBin() is excessive: traverses whole $Recycle.Bin directory tree each time!!!! But it's safe and correct.

    //2. we can't simply buffer the ::SHQueryRecycleBin() based on volume serial number:
    //	"subst S:\ C:\" => GetVolumeInformation() returns same serial for C:\ and S:\, but S:\ does not support recycle bin!

    //3. we would prefer to use CLSID_RecycleBinManager beginning with Vista... if only this interface were documented!!!

    //4. check directory existence of "C:\$Recycle.Bin, C:\RECYCLER, C:\RECYCLED"
    // -> not upward-compatible, wrong result for subst-alias: recycler assumed existing, although it is not!

    //5. alternative approach a'la Raymond Chen: http://blogs.msdn.com/b/oldnewthing/archive/2008/09/18/8956382.aspx
    //caveat: might not be reliable, e.g. "subst"-alias of volume contains "$Recycle.Bin" although it is not available!

    /*
        Zstring rootPathPf = appendSeparator(&buffer[0]);

    	const bool canUseFastCheckForRecycler = winXpOrLater();
        if (!canUseFastCheckForRecycler) //== "checkForRecycleBin"
            return STATUS_REC_UNKNOWN;

        using namespace fileop;
        const DllFun<FunType_checkRecycler> checkRecycler(getDllName(), funName_checkRecycler);

        if (!checkRecycler)
            return STATUS_REC_UNKNOWN; //actually an error since we're >= XP

        //search root directories for recycle bin folder...

        WIN32_FIND_DATA dataRoot = {};
        HANDLE hFindRoot = ::FindFirstFile(applyLongPathPrefix(rootPathPf + L'*').c_str(), &dataRoot);
        if (hFindRoot == INVALID_HANDLE_VALUE)
            return STATUS_REC_UNKNOWN;
        ZEN_ON_SCOPE_EXIT(FindClose(hFindRoot));

        auto shouldSkip = [](const Zstring& shortname) { return shortname == L"."  || shortname == L".."; };

        do
        {
            if (!shouldSkip(dataRoot.cFileName) &&
                (dataRoot.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 &&
                (dataRoot.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM   ) != 0 && //risky to rely on these attributes!!!
                (dataRoot.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN   ) != 0)   //
            {
                WIN32_FIND_DATA dataChild = {};
                const Zstring childDirPf = rootPathPf + dataRoot.cFileName + L"\\";

                HANDLE hFindChild = ::FindFirstFile(applyLongPathPrefix(childDirPf + L'*').c_str(), &dataChild);
                if (hFindChild != INVALID_HANDLE_VALUE) //if we can't access a subdir, it's probably not the recycler
                {
                    ZEN_ON_SCOPE_EXIT(FindClose(hFindChild));
                    do
                    {
                        if (!shouldSkip(dataChild.cFileName) &&
                            (dataChild.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
                        {
                            bool isRecycler = false;
                            if (checkRecycler((childDirPf + dataChild.cFileName).c_str(), isRecycler))
                            {
                                if (isRecycler)
                                    return STATUS_REC_EXISTS;
                            }
                            else assert(false);
                        }
                    }
                    while (::FindNextFile(hFindChild, &dataChild)); //ignore errors other than ERROR_NO_MORE_FILES
                }
            }
        }
        while (::FindNextFile(hFindRoot, &dataRoot)); //

        return STATUS_REC_MISSING;
    */
}

#elif defined ZEN_LINUX || defined ZEN_MAC
/*
We really need access to a similar function to check whether a directory supports trashing and emit a warning if it does not!

The following function looks perfect, alas it is restricted to local files and to the implementation of GIO only:

    gboolean _g_local_file_has_trash_dir(const char* dirpath, dev_t dir_dev);
    See: http://www.netmite.com/android/mydroid/2.0/external/bluetooth/glib/gio/glocalfileinfo.h

    Just checking for "G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH" is not correct, since we find in
    http://www.netmite.com/android/mydroid/2.0/external/bluetooth/glib/gio/glocalfileinfo.c

            g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH,
                                           writable && parent_info->has_trash_dir);

    => We're NOT interested in whether the specified folder can be trashed, but whether it supports thrashing its child elements! (Only support, not actual write access!)
    This renders G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH useless for this purpose.
*/
#endif
bgstack15