From bd6336c629841c6db3a6ca53a936d629d34db53b Mon Sep 17 00:00:00 2001 From: Daniel Wilhelm Date: Fri, 18 Apr 2014 17:15:16 +0200 Subject: 4.1 --- zen/file_handling.cpp | 2060 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2060 insertions(+) create mode 100644 zen/file_handling.cpp (limited to 'zen/file_handling.cpp') diff --git a/zen/file_handling.cpp b/zen/file_handling.cpp new file mode 100644 index 00000000..7b46181b --- /dev/null +++ b/zen/file_handling.cpp @@ -0,0 +1,2060 @@ +// ************************************************************************** +// * 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) 2008-2011 ZenJu (zhnmju123 AT gmx.de) * +// ************************************************************************** + +#include "file_handling.h" +#include +#include +#include +#include "file_traverser.h" +#include "scope_guard.h" +#include "symlink_target.h" +#include "file_io.h" +#include "assert_static.h" +#include +#include +#include "file_id_internal.h" + +#ifdef FFS_WIN +#include "privilege.h" +#include "dll.h" +#include "win.h" //includes "windows.h" +#include "long_path_prefix.h" +#include +#include "dst_hack.h" +#include "file_update_handle.h" +#include "win_ver.h" + +#elif defined FFS_LINUX +#include +#include +#include +#include +#include + +#ifdef HAVE_SELINUX +#include +#endif +#endif + +using namespace zen; + + +bool zen::fileExists(const Zstring& filename) +{ + //symbolic links (broken or not) are also treated as existing files! +#ifdef FFS_WIN + const DWORD ret = ::GetFileAttributes(applyLongPathPrefix(filename).c_str()); + return ret != INVALID_FILE_ATTRIBUTES && (ret & FILE_ATTRIBUTE_DIRECTORY) == 0; //returns true for (file-)symlinks also + +#elif defined FFS_LINUX + struct stat fileInfo = {}; + return ::lstat(filename.c_str(), &fileInfo) == 0 && + (S_ISLNK(fileInfo.st_mode) || S_ISREG(fileInfo.st_mode)); //in Linux a symbolic link is neither file nor directory +#endif +} + + +bool zen::dirExists(const Zstring& dirname) +{ + //symbolic links (broken or not) are also treated as existing directories! +#ifdef FFS_WIN + const DWORD ret = ::GetFileAttributes(applyLongPathPrefix(dirname).c_str()); + return ret != INVALID_FILE_ATTRIBUTES && (ret & FILE_ATTRIBUTE_DIRECTORY) != 0; //returns true for (dir-)symlinks also + +#elif defined FFS_LINUX + struct stat dirInfo = {}; + return ::lstat(dirname.c_str(), &dirInfo) == 0 && + (S_ISLNK(dirInfo.st_mode) || S_ISDIR(dirInfo.st_mode)); //in Linux a symbolic link is neither file nor directory +#endif +} + + +bool zen::symlinkExists(const Zstring& objname) +{ +#ifdef FFS_WIN + const DWORD ret = ::GetFileAttributes(applyLongPathPrefix(objname).c_str()); + return ret != INVALID_FILE_ATTRIBUTES && (ret & FILE_ATTRIBUTE_REPARSE_POINT) != 0; + +#elif defined FFS_LINUX + struct stat fileInfo = {}; + return ::lstat(objname.c_str(), &fileInfo) == 0 && + S_ISLNK(fileInfo.st_mode); //symbolic link +#endif +} + + +bool zen::somethingExists(const Zstring& objname) //throw() check whether any object with this name exists +{ +#ifdef FFS_WIN + return ::GetFileAttributes(applyLongPathPrefix(objname).c_str()) != INVALID_FILE_ATTRIBUTES; + +#elif defined FFS_LINUX + struct stat fileInfo = {}; + return ::lstat(objname.c_str(), &fileInfo) == 0; +#endif +} + + +namespace +{ +void getFileAttrib(const Zstring& filename, FileAttrib& attr, ProcSymlink procSl) //throw FileError +{ +#ifdef FFS_WIN + WIN32_FIND_DATA fileInfo = {}; + { + const HANDLE searchHandle = ::FindFirstFile(applyLongPathPrefix(filename).c_str(), &fileInfo); + if (searchHandle == INVALID_HANDLE_VALUE) + throw FileError(_("Error reading file attributes:") + "\n\"" + filename + "\"" + "\n\n" + getLastErrorFormatted()); + ::FindClose(searchHandle); + } + // WIN32_FILE_ATTRIBUTE_DATA sourceAttr = {}; + // if (!::GetFileAttributesEx(applyLongPathPrefix(sourceObj).c_str(), //__in LPCTSTR lpFileName, + // GetFileExInfoStandard, //__in GET_FILEEX_INFO_LEVELS fInfoLevelId, + // &sourceAttr)) //__out LPVOID lpFileInformation + + const bool isSymbolicLink = (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; + if (!isSymbolicLink || procSl == SYMLINK_DIRECT) + { + //####################################### DST hack ########################################### + const bool isDirectory = (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + if (!isDirectory && dst::isFatDrive(filename)) //throw() + { + const dst::RawTime rawTime(fileInfo.ftCreationTime, fileInfo.ftLastWriteTime); + if (dst::fatHasUtcEncoded(rawTime)) //throw (std::runtime_error) + { + fileInfo.ftLastWriteTime = dst::fatDecodeUtcTime(rawTime); //return last write time in real UTC, throw (std::runtime_error) + ::GetSystemTimeAsFileTime(&fileInfo.ftCreationTime); //real creation time information is not available... + } + } + //####################################### DST hack ########################################### + + attr.fileSize = UInt64(fileInfo.nFileSizeLow, fileInfo.nFileSizeHigh); + attr.modificationTime = toTimeT(fileInfo.ftLastWriteTime); + } + else + { + const HANDLE hFile = ::CreateFile(applyLongPathPrefix(filename).c_str(), //open handle to target of symbolic link + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + 0, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + if (hFile == INVALID_HANDLE_VALUE) + throw FileError(_("Error reading file attributes:") + "\n\"" + filename + "\"" + "\n\n" + getLastErrorFormatted()); + ZEN_ON_BLOCK_EXIT(::CloseHandle(hFile)); + + BY_HANDLE_FILE_INFORMATION fileInfoHnd = {}; + if (!::GetFileInformationByHandle(hFile, &fileInfoHnd)) + throw FileError(_("Error reading file attributes:") + "\n\"" + filename + "\"" + "\n\n" + getLastErrorFormatted()); + + attr.fileSize = UInt64(fileInfoHnd.nFileSizeLow, fileInfoHnd.nFileSizeHigh); + attr.modificationTime = toTimeT(fileInfoHnd.ftLastWriteTime); + } + +#elif defined FFS_LINUX + struct stat fileInfo = {}; + + const int rv = procSl == SYMLINK_FOLLOW ? + :: stat(filename.c_str(), &fileInfo) : + ::lstat(filename.c_str(), &fileInfo); + if (rv != 0) //follow symbolic links + throw FileError(_("Error reading file attributes:") + "\n\"" + filename + "\"" + "\n\n" + getLastErrorFormatted()); + + attr.fileSize = UInt64(fileInfo.st_size); + attr.modificationTime = fileInfo.st_mtime; +#endif +} +} + + +UInt64 zen::getFilesize(const Zstring& filename) //throw FileError +{ + FileAttrib attr; + getFileAttrib(filename, attr, SYMLINK_FOLLOW); //throw FileError + return attr.fileSize; +} + + +Int64 zen::getFileTime(const Zstring& filename, ProcSymlink procSl) //throw FileError +{ + FileAttrib attr; + getFileAttrib(filename, attr, procSl); //throw FileError + return attr.modificationTime; +} + + +namespace +{ + + +#ifdef FFS_WIN +DWORD retrieveVolumeSerial(const Zstring& pathName) //return 0 on error! +{ + std::vector buffer(10000); + + //full pathName need not yet exist! + if (!::GetVolumePathName(pathName.c_str(), //__in LPCTSTR lpszFileName, + &buffer[0], //__out LPTSTR lpszVolumePathName, + static_cast(buffer.size()))) //__in DWORD cchBufferLength + return 0; + + Zstring volumePath = &buffer[0]; + if (!endsWith(volumePath, FILE_NAME_SEPARATOR)) + volumePath += FILE_NAME_SEPARATOR; + + DWORD volumeSerial = 0; + if (!::GetVolumeInformation(volumePath.c_str(), //__in_opt LPCTSTR lpRootPathName, + NULL, //__out LPTSTR lpVolumeNameBuffer, + 0, //__in DWORD nVolumeNameSize, + &volumeSerial, //__out_opt LPDWORD lpVolumeSerialNumber, + NULL, //__out_opt LPDWORD lpMaximumComponentLength, + NULL, //__out_opt LPDWORD lpFileSystemFlags, + NULL, //__out LPTSTR lpFileSystemNameBuffer, + 0)) //__in DWORD nFileSystemNameSize + return 0; + + return volumeSerial; +} +#elif defined FFS_LINUX + +dev_t retrieveVolumeSerial(const Zstring& pathName) //return 0 on error! +{ + Zstring volumePathName = pathName; + + //remove trailing slash + if (volumePathName.size() > 1 && endsWith(volumePathName, FILE_NAME_SEPARATOR)) //exception: allow '/' + volumePathName = beforeLast(volumePathName, FILE_NAME_SEPARATOR); + + struct stat fileInfo = {}; + while (::lstat(volumePathName.c_str(), &fileInfo) != 0) //go up in folder hierarchy until existing folder is found + { + volumePathName = beforeLast(volumePathName, FILE_NAME_SEPARATOR); //returns empty string if ch not found + if (volumePathName.empty()) + return 0; //this includes path "/" also! + } + + return fileInfo.st_dev; +} +#endif +} + + +zen::ResponseSame zen::onSameVolume(const Zstring& folderLeft, const Zstring& folderRight) //throw() +{ + const auto serialLeft = retrieveVolumeSerial(folderLeft); //returns 0 on error! + const auto serialRight = retrieveVolumeSerial(folderRight); //returns 0 on error! + if (serialLeft == 0 || serialRight == 0) + return IS_SAME_CANT_SAY; + + return serialLeft == serialRight ? IS_SAME_YES : IS_SAME_NO; +} + + +bool zen::removeFile(const Zstring& filename) //throw FileError; +{ +#ifdef FFS_WIN + //remove file, support for \\?\-prefix + const Zstring filenameFmt = applyLongPathPrefix(filename); + if (!::DeleteFile(filenameFmt.c_str())) +#elif defined FFS_LINUX + if (::unlink(filename.c_str()) != 0) +#endif + { +#ifdef FFS_WIN + //perf: apply ONLY when necessary! + if (::GetLastError() == ERROR_ACCESS_DENIED) //function fails if file is read-only + { + //(try to) normalize file attributes + ::SetFileAttributes(filenameFmt.c_str(), FILE_ATTRIBUTE_NORMAL); + + //now try again... + if (::DeleteFile(filenameFmt.c_str())) + return true; + } + //eval error code before next call + DWORD lastError = ::GetLastError(); +#elif defined FFS_LINUX + int lastError = errno; +#endif + + //no error situation if file is not existing! manual deletion relies on it! + //perf: check is placed in error handling block + //warning: this call changes error code!! + if (!somethingExists(filename)) + return false; //neither file nor any other object (e.g. broken symlink) with that name existing + + throw FileError(_("Error deleting file:") + "\n\"" + filename + "\"" + "\n\n" + getLastErrorFormatted(lastError)); + } + return true; +} + + +namespace +{ +DEFINE_NEW_FILE_ERROR(ErrorDifferentVolume); + +/* 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 FFS_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(); + 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); + } + } + } + } + + std::wstring errorMessage = _("Error moving file:") + "\n\"" + oldName + "\" ->\n\"" + newName + "\"" + "\n\n" + getLastErrorFormatted(lastError); + + if (lastError == ERROR_NOT_SAME_DEVICE) + throw ErrorDifferentVolume(errorMessage); + else if (lastError == ERROR_FILE_EXISTS) + throw ErrorTargetExisting(errorMessage); + else + throw FileError(errorMessage); + } + +#elif defined FFS_LINUX + if (::rename(oldName.c_str(), newName.c_str()) != 0) + { + const int lastError = errno; + + std::wstring errorMessage = _("Error moving file:") + "\n\"" + oldName + "\" ->\n\"" + newName + "\"" + "\n\n" + getLastErrorFormatted(lastError); + + if (lastError == EXDEV) + throw ErrorDifferentVolume(errorMessage); + else if (lastError == EEXIST) + throw ErrorTargetExisting(errorMessage); + else + throw FileError(errorMessage); + } +#endif +} + + +#ifdef FFS_WIN +/*small wrapper around +::GetShortPathName() +::GetLongPathName() */ +template +Zstring getFilenameFmt(const Zstring& filename, Function fun) //throw(); returns empty string on error +{ + const Zstring filenameFmt = applyLongPathPrefix(filename); + + const DWORD bufferSize = fun(filenameFmt.c_str(), NULL, 0); + if (bufferSize == 0) + return Zstring(); + + std::vector buffer(bufferSize); + + const DWORD rv = fun(filenameFmt.c_str(), //__in LPCTSTR lpszShortPath, + &buffer[0], //__out LPTSTR lpszLongPath, + static_cast(buffer.size())); //__in DWORD cchBuffer + if (rv == 0 || rv >= buffer.size()) + return Zstring(); + + return &buffer[0]; +} + + +Zstring findUnused8Dot3Name(const Zstring& filename) //find a unique 8.3 short name +{ + const Zstring pathPrefix = filename.find(FILE_NAME_SEPARATOR) != Zstring::npos ? + (beforeLast(filename, FILE_NAME_SEPARATOR) + FILE_NAME_SEPARATOR) : Zstring(); + + Zstring extension = afterLast(afterLast(filename, FILE_NAME_SEPARATOR), Zchar('.')); //extension needn't contain reasonable data + if (extension.empty()) + extension = Zstr("FFS"); + truncate(extension, 3); + + for (int index = 0; index < 100000000; ++index) //filename must be representable by <= 8 characters + { + const Zstring output = pathPrefix + toString(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...\n") + utf8CvrtTo(pathPrefix)); +} + + +bool have8dot3NameClash(const Zstring& filename) +{ + if (filename.find(FILE_NAME_SEPARATOR) == Zstring::npos) + return false; + + if (somethingExists(filename)) //name OR directory! + { + const Zstring origName = afterLast(filename, FILE_NAME_SEPARATOR); //returns the whole string if ch not found + const Zstring shortName = afterLast(getFilenameFmt(filename, ::GetShortPathName), FILE_NAME_SEPARATOR); //throw() returns empty string on error + const Zstring longName = afterLast(getFilenameFmt(filename, ::GetLongPathName) , FILE_NAME_SEPARATOR); // + + if (!shortName.empty() && + !longName.empty() && + EqualFilename()(origName, shortName) && + !EqualFilename()(shortName, longName)) + { + //for filename short and long file name are equal and another unrelated file happens to have the same short name + //e.g. filename == "TESTWE~1", but another file is existing named "TestWeb" with short name ""TESTWE~1" + return true; + } + } + return false; +} + +class Fix8Dot3NameClash +{ +public: + Fix8Dot3NameClash(const Zstring& filename) + { + const Zstring longName = afterLast(getFilenameFmt(filename, ::GetLongPathName), FILE_NAME_SEPARATOR); //throw() returns empty string on error + + unrelatedFile = beforeLast(filename, 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(filename); + + //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 (...) {} + } +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 FileError&) + { +#ifdef FFS_WIN + //try to handle issues with already existing short 8.3 file names on Windows + if (have8dot3NameClash(newName)) + { + Fix8Dot3NameClash dummy(newName); //move clashing filename to the side + //now try again... + renameFile_sub(oldName, newName); //throw FileError + return; + } +#endif + throw; + } +} + + +class CopyCallbackImpl : public zen::CallbackCopyFile //callback functionality +{ +public: + CopyCallbackImpl(const Zstring& sourceFile, CallbackMoveFile& callback) : sourceFile_(sourceFile), moveCallback(callback) {} + + virtual void deleteTargetFile(const Zstring& targetFile) { assert(!fileExists(targetFile)); } + + virtual void updateCopyStatus(UInt64 totalBytesTransferred) + { + moveCallback.requestUiRefresh(sourceFile_); + } + +private: + CopyCallbackImpl(const CopyCallbackImpl&); + CopyCallbackImpl& operator=(const CopyCallbackImpl&); + + const Zstring sourceFile_; + CallbackMoveFile& moveCallback; +}; + + +void zen::moveFile(const Zstring& sourceFile, const Zstring& targetFile, bool ignoreExisting, CallbackMoveFile* callback) //throw FileError; +{ + //call back once per file (moveFile() is called by moveDirectory()) + if (callback) + callback->requestUiRefresh(sourceFile); + + const bool targetExisting = fileExists(targetFile); + + if (targetExisting && !ignoreExisting) //test file existence: e.g. Linux might silently overwrite existing symlinks + throw FileError(_("Error moving file:") + "\n\"" + sourceFile + "\" ->\n\"" + targetFile + "\"" + + "\n\n" + _("Target file already existing!")); + + if (!targetExisting) + { + //try to move the file directly without copying + try + { + renameFile(sourceFile, targetFile); //throw FileError, ErrorDifferentVolume + return; //great, we get away cheaply! + } + //if moving failed treat as error (except when it tried to move to a different volume: in this case we will copy the file) + catch (const ErrorDifferentVolume&) {} + + //file is on a different volume: let's copy it + std::unique_ptr copyCallback(callback != NULL ? new CopyCallbackImpl(sourceFile, *callback) : NULL); + + if (symlinkExists(sourceFile)) + copySymlink(sourceFile, targetFile, false); //throw FileError; don't copy filesystem permissions + else + copyFile(sourceFile, targetFile, false, true, copyCallback.get()); //throw FileError; + + //attention: if copy-operation was cancelled an exception is thrown => sourcefile is not deleted, as we wish! + } + + removeFile(sourceFile); //throw FileError + //note: copying file is NOT undone in case of exception: currently this function is called in context of user-defined deletion dir, where this behavior is fine +} + +namespace +{ +class TraverseOneLevel : public zen::TraverseCallback +{ +public: + typedef std::pair NamePair; + typedef std::vector NameList; + + TraverseOneLevel(NameList& files, NameList& dirs) : + files_(files), + dirs_(dirs) {} + + virtual void onFile(const Zchar* shortName, const Zstring& fullName, const FileInfo& details) + { + files_.push_back(NamePair(shortName, fullName)); + } + + virtual void onSymlink(const Zchar* shortName, const Zstring& fullName, const SymlinkInfo& details) + { + if (details.dirLink) + dirs_.push_back(NamePair(shortName, fullName)); + else + files_.push_back(NamePair(shortName, fullName)); + } + + virtual ReturnValDir onDir(const Zchar* shortName, const Zstring& fullName) + { + dirs_.push_back(NamePair(shortName, fullName)); + return Int2Type(); //DON'T traverse into subdirs; moveDirectory works recursively! + } + + virtual HandleError onError(const std::wstring& errorText) { throw FileError(errorText); } + +private: + TraverseOneLevel(const TraverseOneLevel&); + TraverseOneLevel& operator=(const TraverseOneLevel&); + + NameList& files_; + NameList& dirs_; +}; + + +struct RemoveCallbackImpl : public CallbackRemoveDir +{ + RemoveCallbackImpl(const Zstring& sourceDir, + CallbackMoveFile& moveCallback) : + sourceDir_(sourceDir), + moveCallback_(moveCallback) {} + + virtual void notifyFileDeletion(const Zstring& filename) { moveCallback_.requestUiRefresh(sourceDir_); } + virtual void notifyDirDeletion(const Zstring& dirname) { moveCallback_.requestUiRefresh(sourceDir_); } + +private: + RemoveCallbackImpl(const RemoveCallbackImpl&); + RemoveCallbackImpl& operator=(const RemoveCallbackImpl&); + + const Zstring sourceDir_; + CallbackMoveFile& moveCallback_; +}; +} + + +void moveDirectoryImpl(const Zstring& sourceDir, const Zstring& targetDir, bool ignoreExisting, CallbackMoveFile* callback) //throw FileError; +{ + //call back once per folder + if (callback) + callback->requestUiRefresh(sourceDir); + + const bool targetExisting = dirExists(targetDir); + + if (targetExisting && !ignoreExisting) //directory or symlink exists (or even a file... this error will be caught later) + throw FileError(_("Error moving directory:") + "\n\"" + sourceDir + "\" ->\n\"" + targetDir + "\"" + + "\n\n" + _("Target directory already existing!")); + + const bool isSymlink = symlinkExists(sourceDir); + + if (!targetExisting) + { + //first try to move the directory directly without copying + try + { + renameFile(sourceDir, targetDir); //throw FileError, ErrorDifferentVolume, ErrorTargetExisting + return; //great, we get away cheaply! + } + //if moving failed treat as error (except when it tried to move to a different volume: in this case we will copy the directory) + catch (const ErrorDifferentVolume&) {} + + //create target + if (isSymlink) + copySymlink(sourceDir, targetDir, false); //throw FileError -> don't copy permissions + else + createDirectory(targetDir, sourceDir, false); //throw FileError + } + + if (!isSymlink) //handle symbolic links + { + //move files/folders recursively + TraverseOneLevel::NameList fileList; //list of names: 1. short 2.long + TraverseOneLevel::NameList dirList; // + + //traverse source directory one level + TraverseOneLevel traverseCallback(fileList, dirList); + traverseFolder(sourceDir, false, traverseCallback); //traverse one level, don't follow symlinks + + const Zstring targetDirFormatted = endsWith(targetDir, FILE_NAME_SEPARATOR) ? //ends with path separator + targetDir : + targetDir + FILE_NAME_SEPARATOR; + + //move files + for (TraverseOneLevel::NameList::const_iterator i = fileList.begin(); i != fileList.end(); ++i) + moveFile(i->second, targetDirFormatted + i->first, ignoreExisting, callback); //throw FileError, ErrorTargetExisting + + //move directories + for (TraverseOneLevel::NameList::const_iterator i = dirList.begin(); i != dirList.end(); ++i) + ::moveDirectoryImpl(i->second, targetDirFormatted + i->first, ignoreExisting, callback); + + //attention: if move-operation was cancelled an exception is thrown => sourceDir is not deleted, as we wish! + } + + //delete source + std::unique_ptr removeCallback(callback != NULL ? new RemoveCallbackImpl(sourceDir, *callback) : NULL); + removeDirectory(sourceDir, removeCallback.get()); //throw FileError; +} + + +void zen::moveDirectory(const Zstring& sourceDir, const Zstring& targetDir, bool ignoreExisting, CallbackMoveFile* callback) //throw FileError; +{ +#ifdef FFS_WIN + const Zstring& sourceDirFormatted = sourceDir; + const Zstring& targetDirFormatted = targetDir; + +#elif defined FFS_LINUX + const Zstring sourceDirFormatted = //remove trailing slash + sourceDir.size() > 1 && endsWith(sourceDir, FILE_NAME_SEPARATOR) ? //exception: allow '/' + beforeLast(sourceDir, FILE_NAME_SEPARATOR) : + sourceDir; + const Zstring targetDirFormatted = //remove trailing slash + targetDir.size() > 1 && endsWith(targetDir, FILE_NAME_SEPARATOR) ? //exception: allow '/' + beforeLast(targetDir, FILE_NAME_SEPARATOR) : + targetDir; +#endif + + ::moveDirectoryImpl(sourceDirFormatted, targetDirFormatted, ignoreExisting, callback); +} + + +class FilesDirsOnlyTraverser : public zen::TraverseCallback +{ +public: + FilesDirsOnlyTraverser(std::vector& files, std::vector& dirs) : + m_files(files), + m_dirs(dirs) {} + + virtual void onFile(const Zchar* shortName, const Zstring& fullName, const FileInfo& details) + { + m_files.push_back(fullName); + } + virtual void onSymlink(const Zchar* shortName, const Zstring& fullName, const SymlinkInfo& details) + { + if (details.dirLink) + m_dirs.push_back(fullName); + else + m_files.push_back(fullName); + } + virtual ReturnValDir onDir(const Zchar* shortName, const Zstring& fullName) + { + m_dirs.push_back(fullName); + return Int2Type(); //DON'T traverse into subdirs; removeDirectory works recursively! + } + virtual HandleError onError(const std::wstring& errorText) { throw FileError(errorText); } + +private: + FilesDirsOnlyTraverser(const FilesDirsOnlyTraverser&); + FilesDirsOnlyTraverser& operator=(const FilesDirsOnlyTraverser&); + + std::vector& m_files; + std::vector& m_dirs; +}; + + +void zen::removeDirectory(const Zstring& directory, CallbackRemoveDir* callback) +{ + //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 + +#ifdef FFS_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 + { +#ifdef FFS_WIN + if (!::RemoveDirectory(directoryFmt.c_str())) +#elif defined FFS_LINUX + if (::unlink(directory.c_str()) != 0) +#endif + throw FileError(_("Error deleting directory:") + "\n\"" + directory + "\"" + "\n\n" + getLastErrorFormatted()); + + if (callback) + callback->notifyDirDeletion(directory); //once per symlink + return; + } + + std::vector fileList; + std::vector dirList; + + //get all files and directories from current directory (WITHOUT subdirectories!) + FilesDirsOnlyTraverser traverser(fileList, dirList); + traverseFolder(directory, false, traverser); //don't follow symlinks + + //delete files + for (std::vector::const_iterator i = fileList.begin(); i != fileList.end(); ++i) + { + const bool workDone = removeFile(*i); + if (callback && workDone) + callback->notifyFileDeletion(*i); //call once per file + } + + //delete directories recursively + for (std::vector::const_iterator i = dirList.begin(); i != dirList.end(); ++i) + removeDirectory(*i, callback); //call recursively to correctly handle symbolic links + + //parent directory is deleted last +#ifdef FFS_WIN + if (!::RemoveDirectory(directoryFmt.c_str())) //remove directory, support for \\?\-prefix +#else + if (::rmdir(directory.c_str()) != 0) +#endif + { + throw FileError(_("Error deleting directory:") + "\n\"" + directory + "\"" + "\n\n" + getLastErrorFormatted()); + } + if (callback) + callback->notifyDirDeletion(directory); //and once per folder +} + + +void zen::setFileTime(const Zstring& filename, const Int64& modificationTime, ProcSymlink procSl) //throw FileError +{ +#ifdef FFS_WIN + FILETIME creationTime = {}; + FILETIME lastWriteTime = tofiletime(modificationTime); + + //####################################### DST hack ########################################### + if (dst::isFatDrive(filename)) //throw() + { + const dst::RawTime encodedTime = dst::fatEncodeUtcTime(lastWriteTime); //throw (std::runtime_error) + creationTime = encodedTime.createTimeRaw; + lastWriteTime = encodedTime.writeTimeRaw; + } + //####################################### DST hack ########################################### + + //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) + { + */ + + //may need to remove the readonly-attribute (e.g. FAT usb drives) + FileUpdateHandle targetHandle(filename, [ = ]() + { + return ::CreateFile(applyLongPathPrefix(filename).c_str(), + GENERIC_READ | GENERIC_WRITE, //use both when writing over network, see comment in file_io.cpp + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + 0, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | //needed to open a directory + (procSl == SYMLINK_DIRECT ? FILE_FLAG_OPEN_REPARSE_POINT : 0), //process symlinks + NULL); + }); + + if (targetHandle.get() == INVALID_HANDLE_VALUE) + throw FileError(_("Error changing modification time:") + "\n\"" + filename + "\"" + "\n\n" + getLastErrorFormatted()); + + /* + if (hTarget == INVALID_HANDLE_VALUE && ::GetLastError() == ERROR_SHARING_VIOLATION) + ::Sleep(retryInterval); //wait then retry + else //success or unknown failure + break; + } + */ + + auto isNullTime = [](const FILETIME & ft) { return ft.dwLowDateTime == 0 && ft.dwHighDateTime == 0; }; + + if (!::SetFileTime(targetHandle.get(), + isNullTime(creationTime) ? NULL : &creationTime, + NULL, + &lastWriteTime)) + throw FileError(_("Error changing modification time:") + "\n\"" + filename + "\"" + "\n\n" + getLastErrorFormatted()); + +#ifndef NDEBUG //dst hack: verify data written + if (dst::isFatDrive(filename) && !dirExists(filename)) //throw() + { + WIN32_FILE_ATTRIBUTE_DATA debugeAttr = {}; + assert(::GetFileAttributesEx(applyLongPathPrefix(filename).c_str(), //__in LPCTSTR lpFileName, + GetFileExInfoStandard, //__in GET_FILEEX_INFO_LEVELS fInfoLevelId, + &debugeAttr)); //__out LPVOID lpFileInformation + + assert(::CompareFileTime(&debugeAttr.ftCreationTime, &creationTime) == 0); + assert(::CompareFileTime(&debugeAttr.ftLastWriteTime, &lastWriteTime) == 0); + } +#endif + +#elif defined FFS_LINUX + if (procSl == SYMLINK_FOLLOW) + { + struct utimbuf newTimes = {}; + newTimes.actime = ::time(NULL); + newTimes.modtime = to(modificationTime); + + // set new "last write time" + if (::utime(filename.c_str(), &newTimes) != 0) + throw FileError(_("Error changing modification time:") + "\n\"" + filename + "\"" + "\n\n" + getLastErrorFormatted()); + } + else + { + struct timeval newTimes[2] = {}; + newTimes[0].tv_sec = ::time(NULL); /* seconds */ + newTimes[0].tv_usec = 0; /* microseconds */ + + newTimes[1].tv_sec = to(modificationTime); + newTimes[1].tv_usec = 0; + + if (::lutimes(filename.c_str(), newTimes) != 0) + throw FileError(_("Error changing modification time:") + "\n\"" + filename + "\"" + "\n\n" + getLastErrorFormatted()); + } +#endif +} + + +namespace +{ +#ifdef FFS_WIN +Zstring resolveDirectorySymlink(const Zstring& dirLinkName) //get full target path of symbolic link to a directory; throw (FileError) +{ + //open handle to target of symbolic link + const HANDLE hDir = ::CreateFile(applyLongPathPrefix(dirLinkName).c_str(), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + 0, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, //needed to open a directory + NULL); + if (hDir == INVALID_HANDLE_VALUE) + throw FileError(_("Error resolving symbolic link:") + "\n\"" + dirLinkName + "\"" + "\n\n" + getLastErrorFormatted()); + ZEN_ON_BLOCK_EXIT(::CloseHandle(hDir)); + + const DWORD BUFFER_SIZE = 10000; + std::vector targetPath(BUFFER_SIZE); + + //dynamically load windows API function + typedef DWORD (WINAPI *GetFinalPathNameByHandleWFunc)(HANDLE hFile, + LPTSTR lpszFilePath, + DWORD cchFilePath, + DWORD dwFlags); + const SysDllFun getFinalPathNameByHandle(L"kernel32.dll", "GetFinalPathNameByHandleW"); + if (!getFinalPathNameByHandle) + throw FileError(_("Error loading library function:") + "\n\"" + "GetFinalPathNameByHandleW" + "\""); + + const DWORD charsWritten = getFinalPathNameByHandle(hDir, //__in HANDLE hFile, + &targetPath[0], //__out LPTSTR lpszFilePath, + BUFFER_SIZE, //__in DWORD cchFilePath, + FILE_NAME_NORMALIZED); //__in DWORD dwFlags + if (charsWritten >= BUFFER_SIZE || charsWritten == 0) + { + std::wstring errorMessage = _("Error resolving symbolic link:") + "\n\"" + dirLinkName + "\""; + if (charsWritten == 0) + errorMessage += L"\n\n" + getLastErrorFormatted(); + throw FileError(errorMessage); + } + + return Zstring(&targetPath[0], charsWritten); +} +#endif + + +#ifdef HAVE_SELINUX +//copy SELinux security context +void copySecurityContext(const Zstring& source, const Zstring& target, ProcSymlink procSl) //throw FileError +{ + security_context_t contextSource = NULL; + const int rv = procSl == SYMLINK_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; + + throw FileError(_("Error reading security context:") + "\n\"" + source + "\"" + "\n\n" + getLastErrorFormatted()); + } + ZEN_ON_BLOCK_EXIT(::freecon(contextSource)); + + { + security_context_t contextTarget = NULL; + const int rv2 = procSl == SYMLINK_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_BLOCK_EXIT(::freecon(contextTarget)); + + if (::strcmp(contextSource, contextTarget) == 0) //nothing to do + return; + } + } + + const int rv3 = procSl == SYMLINK_FOLLOW ? + ::setfilecon(target.c_str(), contextSource) : + ::lsetfilecon(target.c_str(), contextSource); + if (rv3 < 0) + throw FileError(_("Error writing security context:") + "\n\"" + target + "\"" + "\n\n" + getLastErrorFormatted()); +} +#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 FFS_WIN + //setting privileges requires admin rights! + try + { + //enable privilege: required to read/write SACL information + Privileges::getInstance().ensureActive(SE_SECURITY_NAME); //polling allowed... + + //enable privilege: required to copy owner information + Privileges::getInstance().ensureActive(SE_RESTORE_NAME); + + //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) + Privileges::getInstance().ensureActive(SE_BACKUP_NAME); + } + catch (const FileError& e) + { + throw FileError(_("Error copying file permissions:") + "\n\"" + source + "\" ->\n\"" + target + "\"" + "\n\n" + e.msg()); + } + + PSECURITY_DESCRIPTOR buffer = NULL; + PSID owner = NULL; + PSID group = NULL; + PACL dacl = NULL; + PACL sacl = NULL; + + //http://msdn.microsoft.com/en-us/library/aa364399(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, + 0, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | (procSl == SYMLINK_DIRECT ? FILE_FLAG_OPEN_REPARSE_POINT : 0), //FILE_FLAG_BACKUP_SEMANTICS needed to open a directory + NULL); + if (hSource == INVALID_HANDLE_VALUE) + throw FileError(_("Error copying file permissions:") + "\n\"" + source + "\" ->\n\"" + target + "\"" + "\n\n" + getLastErrorFormatted() + " (OR)"); + ZEN_ON_BLOCK_EXIT(::CloseHandle(hSource)); + + // DWORD rc = ::GetNamedSecurityInfo(const_cast(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(_("Error copying file permissions:") + "\n\"" + source + "\" ->\n\"" + target + "\"" + "\n\n" + getLastErrorFormatted(rc) + " (R)"); + ZEN_ON_BLOCK_EXIT(::LocalFree(buffer)); + + //may need to remove the readonly-attribute (e.g. FAT usb drives) + 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 + 0, // lpSecurityAttributes + OPEN_EXISTING, // dwCreationDisposition + FILE_FLAG_BACKUP_SEMANTICS | (procSl == SYMLINK_DIRECT ? FILE_FLAG_OPEN_REPARSE_POINT : 0), // dwFlagsAndAttributes + NULL); // hTemplateFile + }); + + if (targetHandle.get() == INVALID_HANDLE_VALUE) + throw FileError(_("Error copying file permissions:") + "\n\"" + source + "\" ->\n\"" + target + "\"" + "\n\n" + getLastErrorFormatted() + " (OW)"); + + // rc = ::SetNamedSecurityInfo(const_cast(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, + OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION, //__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(_("Error copying file permissions:") + "\n\"" + source + "\" ->\n\"" + target + "\"" + "\n\n" + getLastErrorFormatted(rc) + " (W)"); + +#elif defined FFS_LINUX + +#ifdef HAVE_SELINUX //copy SELinux security context + copySecurityContext(source, target, procSl); //throw FileError +#endif + + struct stat fileInfo = {}; + if (procSl == SYMLINK_FOLLOW) + { + if (::stat(source.c_str(), &fileInfo) != 0 || + ::chown(target.c_str(), fileInfo.st_uid, fileInfo.st_gid) != 0 || // may require admin rights! + ::chmod(target.c_str(), fileInfo.st_mode) != 0) + throw FileError(_("Error copying file permissions:") + "\n\"" + source + "\" ->\n\"" + target + "\"" + "\n\n" + getLastErrorFormatted() + " (R)"); + } + else + { + if (::lstat(source.c_str(), &fileInfo) != 0 || + ::lchown(target.c_str(), fileInfo.st_uid, fileInfo.st_gid) != 0 || // may require admin rights! + (!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() + throw FileError(_("Error copying file permissions:") + "\n\"" + source + "\" ->\n\"" + target + "\"" + "\n\n" + getLastErrorFormatted() + " (W)"); + } +#endif +} + + +void createDirectory_straight(const Zstring& directory, const Zstring& templateDir, bool copyFilePermissions, int level) +{ + //default directory creation +#ifdef FFS_WIN + //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(), NULL)) +#elif defined FFS_LINUX + if (::mkdir(directory.c_str(), 0755) != 0) +#endif + { + if (level != 0) return; + throw FileError(_("Error creating directory:") + "\n\"" + directory + "\"" + "\n\n" + getLastErrorFormatted()); + } + + if (!templateDir.empty()) + { +#ifdef FFS_WIN + //try to copy file attributes + Zstring sourcePath; + + if (symlinkExists(templateDir)) //dereference symlink! + { + try + { + //get target directory of symbolic link + sourcePath = resolveDirectorySymlink(templateDir); //throw FileError + } + catch (FileError&) {} //dereferencing a symbolic link usually fails if it is located on network drive or client is XP: NOT really an error... + } + else //no symbolic link + sourcePath = templateDir; + + //try to copy file attributes + if (!sourcePath.empty()) + { + const DWORD sourceAttr = ::GetFileAttributes(applyLongPathPrefix(sourcePath).c_str()); + if (sourceAttr != INVALID_FILE_ATTRIBUTES) + { + const bool isCompressed = (sourceAttr & FILE_ATTRIBUTE_COMPRESSED) != 0; + const bool isEncrypted = (sourceAttr & FILE_ATTRIBUTE_ENCRYPTED) != 0; + + ::SetFileAttributes(applyLongPathPrefix(directory).c_str(), sourceAttr); + + if (isEncrypted) + ::EncryptFile(directory.c_str()); //seems no long path is required (check passed!) + + if (isCompressed) + { + HANDLE hDir = ::CreateFile(applyLongPathPrefix(directory).c_str(), + GENERIC_READ | GENERIC_WRITE, //read access required for FSCTL_SET_COMPRESSION + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + 0, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + if (hDir != INVALID_HANDLE_VALUE) + { + ZEN_ON_BLOCK_EXIT(::CloseHandle(hDir)); + + USHORT cmpState = COMPRESSION_FORMAT_DEFAULT; + + DWORD bytesReturned = 0; + ::DeviceIoControl(hDir, //handle to file or directory + FSCTL_SET_COMPRESSION, //dwIoControlCode + &cmpState, //input buffer + sizeof(cmpState), //size of input buffer + NULL, //lpOutBuffer + 0, //OutBufferSize + &bytesReturned, //number of bytes returned + NULL); //OVERLAPPED structure + } + } + } + } +#endif + zen::ScopeGuard guardNewDir = zen::makeGuard([&]() { removeDirectory(directory); }); //ensure cleanup: + + //enforce copying file permissions: it's advertized on GUI... + if (copyFilePermissions) + copyObjectPermissions(templateDir, directory, SYMLINK_FOLLOW); //throw FileError + + guardNewDir.dismiss(); //target has been created successfully! + } +} + + +void createDirectoryRecursively(const Zstring& directory, const Zstring& templateDir, bool copyFilePermissions, int level) +{ + if (level == 100) //catch endless recursion + return; + +#ifdef FFS_WIN + std::unique_ptr fnc; + if (somethingExists(directory)) + { + //handle issues with already existing short 8.3 file names on Windows + if (have8dot3NameClash(directory)) + fnc.reset(new Fix8Dot3NameClash(directory)); //move clashing object to the side + else if (dirExists(directory)) + return; + } +#elif defined FFS_LINUX + if (dirExists(directory)) + return; +#endif + else //if "somethingExists" we needn't create the parent directory + { + //try to create parent folders first + const Zstring dirParent = beforeLast(directory, FILE_NAME_SEPARATOR); + if (!dirParent.empty() && !dirExists(dirParent)) + { + //call function recursively + const Zstring templateParent = beforeLast(templateDir, FILE_NAME_SEPARATOR); //returns empty string if ch not found + createDirectoryRecursively(dirParent, templateParent, copyFilePermissions, level + 1); + } + } + + //now creation should be possible + createDirectory_straight(directory, templateDir, copyFilePermissions, level); //throw FileError +} +} + + +void zen::createDirectory(const Zstring& directory, const Zstring& templateDir, bool copyFilePermissions) +{ + //remove trailing separator + const Zstring dirFormatted = endsWith(directory, FILE_NAME_SEPARATOR) ? + beforeLast(directory, FILE_NAME_SEPARATOR) : + directory; + + const Zstring templateFormatted = endsWith(templateDir, FILE_NAME_SEPARATOR) ? + beforeLast(templateDir, FILE_NAME_SEPARATOR) : + templateDir; + + createDirectoryRecursively(dirFormatted, templateFormatted, copyFilePermissions, 0); +} + + +void zen::createDirectory(const Zstring& directory) +{ + zen::createDirectory(directory, Zstring(), false); +} + + +void zen::copySymlink(const Zstring& sourceLink, const Zstring& targetLink, bool copyFilePermissions) //throw FileError +{ + const Zstring linkPath = getSymlinkRawTargetString(sourceLink); //accept broken symlinks; throw FileError + +#ifdef FFS_WIN + const bool isDirLink = [&]() -> bool + { + const DWORD ret = ::GetFileAttributes(applyLongPathPrefix(sourceLink).c_str()); + return ret != INVALID_FILE_ATTRIBUTES && (ret& FILE_ATTRIBUTE_DIRECTORY); + }(); + + //dynamically load windows API function + typedef BOOLEAN (WINAPI *CreateSymbolicLinkFunc)(LPCTSTR lpSymlinkFileName, LPCTSTR lpTargetFileName, DWORD dwFlags); + + const SysDllFun createSymbolicLink(L"kernel32.dll", "CreateSymbolicLinkW"); + if (!createSymbolicLink) + throw FileError(_("Error loading library function:") + "\n\"" + "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 FFS_LINUX + if (::symlink(linkPath.c_str(), targetLink.c_str()) != 0) +#endif + throw FileError(_("Error copying symbolic link:") + "\n\"" + sourceLink + "\" ->\n\"" + targetLink + "\"" + "\n\n" + getLastErrorFormatted()); + + //allow only consistent objects to be created -> don't place before ::symlink, targetLink may already exist + zen::ScopeGuard guardNewDir = zen::makeGuard([&]() + { +#ifdef FFS_WIN + if (isDirLink) + removeDirectory(targetLink); + else +#endif + removeFile(targetLink); + }); + + //file times: essential for a symlink: enforce this! (don't just try!) + { + const Int64 modTime = getFileTime(sourceLink, SYMLINK_DIRECT); //throw FileError + setFileTime(targetLink, modTime, SYMLINK_DIRECT); //throw FileError + } + + if (copyFilePermissions) + copyObjectPermissions(sourceLink, targetLink, SYMLINK_DIRECT); //throw FileError + + guardNewDir.dismiss(); //target has been created successfully! +} + + +namespace +{ +Zstring createTempName(const Zstring& filename) +{ + Zstring output = filename + zen::TEMP_FILE_ENDING; + + //ensure uniqueness + for (int i = 1; somethingExists(output); ++i) + output = filename + Zchar('_') + toString(i) + zen::TEMP_FILE_ENDING; + + return output; +} + +#ifdef FFS_WIN +class CallbackData +{ +public: + CallbackData(CallbackCopyFile* cb, //may be NULL + const Zstring& sourceFile, + const Zstring& targetFile, + bool osIsvistaOrLater) : + userCallback(cb), + sourceFile_(sourceFile), + targetFile_(targetFile), + osIsvistaOrLater_(osIsvistaOrLater), + exceptionInUserCallback(false) {} + + CallbackCopyFile* userCallback; //optional! + const Zstring& sourceFile_; + const Zstring& targetFile_; + const bool osIsvistaOrLater_; + + //there is some mixed responsibility in this class, pure read-only data and abstraction for error reporting + //however we need to keep it at one place as ::CopyFileEx() requires! + + void reportUserException(const UInt64& bytesTransferred) + { + exceptionInUserCallback = true; + bytesTransferredOnException = bytesTransferred; + } + + void reportError(const std::wstring& message) { errorMsg = message; } + + void evaluateErrors() //throw + { + if (exceptionInUserCallback) + { + assert(userCallback); + if (userCallback) + userCallback->updateCopyStatus(bytesTransferredOnException); //rethrow (hopefully!) + } + if (!errorMsg.empty()) + throw FileError(errorMsg); + } + + void setSrcAttr(const FileAttrib& attr) { sourceAttr = attr; } + FileAttrib getSrcAttr() const { assert(sourceAttr.modificationTime != 0); return sourceAttr; } + +private: + CallbackData(const CallbackData&); + CallbackData& operator=(const CallbackData&); + + FileAttrib sourceAttr; + std::wstring errorMsg; // + bool exceptionInUserCallback; //these two are exclusive! + UInt64 bytesTransferredOnException; +}; + + +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 is dependent from 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 copy file modification time (only) over from source file AFTER the last inocation of this callback + //=> it is possible to adapt file creation time of target in here, but NOT file modification time! + + CallbackData& cbd = *static_cast(lpData); + + //#################### return source file attributes ################################ + if (dwCallbackReason == CALLBACK_STREAM_SWITCH) //called up front for every file (even if 0-sized) + { + BY_HANDLE_FILE_INFORMATION fileInfo = {}; + if (!::GetFileInformationByHandle(hSourceFile, &fileInfo)) + { + cbd.reportError(_("Error reading file attributes:") + "\n\"" + cbd.sourceFile_ + "\"" + "\n\n" + getLastErrorFormatted()); + return PROGRESS_CANCEL; + } + + const FileAttrib attr = { UInt64(fileInfo.nFileSizeLow, fileInfo.nFileSizeHigh), + toTimeT(fileInfo.ftLastWriteTime) + }; + //extractFileID(fileInfo)); + + cbd.setSrcAttr(attr); + + //#################### copy file creation time ################################ + FILETIME creationTime = {}; + + if (!::GetFileTime(hSourceFile, //__in HANDLE hFile, + &creationTime, //__out_opt LPFILETIME lpCreationTime, + NULL, //__out_opt LPFILETIME lpLastAccessTime, + NULL)) //__out_opt LPFILETIME lpLastWriteTime + { + cbd.reportError(_("Error reading file attributes:") + "\n\"" + cbd.sourceFile_ + "\"" + "\n\n" + getLastErrorFormatted()); + return PROGRESS_CANCEL; + } + + if (!::SetFileTime(hDestinationFile, + &creationTime, + NULL, + NULL)) + { + cbd.reportError(_("Error changing modification time:") + "\n\"" + cbd.targetFile_ + "\"" + "\n\n" + getLastErrorFormatted()); + return PROGRESS_CANCEL; + } + //############################################################################## + } + + //if (totalFileSize.QuadPart == totalBytesTransferred.QuadPart) {} //called after copy operation is finished - note: for 0-sized files this callback is invoked just ONCE! + + if (cbd.userCallback) + { + //some odd check for some possible(?) error condition + if (totalBytesTransferred.QuadPart < 0) //let's see if someone answers the call... + ::MessageBox(NULL, L"You've just discovered a bug in WIN32 API function \"CopyFileEx\"! \n\n\ + Please write a mail to the author of FreeFileSync at zhnmju123@gmx.de and simply state that\n\ + \"totalBytesTransferred.HighPart can be below zero\"!\n\n\ + This will then be handled in future versions of FreeFileSync.\n\nThanks -ZenJu", + NULL, 0); + try + { + cbd.userCallback->updateCopyStatus(UInt64(totalBytesTransferred.QuadPart)); + } + catch (...) + { + //#warning migrate to std::exception_ptr when available + + cbd.reportUserException(UInt64(totalBytesTransferred.QuadPart)); + return PROGRESS_CANCEL; + } + } + return PROGRESS_CONTINUE; +} + + +#ifndef COPY_FILE_ALLOW_DECRYPTED_DESTINATION +#define COPY_FILE_ALLOW_DECRYPTED_DESTINATION 0x00000008 +#endif + +void rawCopyWinApi_sub(const Zstring& sourceFile, + const Zstring& targetFile, + CallbackCopyFile* callback, + FileAttrib* sourceAttr) //throw FileError, ErrorTargetPathMissing, ErrorTargetExisting, ErrorFileLocked +{ + zen::ScopeGuard guardTarget = zen::makeGuard([&]() { removeFile(targetFile); }); //transactional behavior: guard just before starting copy, we don't trust ::CopyFileEx(), do we ;) + + DWORD copyFlags = COPY_FILE_FAIL_IF_EXISTS; + + //allow copying from encrypted to non-encrytped location + static bool nonEncSupported = false; + { + static boost::once_flag initNonEncOnce = BOOST_ONCE_INIT; //caveat: function scope static initialization is not thread-safe in VS 2010! + boost::call_once(initNonEncOnce, []() { nonEncSupported = winXpOrLater(); }); //encrypted destination is not supported with Windows 2000 + } + if (nonEncSupported) + copyFlags |= COPY_FILE_ALLOW_DECRYPTED_DESTINATION; + + static bool osIsvistaOrLater = false; + { + static boost::once_flag initVistaLaterOnce = BOOST_ONCE_INIT; //caveat: function scope static initialization is not thread-safe in VS 2010! + boost::call_once(initVistaLaterOnce, []() { osIsvistaOrLater = vistaOrLater(); }); + } + + CallbackData cbd(callback, sourceFile, targetFile, osIsvistaOrLater); + + const bool success = ::CopyFileEx( //same performance like CopyFile() + applyLongPathPrefix(sourceFile).c_str(), + applyLongPathPrefix(targetFile).c_str(), + copyCallbackInternal, + &cbd, + NULL, + copyFlags) == TRUE; //silence x64 perf warning + + cbd.evaluateErrors(); //throw ?, process errors in callback first! + if (!success) + { + const DWORD lastError = ::GetLastError(); + + //don't suppress "lastError == ERROR_REQUEST_ABORTED": a user aborted operation IS an error condition! + + //assemble error message... + std::wstring errorMessage = _("Error copying file:") + "\n\"" + sourceFile + "\" ->\n\"" + targetFile + "\"" + + "\n\n" + getLastErrorFormatted(lastError); + + //if file is locked (try to) use Windows Volume Shadow Copy Service + if (lastError == ERROR_SHARING_VIOLATION || + lastError == ERROR_LOCK_VIOLATION) + throw ErrorFileLocked(errorMessage); + + if (lastError == ERROR_FILE_EXISTS) //if target is existing this functions is expected to throw ErrorTargetExisting!!! + { + guardTarget.dismiss(); //don't delete file that existed previously! + throw ErrorTargetExisting(errorMessage); + } + + if (lastError == ERROR_PATH_NOT_FOUND) + { + guardTarget.dismiss(); //not relevant + throw ErrorTargetPathMissing(errorMessage); + } + + 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 && + dst::isFatDrive(targetFile) && + getFilesize(sourceFile) >= 4U * UInt64(1024U * 1024 * 1024)) //throw FileError + errorMessage += L"\nFAT volume cannot store files larger than 4 gigabyte!"; + + //note: ERROR_INVALID_PARAMETER can also occur when copying to a SharePoint server or MS SkyDrive and the target filename is of a restricted type. + } + catch (...) {} + + throw FileError(errorMessage); + } + + if (sourceAttr) + *sourceAttr = cbd.getSrcAttr(); + + { + const Int64 modTime = getFileTime(sourceFile, SYMLINK_FOLLOW); //throw FileError + setFileTime(targetFile, modTime, SYMLINK_FOLLOW); //throw FileError + //note: this sequence leads to a loss of precision of up to 1 sec! + //perf-loss on USB sticks with many small files of about 30%! damn! + } + + guardTarget.dismiss(); //target has been created successfully! +} + + +inline +void rawCopyWinApi(const Zstring& sourceFile, + const Zstring& targetFile, + CallbackCopyFile* callback, + FileAttrib* sourceAttr) //throw FileError, ErrorTargetPathMissing, ErrorTargetExisting, ErrorFileLocked +{ + try + { + rawCopyWinApi_sub(sourceFile, targetFile, callback, sourceAttr); // throw ... + } + catch (ErrorTargetExisting&) + { + //try to handle issues with already existing short 8.3 file names on Windows + if (have8dot3NameClash(targetFile)) + { + Fix8Dot3NameClash dummy(targetFile); //move clashing filename to the side + rawCopyWinApi_sub(sourceFile, targetFile, callback, sourceAttr); //throw FileError; the short filename name clash is solved, this should work now + return; + } + throw; + } +} + +//void rawCopyWinOptimized(const Zstring& sourceFile, +// const Zstring& targetFile, +// CallbackCopyFile* callback) //throw FileError, ErrorTargetPathMissing, ErrorTargetExisting, ErrorFileLocked +//{ +// /* +// BackupRead() FileRead() CopyFileEx() +// -------------------------------------------- +// Attributes NO NO YES +// create time NO NO NO +// ADS YES NO YES +// Encrypted NO(silent fail) NO YES +// Compressed NO NO NO +// Sparse YES NO NO +// PERF 6% faster - +// +// Mark stream as compressed: FSCTL_SET_COMPRESSION +// compatible with: BackupRead() FileRead() +// */ +// +// //open sourceFile for reading +// HANDLE hFileIn = ::CreateFile(applyLongPathPrefix(sourceFile).c_str(), +// GENERIC_READ, +// FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, //all shared modes are required to read files that are open in other applications +// 0, +// OPEN_EXISTING, +// FILE_FLAG_SEQUENTIAL_SCAN, +// NULL); +// if (hFileIn == INVALID_HANDLE_VALUE) +// { +// const DWORD lastError = ::GetLastError(); +// const std::wstring& errorMessage = _("Error opening file:") + "\n\"" + sourceFile + "\"" + "\n\n" + getLastErrorFormatted(lastError); +// +// //if file is locked (try to) use Windows Volume Shadow Copy Service +// if (lastError == ERROR_SHARING_VIOLATION || +// lastError == ERROR_LOCK_VIOLATION) +// throw ErrorFileLocked(errorMessage); +// +// throw FileError(errorMessage); +// } +// ZEN_ON_BLOCK_EXIT(::CloseHandle, hFileIn); +// +// +// BY_HANDLE_FILE_INFORMATION infoFileIn = {}; +// if (!::GetFileInformationByHandle(hFileIn, &infoFileIn)) +// throw FileError(_("Error reading file attributes:") + "\n\"" + sourceFile + "\"" + "\n\n" + getLastErrorFormatted()); +// +// //####################################### DST hack ########################################### +// if (dst::isFatDrive(sourceFile)) //throw() +// { +// const dst::RawTime rawTime(infoFileIn.ftCreationTime, infoFileIn.ftLastWriteTime); +// if (dst::fatHasUtcEncoded(rawTime)) //throw (std::runtime_error) +// { +// infoFileIn.ftLastWriteTime = dst::fatDecodeUtcTime(rawTime); //return last write time in real UTC, throw (std::runtime_error) +// ::GetSystemTimeAsFileTime(&infoFileIn.ftCreationTime); //real creation time information is not available... +// } +// } +// +// if (dst::isFatDrive(targetFile)) //throw() +// { +// const dst::RawTime encodedTime = dst::fatEncodeUtcTime(infoFileIn.ftLastWriteTime); //throw (std::runtime_error) +// infoFileIn.ftCreationTime = encodedTime.createTimeRaw; +// infoFileIn.ftLastWriteTime = encodedTime.writeTimeRaw; +// } +// //####################################### DST hack ########################################### +// +// const DWORD validAttribs = 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; +// +// //create targetFile and open it for writing +// HANDLE hFileOut = ::CreateFile(applyLongPathPrefix(targetFile).c_str(), +// GENERIC_READ | GENERIC_WRITE, //read access required for FSCTL_SET_COMPRESSION +// FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, +// 0, +// CREATE_NEW, +// (infoFileIn.dwFileAttributes & validAttribs) | FILE_FLAG_SEQUENTIAL_SCAN, +// NULL); +// if (hFileOut == INVALID_HANDLE_VALUE) +// { +// const DWORD lastError = ::GetLastError(); +// const std::wstring& errorMessage = _("Error writing file:") + "\n\"" + targetFile + "\"" + +// "\n\n" + getLastErrorFormatted(lastError); +// +// if (lastError == ERROR_FILE_EXISTS) +// throw ErrorTargetExisting(errorMessage); +// +// if (lastError == ERROR_PATH_NOT_FOUND) +// throw ErrorTargetPathMissing(errorMessage); +// +// throw FileError(errorMessage); +// } +// Loki::ScopeGuard guardTarget = Loki::MakeGuard(&removeFile, targetFile); //transactional behavior: guard just after opening target and before managing hFileOut +// +// ZEN_ON_BLOCK_EXIT(::CloseHandle, hFileOut); +// +// +//#ifndef _MSC_VER +//#warning teste perf von GetVolumeInformationByHandleW +//#endif +// DWORD fsFlags = 0; +// if (!GetVolumeInformationByHandleW(hFileOut, //__in HANDLE hFile, +// NULL, //__out_opt LPTSTR lpVolumeNameBuffer, +// 0, //__in DWORD nVolumeNameSize, +// NULL, //__out_opt LPDWORD lpVolumeSerialNumber, +// NULL, //__out_opt LPDWORD lpMaximumComponentLength, +// &fsFlags, //__out_opt LPDWORD lpFileSystemFlags, +// NULL, //__out LPTSTR lpFileSystemNameBuffer, +// 0)) //__in DWORD nFileSystemNameSize +// throw FileError(_("Error reading file attributes:") + "\n\"" + sourceFile + "\"" + "\n\n" + getLastErrorFormatted()); +// +// const bool sourceIsEncrypted = (infoFileIn.dwFileAttributes & FILE_ATTRIBUTE_ENCRYPTED) != 0; +// const bool sourceIsCompressed = (infoFileIn.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED) != 0; +// const bool sourceIsSparse = (infoFileIn.dwFileAttributes & FILE_ATTRIBUTE_SPARSE_FILE) != 0; +// +// bool targetSupportSparse = (fsFlags & FILE_SUPPORTS_SPARSE_FILES) != 0; +// bool targetSupportCompressed = (fsFlags & FILE_FILE_COMPRESSION ) != 0; +// bool targetSupportStreams = (fsFlags & FILE_NAMED_STREAMS ) != 0; +// +// +// const bool useBackupFun = !sourceIsEncrypted; //http://msdn.microsoft.com/en-us/library/aa362509(v=VS.85).aspx +// +// if (sourceIsCompressed && targetSupportCompressed) +// { +// USHORT cmpState = COMPRESSION_FORMAT_DEFAULT; +// +// DWORD bytesReturned = 0; +// if (!DeviceIoControl(hFileOut, //handle to file or directory +// FSCTL_SET_COMPRESSION, //dwIoControlCode +// &cmpState, //input buffer +// sizeof(cmpState), //size of input buffer +// NULL, //lpOutBuffer +// 0, //OutBufferSize +// &bytesReturned, //number of bytes returned +// NULL)) //OVERLAPPED structure +// throw FileError(_("Error writing file:") + "\n\"" + targetFile + "\"" + +// "\n\n" + getLastErrorFormatted() + +// "\nFailed to write NTFS compressed attribute!"); +// } +// +// //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 +// if (sourceIsSparse && targetSupportSparse) +// { +// if (useBackupFun) +// { +// DWORD bytesReturned = 0; +// if (!DeviceIoControl(hFileOut, //handle to file +// FSCTL_SET_SPARSE, //dwIoControlCode +// NULL, //input buffer +// 0, //size of input buffer +// NULL, //lpOutBuffer +// 0, //OutBufferSize +// &bytesReturned, //number of bytes returned +// NULL)) //OVERLAPPED structure +// throw FileError(_("Error writing file:") + "\n\"" + targetFile + "\"" + +// "\n\n" + getLastErrorFormatted() + +// "\nFailed to write NTFS sparse attribute!"); +// } +// } +// +// +// const DWORD BUFFER_SIZE = 512 * 1024; //512 kb seems to be a reasonable buffer size +// static boost::thread_specific_ptr> cpyBuf; +// if (!cpyBuf.get()) +// cpyBuf.reset(new std::vector(BUFFER_SIZE)); //512 kb seems to be a reasonable buffer size +// std::vector& buffer = *cpyBuf; +// +// struct ManageCtxt //manage context for BackupRead()/BackupWrite() +// { +// ManageCtxt() : read(NULL), write(NULL) {} +// ~ManageCtxt() +// { +// if (read != NULL) +// ::BackupRead (0, NULL, 0, NULL, true, false, &read); +// if (write != NULL) +// ::BackupWrite(0, NULL, 0, NULL, true, false, &write); +// } +// +// LPVOID read; +// LPVOID write; +// } context; +// +// //copy contents of sourceFile to targetFile +// UInt64 totalBytesTransferred; +// +// bool eof = false; +// do +// { +// DWORD bytesRead = 0; +// +// if (useBackupFun) +// { +// if (!::BackupRead(hFileIn, //__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, +// &context.read)) //__out LPVOID *lpContext +// throw FileError(_("Error reading file:") + "\n\"" + sourceFile + "\"" + +// "\n\n" + getLastErrorFormatted()); +// } +// else if (!::ReadFile(hFileIn, //__in HANDLE hFile, +// &buffer[0], //__out LPVOID lpBuffer, +// BUFFER_SIZE, //__in DWORD nNumberOfBytesToRead, +// &bytesRead, //__out_opt LPDWORD lpNumberOfBytesRead, +// NULL)) //__inout_opt LPOVERLAPPED lpOverlapped +// throw FileError(_("Error reading file:") + "\n\"" + sourceFile + "\"" + +// "\n\n" + getLastErrorFormatted()); +// +// if (bytesRead > BUFFER_SIZE) +// throw FileError(_("Error reading file:") + "\n\"" + sourceFile + "\"" + +// "\n\n" + "buffer overflow"); +// +// if (bytesRead < BUFFER_SIZE) +// eof = true; +// +// DWORD bytesWritten = 0; +// +// if (useBackupFun) +// { +// if (!::BackupWrite(hFileOut, //__in HANDLE hFile, +// &buffer[0], //__in LPBYTE lpBuffer, +// bytesRead, //__in DWORD nNumberOfBytesToWrite, +// &bytesWritten, //__out LPDWORD lpNumberOfBytesWritten, +// false, //__in BOOL bAbort, +// false, //__in BOOL bProcessSecurity, +// &context.write)) //__out LPVOID *lpContext +// throw FileError(_("Error writing file:") + "\n\"" + targetFile + "\"" + +// "\n\n" + getLastErrorFormatted() + " (w)"); //w -> distinguish from fopen error message! +// } +// else if (!::WriteFile(hFileOut, //__in HANDLE hFile, +// &buffer[0], //__out LPVOID lpBuffer, +// bytesRead, //__in DWORD nNumberOfBytesToWrite, +// &bytesWritten, //__out_opt LPDWORD lpNumberOfBytesWritten, +// NULL)) //__inout_opt LPOVERLAPPED lpOverlapped +// throw FileError(_("Error writing file:") + "\n\"" + targetFile + "\"" + +// "\n\n" + getLastErrorFormatted() + " (w)"); //w -> distinguish from fopen error message! +// +// if (bytesWritten != bytesRead) +// throw FileError(_("Error writing file:") + "\n\"" + targetFile + "\"" + "\n\n" + "incomplete write"); +// +// totalBytesTransferred += bytesRead; +// +//#ifndef _MSC_VER +//#warning totalBytesTransferred kann größer als filesize sein!! +//#endif +// +// //invoke callback method to update progress indicators +// if (callback != NULL) +// switch (callback->updateCopyStatus(totalBytesTransferred)) +// { +// case CallbackCopyFile::CONTINUE: +// break; +// +// case CallbackCopyFile::CANCEL: //a user aborted operation IS an error condition! +// throw FileError(_("Error copying file:") + "\n\"" + sourceFile + "\" ->\n\"" + +// targetFile + "\"\n\n" + _("Operation aborted!")); +// } +// } +// while (!eof); +// +// +// if (totalBytesTransferred == 0) //BackupRead silently fails reading encrypted files -> double check! +// { +// LARGE_INTEGER inputSize = {}; +// if (!::GetFileSizeEx(hFileIn, &inputSize)) +// throw FileError(_("Error reading file attributes:") + "\n\"" + sourceFile + "\"" + "\n\n" + getLastErrorFormatted()); +// +// if (inputSize.QuadPart != 0) +// throw FileError(_("Error reading file:") + "\n\"" + sourceFile + "\"" + "\n\n" + "unknown error"); +// } +// +// //time needs to be set at the end: BackupWrite() changes file time +// if (!::SetFileTime(hFileOut, +// &infoFileIn.ftCreationTime, +// NULL, +// &infoFileIn.ftLastWriteTime)) +// throw FileError(_("Error changing modification time:") + "\n\"" + targetFile + "\"" + "\n\n" + getLastErrorFormatted()); +// +// +//#ifndef NDEBUG //dst hack: verify data written +// if (dst::isFatDrive(targetFile)) //throw() +// { +// WIN32_FILE_ATTRIBUTE_DATA debugeAttr = {}; +// assert(::GetFileAttributesEx(applyLongPathPrefix(targetFile).c_str(), //__in LPCTSTR lpFileName, +// GetFileExInfoStandard, //__in GET_FILEEX_INFO_LEVELS fInfoLevelId, +// &debugeAttr)); //__out LPVOID lpFileInformation +// +// assert(::CompareFileTime(&debugeAttr.ftCreationTime, &infoFileIn.ftCreationTime) == 0); +// assert(::CompareFileTime(&debugeAttr.ftLastWriteTime, &infoFileIn.ftLastWriteTime) == 0); +// } +//#endif +// +// guardTarget.Dismiss(); +// +// /* +// //create test sparse file +// 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, +// 0, +// CREATE_NEW, +// FILE_FLAG_SEQUENTIAL_SCAN, +// NULL); +// DWORD br = 0; +// if (!::DeviceIoControl(hSparse, FSCTL_SET_SPARSE, NULL, 0, NULL, 0, &br,NULL)) +// throw 1; +// +// LARGE_INTEGER liDistanceToMove = {}; +// liDistanceToMove.QuadPart = 1024 * 1024 * 1024; //create 5 TB sparse file +// liDistanceToMove.QuadPart *= 5 * 1024; // +// if (!::SetFilePointerEx(hSparse, liDistanceToMove, NULL, FILE_BEGIN)) +// throw 1; +// +// if (!SetEndOfFile(hSparse)) +// throw 1; +// +// FILE_ZERO_DATA_INFORMATION zeroInfo = {}; +// zeroInfo.BeyondFinalZero.QuadPart = liDistanceToMove.QuadPart; +// if (!::DeviceIoControl(hSparse, FSCTL_SET_ZERO_DATA, &zeroInfo, sizeof(zeroInfo), NULL, 0, &br, NULL)) +// throw 1; +// +// ::CloseHandle(hSparse); +// */ +//} +#endif + +#ifdef FFS_LINUX +void rawCopyStream(const Zstring& sourceFile, + const Zstring& targetFile, + CallbackCopyFile* callback, + FileAttrib* sourceAttr) //throw FileError, ErrorTargetPathMissing, ErrorTargetExisting +{ + zen::ScopeGuard guardTarget = zen::makeGuard([&]() { removeFile(targetFile); }); //transactional behavior: place guard before lifetime of FileOutput + try + { + //open sourceFile for reading + FileInput fileIn(sourceFile); //throw FileError + + //create targetFile and open it for writing + FileOutput fileOut(targetFile, FileOutput::ACC_CREATE_NEW); //throw FileError, ErrorTargetPathMissing, ErrorTargetExisting + + static boost::thread_specific_ptr> cpyBuf; + if (!cpyBuf.get()) + cpyBuf.reset(new std::vector(512 * 1024)); //512 kb seems to be a reasonable buffer size + std::vector& buffer = *cpyBuf; + + //copy contents of sourceFile to targetFile + UInt64 totalBytesTransferred; + do + { + const size_t bytesRead = fileIn.read(&buffer[0], buffer.size()); //throw FileError + + fileOut.write(&buffer[0], bytesRead); //throw FileError + + totalBytesTransferred += bytesRead; + + //invoke callback method to update progress indicators + if (callback) + callback->updateCopyStatus(totalBytesTransferred); + } + while (!fileIn.eof()); + } + catch (ErrorTargetExisting&) + { + guardTarget.dismiss(); //don't delete file that existed previously! + throw; + } + + //adapt file modification time: + { + struct stat srcInfo = {}; + if (::stat(sourceFile.c_str(), &srcInfo) != 0) //read file attributes from source directory + throw FileError(_("Error reading file attributes:") + "\n\"" + sourceFile + "\"" + "\n\n" + getLastErrorFormatted()); + + if (sourceAttr) + { + sourceAttr->fileSize = UInt64(srcInfo.st_size); + sourceAttr->modificationTime = srcInfo.st_mtime; + } + + struct utimbuf newTimes = {}; + newTimes.actime = srcInfo.st_atime; + newTimes.modtime = srcInfo.st_mtime; + + //set new "last write time" + if (::utime(targetFile.c_str(), &newTimes) != 0) + throw FileError(_("Error changing modification time:") + "\n\"" + targetFile + "\"" + "\n\n" + getLastErrorFormatted()); + } + + guardTarget.dismiss(); //target has been created successfully! +} +#endif + + +inline +void copyFileImpl(const Zstring& sourceFile, + const Zstring& targetFile, + CallbackCopyFile* callback, + FileAttrib* sourceAttr) //throw FileError, ErrorTargetPathMissing, ErrorTargetExisting, ErrorFileLocked +{ +#ifdef FFS_WIN + /* + rawCopyWinApi() rawCopyWinOptimized() + ------------------------------------- + Attributes YES YES + Filetimes YES YES + ADS YES YES + Encrypted YES YES + Compressed NO YES + Sparse NO YES + PERF - 6% faster + SAMBA, ect. YES UNKNOWN! -> issues writing ADS to Samba, issues reading from NAS, error copying files having "blocked" state... ect. damn! + */ + + rawCopyWinApi(sourceFile, targetFile, callback, sourceAttr); //throw FileError, ErrorTargetPathMissing, ErrorTargetExisting, ErrorFileLocked + //rawCopyWinOptimized(sourceFile, targetFile, callback); //throw FileError, ErrorTargetPathMissing, ErrorTargetExisting, ErrorFileLocked ->about 8% faster + +#elif defined FFS_LINUX + rawCopyStream(sourceFile, targetFile, callback, sourceAttr); //throw FileError, ErrorTargetPathMissing, ErrorTargetExisting +#endif +} +} + + +void zen::copyFile(const Zstring& sourceFile, //throw FileError, ErrorTargetPathMissing, ErrorFileLocked + const Zstring& targetFile, + bool copyFilePermissions, + bool transactionalCopy, + CallbackCopyFile* callback, + FileAttrib* sourceAttr) +{ + if (transactionalCopy) + { + Zstring temporary = targetFile + zen::TEMP_FILE_ENDING; //use temporary file until a correct date has been set + zen::ScopeGuard guardTempFile = zen::makeGuard([&]() { removeFile(temporary); }); //transactional behavior: ensure cleanup (e.g. network drop) -> ref to temporary[!] + + //raw file copy + try + { + copyFileImpl(sourceFile, temporary, callback, sourceAttr); //throw FileError: ErrorTargetPathMissing, ErrorTargetExisting, ErrorFileLocked + } + catch (ErrorTargetExisting&) + { + //determine non-used temp file name "first": + //using optimistic strategy: assume everything goes well, but recover on error -> minimize file accesses + temporary = createTempName(targetFile); + + //retry + copyFileImpl(sourceFile, temporary, callback, sourceAttr); //throw FileError + } + + //have target file deleted (after read access on source and target has been confirmed) => allow for almost transactional overwrite + if (callback) + callback->deleteTargetFile(targetFile); + + //rename temporary file: + //perf: this call is REALLY expensive on unbuffered volumes! ~40% performance decrease on FAT USB stick! + renameFile(temporary, targetFile); //throw FileError + + guardTempFile.dismiss(); + } + else + { + //have target file deleted + if (callback) callback->deleteTargetFile(targetFile); + + copyFileImpl(sourceFile, targetFile, callback, sourceAttr); //throw FileError: ErrorTargetPathMissing, ErrorTargetExisting, ErrorFileLocked + } + /* + 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) + */ + + //set file permissions + if (copyFilePermissions) + { + zen::ScopeGuard guardTargetFile = zen::makeGuard([&]() { removeFile(targetFile);}); + copyObjectPermissions(sourceFile, targetFile, SYMLINK_FOLLOW); //throw FileError + guardTargetFile.dismiss(); //target has been created successfully! + } +} -- cgit