summaryrefslogtreecommitdiff
path: root/zen/file_access.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'zen/file_access.cpp')
-rw-r--r--zen/file_access.cpp2313
1 files changed, 2313 insertions, 0 deletions
diff --git a/zen/file_access.cpp b/zen/file_access.cpp
new file mode 100644
index 00000000..c4768dd2
--- /dev/null
+++ b/zen/file_access.cpp
@@ -0,0 +1,2313 @@
+// **************************************************************************
+// * 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 "file_access.h"
+#include <map>
+#include <algorithm>
+#include <stdexcept>
+#include "int64.h"
+#include "file_traverser.h"
+#include "scope_guard.h"
+#include "symlink_target.h"
+#include "file_io.h"
+#include "file_id_def.h"
+
+#ifdef ZEN_WIN
+#include <Aclapi.h>
+#include "privilege.h"
+#include "dll.h"
+#include "long_path_prefix.h"
+#include "win_ver.h"
+#include "IFileOperation/file_op.h"
+
+#elif defined ZEN_LINUX
+#include <sys/vfs.h> //statfs
+#include <fcntl.h> //AT_SYMLINK_NOFOLLOW, UTIME_OMIT
+#ifdef HAVE_SELINUX
+#include <selinux/selinux.h>
+#endif
+
+#elif defined ZEN_MAC
+#include <sys/mount.h> //statfs
+#endif
+
+#if defined ZEN_LINUX || defined ZEN_MAC
+#include <sys/stat.h>
+#include <sys/time.h> //lutimes
+#endif
+
+using namespace zen;
+
+
+bool zen::fileExists(const Zstring& filepath)
+{
+ //symbolic links (broken or not) are also treated as existing files!
+#ifdef ZEN_WIN
+ const DWORD attr = ::GetFileAttributes(applyLongPathPrefix(filepath).c_str());
+ if (attr != INVALID_FILE_ATTRIBUTES)
+ return (attr & FILE_ATTRIBUTE_DIRECTORY) == 0; //returns true for (file-)symlinks also
+
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ struct ::stat fileInfo = {};
+ if (::stat(filepath.c_str(), &fileInfo) == 0) //follow symlinks!
+ return S_ISREG(fileInfo.st_mode);
+#endif
+ return false;
+}
+
+
+bool zen::dirExists(const Zstring& dirpath)
+{
+ //symbolic links (broken or not) are also treated as existing directories!
+#ifdef ZEN_WIN
+ const DWORD attr = ::GetFileAttributes(applyLongPathPrefix(dirpath).c_str());
+ if (attr != INVALID_FILE_ATTRIBUTES)
+ return (attr & FILE_ATTRIBUTE_DIRECTORY) != 0; //returns true for (dir-)symlinks also
+
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ struct ::stat dirInfo = {};
+ if (::stat(dirpath.c_str(), &dirInfo) == 0) //follow symlinks!
+ return S_ISDIR(dirInfo.st_mode);
+#endif
+ return false;
+}
+
+
+bool zen::symlinkExists(const Zstring& linkname)
+{
+#ifdef ZEN_WIN
+ WIN32_FIND_DATA linkInfo = {};
+ const HANDLE searchHandle = ::FindFirstFile(applyLongPathPrefix(linkname).c_str(), &linkInfo);
+ if (searchHandle != INVALID_HANDLE_VALUE)
+ {
+ ::FindClose(searchHandle);
+ return isSymlink(linkInfo);
+ }
+
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ struct ::stat linkInfo = {};
+ if (::lstat(linkname.c_str(), &linkInfo) == 0)
+ return S_ISLNK(linkInfo.st_mode);
+#endif
+ return false;
+}
+
+
+bool zen::somethingExists(const Zstring& objname)
+{
+#ifdef ZEN_WIN
+ const DWORD attr = ::GetFileAttributes(applyLongPathPrefix(objname).c_str());
+ if (attr != INVALID_FILE_ATTRIBUTES)
+ return true;
+ const DWORD lastError = ::GetLastError();
+
+ //handle obscure file permission problem where ::GetFileAttributes() fails with ERROR_ACCESS_DENIED or ERROR_SHARING_VIOLATION
+ //while parent directory traversal is successful: e.g. "C:\pagefile.sys"
+ if (lastError != ERROR_PATH_NOT_FOUND && //perf: short circuit for common "not existing" error codes
+ lastError != ERROR_FILE_NOT_FOUND && //
+ lastError != ERROR_BAD_NETPATH && //
+ lastError != ERROR_BAD_NET_NAME) //
+ {
+ WIN32_FIND_DATA fileInfo = {};
+ const HANDLE searchHandle = ::FindFirstFile(applyLongPathPrefix(objname).c_str(), &fileInfo);
+ if (searchHandle != INVALID_HANDLE_VALUE)
+ {
+ ::FindClose(searchHandle);
+ return true;
+ }
+ }
+
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ struct ::stat fileInfo = {};
+ if (::lstat(objname.c_str(), &fileInfo) == 0)
+ return true;
+#endif
+ return false;
+}
+
+
+namespace
+{
+#ifdef ZEN_WIN
+//fast ::GetVolumePathName() clone: let's hope it's not too simple (doesn't honor mount points)
+Zstring getVolumeNameFast(const Zstring& filepath)
+{
+ //this call is expensive: ~1.5 ms!
+ // if (!::GetVolumePathName(filepath.c_str(), //__in LPCTSTR lpszFileName,
+ // fsName, //__out LPTSTR lpszVolumePathName,
+ // BUFFER_SIZE)) //__in DWORD cchBufferLength
+ // ...
+ // Zstring volumePath = appendSeparator(fsName);
+
+ const Zstring nameFmt = appendSeparator(removeLongPathPrefix(filepath)); //throw()
+
+ if (startsWith(nameFmt, Zstr("\\\\"))) //UNC path: "\\ComputerName\SharedFolder\"
+ {
+ size_t nameSize = nameFmt.size();
+ const size_t posFirstSlash = nameFmt.find(Zstr("\\"), 2);
+ if (posFirstSlash != Zstring::npos)
+ {
+ nameSize = posFirstSlash + 1;
+ const size_t posSecondSlash = nameFmt.find(Zstr("\\"), posFirstSlash + 1);
+ if (posSecondSlash != Zstring::npos)
+ nameSize = posSecondSlash + 1;
+ }
+ return Zstring(nameFmt.c_str(), nameSize); //include trailing backslash!
+ }
+ else //local path: "C:\Folder\"
+ {
+ const size_t pos = nameFmt.find(Zstr(":\\"));
+ if (pos == 1) //expect single letter volume
+ return Zstring(nameFmt.c_str(), 3);
+ }
+
+ return Zstring();
+}
+
+
+bool isFatDrive(const Zstring& filepath) //throw()
+{
+ const Zstring volumePath = getVolumeNameFast(filepath);
+ if (volumePath.empty())
+ return false;
+
+ const DWORD bufferSize = MAX_PATH + 1;
+ wchar_t fsName[bufferSize] = {};
+
+ //suprisingly fast: ca. 0.03 ms per call!
+ if (!::GetVolumeInformation(appendSeparator(volumePath).c_str(), //__in_opt LPCTSTR lpRootPathName,
+ nullptr, //__out LPTSTR lpVolumeNameBuffer,
+ 0, //__in DWORD nVolumeNameSize,
+ nullptr, //__out_opt LPDWORD lpVolumeSerialNumber,
+ nullptr, //__out_opt LPDWORD lpMaximumComponentLength,
+ nullptr, //__out_opt LPDWORD lpFileSystemFlags,
+ fsName, //__out LPTSTR lpFileSystemNameBuffer,
+ bufferSize)) //__in DWORD nFileSystemNameSize
+ {
+ assert(false); //shouldn't happen
+ return false;
+ }
+ //DST hack seems to be working equally well for FAT and FAT32 (in particular creation time has 10^-2 s precision as advertised)
+ return fsName == Zstring(L"FAT") ||
+ fsName == Zstring(L"FAT32");
+}
+
+
+//(try to) enhance error messages by showing which processes lock the file
+Zstring getLockingProcessNames(const Zstring& filepath) //throw(), empty string if none found or error occurred
+{
+ if (vistaOrLater())
+ {
+ using namespace fileop;
+ const DllFun<FunType_getLockingProcesses> getLockingProcesses(getDllName(), funName_getLockingProcesses);
+ const DllFun<FunType_freeString> freeString (getDllName(), funName_freeString);
+
+ if (getLockingProcesses && freeString)
+ if (const wchar_t* procList = getLockingProcesses(filepath.c_str()))
+ {
+ ZEN_ON_SCOPE_EXIT(freeString(procList));
+ return procList;
+ }
+ }
+ return Zstring();
+}
+#endif
+}
+
+
+std::uint64_t zen::getFilesize(const Zstring& filepath) //throw FileError
+{
+#ifdef ZEN_WIN
+ {
+ WIN32_FIND_DATA fileInfo = {};
+ const HANDLE searchHandle = ::FindFirstFile(applyLongPathPrefix(filepath).c_str(), &fileInfo);
+ if (searchHandle == INVALID_HANDLE_VALUE)
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(filepath)), L"FindFirstFile", getLastError());
+ ::FindClose(searchHandle);
+
+ if (!isSymlink(fileInfo))
+ return get64BitUInt(fileInfo.nFileSizeLow, fileInfo.nFileSizeHigh);
+ }
+ // WIN32_FILE_ATTRIBUTE_DATA sourceAttr = {};
+ // if (!::GetFileAttributesEx(applyLongPathPrefix(filepath).c_str(), //__in LPCTSTR lpFileName,
+ // GetFileExInfoStandard, //__in GET_FILEEX_INFO_LEVELS fInfoLevelId,
+ // &sourceAttr)) //__out LPVOID lpFileInformation
+
+ //open handle to target of symbolic link
+ const HANDLE hFile = ::CreateFile(applyLongPathPrefix(filepath).c_str(), //_In_ LPCTSTR lpFileName,
+ 0, //_In_ DWORD dwDesiredAccess,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, //_In_ DWORD dwShareMode,
+ nullptr, //_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
+ OPEN_EXISTING, //_In_ DWORD dwCreationDisposition,
+ FILE_FLAG_BACKUP_SEMANTICS, /*needed to open a directory*/ //_In_ DWORD dwFlagsAndAttributes,
+ nullptr); //_In_opt_ HANDLE hTemplateFile
+ if (hFile == INVALID_HANDLE_VALUE)
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(filepath)), L"CreateFile", getLastError());
+ ZEN_ON_SCOPE_EXIT(::CloseHandle(hFile));
+
+ BY_HANDLE_FILE_INFORMATION fileInfoHnd = {};
+ if (!::GetFileInformationByHandle(hFile, &fileInfoHnd))
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(filepath)), L"GetFileInformationByHandle", getLastError());
+
+ return get64BitUInt(fileInfoHnd.nFileSizeLow, fileInfoHnd.nFileSizeHigh);
+
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ struct ::stat fileInfo = {};
+ if (::stat(filepath.c_str(), &fileInfo) != 0)
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(filepath)), L"stat", getLastError());
+
+ return fileInfo.st_size;
+#endif
+}
+
+
+std::uint64_t zen::getFreeDiskSpace(const Zstring& path) //throw FileError
+{
+#ifdef ZEN_WIN
+ ULARGE_INTEGER bytesFree = {};
+ if (!::GetDiskFreeSpaceEx(appendSeparator(path).c_str(), //__in_opt LPCTSTR lpDirectoryName, -> "UNC name [...] must include a trailing backslash, for example, "\\MyServer\MyShare\"
+ &bytesFree, //__out_opt PULARGE_INTEGER lpFreeBytesAvailable,
+ nullptr, //__out_opt PULARGE_INTEGER lpTotalNumberOfBytes,
+ nullptr)) //__out_opt PULARGE_INTEGER lpTotalNumberOfFreeBytes
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(path)), L"GetDiskFreeSpaceEx", getLastError());
+
+ return get64BitUInt(bytesFree.LowPart, bytesFree.HighPart);
+
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ struct statfs info = {};
+ if (::statfs(path.c_str(), &info) != 0)
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(path)), L"statfs", getLastError());
+
+ return static_cast<std::uint64_t>(info.f_bsize) * info.f_bavail;
+#endif
+}
+
+
+bool zen::removeFile(const Zstring& filepath) //throw FileError
+{
+#ifdef ZEN_WIN
+ const wchar_t functionName[] = L"DeleteFile";
+ const Zstring& filepathFmt = applyLongPathPrefix(filepath);
+ if (!::DeleteFile(filepathFmt.c_str()))
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ const wchar_t functionName[] = L"unlink";
+ if (::unlink(filepath.c_str()) != 0)
+#endif
+ {
+ ErrorCode lastError = getLastError();
+#ifdef ZEN_WIN
+ if (lastError == ERROR_ACCESS_DENIED) //function fails if file is read-only
+ {
+ ::SetFileAttributes(filepathFmt.c_str(), FILE_ATTRIBUTE_NORMAL); //(try to) normalize file attributes
+
+ if (::DeleteFile(filepathFmt.c_str())) //now try again...
+ return true;
+ lastError = ::GetLastError();
+ }
+#endif
+ if (!somethingExists(filepath)) //warning: changes global error code!!
+ return false; //neither file nor any other object (e.g. broken symlink) with that name existing - caveat: what if "access is denied"!?!??!?!?
+
+ //begin of "regular" error reporting
+ const std::wstring errorMsg = replaceCpy(_("Cannot delete file %x."), L"%x", fmtFileName(filepath));
+ std::wstring errorDescr = formatSystemError(functionName, lastError);
+
+#ifdef ZEN_WIN
+ if (lastError == ERROR_SHARING_VIOLATION || //-> enhance error message!
+ lastError == ERROR_LOCK_VIOLATION)
+ {
+ const Zstring procList = getLockingProcessNames(filepath); //throw()
+ if (!procList.empty())
+ errorDescr = _("The file is locked by another process:") + L"\n" + procList;
+ }
+#endif
+ throw FileError(errorMsg, errorDescr);
+ }
+ return true;
+}
+
+
+namespace
+{
+/* Usage overview: (avoid circular pattern!)
+
+ renameFile() --> renameFile_sub()
+ | /|\
+ \|/ |
+ Fix8Dot3NameClash()
+*/
+//wrapper for file system rename function:
+void renameFile_sub(const Zstring& oldName, const Zstring& newName) //throw FileError, ErrorDifferentVolume, ErrorTargetExisting
+{
+#ifdef ZEN_WIN
+ const Zstring oldNameFmt = applyLongPathPrefix(oldName);
+ const Zstring newNameFmt = applyLongPathPrefix(newName);
+
+ if (!::MoveFileEx(oldNameFmt.c_str(), //__in LPCTSTR lpExistingFileName,
+ newNameFmt.c_str(), //__in_opt LPCTSTR lpNewFileName,
+ 0)) //__in DWORD dwFlags
+ {
+ DWORD lastError = ::GetLastError(); //copy before directly or indirectly making other system calls!
+
+ if (lastError == ERROR_ACCESS_DENIED) //MoveFileEx may fail to rename a read-only file on a SAMBA-share -> (try to) handle this
+ {
+ const DWORD oldAttr = ::GetFileAttributes(oldNameFmt.c_str());
+ if (oldAttr != INVALID_FILE_ATTRIBUTES && (oldAttr & FILE_ATTRIBUTE_READONLY))
+ {
+ if (::SetFileAttributes(oldNameFmt.c_str(), FILE_ATTRIBUTE_NORMAL)) //remove readonly-attribute
+ {
+ //try again...
+ if (::MoveFileEx(oldNameFmt.c_str(), //__in LPCTSTR lpExistingFileName,
+ newNameFmt.c_str(), //__in_opt LPCTSTR lpNewFileName,
+ 0)) //__in DWORD dwFlags
+ {
+ //(try to) restore file attributes
+ ::SetFileAttributes(newNameFmt.c_str(), oldAttr); //don't handle error
+ return;
+ }
+ else
+ {
+ lastError = ::GetLastError(); //use error code from second call to ::MoveFileEx()
+ //cleanup: (try to) restore file attributes: assume oldName is still existing
+ ::SetFileAttributes(oldNameFmt.c_str(), oldAttr);
+ }
+ }
+ }
+ }
+
+ const std::wstring errorMsg = replaceCpy(replaceCpy(_("Cannot move file %x to %y."), L"%x", L"\n" + fmtFileName(oldName)), L"%y", L"\n" + fmtFileName(newName));
+ std::wstring errorDescr = formatSystemError(L"MoveFileEx", lastError);
+
+ //try to enhance error message:
+ if (lastError == ERROR_SHARING_VIOLATION ||
+ lastError == ERROR_LOCK_VIOLATION)
+ {
+ const Zstring procList = getLockingProcessNames(oldName); //throw()
+ if (!procList.empty())
+ errorDescr = _("The file is locked by another process:") + L"\n" + procList;
+ }
+
+ if (lastError == ERROR_NOT_SAME_DEVICE)
+ throw ErrorDifferentVolume(errorMsg, errorDescr);
+ else if (lastError == ERROR_ALREADY_EXISTS || //-> used on Win7 x64
+ lastError == ERROR_FILE_EXISTS) //-> used by XP???
+ throw ErrorTargetExisting(errorMsg, errorDescr);
+ else
+ throw FileError(errorMsg, errorDescr);
+ }
+
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ if (::rename(oldName.c_str(), newName.c_str()) != 0)
+ {
+ const int lastError = errno; //copy before directly or indirectly making other system calls!
+ const std::wstring errorMsg = replaceCpy(replaceCpy(_("Cannot move file %x to %y."), L"%x", L"\n" + fmtFileName(oldName)), L"%y", L"\n" + fmtFileName(newName));
+ const std::wstring errorDescr = formatSystemError(L"rename", lastError);
+
+ if (lastError == EXDEV)
+ throw ErrorDifferentVolume(errorMsg, errorDescr);
+ else if (lastError == EEXIST)
+ throw ErrorTargetExisting(errorMsg, errorDescr);
+ else
+ throw FileError(errorMsg, errorDescr);
+ }
+#endif
+}
+
+
+#ifdef ZEN_WIN
+/*small wrapper around
+::GetShortPathName()
+::GetLongPathName() */
+template <typename Function>
+Zstring getFilenameFmt(const Zstring& filepath, Function fun) //throw(); returns empty string on error
+{
+ const Zstring filepathFmt = applyLongPathPrefix(filepath);
+
+ const DWORD bufferSize = fun(filepathFmt.c_str(), nullptr, 0);
+ if (bufferSize == 0)
+ return Zstring();
+
+ std::vector<wchar_t> buffer(bufferSize);
+
+ const DWORD charsWritten = fun(filepathFmt.c_str(), //__in LPCTSTR lpszShortPath,
+ &buffer[0], //__out LPTSTR lpszLongPath,
+ bufferSize); //__in DWORD cchBuffer
+ if (charsWritten == 0 || charsWritten >= bufferSize)
+ return Zstring();
+
+ return &buffer[0];
+}
+
+
+Zstring findUnused8Dot3Name(const Zstring& filepath) //find a unique 8.3 short name
+{
+ const Zstring pathPrefix = contains(filepath, FILE_NAME_SEPARATOR) ?
+ (beforeLast(filepath, FILE_NAME_SEPARATOR) + FILE_NAME_SEPARATOR) : Zstring();
+
+ Zstring extension = afterLast(afterLast(filepath, FILE_NAME_SEPARATOR), Zchar('.')); //extension needn't contain reasonable data
+ if (extension.empty())
+ extension = Zstr("FFS");
+ else if (extension.length() > 3)
+ extension.resize(3);
+
+ for (int index = 0; index < 100000000; ++index) //filepath must be representable by <= 8 characters
+ {
+ const Zstring output = pathPrefix + numberTo<Zstring>(index) + Zchar('.') + extension;
+ if (!somethingExists(output)) //ensure uniqueness
+ return output;
+ }
+ throw std::runtime_error(std::string("100000000 files, one for each number, exist in this directory? You're kidding...") + utfCvrtTo<std::string>(pathPrefix));
+}
+
+
+bool have8dot3NameClash(const Zstring& filepath)
+{
+ if (!contains(filepath, FILE_NAME_SEPARATOR))
+ return false;
+
+ if (somethingExists(filepath)) //name OR directory!
+ {
+ const Zstring origName = afterLast(filepath, FILE_NAME_SEPARATOR); //returns the whole string if ch not found
+ const Zstring shortName = afterLast(getFilenameFmt(filepath, ::GetShortPathName), FILE_NAME_SEPARATOR); //throw() returns empty string on error
+ const Zstring longName = afterLast(getFilenameFmt(filepath, ::GetLongPathName ), FILE_NAME_SEPARATOR); //
+
+ if (!shortName.empty() &&
+ !longName .empty() &&
+ EqualFilename()(origName, shortName) &&
+ !EqualFilename()(shortName, longName))
+ {
+ //for filepath short and long file name are equal and another unrelated file happens to have the same short name
+ //e.g. filepath == "TESTWE~1", but another file is existing named "TestWeb" with short name ""TESTWE~1"
+ return true;
+ }
+ }
+ return false;
+}
+
+class Fix8Dot3NameClash //throw FileError
+{
+public:
+ Fix8Dot3NameClash(const Zstring& filepath)
+ {
+ const Zstring longName = afterLast(getFilenameFmt(filepath, ::GetLongPathName), FILE_NAME_SEPARATOR); //throw() returns empty string on error
+
+ unrelatedFile = beforeLast(filepath, FILE_NAME_SEPARATOR) + FILE_NAME_SEPARATOR + longName;
+
+ //find another name in short format: this ensures the actual short name WILL be renamed as well!
+ unrelatedFileParked = findUnused8Dot3Name(filepath);
+
+ //move already existing short name out of the way for now
+ renameFile_sub(unrelatedFile, unrelatedFileParked); //throw FileError, ErrorDifferentVolume
+ //DON'T call renameFile() to avoid reentrance!
+ }
+
+ ~Fix8Dot3NameClash()
+ {
+ //the file system should assign this unrelated file a new (unique) short name
+ try
+ {
+ renameFile_sub(unrelatedFileParked, unrelatedFile); //throw FileError, ErrorDifferentVolume
+ }
+ catch (FileError&) {}
+ }
+private:
+ Zstring unrelatedFile;
+ Zstring unrelatedFileParked;
+};
+#endif
+}
+
+
+//rename file: no copying!!!
+void zen::renameFile(const Zstring& oldName, const Zstring& newName) //throw FileError, ErrorDifferentVolume, ErrorTargetExisting
+{
+ try
+ {
+ renameFile_sub(oldName, newName); //throw FileError, ErrorDifferentVolume, ErrorTargetExisting
+ }
+ catch (const ErrorTargetExisting&)
+ {
+#ifdef ZEN_WIN
+ //try to handle issues with already existing short 8.3 file names on Windows
+ if (have8dot3NameClash(newName))
+ {
+ Fix8Dot3NameClash dummy(newName); //throw FileError; move clashing filepath to the side
+ //now try again...
+ renameFile_sub(oldName, newName); //throw FileError
+ return;
+ }
+#endif
+ throw;
+ }
+}
+
+
+namespace
+{
+class CollectFilesFlat : public zen::TraverseCallback
+{
+public:
+ CollectFilesFlat(std::vector<Zstring>& files, std::vector<Zstring>& dirs) :
+ files_(files),
+ dirs_(dirs) {}
+
+ void onFile(const Zchar* shortName, const Zstring& filepath, const FileInfo& details) override
+ {
+ files_.push_back(filepath);
+ }
+ HandleLink onSymlink(const Zchar* shortName, const Zstring& linkpath, const SymlinkInfo& details) override
+ {
+ if (dirExists(linkpath)) //dir symlink
+ dirs_.push_back(shortName);
+ else //file symlink, broken symlink
+ files_.push_back(shortName);
+ return LINK_SKIP;
+ }
+ TraverseCallback* onDir(const Zchar* shortName, const Zstring& dirpath) override
+ {
+ dirs_.push_back(dirpath);
+ return nullptr; //DON'T traverse into subdirs; removeDirectory works recursively!
+ }
+ HandleError reportDirError (const std::wstring& msg, size_t retryNumber) override { throw FileError(msg); }
+ HandleError reportItemError(const std::wstring& msg, size_t retryNumber, const Zchar* shortName) override { throw FileError(msg); }
+
+private:
+ CollectFilesFlat (const CollectFilesFlat&) = delete;
+ CollectFilesFlat& operator=(const CollectFilesFlat&) = delete;
+
+ std::vector<Zstring>& files_;
+ std::vector<Zstring>& dirs_;
+};
+
+
+void removeDirectoryImpl(const Zstring& directory, //throw FileError
+ const std::function<void (const Zstring& filepath)>& onBeforeFileDeletion,
+ const std::function<void (const Zstring& dirpath )>& onBeforeDirDeletion)
+{
+ assert(somethingExists(directory)); //[!]
+
+#ifdef ZEN_WIN
+ const Zstring directoryFmt = applyLongPathPrefix(directory); //support for \\?\-prefix
+
+ //(try to) normalize file attributes: actually NEEDED for symbolic links also!
+ ::SetFileAttributes(directoryFmt.c_str(), FILE_ATTRIBUTE_NORMAL);
+#endif
+
+ //attention: check if directory is a symlink! Do NOT traverse into it deleting contained files!!!
+ if (symlinkExists(directory)) //remove symlink directly
+ {
+ if (onBeforeDirDeletion)
+ onBeforeDirDeletion(directory); //once per symlink
+#ifdef ZEN_WIN
+ const wchar_t functionName[] = L"RemoveDirectory";
+ if (!::RemoveDirectory(directoryFmt.c_str()))
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ const wchar_t functionName[] = L"unlink";
+ if (::unlink(directory.c_str()) != 0)
+#endif
+ throwFileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtFileName(directory)), functionName, getLastError());
+ }
+ else
+ {
+ std::vector<Zstring> fileList;
+ std::vector<Zstring> dirList;
+ {
+ //get all files and directories from current directory (WITHOUT subdirectories!)
+ CollectFilesFlat cff(fileList, dirList);
+ traverseFolder(directory, cff); //don't follow symlinks
+ }
+
+ //delete directories recursively
+ for (const Zstring& dirpath : dirList)
+ removeDirectoryImpl(dirpath, onBeforeFileDeletion, onBeforeDirDeletion); //throw FileError; call recursively to correctly handle symbolic links
+
+ //delete files
+ for (const Zstring& filepath : fileList)
+ {
+ if (onBeforeFileDeletion)
+ onBeforeFileDeletion(filepath); //call once per file
+ removeFile(filepath); //throw FileError
+ }
+
+ //parent directory is deleted last
+ if (onBeforeDirDeletion)
+ onBeforeDirDeletion(directory); //and once per folder
+#ifdef ZEN_WIN
+ const wchar_t functionName[] = L"RemoveDirectory";
+ if (!::RemoveDirectory(directoryFmt.c_str()))
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ const wchar_t functionName[] = L"rmdir";
+ if (::rmdir(directory.c_str()) != 0)
+#endif
+ throwFileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtFileName(directory)), functionName, getLastError());
+ //may spuriously fail with ERROR_DIR_NOT_EMPTY(145) even though all child items have
+ //successfully been *marked* for deletion, but some application still has a handle open!
+ //e.g. Open "C:\Test\Dir1\Dir2" (filled with lots of files) in Explorer, then delete "C:\Test\Dir1" via ::RemoveDirectory() => Error 145
+ //Sample code: http://us.generation-nt.com/answer/createfile-directory-handles-removing-parent-help-29126332.html
+ }
+}
+
+
+#ifdef ZEN_WIN
+void setFileTimeRaw(const Zstring& filepath,
+ const FILETIME* creationTime, //optional
+ const FILETIME& lastWriteTime,
+ ProcSymlink procSl) //throw FileError
+{
+ {
+ //extra scope for debug check below
+
+ //privilege SE_BACKUP_NAME doesn't seem to be required here for symbolic links
+ //note: setting privileges requires admin rights!
+
+ //opening newly created target file may fail due to some AV-software scanning it: no error, we will wait!
+ //http://support.microsoft.com/?scid=kb%3Ben-us%3B316609&x=17&y=20
+ //-> enable as soon it turns out it is required!
+
+ /*const int retryInterval = 50;
+ const int maxRetries = 2000 / retryInterval;
+ for (int i = 0; i < maxRetries; ++i)
+ {
+ */
+
+ /*
+ if (hTarget == INVALID_HANDLE_VALUE && ::GetLastError() == ERROR_SHARING_VIOLATION)
+ ::Sleep(retryInterval); //wait then retry
+ else //success or unknown failure
+ break;
+ }
+ */
+ //temporarily reset read-only flag if required
+ DWORD attribs = INVALID_FILE_ATTRIBUTES;
+ ZEN_ON_SCOPE_EXIT(
+ if (attribs != INVALID_FILE_ATTRIBUTES)
+ ::SetFileAttributes(applyLongPathPrefix(filepath).c_str(), attribs);
+ );
+
+ auto removeReadonly = [&]() -> bool //throw FileError; may need to remove the readonly-attribute (e.g. on FAT usb drives)
+ {
+ if (attribs == INVALID_FILE_ATTRIBUTES)
+ {
+ const DWORD tmpAttr = ::GetFileAttributes(applyLongPathPrefix(filepath).c_str());
+ if (tmpAttr == INVALID_FILE_ATTRIBUTES)
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(filepath)), L"GetFileAttributes", getLastError());
+
+ if (tmpAttr & FILE_ATTRIBUTE_READONLY)
+ {
+ if (!::SetFileAttributes(applyLongPathPrefix(filepath).c_str(), FILE_ATTRIBUTE_NORMAL))
+ throwFileError(replaceCpy(_("Cannot write file attributes of %x."), L"%x", fmtFileName(filepath)), L"SetFileAttributes", getLastError());
+
+ attribs = tmpAttr; //reapplied on scope exit
+ return true;
+ }
+ }
+ return false;
+ };
+
+ auto openFile = [&](bool conservativeApproach)
+ {
+ return ::CreateFile(applyLongPathPrefix(filepath).c_str(), //_In_ LPCTSTR lpFileName,
+ (conservativeApproach ?
+ //some NAS seem to have issues with FILE_WRITE_ATTRIBUTES, even worse, they may fail silently!
+ //http://sourceforge.net/tracker/?func=detail&atid=1093081&aid=3536680&group_id=234430
+ //Citrix shares seem to have this issue, too, but at least fail with "access denied" => try generic access first:
+ GENERIC_READ | GENERIC_WRITE :
+ //avoids mysterious "access denied" when using "GENERIC_READ | GENERIC_WRITE" on a read-only file, even *after* read-only was removed directly before the call!
+ //http://sourceforge.net/tracker/?func=detail&atid=1093080&aid=3514569&group_id=234430
+ //since former gives an error notification we may very well try FILE_WRITE_ATTRIBUTES second.
+ FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES), //_In_ DWORD dwDesiredAccess,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, //_In_ DWORD dwShareMode,
+ nullptr, //_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
+ OPEN_EXISTING, //_In_ DWORD dwCreationDisposition,
+ (procSl == ProcSymlink::DIRECT ? FILE_FLAG_OPEN_REPARSE_POINT : 0) |
+ FILE_FLAG_BACKUP_SEMANTICS, /*needed to open a directory*/ //_In_ DWORD dwFlagsAndAttributes,
+ nullptr); //_In_opt_ HANDLE hTemplateFile
+ };
+
+ HANDLE hFile = INVALID_HANDLE_VALUE;
+ for (int i = 0; i < 2; ++i) //we will get this handle, no matter what! :)
+ {
+ //1. be conservative
+ hFile = openFile(true);
+ if (hFile == INVALID_HANDLE_VALUE)
+ {
+ if (::GetLastError() == ERROR_ACCESS_DENIED) //fails if file is read-only (or for "other" reasons)
+ if (removeReadonly()) //throw FileError
+ continue;
+
+ //2. be a *little* fancy
+ hFile = openFile(false);
+ if (hFile == INVALID_HANDLE_VALUE)
+ {
+ const DWORD lastError = ::GetLastError(); //copy before directly or indirectly making other system calls!
+ if (lastError == ERROR_ACCESS_DENIED)
+ if (removeReadonly()) //throw FileError
+ continue;
+
+ //3. after these herculean stunts we give up...
+ throwFileError(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtFileName(filepath)), L"CreateFile", lastError);
+ }
+ }
+ break;
+ }
+ ZEN_ON_SCOPE_EXIT(::CloseHandle(hFile));
+
+
+ auto isNullTime = [](const FILETIME& ft) { return ft.dwLowDateTime == 0 && ft.dwHighDateTime == 0; };
+
+ if (!::SetFileTime(hFile, //__in HANDLE hFile,
+ creationTime, //__in_opt const FILETIME *lpCreationTime,
+ nullptr, //__in_opt const FILETIME *lpLastAccessTime,
+ &lastWriteTime)) //__in_opt const FILETIME *lpLastWriteTime
+ {
+ ErrorCode lastError = getLastError(); //copy before directly or indirectly making other system calls!
+
+ //function may fail if file is read-only: https://sourceforge.net/tracker/?func=detail&atid=1093080&aid=3514569&group_id=234430
+ if (lastError == ERROR_ACCESS_DENIED)
+ {
+ //dynamically load windows API function: available with Windows Vista and later
+ typedef BOOL (WINAPI* SetFileInformationByHandleFunc)(HANDLE hFile, FILE_INFO_BY_HANDLE_CLASS FileInformationClass, LPVOID lpFileInformation, DWORD dwBufferSize);
+ const SysDllFun<SetFileInformationByHandleFunc> setFileInformationByHandle(L"kernel32.dll", "SetFileInformationByHandle");
+
+ if (setFileInformationByHandle) //if not: let the original error propagate!
+ {
+ auto setFileInfo = [&](FILE_BASIC_INFO basicInfo) //throw FileError; no const& since SetFileInformationByHandle() requires non-const parameter!
+ {
+ if (!setFileInformationByHandle(hFile, //__in HANDLE hFile,
+ FileBasicInfo, //__in FILE_INFO_BY_HANDLE_CLASS FileInformationClass,
+ &basicInfo, //__in LPVOID lpFileInformation,
+ sizeof(basicInfo))) //__in DWORD dwBufferSize
+ throwFileError(replaceCpy(_("Cannot write file attributes of %x."), L"%x", fmtFileName(filepath)), L"SetFileInformationByHandle", getLastError());
+ };
+
+ auto toLargeInteger = [](const FILETIME& ft) -> LARGE_INTEGER
+ {
+ LARGE_INTEGER tmp = {};
+ tmp.LowPart = ft.dwLowDateTime;
+ tmp.HighPart = ft.dwHighDateTime;
+ return tmp;
+ };
+ //---------------------------------------------------------------------------
+
+ BY_HANDLE_FILE_INFORMATION fileInfo = {};
+ if (::GetFileInformationByHandle(hFile, &fileInfo))
+ if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
+ {
+ FILE_BASIC_INFO basicInfo = {}; //undocumented: file times of "0" stand for "don't change"
+ basicInfo.FileAttributes = FILE_ATTRIBUTE_NORMAL; //[!] the bug in the ticket above requires we set this together with file times!!!
+ basicInfo.LastWriteTime = toLargeInteger(lastWriteTime); //
+ if (creationTime)
+ basicInfo.CreationTime = toLargeInteger(*creationTime);
+
+ //set file time + attributes
+ setFileInfo(basicInfo); //throw FileError
+
+ try //... to restore original file attributes
+ {
+ FILE_BASIC_INFO basicInfo2 = {};
+ basicInfo2.FileAttributes = fileInfo.dwFileAttributes;
+ setFileInfo(basicInfo2); //throw FileError
+ }
+ catch (FileError&) {}
+
+ lastError = ERROR_SUCCESS;
+ }
+ }
+ }
+
+ std::wstring errorMsg = replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtFileName(filepath));
+
+ //add more meaningful message: FAT accepts only a subset of the NTFS date range
+ if (lastError == ERROR_INVALID_PARAMETER &&
+ isFatDrive(filepath))
+ {
+ //we need a low-level reliable routine to format a potentially invalid date => don't use strftime!!!
+ auto fmtDate = [](const FILETIME& ft) -> Zstring
+ {
+ SYSTEMTIME st = {};
+ if (!::FileTimeToSystemTime(&ft, //__in const FILETIME *lpFileTime,
+ &st)) //__out LPSYSTEMTIME lpSystemTime
+ return Zstring();
+
+ Zstring dateTime;
+ {
+ const int bufferSize = ::GetDateFormat(LOCALE_USER_DEFAULT, 0, &st, nullptr, nullptr, 0);
+ if (bufferSize > 0)
+ {
+ std::vector<wchar_t> buffer(bufferSize);
+ if (::GetDateFormat(LOCALE_USER_DEFAULT, //_In_ LCID Locale,
+ 0, //_In_ DWORD dwFlags,
+ &st, //_In_opt_ const SYSTEMTIME *lpDate,
+ nullptr, //_In_opt_ LPCTSTR lpFormat,
+ &buffer[0], //_Out_opt_ LPTSTR lpDateStr,
+ bufferSize) > 0) //_In_ int cchDate
+ dateTime = &buffer[0]; //GetDateFormat() returns char count *including* 0-termination!
+ }
+ }
+
+ const int bufferSize = ::GetTimeFormat(LOCALE_USER_DEFAULT, 0, &st, nullptr, nullptr, 0);
+ if (bufferSize > 0)
+ {
+ std::vector<wchar_t> buffer(bufferSize);
+ if (::GetTimeFormat(LOCALE_USER_DEFAULT, 0, &st, nullptr, &buffer[0], bufferSize) > 0)
+ {
+ dateTime += L" ";
+ dateTime += &buffer[0]; //GetDateFormat() returns char count *including* 0-termination!
+ }
+ }
+ return dateTime;
+ };
+
+ errorMsg += std::wstring(L"\nA FAT volume can only store dates between 1980 and 2107:\n") +
+ L"\twrite (UTC): \t" + fmtDate(lastWriteTime) +
+ (creationTime ? L"\n\tcreate (UTC): \t" + fmtDate(*creationTime) : L"");
+ }
+
+ if (lastError != ERROR_SUCCESS)
+ throwFileError(errorMsg, L"SetFileTime", lastError);
+ }
+ }
+#ifndef NDEBUG //verify written data: mainly required to check consistency of DST hack
+ FILETIME creationTimeDbg = {};
+ FILETIME lastWriteTimeDbg = {};
+
+ HANDLE hFile = ::CreateFile(applyLongPathPrefix(filepath).c_str(), //_In_ LPCTSTR lpFileName,
+ FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES, //_In_ DWORD dwDesiredAccess,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, //_In_ DWORD dwShareMode,
+ nullptr, //_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
+ OPEN_EXISTING, //_In_ DWORD dwCreationDisposition,
+ (procSl == ProcSymlink::DIRECT ? FILE_FLAG_OPEN_REPARSE_POINT : 0) |
+ FILE_FLAG_BACKUP_SEMANTICS, /*needed to open a directory*/ //_In_ DWORD dwFlagsAndAttributes,
+ nullptr); //_In_opt_ HANDLE hTemplateFile
+ assert(hFile != INVALID_HANDLE_VALUE);
+ ZEN_ON_SCOPE_EXIT(::CloseHandle(hFile));
+
+ assert(::GetFileTime(hFile, //probably more up to date than GetFileAttributesEx()!?
+ &creationTimeDbg,
+ nullptr,
+ &lastWriteTimeDbg));
+
+ assert(std::abs(filetimeToTimeT(lastWriteTimeDbg) - filetimeToTimeT(lastWriteTime)) <= 2); //respect 2 second FAT/FAT32 precision
+ //assert(std::abs(filetimeToTimeT(creationTimeDbg ) - filetimeToTimeT(creationTime )) <= 2); -> creation time not available for Linux-hosted Samba shares!
+#endif
+}
+#endif
+}
+
+
+void zen::removeDirectory(const Zstring& directory, //throw FileError
+ const std::function<void (const Zstring& filepath)>& onBeforeFileDeletion,
+ const std::function<void (const Zstring& dirpath)>& onBeforeDirDeletion)
+{
+ //no error situation if directory is not existing! manual deletion relies on it!
+ if (!somethingExists(directory))
+ return; //neither directory nor any other object (e.g. broken symlink) with that name existing
+ removeDirectoryImpl(directory, onBeforeFileDeletion, onBeforeDirDeletion);
+}
+
+
+void zen::setFileTime(const Zstring& filepath, std::int64_t modTime, ProcSymlink procSl) //throw FileError
+{
+#ifdef ZEN_WIN
+ setFileTimeRaw(filepath, nullptr, timetToFileTime(modTime), procSl); //throw FileError
+
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ //sigh, we can't use utimensat on NTFS volumes on Ubuntu: silent failure!!! what morons are programming this shit???
+
+ // struct ::timespec newTimes[2] = {};
+ // newTimes[0].tv_nsec = UTIME_OMIT; //omit access time
+ // newTimes[1].tv_sec = to<time_t>(modTime); //modification time (seconds)
+ //
+ // if (::utimensat(AT_FDCWD, filepath.c_str(), newTimes, procSl == SYMLINK_DIRECT ? AT_SYMLINK_NOFOLLOW : 0) != 0)
+ // throwFileError(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtFileName(filepath)), L"utimensat", getLastError());
+
+ //=> fallback to "retarded-idiot version"! -- DarkByte
+
+ //OS X: utime() is obsoleted by utimes()! utimensat() not yet implemented
+
+ struct ::timeval newTimes[2] = {};
+ newTimes[0].tv_sec = ::time(nullptr); //access time (seconds)
+ newTimes[1].tv_sec = modTime; //modification time (seconds)
+
+ const int rv = procSl == ProcSymlink::FOLLOW ?
+ :: utimes(filepath.c_str(), newTimes) :
+ ::lutimes(filepath.c_str(), newTimes);
+ if (rv != 0)
+ throwFileError(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtFileName(filepath)), L"utimes", getLastError());
+#endif
+}
+
+
+bool zen::supportsPermissions(const Zstring& dirpath) //throw FileError
+{
+#ifdef ZEN_WIN
+ const DWORD bufferSize = MAX_PATH + 1;
+ std::vector<wchar_t> buffer(bufferSize);
+ if (!::GetVolumePathName(dirpath.c_str(), //__in LPCTSTR lpszFileName,
+ &buffer[0], //__out LPTSTR lpszVolumePathName,
+ bufferSize)) //__in DWORD cchBufferLength
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(dirpath)), L"GetVolumePathName", getLastError());
+
+ DWORD fsFlags = 0;
+ if (!::GetVolumeInformation(&buffer[0], //__in_opt LPCTSTR lpRootPathName,
+ nullptr, //__out LPTSTR lpVolumeNameBuffer,
+ 0, //__in DWORD nVolumeNameSize,
+ nullptr, //__out_opt LPDWORD lpVolumeSerialNumber,
+ nullptr, //__out_opt LPDWORD lpMaximumComponentLength,
+ &fsFlags, //__out_opt LPDWORD lpFileSystemFlags,
+ nullptr, //__out LPTSTR lpFileSystemNameBuffer,
+ 0)) //__in DWORD nFileSystemNameSize
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(dirpath)), L"GetVolumeInformation", getLastError());
+
+ return (fsFlags & FILE_PERSISTENT_ACLS) != 0;
+
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ return true;
+#endif
+}
+
+
+namespace
+{
+#ifdef HAVE_SELINUX
+//copy SELinux security context
+void copySecurityContext(const Zstring& source, const Zstring& target, ProcSymlink procSl) //throw FileError
+{
+ security_context_t contextSource = nullptr;
+ const int rv = procSl == ProcSymlink::FOLLOW ?
+ ::getfilecon(source.c_str(), &contextSource) :
+ ::lgetfilecon(source.c_str(), &contextSource);
+ if (rv < 0)
+ {
+ if (errno == ENODATA || //no security context (allegedly) is not an error condition on SELinux
+ errno == EOPNOTSUPP) //extended attributes are not supported by the filesystem
+ return;
+
+ throwFileError(replaceCpy(_("Cannot read security context of %x."), L"%x", fmtFileName(source)), L"getfilecon", getLastError());
+ }
+ ZEN_ON_SCOPE_EXIT(::freecon(contextSource));
+
+ {
+ security_context_t contextTarget = nullptr;
+ const int rv2 = procSl == ProcSymlink::FOLLOW ?
+ ::getfilecon(target.c_str(), &contextTarget) :
+ ::lgetfilecon(target.c_str(), &contextTarget);
+ if (rv2 < 0)
+ {
+ if (errno == EOPNOTSUPP)
+ return;
+ //else: still try to set security context
+ }
+ else
+ {
+ ZEN_ON_SCOPE_EXIT(::freecon(contextTarget));
+
+ if (::strcmp(contextSource, contextTarget) == 0) //nothing to do
+ return;
+ }
+ }
+
+ const int rv3 = procSl == ProcSymlink::FOLLOW ?
+ ::setfilecon(target.c_str(), contextSource) :
+ ::lsetfilecon(target.c_str(), contextSource);
+ if (rv3 < 0)
+ throwFileError(replaceCpy(_("Cannot write security context of %x."), L"%x", fmtFileName(target)), L"setfilecon", getLastError());
+}
+#endif //HAVE_SELINUX
+
+
+//copy permissions for files, directories or symbolic links: requires admin rights
+void copyObjectPermissions(const Zstring& source, const Zstring& target, ProcSymlink procSl) //throw FileError
+{
+#ifdef ZEN_WIN
+ //in contrast to ::SetSecurityInfo(), ::SetFileSecurity() seems to honor the "inherit DACL/SACL" flags
+ //CAVEAT: if a file system does not support ACLs, GetFileSecurity() will return successfully with a *valid* security descriptor containing *no* ACL entries!
+
+ //NOTE: ::GetFileSecurity()/::SetFileSecurity() do NOT follow Symlinks! getResolvedFilePath() requires Vista or later!
+ const Zstring sourceResolved = procSl == ProcSymlink::FOLLOW && symlinkExists(source) ? getResolvedFilePath(source) : source; //throw FileError
+ const Zstring targetResolved = procSl == ProcSymlink::FOLLOW && symlinkExists(target) ? getResolvedFilePath(target) : target; //
+
+ //setting privileges requires admin rights!
+ try
+ {
+ //enable privilege: required to read/write SACL information (only)
+ activatePrivilege(SE_SECURITY_NAME); //throw FileError
+ //Note: trying to copy SACL (SACL_SECURITY_INFORMATION) may return ERROR_PRIVILEGE_NOT_HELD (1314) on Samba shares. This is not due to missing privileges!
+ //However, this is okay, since copying NTFS permissions doesn't make sense in this case anyway
+
+ //enable privilege: required to copy owner information
+ activatePrivilege(SE_RESTORE_NAME); //throw FileError
+
+ //the following privilege may be required according to http://msdn.microsoft.com/en-us/library/aa364399(VS.85).aspx (although not needed nor active in my tests)
+ activatePrivilege(SE_BACKUP_NAME); //throw FileError
+ }
+ catch (const FileError& e)//add some more context description (e.g. user is not an admin)
+ {
+ throw FileError(replaceCpy(_("Cannot read permissions of %x."), L"%x", fmtFileName(sourceResolved)), e.toString());
+ }
+
+
+ std::vector<char> buffer(10000); //example of actually required buffer size: 192 bytes
+ for (;;)
+ {
+ DWORD bytesNeeded = 0;
+ if (::GetFileSecurity(applyLongPathPrefix(sourceResolved).c_str(), //__in LPCTSTR lpFileName, -> long path prefix IS needed, although it is NOT mentioned on MSDN!!!
+ OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION |
+ DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION, //__in SECURITY_INFORMATION RequestedInformation,
+ reinterpret_cast<PSECURITY_DESCRIPTOR>(&buffer[0]), //__out_opt PSECURITY_DESCRIPTOR pSecurityDescriptor,
+ static_cast<DWORD>(buffer.size()), //__in DWORD nLength,
+ &bytesNeeded)) //__out LPDWORD lpnLengthNeeded
+ break;
+ //failure: ...
+ if (bytesNeeded > buffer.size())
+ buffer.resize(bytesNeeded);
+ else
+ throwFileError(replaceCpy(_("Cannot read permissions of %x."), L"%x", fmtFileName(sourceResolved)), L"GetFileSecurity", getLastError());
+ }
+ SECURITY_DESCRIPTOR& secDescr = reinterpret_cast<SECURITY_DESCRIPTOR&>(buffer[0]);
+
+ /*
+ SECURITY_DESCRIPTOR_CONTROL secCtrl = 0;
+ {
+ DWORD ctrlRev = 0;
+ if (!::GetSecurityDescriptorControl(&secDescr, // __in PSECURITY_DESCRIPTOR pSecurityDescriptor,
+ &secCtrl, // __out PSECURITY_DESCRIPTOR_CONTROL pControl,
+ &ctrlRev)) //__out LPDWORD lpdwRevision
+ throw FileErro
+ }
+ //interesting flags:
+ //#define SE_DACL_PRESENT (0x0004)
+ //#define SE_SACL_PRESENT (0x0010)
+ //#define SE_DACL_PROTECTED (0x1000)
+ //#define SE_SACL_PROTECTED (0x2000)
+ */
+
+ if (!::SetFileSecurity(applyLongPathPrefix(targetResolved).c_str(), //__in LPCTSTR lpFileName, -> long path prefix IS needed, although it is NOT mentioned on MSDN!!!
+ OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION |
+ DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION, //__in SECURITY_INFORMATION SecurityInformation,
+ &secDescr)) //__in PSECURITY_DESCRIPTOR pSecurityDescriptor
+ throwFileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtFileName(targetResolved)), L"SetFileSecurity", getLastError());
+
+ /*
+ PSECURITY_DESCRIPTOR buffer = nullptr;
+ PSID owner = nullptr;
+ PSID group = nullptr;
+ PACL dacl = nullptr;
+ PACL sacl = nullptr;
+
+ //File Security and Access Rights: http://msdn.microsoft.com/en-us/library/aa364399(v=VS.85).aspx
+ //SECURITY_INFORMATION Access Rights: http://msdn.microsoft.com/en-us/library/windows/desktop/aa379573(v=vs.85).aspx
+ const HANDLE hSource = ::CreateFile(applyLongPathPrefix(source).c_str(),
+ READ_CONTROL | ACCESS_SYSTEM_SECURITY, //ACCESS_SYSTEM_SECURITY required for SACL access
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ nullptr,
+ OPEN_EXISTING,
+ FILE_FLAG_BACKUP_SEMANTICS | (procSl == SYMLINK_DIRECT ? FILE_FLAG_OPEN_REPARSE_POINT : 0), //FILE_FLAG_BACKUP_SEMANTICS needed to open a directory
+ nullptr);
+ if (hSource == INVALID_HANDLE_VALUE)
+ throw FileError
+ ZEN_ON_SCOPE_EXIT(::CloseHandle(hSource));
+
+ // DWORD rc = ::GetNamedSecurityInfo(const_cast<WCHAR*>(applyLongPathPrefix(source).c_str()), -> does NOT dereference symlinks!
+ DWORD rc = ::GetSecurityInfo(hSource, //__in LPTSTR pObjectName,
+ SE_FILE_OBJECT, //__in SE_OBJECT_TYPE ObjectType,
+ OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION |
+ DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION, //__in SECURITY_INFORMATION SecurityInfo,
+ &owner, //__out_opt PSID *ppsidOwner,
+ &group, //__out_opt PSID *ppsidGroup,
+ &dacl, //__out_opt PACL *ppDacl,
+ &sacl, //__out_opt PACL *ppSacl,
+ &buffer); //__out_opt PSECURITY_DESCRIPTOR *ppSecurityDescriptor
+ if (rc != ERROR_SUCCESS)
+ throw FileError
+ ZEN_ON_SCOPE_EXIT(::LocalFree(buffer));
+
+ SECURITY_DESCRIPTOR_CONTROL secCtrl = 0;
+ {
+ DWORD ctrlRev = 0;
+ if (!::GetSecurityDescriptorControl(buffer, // __in PSECURITY_DESCRIPTOR pSecurityDescriptor,
+ &secCtrl, // __out PSECURITY_DESCRIPTOR_CONTROL pControl,
+ &ctrlRev))//__out LPDWORD lpdwRevision
+ throw FileError
+ }
+
+ //may need to remove the readonly-attribute
+ FileUpdateHandle targetHandle(target, [=]
+ {
+ return ::CreateFile(applyLongPathPrefix(target).c_str(), // lpFileName
+ GENERIC_WRITE | WRITE_OWNER | WRITE_DAC | ACCESS_SYSTEM_SECURITY, // dwDesiredAccess: all four seem to be required!!!
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // dwShareMode
+ nullptr, // lpSecurityAttributes
+ OPEN_EXISTING, // dwCreationDisposition
+ FILE_FLAG_BACKUP_SEMANTICS | (procSl == SYMLINK_DIRECT ? FILE_FLAG_OPEN_REPARSE_POINT : 0), // dwFlagsAndAttributes
+ nullptr); // hTemplateFile
+ });
+
+ if (targetHandle.get() == INVALID_HANDLE_VALUE)
+ throw FileError
+
+ SECURITY_INFORMATION secFlags = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION;
+
+ //SACL/DACL inheritence flag is NOT copied by default: we have to tell ::SetSecurityInfo(() to enable/disable it manually!
+ //if (secCtrl & SE_DACL_PRESENT)
+ secFlags |= (secCtrl & SE_DACL_PROTECTED) ? PROTECTED_DACL_SECURITY_INFORMATION : UNPROTECTED_DACL_SECURITY_INFORMATION;
+ //if (secCtrl & SE_SACL_PRESENT)
+ secFlags |= (secCtrl & SE_SACL_PROTECTED) ? PROTECTED_SACL_SECURITY_INFORMATION : UNPROTECTED_SACL_SECURITY_INFORMATION;
+
+
+ // rc = ::SetNamedSecurityInfo(const_cast<WCHAR*>(applyLongPathPrefix(target).c_str()), //__in LPTSTR pObjectName, -> does NOT dereference symlinks!
+ rc = ::SetSecurityInfo(targetHandle.get(), //__in LPTSTR pObjectName,
+ SE_FILE_OBJECT, //__in SE_OBJECT_TYPE ObjectType,
+ secFlags, //__in SECURITY_INFORMATION SecurityInfo,
+ owner, //__in_opt PSID psidOwner,
+ group, //__in_opt PSID psidGroup,
+ dacl, //__in_opt PACL pDacl,
+ sacl); //__in_opt PACL pSacl
+
+ if (rc != ERROR_SUCCESS)
+ throw FileError
+ */
+
+#elif defined ZEN_LINUX || defined ZEN_MAC
+
+#ifdef HAVE_SELINUX //copy SELinux security context
+ copySecurityContext(source, target, procSl); //throw FileError
+#endif
+
+ struct stat fileInfo = {};
+ if (procSl == ProcSymlink::FOLLOW)
+ {
+ if (::stat(source.c_str(), &fileInfo) != 0)
+ throwFileError(replaceCpy(_("Cannot read permissions of %x."), L"%x", fmtFileName(source)), L"stat", getLastError());
+
+ if (::chown(target.c_str(), fileInfo.st_uid, fileInfo.st_gid) != 0) // may require admin rights!
+ throwFileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtFileName(target)), L"chown", getLastError());
+
+ if (::chmod(target.c_str(), fileInfo.st_mode) != 0)
+ throwFileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtFileName(target)), L"chmod", getLastError());
+ }
+ else
+ {
+ if (::lstat(source.c_str(), &fileInfo) != 0)
+ throwFileError(replaceCpy(_("Cannot read permissions of %x."), L"%x", fmtFileName(source)), L"lstat", getLastError());
+
+ if (::lchown(target.c_str(), fileInfo.st_uid, fileInfo.st_gid) != 0) // may require admin rights!
+ throwFileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtFileName(target)), L"lchown", getLastError());
+
+ if (!symlinkExists(target) && ::chmod(target.c_str(), fileInfo.st_mode) != 0) //setting access permissions doesn't make sense for symlinks on Linux: there is no lchmod()
+ throwFileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtFileName(target)), L"chmod", getLastError());
+ }
+#endif
+}
+
+
+void makeDirectoryRecursively(const Zstring& directory) //FileError, ErrorTargetExisting
+{
+ assert(!endsWith(directory, FILE_NAME_SEPARATOR)); //even "C:\" should be "C:" as input!
+
+ try
+ {
+ makeDirectoryPlain(directory, Zstring(), false); //throw FileError, ErrorTargetExisting, ErrorTargetPathMissing
+ }
+ catch (const ErrorTargetPathMissing&)
+ {
+ //we need to create parent directories first
+ const Zstring dirParent = beforeLast(directory, FILE_NAME_SEPARATOR);
+ if (!dirParent.empty())
+ {
+ //recurse...
+ try
+ {
+ makeDirectoryRecursively(dirParent); //throw FileError, (ErrorTargetExisting)
+ }
+ catch (const ErrorTargetExisting&) { /*parent directory created externally in the meantime? => NOT AN ERROR*/ }
+
+ //now try again...
+ makeDirectoryPlain(directory, Zstring(), false); //throw FileError, (ErrorTargetExisting), (ErrorTargetPathMissing)
+ return;
+ }
+ throw;
+ }
+}
+}
+
+
+void zen::makeDirectory(const Zstring& directory, bool failIfExists) //throw FileError, ErrorTargetExisting
+{
+ //remove trailing separator (even for C:\ root directories)
+ const Zstring dirFormatted = endsWith(directory, FILE_NAME_SEPARATOR) ?
+ beforeLast(directory, FILE_NAME_SEPARATOR) :
+ directory;
+
+ try
+ {
+ makeDirectoryRecursively(dirFormatted); //FileError, ErrorTargetExisting
+ }
+ catch (const ErrorTargetExisting&)
+ {
+ //avoid any file system race-condition by *not* checking directory existence again here!!!
+ if (failIfExists)
+ throw;
+ }
+ catch (const FileError&)
+ {
+ /*
+ could there be situations where a directory/network path exists,
+ but creation fails with error different than "ErrorTargetExisting"??
+ - creation of C:\ fails with ERROR_ACCESS_DENIED rather than ERROR_ALREADY_EXISTS
+ */
+ if (somethingExists(directory)) //a file system race-condition! don't use dirExists() => harmonize with ErrorTargetExisting!
+ {
+ assert(false);
+ if (failIfExists)
+ throw; //do NOT convert to ErrorTargetExisting: if "failIfExists", not getting a ErrorTargetExisting *atomically* is unexpected!
+ }
+ else
+ throw;
+ }
+}
+
+
+void zen::makeDirectoryPlain(const Zstring& directory, //throw FileError, ErrorTargetExisting, ErrorTargetPathMissing
+ const Zstring& templateDir,
+ bool copyFilePermissions)
+{
+#ifdef ZEN_WIN
+ //special handling for volume root: trying to create existing root directory results in ERROR_ACCESS_DENIED rather than ERROR_ALREADY_EXISTS!
+ Zstring dirTmp = removeLongPathPrefix(endsWith(directory, FILE_NAME_SEPARATOR) ?
+ beforeLast(directory, FILE_NAME_SEPARATOR) :
+ directory);
+ if (dirTmp.size() == 2 &&
+ std::iswalpha(dirTmp[0]) && dirTmp[1] == L':')
+ {
+ dirTmp += FILE_NAME_SEPARATOR; //we do not support "C:" to represent a relative path!
+
+ const ErrorCode lastError = somethingExists(dirTmp) ? ERROR_ALREADY_EXISTS : ERROR_PATH_NOT_FOUND; //don't use dirExists() => harmonize with ErrorTargetExisting!
+
+ const std::wstring errorMsg = replaceCpy(_("Cannot create directory %x."), L"%x", fmtFileName(dirTmp));
+ const std::wstring errorDescr = formatSystemError(L"CreateDirectory", lastError);
+
+ if (lastError == ERROR_ALREADY_EXISTS)
+ throw ErrorTargetExisting(errorMsg, errorDescr);
+ throw FileError(errorMsg, errorDescr); //[!] this is NOT a ErrorTargetPathMissing case!
+ }
+
+ //don't use ::CreateDirectoryEx:
+ //- it may fail with "wrong parameter (error code 87)" when source is on mapped online storage
+ //- automatically copies symbolic links if encountered: unfortunately it doesn't copy symlinks over network shares but silently creates empty folders instead (on XP)!
+ //- it isn't able to copy most junctions because of missing permissions (although target path can be retrieved alternatively!)
+ if (!::CreateDirectory(applyLongPathPrefixCreateDir(directory).c_str(), //__in LPCTSTR lpPathName,
+ nullptr)) //__in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes
+ {
+ DWORD lastError = ::GetLastError(); //copy before directly or indirectly making other system calls!
+
+ //handle issues with already existing short 8.3 file names on Windows
+ if (lastError == ERROR_ALREADY_EXISTS)
+ if (have8dot3NameClash(directory))
+ {
+ Fix8Dot3NameClash dummy(directory); //throw FileError; move clashing object to the side
+
+ //now try again...
+ if (::CreateDirectory(applyLongPathPrefixCreateDir(directory).c_str(), nullptr))
+ lastError = ERROR_SUCCESS;
+ else
+ lastError = ::GetLastError();
+ }
+
+ if (lastError != ERROR_SUCCESS)
+ {
+ const std::wstring errorMsg = replaceCpy(_("Cannot create directory %x."), L"%x", fmtFileName(directory));
+ const std::wstring errorDescr = formatSystemError(L"CreateDirectory", lastError);
+
+ if (lastError == ERROR_ALREADY_EXISTS)
+ throw ErrorTargetExisting(errorMsg, errorDescr);
+ else if (lastError == ERROR_PATH_NOT_FOUND)
+ throw ErrorTargetPathMissing(errorMsg, errorDescr);
+ throw FileError(errorMsg, errorDescr);
+ }
+ }
+
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ if (::mkdir(directory.c_str(), 0755) != 0) //mode: drwxr-xr-x
+ {
+ const int lastError = errno; //copy before directly or indirectly making other system calls!
+ const std::wstring errorMsg = replaceCpy(_("Cannot create directory %x."), L"%x", fmtFileName(directory));
+ const std::wstring errorDescr = formatSystemError(L"mkdir", lastError);
+
+ if (lastError == EEXIST)
+ throw ErrorTargetExisting(errorMsg, errorDescr);
+ else if (lastError == ENOENT)
+ throw ErrorTargetPathMissing(errorMsg, errorDescr);
+ throw FileError(errorMsg, errorDescr);
+ }
+#endif
+
+ if (!templateDir.empty())
+ {
+#ifdef ZEN_WIN
+ //optional: try to copy file attributes (dereference symlinks and junctions)
+ const HANDLE hDirSrc = ::CreateFile(zen::applyLongPathPrefix(templateDir).c_str(), //_In_ LPCTSTR lpFileName,
+ 0, //_In_ DWORD dwDesiredAccess,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, //_In_ DWORD dwShareMode,
+ nullptr, //_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
+ OPEN_EXISTING, //_In_ DWORD dwCreationDisposition,
+ // FILE_FLAG_OPEN_REPARSE_POINT -> no, we follow symlinks!
+ FILE_FLAG_BACKUP_SEMANTICS, /*needed to open a directory*/ //_In_ DWORD dwFlagsAndAttributes,
+ nullptr); //_In_opt_ HANDLE hTemplateFile
+ if (hDirSrc != INVALID_HANDLE_VALUE) //dereferencing a symbolic link usually fails if it is located on network drive or client is XP: NOT really an error...
+ {
+ ZEN_ON_SCOPE_EXIT(::CloseHandle(hDirSrc));
+
+ BY_HANDLE_FILE_INFORMATION dirInfo = {};
+ if (::GetFileInformationByHandle(hDirSrc, &dirInfo))
+ {
+ ::SetFileAttributes(applyLongPathPrefix(directory).c_str(), dirInfo.dwFileAttributes);
+ //copy "read-only and system attributes": http://blogs.msdn.com/b/oldnewthing/archive/2003/09/30/55100.aspx
+
+ const bool isEncrypted = (dirInfo.dwFileAttributes & FILE_ATTRIBUTE_ENCRYPTED) != 0;
+ const bool isCompressed = (dirInfo.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED) != 0;
+
+ if (isEncrypted)
+ ::EncryptFile(directory.c_str()); //seems no long path is required (check passed!)
+
+ HANDLE hDirTrg = ::CreateFile(applyLongPathPrefix(directory).c_str(), //_In_ LPCTSTR lpFileName,
+ GENERIC_READ | GENERIC_WRITE, //_In_ DWORD dwDesiredAccess,
+ /*read access required for FSCTL_SET_COMPRESSION*/
+ FILE_SHARE_READ |
+ FILE_SHARE_WRITE |
+ FILE_SHARE_DELETE, //_In_ DWORD dwShareMode,
+ nullptr, //_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
+ OPEN_EXISTING, //_In_ DWORD dwCreationDisposition,
+ FILE_FLAG_BACKUP_SEMANTICS, //_In_ DWORD dwFlagsAndAttributes,
+ nullptr); //_In_opt_ HANDLE hTemplateFile
+ if (hDirTrg != INVALID_HANDLE_VALUE)
+ {
+ ZEN_ON_SCOPE_EXIT(::CloseHandle(hDirTrg));
+
+ if (isCompressed)
+ {
+ USHORT cmpState = COMPRESSION_FORMAT_DEFAULT;
+ DWORD bytesReturned = 0;
+ /*bool rv = */::DeviceIoControl(hDirTrg, //_In_ HANDLE hDevice,
+ FSCTL_SET_COMPRESSION, //_In_ DWORD dwIoControlCode,
+ &cmpState, //_In_opt_ LPVOID lpInBuffer,
+ sizeof(cmpState), //_In_ DWORD nInBufferSize,
+ nullptr, //_Out_opt_ LPVOID lpOutBuffer,
+ 0, //_In_ DWORD nOutBufferSize,
+ &bytesReturned, //_Out_opt_ LPDWORD lpBytesReturned,
+ nullptr); //_Inout_opt_ LPOVERLAPPED lpOverlapped
+ }
+
+ //(try to) set creation and modification time
+ /*bool rv = */::SetFileTime(hDirTrg, //_In_ HANDLE hFile,
+ &dirInfo.ftCreationTime, //_Out_opt_ LPFILETIME lpCreationTime,
+ nullptr, //_Out_opt_ LPFILETIME lpLastAccessTime,
+ &dirInfo.ftLastWriteTime); //_Out_opt_ LPFILETIME lpLastWriteTime
+ }
+ }
+ }
+#endif
+
+ zen::ScopeGuard guardNewDir = zen::makeGuard([&] { try { removeDirectory(directory); } catch (FileError&) {} }); //ensure cleanup:
+
+ //enforce copying file permissions: it's advertized on GUI...
+ if (copyFilePermissions)
+ copyObjectPermissions(templateDir, directory, ProcSymlink::FOLLOW); //throw FileError
+
+ guardNewDir.dismiss(); //target has been created successfully!
+ }
+}
+
+
+void zen::copySymlink(const Zstring& sourceLink, const Zstring& targetLink, bool copyFilePermissions) //throw FileError
+{
+ const Zstring linkPath = getSymlinkTargetRaw(sourceLink); //throw FileError; accept broken symlinks
+
+#ifdef ZEN_WIN
+ const bool isDirLink = [&]() -> bool
+ {
+ const DWORD ret = ::GetFileAttributes(applyLongPathPrefix(sourceLink).c_str());
+ return ret != INVALID_FILE_ATTRIBUTES && (ret & FILE_ATTRIBUTE_DIRECTORY);
+ }();
+
+ typedef BOOLEAN (WINAPI* CreateSymbolicLinkFunc)(LPCTSTR lpSymlinkFileName, LPCTSTR lpTargetFileName, DWORD dwFlags);
+ const SysDllFun<CreateSymbolicLinkFunc> createSymbolicLink(L"kernel32.dll", "CreateSymbolicLinkW");
+
+ if (!createSymbolicLink)
+ throw FileError(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), L"%x", L"\n" + fmtFileName(sourceLink)), L"%y", L"\n" + fmtFileName(targetLink)),
+ replaceCpy(_("Cannot find system function %x."), L"%x", L"\"CreateSymbolicLinkW\""));
+
+ const wchar_t functionName[] = L"CreateSymbolicLinkW";
+ if (!createSymbolicLink(targetLink.c_str(), //__in LPTSTR lpSymlinkFileName, - seems no long path prefix is required...
+ linkPath.c_str(), //__in LPTSTR lpTargetFileName,
+ (isDirLink ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0))) //__in DWORD dwFlags
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ const wchar_t functionName[] = L"symlink";
+ if (::symlink(linkPath.c_str(), targetLink.c_str()) != 0)
+#endif
+ throwFileError(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), L"%x", L"\n" + fmtFileName(sourceLink)), L"%y", L"\n" + fmtFileName(targetLink)), functionName, getLastError());
+
+ //allow only consistent objects to be created -> don't place before ::symlink, targetLink may already exist!
+ zen::ScopeGuard guardNewLink = zen::makeGuard([&]
+ {
+ try
+ {
+#ifdef ZEN_WIN
+ if (isDirLink)
+ removeDirectory(targetLink); //throw FileError
+ else
+#endif
+ removeFile(targetLink); //throw FileError
+ }
+ catch (FileError&) {}
+ });
+
+ //file times: essential for sync'ing a symlink: enforce this! (don't just try!)
+#ifdef ZEN_WIN
+ WIN32_FILE_ATTRIBUTE_DATA sourceAttr = {};
+ if (!::GetFileAttributesEx(applyLongPathPrefix(sourceLink).c_str(), //__in LPCTSTR lpFileName,
+ GetFileExInfoStandard, //__in GET_FILEEX_INFO_LEVELS fInfoLevelId,
+ &sourceAttr)) //__out LPVOID lpFileInformation
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(sourceLink)), L"GetFileAttributesEx", getLastError());
+
+ setFileTimeRaw(targetLink, &sourceAttr.ftCreationTime, sourceAttr.ftLastWriteTime, ProcSymlink::DIRECT); //throw FileError
+
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ struct ::stat srcInfo = {};
+ if (::lstat(sourceLink.c_str(), &srcInfo) != 0)
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(sourceLink)), L"lstat", getLastError());
+
+ setFileTime(targetLink, srcInfo.st_mtime, ProcSymlink::DIRECT); //throw FileError
+#endif
+
+ if (copyFilePermissions)
+ copyObjectPermissions(sourceLink, targetLink, ProcSymlink::DIRECT); //throw FileError
+
+ guardNewLink.dismiss(); //target has been created successfully!
+}
+
+
+namespace
+{
+#ifdef ZEN_WIN
+/*
+ CopyFileEx() BackupRead() FileRead()
+ --------------------------------------------
+Attributes YES NO NO
+create time NO NO NO
+ADS YES YES NO
+Encrypted YES NO(silent fail!) NO
+Compressed NO NO NO
+Sparse NO YES NO
+Nonstandard FS YES UNKNOWN -> issues writing ADS to Samba, issues reading from NAS, error copying files having "blocked" state... ect.
+PERF - 6% faster
+
+Mark stream as compressed: FSCTL_SET_COMPRESSION - compatible with both BackupRead() and FileRead()
+
+
+Current support for combinations of NTFS extended attributes:
+
+source attr | tf normal | tf compressed | tf encrypted | handled by
+============|==================================================================
+ --- | --- -C- E-- copyFileWindowsDefault
+ --S | --S -CS E-S copyFileWindowsSparse
+ -C- | -C- -C- E-- copyFileWindowsDefault
+ -CS | -CS -CS E-S copyFileWindowsSparse
+ E-- | E-- E-- E-- copyFileWindowsDefault
+ E-S | E-- (NOK) E-- (NOK) E-- (NOK) copyFileWindowsDefault -> may fail with ERROR_DISK_FULL!!
+
+tf := target folder
+E := encrypted
+C := compressed
+S := sparse
+NOK := current behavior is not optimal/OK yet.
+
+Note: - if target parent folder is compressed or encrypted, both attributes are added automatically during file creation!
+ - "compressed" and "encrypted" are mutually exclusive: http://support.microsoft.com/kb/223093/en-us
+*/
+
+
+//due to issues on non-NTFS volumes, we should use the copy-as-sparse routine only if required and supported!
+bool canCopyAsSparse(DWORD fileAttrSource, const Zstring& targetFile) //throw ()
+{
+ const bool sourceIsEncrypted = (fileAttrSource & FILE_ATTRIBUTE_ENCRYPTED) != 0;
+ const bool sourceIsSparse = (fileAttrSource & FILE_ATTRIBUTE_SPARSE_FILE) != 0;
+
+ if (sourceIsEncrypted || !sourceIsSparse) //BackupRead() silently fails reading encrypted files!
+ return false; //small perf optimization: don't check "targetFile" if not needed
+
+ //------------------------------------------------------------------------------------
+ const DWORD bufferSize = 10000;
+ std::vector<wchar_t> buffer(bufferSize);
+
+ //full pathName need not yet exist!
+ if (!::GetVolumePathName(targetFile.c_str(), //__in LPCTSTR lpszFileName,
+ &buffer[0], //__out LPTSTR lpszVolumePathName,
+ bufferSize)) //__in DWORD cchBufferLength
+ return false;
+
+ DWORD fsFlagsTarget = 0;
+ if (!::GetVolumeInformation(&buffer[0], //__in_opt LPCTSTR lpRootPathName
+ nullptr, //__out_opt LPTSTR lpVolumeNameBuffer,
+ 0, //__in DWORD nVolumeNameSize,
+ nullptr, //__out_opt LPDWORD lpVolumeSerialNumber,
+ nullptr, //__out_opt LPDWORD lpMaximumComponentLength,
+ &fsFlagsTarget, //__out_opt LPDWORD lpFileSystemFlags,
+ nullptr, //__out LPTSTR lpFileSystemNameBuffer,
+ 0)) //__in DWORD nFileSystemNameSize
+ return false;
+
+ const bool targetSupportSparse = (fsFlagsTarget & FILE_SUPPORTS_SPARSE_FILES) != 0;
+
+ return targetSupportSparse;
+ //both source and target must not be FAT since copyFileWindowsSparse() does no DST hack! implicitly guaranteed at this point!
+}
+
+
+bool canCopyAsSparse(const Zstring& sourceFile, const Zstring& targetFile) //throw ()
+{
+ //follow symlinks!
+ HANDLE hSource = ::CreateFile(applyLongPathPrefix(sourceFile).c_str(), //_In_ LPCTSTR lpFileName,
+ 0, //_In_ DWORD dwDesiredAccess,
+ FILE_SHARE_READ | //all shared modes are required to read files that are open in other applications
+ FILE_SHARE_WRITE |
+ FILE_SHARE_DELETE, //_In_ DWORD dwShareMode,
+ nullptr, //_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
+ OPEN_EXISTING, //_In_ DWORD dwCreationDisposition,
+ 0, //_In_ DWORD dwFlagsAndAttributes,
+ nullptr); //_In_opt_ HANDLE hTemplateFile
+ if (hSource == INVALID_HANDLE_VALUE)
+ return false;
+ ZEN_ON_SCOPE_EXIT(::CloseHandle(hSource));
+
+ BY_HANDLE_FILE_INFORMATION fileInfoSource = {};
+ if (!::GetFileInformationByHandle(hSource, &fileInfoSource))
+ return false;
+
+ return canCopyAsSparse(fileInfoSource.dwFileAttributes, targetFile); //throw ()
+}
+
+
+//precondition: canCopyAsSparse() must return "true"!
+void copyFileWindowsSparse(const Zstring& sourceFile,
+ const Zstring& targetFile,
+ const std::function<void(std::int64_t bytesDelta)>& onUpdateCopyStatus,
+ InSyncAttributes* newAttrib) //throw FileError, ErrorTargetExisting, ErrorFileLocked
+{
+ assert(canCopyAsSparse(sourceFile, targetFile));
+
+ //try to get backup read and write privileges: who knows, maybe this helps solve some obscure "access denied" errors
+ try { activatePrivilege(SE_BACKUP_NAME); }
+ catch (const FileError&) {}
+ try { activatePrivilege(SE_RESTORE_NAME); }
+ catch (const FileError&) {}
+
+ //open sourceFile for reading
+ HANDLE hFileSource = ::CreateFile(applyLongPathPrefix(sourceFile).c_str(), //_In_ LPCTSTR lpFileName,
+ GENERIC_READ, //_In_ DWORD dwDesiredAccess,
+ FILE_SHARE_READ | FILE_SHARE_DELETE, //_In_ DWORD dwShareMode,
+ nullptr, //_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
+ OPEN_EXISTING, //_In_ DWORD dwCreationDisposition,
+ //FILE_FLAG_OVERLAPPED must not be used!
+ //FILE_FLAG_NO_BUFFERING should not be used!
+ FILE_FLAG_SEQUENTIAL_SCAN |
+ FILE_FLAG_BACKUP_SEMANTICS, //_In_ DWORD dwFlagsAndAttributes,
+ nullptr); //_In_opt_ HANDLE hTemplateFile
+ if (hFileSource == INVALID_HANDLE_VALUE)
+ {
+ const DWORD lastError = ::GetLastError(); //copy before directly or indirectly making other system calls!
+
+ const std::wstring errorMsg = replaceCpy(_("Cannot read file %x."), L"%x", fmtFileName(sourceFile));
+ std::wstring errorDescr = formatSystemError(L"CreateFile", lastError);
+
+ //if file is locked throw "ErrorFileLocked" instead!
+ if (lastError == ERROR_SHARING_VIOLATION ||
+ lastError == ERROR_LOCK_VIOLATION)
+ {
+ const Zstring procList = getLockingProcessNames(sourceFile); //throw()
+ if (!procList.empty())
+ errorDescr = _("The file is locked by another process:") + L"\n" + procList;
+ throw ErrorFileLocked(errorMsg, errorDescr);
+ }
+
+ throw FileError(errorMsg, errorDescr);
+ }
+ ZEN_ON_SCOPE_EXIT(::CloseHandle(hFileSource));
+
+ //----------------------------------------------------------------------
+ BY_HANDLE_FILE_INFORMATION fileInfoSource = {};
+ if (!::GetFileInformationByHandle(hFileSource, &fileInfoSource))
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(sourceFile)), L"GetFileInformationByHandle", getLastError());
+
+ //----------------------------------------------------------------------
+ const DWORD validAttribs = FILE_ATTRIBUTE_NORMAL | //"This attribute is valid only if used alone."
+ FILE_ATTRIBUTE_READONLY |
+ FILE_ATTRIBUTE_HIDDEN |
+ FILE_ATTRIBUTE_SYSTEM |
+ FILE_ATTRIBUTE_ARCHIVE | //those two are not set properly (not worse than ::CopyFileEx())
+ FILE_ATTRIBUTE_NOT_CONTENT_INDEXED; //
+ //FILE_ATTRIBUTE_ENCRYPTED -> no!
+
+ //create targetFile and open it for writing
+ HANDLE hFileTarget = ::CreateFile(applyLongPathPrefix(targetFile).c_str(), //_In_ LPCTSTR lpFileName,
+ GENERIC_READ | GENERIC_WRITE, //_In_ DWORD dwDesiredAccess,
+ //read access required for FSCTL_SET_COMPRESSION
+ FILE_SHARE_DELETE, //_In_ DWORD dwShareMode,
+ //FILE_SHARE_DELETE is required to rename file while handle is open!
+ nullptr, //_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
+ CREATE_NEW, //_In_ DWORD dwCreationDisposition,
+ //FILE_FLAG_OVERLAPPED must not be used! FILE_FLAG_NO_BUFFERING should not be used!
+ (fileInfoSource.dwFileAttributes & validAttribs) |
+ FILE_FLAG_SEQUENTIAL_SCAN |
+ FILE_FLAG_BACKUP_SEMANTICS, //_In_ DWORD dwFlagsAndAttributes,
+ nullptr); //_In_opt_ HANDLE hTemplateFile
+ if (hFileTarget == INVALID_HANDLE_VALUE)
+ {
+ const DWORD lastError = ::GetLastError(); //copy before directly or indirectly making other system calls!
+ const std::wstring errorMsg = replaceCpy(_("Cannot write file %x."), L"%x", fmtFileName(targetFile));
+ const std::wstring errorDescr = formatSystemError(L"CreateFile", lastError);
+
+ if (lastError == ERROR_FILE_EXISTS || //confirmed to be used
+ lastError == ERROR_ALREADY_EXISTS) //comment on msdn claims, this one is used on Windows Mobile 6
+ throw ErrorTargetExisting(errorMsg, errorDescr);
+
+ //if (lastError == ERROR_PATH_NOT_FOUND) throw ErrorTargetPathMissing(errorMsg, errorDescr);
+
+ throw FileError(errorMsg, errorDescr);
+ }
+ ScopeGuard guardTarget = makeGuard([&] { try { removeFile(targetFile); } catch (FileError&) {} }); //transactional behavior: guard just after opening target and before managing hFileTarget
+ ZEN_ON_SCOPE_EXIT(::CloseHandle(hFileTarget));
+
+ //----------------------------------------------------------------------
+ BY_HANDLE_FILE_INFORMATION fileInfoTarget = {};
+ if (!::GetFileInformationByHandle(hFileTarget, &fileInfoTarget))
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(targetFile)), L"GetFileInformationByHandle", getLastError());
+
+ //return up-to-date file attributes
+ if (newAttrib)
+ {
+ newAttrib->fileSize = get64BitUInt(fileInfoSource.nFileSizeLow, fileInfoSource.nFileSizeHigh);
+ newAttrib->modificationTime = filetimeToTimeT(fileInfoSource.ftLastWriteTime); //no DST hack (yet)
+ newAttrib->sourceFileId = extractFileId(fileInfoSource);
+ newAttrib->targetFileId = extractFileId(fileInfoTarget);
+ }
+
+ //#################### copy NTFS compressed attribute #########################
+ const bool sourceIsCompressed = (fileInfoSource.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED) != 0;
+ const bool targetIsCompressed = (fileInfoTarget.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED) != 0; //already set by CreateFile if target parent folder is compressed!
+ if (sourceIsCompressed && !targetIsCompressed)
+ {
+ USHORT cmpState = COMPRESSION_FORMAT_DEFAULT;
+ DWORD bytesReturned = 0;
+ if (!::DeviceIoControl(hFileTarget, //_In_ HANDLE hDevice,
+ FSCTL_SET_COMPRESSION, //_In_ DWORD dwIoControlCode,
+ &cmpState, //_In_opt_ LPVOID lpInBuffer,
+ sizeof(cmpState), //_In_ DWORD nInBufferSize,
+ nullptr, //_Out_opt_ LPVOID lpOutBuffer,
+ 0, //_In_ DWORD nOutBufferSize,
+ &bytesReturned, //_Out_opt_ LPDWORD lpBytesReturned,
+ nullptr)) //_Inout_opt_ LPOVERLAPPED lpOverlapped
+ {} //may legitimately fail with ERROR_INVALID_FUNCTION if:
+ // - target folder is encrypted
+ // - target volume does not support compressed attribute -> unlikely in this context
+ }
+ //#############################################################################
+
+ //although it seems the sparse attribute is set automatically by BackupWrite, we are required to do this manually: http://support.microsoft.com/kb/271398/en-us
+ //Quote: It is the responsibility of the backup utility to apply file attributes to a file after it is restored by using BackupWrite.
+ //The application should retrieve the attributes by using GetFileAttributes prior to creating a backup with BackupRead.
+ //If a file originally had the sparse attribute (FILE_ATTRIBUTE_SPARSE_FILE), the backup utility must explicitly set the
+ //attribute on the restored file.
+
+ //if (sourceIsSparse && targetSupportsSparse) -> no need to check, this is our precondition!
+ {
+ DWORD bytesReturned = 0;
+ if (!::DeviceIoControl(hFileTarget, //_In_ HANDLE hDevice,
+ FSCTL_SET_SPARSE, //_In_ DWORD dwIoControlCode,
+ nullptr, //_In_opt_ LPVOID lpInBuffer,
+ 0, //_In_ DWORD nInBufferSize,
+ nullptr, //_Out_opt_ LPVOID lpOutBuffer,
+ 0, //_In_ DWORD nOutBufferSize,
+ &bytesReturned, //_Out_opt_ LPDWORD lpBytesReturned,
+ nullptr)) //_Inout_opt_ LPOVERLAPPED lpOverlapped
+ throwFileError(replaceCpy(_("Cannot write file attributes of %x."), L"%x", fmtFileName(targetFile)), L"DeviceIoControl, FSCTL_SET_SPARSE", getLastError());
+ }
+
+ //----------------------------------------------------------------------
+ const DWORD BUFFER_SIZE = 128 * 1024; //must be greater than sizeof(WIN32_STREAM_ID)
+ std::vector<BYTE> buffer(BUFFER_SIZE);
+
+ LPVOID contextRead = nullptr; //manage context for BackupRead()/BackupWrite()
+ LPVOID contextWrite = nullptr; //
+
+ ZEN_ON_SCOPE_EXIT(
+ if (contextRead ) ::BackupRead (0, nullptr, 0, nullptr, true, false, &contextRead); //lpContext must be passed [...] all other parameters are ignored.
+ if (contextWrite) ::BackupWrite(0, nullptr, 0, nullptr, true, false, &contextWrite); );
+
+ //stream-copy sourceFile to targetFile
+ bool eof = false;
+ bool someBytesWritten = false; //try to detect failure reading encrypted files
+ do
+ {
+ DWORD bytesRead = 0;
+ if (!::BackupRead(hFileSource, //__in HANDLE hFile,
+ &buffer[0], //__out LPBYTE lpBuffer,
+ BUFFER_SIZE, //__in DWORD nNumberOfBytesToRead,
+ &bytesRead, //__out LPDWORD lpNumberOfBytesRead,
+ false, //__in BOOL bAbort,
+ false, //__in BOOL bProcessSecurity,
+ &contextRead)) //__out LPVOID *lpContext
+ throwFileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtFileName(sourceFile)), L"BackupRead", getLastError()); //better use fine-granular error messages "reading/writing"!
+
+ if (bytesRead > BUFFER_SIZE)
+ throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtFileName(sourceFile)), L"buffer overflow"); //user should never see this
+
+ if (bytesRead < BUFFER_SIZE)
+ eof = true;
+
+ DWORD bytesWritten = 0;
+ if (!::BackupWrite(hFileTarget, //__in HANDLE hFile,
+ &buffer[0], //__in LPBYTE lpBuffer,
+ bytesRead, //__in DWORD nNumberOfBytesToWrite,
+ &bytesWritten, //__out LPDWORD lpNumberOfBytesWritten,
+ false, //__in BOOL bAbort,
+ false, //__in BOOL bProcessSecurity,
+ &contextWrite)) //__out LPVOID *lpContext
+ throwFileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtFileName(targetFile)), L"BackupWrite", getLastError());
+
+ if (bytesWritten != bytesRead)
+ throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtFileName(targetFile)), L"incomplete write"); //user should never see this
+
+ //total bytes transferred may be larger than file size! context information + ADS or smaller (sparse, compressed)!
+
+ //invoke callback method to update progress indicators
+ if (onUpdateCopyStatus)
+ onUpdateCopyStatus(bytesRead); //throw X!
+
+ if (bytesRead > 0)
+ someBytesWritten = true;
+ }
+ while (!eof);
+
+ //DST hack not required, since both source and target volumes cannot be FAT!
+
+ //::BackupRead() silently fails reading encrypted files -> double check!
+ if (!someBytesWritten && get64BitUInt(fileInfoSource.nFileSizeLow, fileInfoSource.nFileSizeHigh) != 0U)
+ //note: there is no guaranteed ordering relation beween bytes transferred and file size! Consider ADS (>) and compressed/sparse files (<)!
+ throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtFileName(sourceFile)), L"unknown error"); //user should never see this -> this method is called only if "canCopyAsSparse()"
+
+ //time needs to be set at the end: BackupWrite() changes modification time
+ if (!::SetFileTime(hFileTarget,
+ &fileInfoSource.ftCreationTime,
+ nullptr,
+ &fileInfoSource.ftLastWriteTime))
+ throwFileError(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtFileName(targetFile)), L"SetFileTime", getLastError());
+
+ guardTarget.dismiss();
+
+ /*
+ //create sparse file for testing:
+ HANDLE hSparse = ::CreateFile(L"C:\\sparse.file",
+ GENERIC_READ | GENERIC_WRITE, //read access required for FSCTL_SET_COMPRESSION
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ nullptr,
+ CREATE_NEW,
+ FILE_FLAG_SEQUENTIAL_SCAN,
+ nullptr);
+ if (hFileTarget == INVALID_HANDLE_VALUE)
+ throw FileError(L"fail");
+ ZEN_ON_SCOPE_EXIT(::CloseHandle(hSparse));
+
+ DWORD br = 0;
+ if (!::DeviceIoControl(hSparse, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &br,nullptr))
+ throw FileError(L"fail");
+
+ LARGE_INTEGER liDistanceToMove = {};
+ liDistanceToMove.QuadPart = 1024 * 1024 * 1024; //create 5 TB sparse file
+ liDistanceToMove.QuadPart *= 5 * 1024; //maximum file size on NTFS: 16 TB - 64 kB
+ if (!::SetFilePointerEx(hSparse, liDistanceToMove, nullptr, FILE_BEGIN))
+ throw FileError(L"fail");
+
+ if (!SetEndOfFile(hSparse))
+ throw FileError(L"fail");
+
+ FILE_ZERO_DATA_INFORMATION zeroInfo = {};
+ zeroInfo.BeyondFinalZero.QuadPart = liDistanceToMove.QuadPart;
+ if (!::DeviceIoControl(hSparse, FSCTL_SET_ZERO_DATA, &zeroInfo, sizeof(zeroInfo), nullptr, 0, &br, nullptr))
+ throw FileError(L"fail");
+ */
+}
+
+
+DEFINE_NEW_FILE_ERROR(ErrorShouldCopyAsSparse);
+
+class ErrorHandling
+{
+public:
+ ErrorHandling() : shouldCopyAsSparse(false) {}
+
+ //call context: copyCallbackInternal()
+ void reportErrorShouldCopyAsSparse() { shouldCopyAsSparse = true; }
+
+ void reportUserException(const std::exception_ptr& e) { exception = e; }
+
+ void reportError(const std::wstring& msg, const std::wstring& description) { errorMsg = std::make_pair(msg, description); }
+
+ //call context: copyFileWindowsDefault()
+ void evaluateErrors() //throw X
+ {
+ if (shouldCopyAsSparse)
+ throw ErrorShouldCopyAsSparse(L"sparse dummy value");
+
+ if (exception)
+ std::rethrow_exception(exception);
+
+ if (!errorMsg.first.empty())
+ throw FileError(errorMsg.first, errorMsg.second);
+ }
+
+private:
+ bool shouldCopyAsSparse; //
+ std::pair<std::wstring, std::wstring> errorMsg; //these are exclusive!
+ std::exception_ptr exception;
+};
+
+
+struct CallbackData
+{
+ CallbackData(const std::function<void(std::int64_t bytesDelta)>& onUpdateCopyStatus,
+ const Zstring& sourceFile,
+ const Zstring& targetFile) :
+ sourceFile_(sourceFile),
+ targetFile_(targetFile),
+ onUpdateCopyStatus_(onUpdateCopyStatus),
+ fileInfoSrc(),
+ fileInfoTrg(),
+ bytesReported() {}
+
+ const Zstring& sourceFile_;
+ const Zstring& targetFile_;
+ const std::function<void(std::int64_t bytesDelta)>& onUpdateCopyStatus_;
+
+ ErrorHandling errorHandler;
+ BY_HANDLE_FILE_INFORMATION fileInfoSrc; //modified by CopyFileEx() at beginning
+ BY_HANDLE_FILE_INFORMATION fileInfoTrg; //
+
+ std::int64_t bytesReported; //used internally to calculate bytes transferred delta
+};
+
+
+DWORD CALLBACK copyCallbackInternal(LARGE_INTEGER totalFileSize,
+ LARGE_INTEGER totalBytesTransferred,
+ LARGE_INTEGER streamSize,
+ LARGE_INTEGER streamBytesTransferred,
+ DWORD dwStreamNumber,
+ DWORD dwCallbackReason,
+ HANDLE hSourceFile,
+ HANDLE hDestinationFile,
+ LPVOID lpData)
+{
+ /*
+ this callback is invoked for block sizes managed by Windows, these may vary from e.g. 64 kB up to 1MB. It seems this depends on file size amongst others.
+
+ symlink handling:
+ if source is a symlink and COPY_FILE_COPY_SYMLINK is specified, this callback is NOT invoked!
+ if source is a symlink and COPY_FILE_COPY_SYMLINK is NOT specified, this callback is called and hSourceFile is a handle to the *target* of the link!
+
+ file time handling:
+ ::CopyFileEx() will (only) copy file modification time over from source file AFTER the last invokation of this callback
+ => it is possible to adapt file creation time of target in here, but NOT file modification time!
+ CAVEAT: if ::CopyFileEx() fails to set modification time, it silently ignores this error and returns success!!!
+ see procmon log in: https://sourceforge.net/tracker/?func=detail&atid=1093080&aid=3514569&group_id=234430
+
+ alternate data stream handling:
+ CopyFileEx() processes multiple streams one after another, stream 1 is the file data stream and always available!
+ Each stream is initialized with CALLBACK_STREAM_SWITCH and provides *new* hSourceFile, hDestinationFile.
+ Calling GetFileInformationByHandle() on hDestinationFile for stream > 1 results in ERROR_ACCESS_DENIED!
+ totalBytesTransferred contains size of *all* streams and so can be larger than the "file size" file attribute
+ */
+
+ CallbackData& cbd = *static_cast<CallbackData*>(lpData);
+
+ if (dwCallbackReason == CALLBACK_STREAM_SWITCH && //called up-front for every file (even if 0-sized)
+ dwStreamNumber == 1) //consider ADS!
+ {
+ //#################### return source file attributes ################################
+ if (!::GetFileInformationByHandle(hSourceFile, &cbd.fileInfoSrc))
+ {
+ const DWORD lastError = ::GetLastError(); //copy before directly or indirectly making other system calls!
+ cbd.errorHandler.reportError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(cbd.sourceFile_)), formatSystemError(L"GetFileInformationByHandle", lastError));
+ return PROGRESS_CANCEL;
+ }
+
+ if (!::GetFileInformationByHandle(hDestinationFile, &cbd.fileInfoTrg))
+ {
+ const DWORD lastError = ::GetLastError(); //copy before directly or indirectly making other system calls!
+ cbd.errorHandler.reportError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(cbd.targetFile_)), formatSystemError(L"GetFileInformationByHandle", lastError));
+ return PROGRESS_CANCEL;
+ }
+
+ //#################### switch to sparse file copy if req. #######################
+ if (canCopyAsSparse(cbd.fileInfoSrc.dwFileAttributes, cbd.targetFile_)) //throw ()
+ {
+ cbd.errorHandler.reportErrorShouldCopyAsSparse(); //use a different copy routine!
+ return PROGRESS_CANCEL;
+ }
+
+ //#################### copy file creation time ################################
+ ::SetFileTime(hDestinationFile, &cbd.fileInfoSrc.ftCreationTime, nullptr, nullptr); //no error handling!
+ //=> not really needed here, creation time is set anyway at the end of copyFileWindowsDefault()!
+
+ //#################### copy NTFS compressed attribute #########################
+ const bool sourceIsCompressed = (cbd.fileInfoSrc.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED) != 0;
+ const bool targetIsCompressed = (cbd.fileInfoTrg.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED) != 0; //already set by CopyFileEx if target parent folder is compressed!
+ if (sourceIsCompressed && !targetIsCompressed)
+ {
+ USHORT cmpState = COMPRESSION_FORMAT_DEFAULT;
+ DWORD bytesReturned = 0;
+ if (!::DeviceIoControl(hDestinationFile, //_In_ HANDLE hDevice,
+ FSCTL_SET_COMPRESSION, //_In_ DWORD dwIoControlCode,
+ &cmpState, //_In_opt_ LPVOID lpInBuffer,
+ sizeof(cmpState), //_In_ DWORD nInBufferSize,
+ nullptr, //_Out_opt_ LPVOID lpOutBuffer,
+ 0, //_In_ DWORD nOutBufferSize,
+ &bytesReturned, //_Out_opt_ LPDWORD lpBytesReturned
+ nullptr)) //_Inout_opt_ LPOVERLAPPED lpOverlapped
+ {} //may legitimately fail with ERROR_INVALID_FUNCTION if
+
+ // - if target folder is encrypted
+ // - target volume does not support compressed attribute
+ //#############################################################################
+ }
+ }
+
+ //called after copy operation is finished - note: for 0-sized files this callback is invoked just ONCE!
+ //if (totalFileSize.QuadPart == totalBytesTransferred.QuadPart && dwStreamNumber == 1) {}
+ if (cbd.onUpdateCopyStatus_ && totalBytesTransferred.QuadPart >= 0) //should be always true, but let's still check
+ try
+ {
+ cbd.onUpdateCopyStatus_(totalBytesTransferred.QuadPart - cbd.bytesReported); //throw X!
+ cbd.bytesReported = totalBytesTransferred.QuadPart;
+ }
+ catch (...)
+ {
+ cbd.errorHandler.reportUserException(std::current_exception());
+ return PROGRESS_CANCEL;
+ }
+ return PROGRESS_CONTINUE;
+}
+
+
+const bool supportNonEncryptedDestination = winXpOrLater(); //encrypted destination is not supported with Windows 2000
+//caveat: function scope static initialization is not thread-safe in VS 2010!
+
+
+void copyFileWindowsDefault(const Zstring& sourceFile,
+ const Zstring& targetFile,
+ const std::function<void(std::int64_t bytesDelta)>& onUpdateCopyStatus,
+ InSyncAttributes* newAttrib) //throw FileError, ErrorTargetExisting, ErrorFileLocked, ErrorShouldCopyAsSparse
+{
+ //try to get backup read and write privileges: who knows, maybe this helps solve some obscure "access denied" errors
+ try { activatePrivilege(SE_BACKUP_NAME); }
+ catch (const FileError&) {}
+ try { activatePrivilege(SE_RESTORE_NAME); }
+ catch (const FileError&) {}
+
+ zen::ScopeGuard guardTarget = zen::makeGuard([&] { try { removeFile(targetFile); } catch (FileError&) {} });
+ //transactional behavior: guard just before starting copy, we don't trust ::CopyFileEx(), do we? ;)
+
+ DWORD copyFlags = COPY_FILE_FAIL_IF_EXISTS;
+
+ if (supportNonEncryptedDestination)
+ copyFlags |= COPY_FILE_ALLOW_DECRYPTED_DESTINATION; //allow copying from encrypted to non-encrypted location
+
+ //if (vistaOrLater()) //see http://blogs.technet.com/b/askperf/archive/2007/05/08/slow-large-file-copy-issues.aspx
+ // copyFlags |= COPY_FILE_NO_BUFFERING; //no perf difference at worst, huge improvement for large files (20% in test NTFS -> NTFS)
+ //It's a shame this flag causes file corruption! https://sourceforge.net/projects/freefilesync/forums/forum/847542/topic/5177950
+ //documentation on CopyFile2() even states: "It is not recommended to pause copies that are using this flag." How dangerous is this thing, why offer it at all???
+ //perf advantage: ~15% faster
+
+ CallbackData cbd(onUpdateCopyStatus, sourceFile, targetFile);
+
+ const bool success = ::CopyFileEx(applyLongPathPrefix(sourceFile).c_str(), //__in LPCTSTR lpExistingFileName,
+ applyLongPathPrefix(targetFile).c_str(), //__in LPCTSTR lpNewFileName,
+ copyCallbackInternal, //__in_opt LPPROGRESS_ROUTINE lpProgressRoutine,
+ &cbd, //__in_opt LPVOID lpData,
+ nullptr, //__in_opt LPBOOL pbCancel,
+ copyFlags) != FALSE; //__in DWORD dwCopyFlags
+
+ cbd.errorHandler.evaluateErrors(); //throw ?, process errors in callback first!
+ if (!success)
+ {
+ const DWORD lastError = ::GetLastError(); //copy before directly or indirectly making other system calls!
+
+ //don't suppress "lastError == ERROR_REQUEST_ABORTED": a user aborted operation IS an error condition!
+
+ //trying to copy huge sparse files may directly fail with ERROR_DISK_FULL before entering the callback function
+ if (canCopyAsSparse(sourceFile, targetFile)) //noexcept
+ throw ErrorShouldCopyAsSparse(L"sparse dummy value2");
+
+ //assemble error message...
+ const std::wstring errorMsg = replaceCpy(replaceCpy(_("Cannot copy file %x to %y."), L"%x", L"\n" + fmtFileName(sourceFile)), L"%y", L"\n" + fmtFileName(targetFile));
+ std::wstring errorDescr = formatSystemError(L"CopyFileEx", lastError);
+
+ //if file is locked throw "ErrorFileLocked" instead!
+ if (lastError == ERROR_SHARING_VIOLATION ||
+ lastError == ERROR_LOCK_VIOLATION)
+ {
+ const Zstring procList = getLockingProcessNames(sourceFile); //throw() -> enhance error message!
+ if (!procList.empty())
+ errorDescr = _("The file is locked by another process:") + L"\n" + procList;
+ throw ErrorFileLocked(replaceCpy(_("Cannot read file %x."), L"%x", fmtFileName(sourceFile)), errorDescr);
+ }
+
+ //if target is existing this functions is expected to throw ErrorTargetExisting!!!
+ if (lastError == ERROR_FILE_EXISTS || //confirmed to be used
+ lastError == ERROR_ALREADY_EXISTS) //not sure if used -> better be safe than sorry!!!
+ {
+ guardTarget.dismiss(); //don't delete file that existed previously!
+ throw ErrorTargetExisting(errorMsg, errorDescr);
+ }
+
+ //if (lastError == ERROR_PATH_NOT_FOUND) throw ErrorTargetPathMissing(errorMsg, errorDescr); //could this also be source path missing!?
+
+ try //add more meaningful message
+ {
+ //trying to copy > 4GB file to FAT/FAT32 volume gives obscure ERROR_INVALID_PARAMETER (FAT can indeed handle files up to 4 Gig, tested!)
+ if (lastError == ERROR_INVALID_PARAMETER &&
+ isFatDrive(targetFile) &&
+ getFilesize(sourceFile) >= 4U * std::uint64_t(1024U * 1024 * 1024)) //throw FileError
+ errorDescr += L"\nFAT volumes cannot store files larger than 4 gigabyte.";
+ //see "Limitations of the FAT32 File System": http://support.microsoft.com/kb/314463/en-us
+
+ //note: ERROR_INVALID_PARAMETER can also occur when copying to a SharePoint server or MS SkyDrive and the target filepath is of a restricted type.
+ }
+ catch (FileError&) {}
+
+ throw FileError(errorMsg, errorDescr);
+ }
+
+ if (newAttrib)
+ {
+ newAttrib->fileSize = get64BitUInt(cbd.fileInfoSrc.nFileSizeLow, cbd.fileInfoSrc.nFileSizeHigh);
+ newAttrib->modificationTime = filetimeToTimeT(cbd.fileInfoSrc.ftLastWriteTime);
+ newAttrib->sourceFileId = extractFileId(cbd.fileInfoSrc);
+ newAttrib->targetFileId = extractFileId(cbd.fileInfoTrg);
+ }
+ warn_static("new perf check + investigate improvements now that DST hcak is gone! =>")
+
+ //caveat: - ::CopyFileEx() silently *ignores* failure to set modification time!!! => we always need to set it again but with proper error checking!
+ // - perf-loss on USB sticks with many small files of about 30%!
+ setFileTimeRaw(targetFile, &cbd.fileInfoSrc.ftCreationTime, cbd.fileInfoSrc.ftLastWriteTime, ProcSymlink::FOLLOW); //throw FileError
+
+ guardTarget.dismiss(); //target has been created successfully!
+}
+
+
+//another layer to support copying sparse files
+inline
+void copyFileWindowsSelectRoutine(const Zstring& sourceFile, const Zstring& targetFile, const std::function<void(std::int64_t bytesDelta)>& onUpdateCopyStatus, InSyncAttributes* sourceAttr)
+{
+ try
+ {
+ copyFileWindowsDefault(sourceFile, targetFile, onUpdateCopyStatus, sourceAttr); //throw FileError, ErrorTargetExisting, ErrorFileLocked, ErrorShouldCopyAsSparse
+ }
+ catch (ErrorShouldCopyAsSparse&) //we quickly check for this condition within callback of ::CopyFileEx()!
+ {
+ copyFileWindowsSparse(sourceFile, targetFile, onUpdateCopyStatus, sourceAttr); //throw FileError, ErrorTargetExisting, ErrorFileLocked
+ }
+}
+
+
+//another layer of indirection solving 8.3 name clashes
+inline
+void copyFileWindows(const Zstring& sourceFile,
+ const Zstring& targetFile,
+ const std::function<void(std::int64_t bytesDelta)>& onUpdateCopyStatus,
+ InSyncAttributes* sourceAttr)
+{
+ try
+ {
+ copyFileWindowsSelectRoutine(sourceFile, targetFile, onUpdateCopyStatus, sourceAttr); //throw FileError, ErrorTargetExisting, ErrorFileLocked
+ }
+ catch (const ErrorTargetExisting&)
+ {
+ //try to handle issues with already existing short 8.3 file names on Windows
+ if (have8dot3NameClash(targetFile))
+ {
+ Fix8Dot3NameClash dummy(targetFile); //throw FileError; move clashing filepath to the side
+ copyFileWindowsSelectRoutine(sourceFile, targetFile, onUpdateCopyStatus, sourceAttr); //throw FileError; the short filepath name clash is solved, this should work now
+ return;
+ }
+ throw;
+ }
+}
+
+
+#elif defined ZEN_LINUX || defined ZEN_MAC
+void copyFileLinuxMac(const Zstring& sourceFile,
+ const Zstring& targetFile,
+ const std::function<void(std::int64_t bytesDelta)>& onUpdateCopyStatus,
+ InSyncAttributes* newAttrib) //throw FileError, ErrorTargetExisting
+{
+ FileInputUnbuffered fileIn(sourceFile); //throw FileError
+
+ struct ::stat sourceInfo = {};
+ if (::fstat(fileIn.getDescriptor(), &sourceInfo) != 0)
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(sourceFile)), L"fstat", getLastError());
+
+ zen::ScopeGuard guardTarget = zen::makeGuard([&] { try { removeFile(targetFile); } catch (FileError&) {} }); //transactional behavior: place guard before lifetime of FileOutput
+ try
+ {
+ FileOutputUnbuffered fileOut(targetFile, sourceInfo.st_mode); //throw FileError, ErrorTargetExisting
+
+ std::vector<char> buffer(128 * 1024); //see comment in FileInputUnbuffered::read
+ do
+ {
+ const size_t bytesRead = fileIn.read(&buffer[0], buffer.size()); //throw FileError
+
+ fileOut.write(&buffer[0], bytesRead); //throw FileError
+
+ if (onUpdateCopyStatus)
+ onUpdateCopyStatus(bytesRead); //throw X!
+ }
+ while (!fileIn.eof());
+
+ //adapt target file modification time:
+ {
+ //read and return file statistics
+ struct ::stat targetInfo = {};
+ if (::fstat(fileOut.getDescriptor(), &targetInfo) != 0)
+ throwFileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtFileName(targetFile)), L"fstat", getLastError());
+
+ if (newAttrib)
+ {
+ newAttrib->fileSize = sourceInfo.st_size;
+ newAttrib->modificationTime = sourceInfo.st_mtime;
+ newAttrib->sourceFileId = extractFileId(sourceInfo);
+ newAttrib->targetFileId = extractFileId(targetInfo);
+ }
+ }
+ }
+ catch (const ErrorTargetExisting&)
+ {
+ guardTarget.dismiss(); //don't delete file that existed previously!
+ throw;
+ }
+
+ //we cannot set the target file times while the file descriptor is open and being written:
+ //this triggers bugs on samba shares where the modification time is set to current time instead.
+ //http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=340236
+ //http://comments.gmane.org/gmane.linux.file-systems.cifs/2854
+ //on the other hand we thereby have to reopen https://sourceforge.net/p/freefilesync/bugs/230/
+ setFileTime(targetFile, sourceInfo.st_mtime, ProcSymlink::FOLLOW); //throw FileError
+
+ guardTarget.dismiss(); //target has been created successfully!
+}
+#endif
+
+/*
+ ------------------
+ |File Copy Layers|
+ ------------------
+ copyFile (setup transactional behavior)
+ |
+ copyFileSelectOs
+ / \
+copyFileLinuxMac copyFileWindows (solve 8.3 issue)
+ |
+ copyFileWindowsSelectRoutine
+ / \
+copyFileWindowsDefault(::CopyFileEx) copyFileWindowsSparse(::BackupRead/::BackupWrite)
+*/
+
+inline
+void copyFileSelectOs(const Zstring& sourceFile,
+ const Zstring& targetFile,
+ bool copyFilePermissions,
+ const std::function<void(std::int64_t bytesDelta)>& onUpdateCopyStatus,
+ InSyncAttributes* sourceAttr)
+{
+#ifdef ZEN_WIN
+ copyFileWindows(sourceFile, targetFile, onUpdateCopyStatus, sourceAttr); //throw FileError, ErrorTargetExisting, ErrorFileLocked
+
+#elif defined ZEN_LINUX || defined ZEN_MAC
+ copyFileLinuxMac(sourceFile, targetFile, onUpdateCopyStatus, sourceAttr); //throw FileError, ErrorTargetExisting
+#endif
+
+ if (copyFilePermissions)
+ {
+ //at this point we know we created a new file, so it's fine to delete it for cleanup!
+ zen::ScopeGuard guardTargetFile = zen::makeGuard([&] { try { removeFile(targetFile); } catch (FileError&) {}});
+
+ copyObjectPermissions(sourceFile, targetFile, ProcSymlink::FOLLOW); //throw FileError
+
+ guardTargetFile.dismiss(); //target has been created successfully!
+ }
+}
+}
+
+
+void zen::copyFile(const Zstring& sourceFile, //throw FileError, ErrorFileLocked
+ const Zstring& targetFile,
+ bool copyFilePermissions,
+ bool transactionalCopy,
+ const std::function<void()>& onDeleteTargetFile,
+ const std::function<void(std::int64_t bytesDelta)>& onUpdateCopyStatus,
+ InSyncAttributes* sourceAttr)
+{
+ if (transactionalCopy)
+ {
+ Zstring tmpTarget = targetFile + TEMP_FILE_ENDING;
+
+ for (int i = 0;; ++i)
+ try
+ {
+ copyFileSelectOs(sourceFile, tmpTarget, copyFilePermissions, onUpdateCopyStatus, sourceAttr); //throw FileError, ErrorTargetExisting, ErrorFileLocked
+ break;
+ }
+ catch (const ErrorTargetExisting&) //optimistic strategy: assume everything goes well, but recover on error -> minimize file accesses
+ {
+ if (i == 10) throw; //avoid endless recursion in pathological cases, e.g. https://sourceforge.net/p/freefilesync/discussion/open-discussion/thread/36adac33
+ tmpTarget = targetFile + Zchar('_') + numberTo<Zstring>(i) + TEMP_FILE_ENDING;
+ }
+
+ //transactional behavior: ensure cleanup; not needed before copyFileSelectOs() which is already transactional
+ zen::ScopeGuard guardTempFile = zen::makeGuard([&] { try { removeFile(tmpTarget); } catch (FileError&) {} });
+
+ //have target file deleted (after read access on source and target has been confirmed) => allow for almost transactional overwrite
+ if (onDeleteTargetFile)
+ onDeleteTargetFile(); //throw X
+
+ //perf: this call is REALLY expensive on unbuffered volumes! ~40% performance decrease on FAT USB stick!
+ renameFile(tmpTarget, targetFile); //throw FileError
+
+ /*
+ CAVEAT on FAT/FAT32: the sequence of deleting the target file and renaming "file.txt.ffs_tmp" to "file.txt" does
+ NOT PRESERVE the creation time of the .ffs_tmp file, but SILENTLY "reuses" whatever creation time the old "file.txt" had!
+ This "feature" is called "File System Tunneling":
+ http://blogs.msdn.com/b/oldnewthing/archive/2005/07/15/439261.aspx
+ http://support.microsoft.com/kb/172190/en-us
+ */
+
+ guardTempFile.dismiss();
+ }
+ else
+ {
+ /*
+ Note: non-transactional file copy solves at least four problems:
+ -> skydrive - doesn't allow for .ffs_tmp extension and returns ERROR_INVALID_PARAMETER
+ -> network renaming issues
+ -> allow for true delete before copy to handle low disk space problems
+ -> higher performance on non-buffered drives (e.g. usb sticks)
+ */
+
+ if (onDeleteTargetFile)
+ onDeleteTargetFile();
+
+ copyFileSelectOs(sourceFile, targetFile, copyFilePermissions, onUpdateCopyStatus, sourceAttr); //throw FileError, ErrorTargetExisting, ErrorFileLocked
+ }
+}
bgstack15