summaryrefslogtreecommitdiff
path: root/FreeFileSync/Source/base/db_file.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'FreeFileSync/Source/base/db_file.cpp')
-rwxr-xr-xFreeFileSync/Source/base/db_file.cpp936
1 files changed, 936 insertions, 0 deletions
diff --git a/FreeFileSync/Source/base/db_file.cpp b/FreeFileSync/Source/base/db_file.cpp
new file mode 100755
index 00000000..5a134e29
--- /dev/null
+++ b/FreeFileSync/Source/base/db_file.cpp
@@ -0,0 +1,936 @@
+// *****************************************************************************
+// * This file is part of the FreeFileSync project. It is distributed under *
+// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
+// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
+// *****************************************************************************
+
+#include "db_file.h"
+#include <zen/guid.h>
+#include <zen/crc.h>
+#include <wx+/zlib_wrap.h>
+
+
+using namespace zen;
+using namespace fff;
+
+
+namespace
+{
+//-------------------------------------------------------------------------------------------------------------------------------
+const char FILE_FORMAT_DESCR[] = "FreeFileSync";
+const int DB_FORMAT_CONTAINER = 10; //since 2017-02-01
+const int DB_FORMAT_STREAM = 3; //
+//-------------------------------------------------------------------------------------------------------------------------------
+
+struct SessionData
+{
+ bool isLeadStream = false;
+ ByteArray rawStream;
+};
+bool operator==(const SessionData& lhs, const SessionData& rhs) { return lhs.isLeadStream == rhs.isLeadStream && lhs.rawStream == rhs.rawStream; }
+
+using UniqueId = std::string;
+using DbStreams = std::map<UniqueId, SessionData>; //list of streams ordered by session UUID
+
+/*------------------------------------------------------------------------------
+ | ensure 32/64 bit portability: use fixed size data types only e.g. uint32_t |
+ ------------------------------------------------------------------------------*/
+
+template <SelectedSide side> inline
+AbstractPath getDatabaseFilePath(const BaseFolderPair& baseFolder, bool tempfile = false)
+{
+ //Linux and Windows builds are binary incompatible: different file id?, problem with case sensitivity?
+ //precomposed/decomposed UTF? are UTC file times really compatible? what about endianess!?
+ //however 32 and 64-bit FreeFileSync are designed to produce binary-identical db files!
+ //Give db files different names.
+ const Zstring dbName = Zstr(".sync"); //files beginning with dots are hidden e.g. in Nautilus
+ Zstring dbFileName;
+ if (tempfile) //generate (hopefully) unique file name to avoid clashing with some remnant ffs_tmp file
+ {
+ const Zstring shortGuid = printNumber<Zstring>(Zstr("%04x"), static_cast<unsigned int>(getCrc16(generateGUID())));
+ dbFileName = dbName + Zchar('.') + shortGuid + AFS::TEMP_FILE_ENDING;
+ }
+ else
+ dbFileName = dbName + SYNC_DB_FILE_ENDING;
+
+ return AFS::appendRelPath(baseFolder.getAbstractPath<side>(), dbFileName);
+}
+
+//#######################################################################################################################################
+
+void saveStreams(const DbStreams& streamList, const AbstractPath& dbPath, const IOCallback& notifyUnbufferedIO) //throw FileError
+{
+ const std::unique_ptr<AFS::OutputStream> fileStreamOut = AFS::getOutputStream(dbPath, //throw FileError
+ nullptr /*streamSize*/,
+ notifyUnbufferedIO /*throw X*/);
+ //write FreeFileSync file identifier
+ writeArray(*fileStreamOut, FILE_FORMAT_DESCR, sizeof(FILE_FORMAT_DESCR)); //throw FileError, X
+
+ //save file format version
+ writeNumber<int32_t>(*fileStreamOut, DB_FORMAT_CONTAINER); //throw FileError, X
+
+ //write stream list
+ writeNumber<uint32_t>(*fileStreamOut, static_cast<uint32_t>(streamList.size())); //throw FileError, X
+
+ for (const auto& stream : streamList)
+ {
+ writeContainer<std::string>(*fileStreamOut, stream.first ); //throw FileError, X
+
+ writeNumber <int8_t >(*fileStreamOut, stream.second.isLeadStream); //
+ writeContainer<ByteArray>(*fileStreamOut, stream.second.rawStream); //
+ }
+
+ //commit and close stream:
+ fileStreamOut->finalize(); //throw FileError, X
+
+}
+
+
+DbStreams loadStreams(const AbstractPath& dbPath, const IOCallback& notifyUnbufferedIO) //throw FileError, FileErrorDatabaseNotExisting
+{
+ try
+ {
+ const std::unique_ptr<AFS::InputStream> fileStreamIn = AFS::getInputStream(dbPath, notifyUnbufferedIO); //throw FileError, ErrorFileLocked, X
+
+ //read FreeFileSync file identifier
+ char formatDescr[sizeof(FILE_FORMAT_DESCR)] = {};
+ readArray(*fileStreamIn, formatDescr, sizeof(formatDescr)); //throw FileError, ErrorFileLocked, X, UnexpectedEndOfStreamError
+
+ if (!std::equal(FILE_FORMAT_DESCR, FILE_FORMAT_DESCR + sizeof(FILE_FORMAT_DESCR), formatDescr))
+ throw FileError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(AFS::getDisplayPath(dbPath))));
+
+ const int version = readNumber<int32_t>(*fileStreamIn); //throw FileError, ErrorFileLocked, X, UnexpectedEndOfStreamError
+
+ //TODO: remove migration code at some time! 2017-02-01
+ if (version != 9 &&
+ version != DB_FORMAT_CONTAINER) //read file format version number
+ throw FileError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(AFS::getDisplayPath(dbPath))));
+
+ DbStreams output;
+
+ //read stream list
+ size_t dbCount = readNumber<uint32_t>(*fileStreamIn); //throw FileError, ErrorFileLocked, X, UnexpectedEndOfStreamError
+ while (dbCount-- != 0)
+ {
+ //DB id of partner databases
+ std::string sessionID = readContainer<std::string>(*fileStreamIn); //throw FileError, ErrorFileLocked, X, UnexpectedEndOfStreamError
+
+ SessionData sessionData = {};
+
+ //TODO: remove migration code at some time! 2017-02-01
+ if (version == 9)
+ {
+ sessionData.rawStream = readContainer<ByteArray>(*fileStreamIn); //throw FileError, ErrorFileLocked, X, UnexpectedEndOfStreamError
+
+ MemoryStreamIn<ByteArray> streamIn(sessionData.rawStream);
+ const int streamVersion = readNumber<int32_t>(streamIn); //throw UnexpectedEndOfStreamError
+ if (streamVersion != 2) //don't throw here due to old stream formats
+ continue;
+ sessionData.isLeadStream = readNumber<int8_t>(streamIn) != 0; //throw FileError, X, UnexpectedEndOfStreamError
+ }
+ else
+ {
+ sessionData.isLeadStream = readNumber<int8_t>(*fileStreamIn) != 0; //throw FileError, ErrorFileLocked, X, UnexpectedEndOfStreamError
+ sessionData.rawStream = readContainer<ByteArray>(*fileStreamIn); //throw FileError, ErrorFileLocked, X, UnexpectedEndOfStreamError
+ }
+
+ output[sessionID] = std::move(sessionData);
+ }
+ return output;
+ }
+ catch (FileError&)
+ {
+ bool dbNotYetExisting = false;
+ try { dbNotYetExisting = !AFS::getItemTypeIfExists(dbPath); /*throw FileError*/ }
+ catch (FileError&) {} //previous exception is more relevant
+
+ if (dbNotYetExisting) //throw FileError
+ throw FileErrorDatabaseNotExisting(_("Initial synchronization:") + L" \n" +
+ replaceCpy(_("Database file %x does not yet exist."), L"%x", fmtPath(AFS::getDisplayPath(dbPath))));
+ else
+ throw;
+ }
+ catch (UnexpectedEndOfStreamError&)
+ {
+ throw FileError(_("Database file is corrupted:") + L"\n" + fmtPath(AFS::getDisplayPath(dbPath)), L"Unexpected end of stream.");
+ }
+ catch (const std::bad_alloc& e) //still required?
+ {
+ throw FileError(_("Database file is corrupted:") + L"\n" + fmtPath(AFS::getDisplayPath(dbPath)),
+ _("Out of memory.") + L" " + utfTo<std::wstring>(e.what()));
+ }
+}
+
+//#######################################################################################################################################
+
+class StreamGenerator
+{
+public:
+ static void execute(const InSyncFolder& dbFolder, //throw FileError
+ const std::wstring& displayFilePathL, //used for diagnostics only
+ const std::wstring& displayFilePathR,
+ ByteArray& streamL,
+ ByteArray& streamR)
+ {
+ MemoryStreamOut<ByteArray> outL;
+ MemoryStreamOut<ByteArray> outR;
+ //save format version
+ writeNumber<int32_t>(outL, DB_FORMAT_STREAM);
+ writeNumber<int32_t>(outR, DB_FORMAT_STREAM);
+
+ auto compStream = [&](const ByteArray& stream) -> ByteArray //throw FileError
+ {
+ try
+ {
+ /* Zlib: optimal level - testcase 1 million files
+ level|size [MB]|time [ms]
+ 0 49.54 272 (uncompressed)
+ 1 14.53 1013
+ 2 14.13 1106
+ 3 13.76 1288 - best compromise between speed and compression
+ 4 13.20 1526
+ 5 12.73 1916
+ 6 12.58 2765
+ 7 12.54 3633
+ 8 12.51 9032
+ 9 12.50 19698 (maximal compression) */
+ return compress(stream, 3); //throw ZlibInternalError
+ }
+ catch (ZlibInternalError&)
+ {
+ throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(displayFilePathL + L"/" + displayFilePathR)), L"zlib internal error");
+ }
+ };
+
+ StreamGenerator generator;
+ //PERF_START
+ generator.recurse(dbFolder);
+ //PERF_STOP
+
+ const ByteArray bufText = compStream(generator.streamOutText_ .ref());
+ const ByteArray bufSmallNum = compStream(generator.streamOutSmallNum_.ref());
+ const ByteArray bufBigNum = compStream(generator.streamOutBigNum_ .ref());
+
+ MemoryStreamOut<ByteArray> streamOut;
+ writeContainer(streamOut, bufText);
+ writeContainer(streamOut, bufSmallNum);
+ writeContainer(streamOut, bufBigNum);
+
+ const ByteArray& buf = streamOut.ref();
+
+ //distribute "outputBoth" over left and right streams:
+ const size_t size1stPart = buf.size() / 2;
+ const size_t size2ndPart = buf.size() - size1stPart;
+
+ writeNumber<uint64_t>(outL, size1stPart);
+ writeNumber<uint64_t>(outR, size2ndPart);
+
+ if (size1stPart > 0) writeArray(outL, &*buf.begin(), size1stPart);
+ if (size2ndPart > 0) writeArray(outR, &*buf.begin() + size1stPart, size2ndPart);
+
+ streamL = outL.ref();
+ streamR = outR.ref();
+ }
+
+private:
+ void recurse(const InSyncFolder& container)
+ {
+ writeNumber<uint32_t>(streamOutSmallNum_, static_cast<uint32_t>(container.files.size()));
+ for (const auto& dbFile : container.files)
+ {
+ writeUtf8(streamOutText_, dbFile.first);
+ writeNumber(streamOutSmallNum_, static_cast<int32_t>(dbFile.second.cmpVar));
+ writeNumber<uint64_t>(streamOutSmallNum_, dbFile.second.fileSize);
+
+ writeFileDescr(streamOutBigNum_, dbFile.second.left);
+ writeFileDescr(streamOutBigNum_, dbFile.second.right);
+ }
+
+ writeNumber<uint32_t>(streamOutSmallNum_, static_cast<uint32_t>(container.symlinks.size()));
+ for (const auto& dbSymlink : container.symlinks)
+ {
+ writeUtf8(streamOutText_, dbSymlink.first);
+ writeNumber(streamOutSmallNum_, static_cast<int32_t>(dbSymlink.second.cmpVar));
+
+ writeLinkDescr(streamOutBigNum_, dbSymlink.second.left);
+ writeLinkDescr(streamOutBigNum_, dbSymlink.second.right);
+ }
+
+ writeNumber<uint32_t>(streamOutSmallNum_, static_cast<uint32_t>(container.folders.size()));
+ for (const auto& dbFolder : container.folders)
+ {
+ writeUtf8(streamOutText_, dbFolder.first);
+ writeNumber<int32_t>(streamOutSmallNum_, dbFolder.second.status);
+
+ recurse(dbFolder.second);
+ }
+ }
+
+ static void writeUtf8(MemoryStreamOut<ByteArray>& streamOut, const Zstring& str) { writeContainer(streamOut, utfTo<Zbase<char>>(str)); }
+
+ static void writeFileDescr(MemoryStreamOut<ByteArray>& streamOut, const InSyncDescrFile& descr)
+ {
+ writeNumber<std::int64_t>(streamOut, descr.modTime);
+ writeContainer(streamOut, descr.fileId);
+ static_assert(std::is_same_v<decltype(descr.fileId), Zbase<char>>);
+ }
+
+ static void writeLinkDescr(MemoryStreamOut<ByteArray>& streamOut, const InSyncDescrLink& descr)
+ {
+ writeNumber<int64_t>(streamOut, descr.modTime);
+ }
+
+ //maximize zlib compression by grouping similar data (=> 20% size reduction!)
+ // -> further ~5% reduction possible by having one container per data type
+ MemoryStreamOut<ByteArray> streamOutText_; //
+ MemoryStreamOut<ByteArray> streamOutSmallNum_; //data with bias to lead side (= always left in this context)
+ MemoryStreamOut<ByteArray> streamOutBigNum_; //
+};
+
+
+class StreamParser
+{
+public:
+ static std::shared_ptr<InSyncFolder> execute(bool leadStreamLeft, //throw FileError
+ const ByteArray& streamL,
+ const ByteArray& streamR,
+ const std::wstring& displayFilePathL, //used for diagnostics only
+ const std::wstring& displayFilePathR)
+ {
+ auto decompStream = [&](const ByteArray& stream) -> ByteArray //throw FileError
+ {
+ try
+ {
+ return decompress(stream); //throw ZlibInternalError
+ }
+ catch (ZlibInternalError&)
+ {
+ throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(displayFilePathL + L"/" + displayFilePathR)), L"Zlib internal error");
+ }
+ };
+
+ try
+ {
+ MemoryStreamIn<ByteArray> streamInL(streamL);
+ MemoryStreamIn<ByteArray> streamInR(streamR);
+
+ const int streamVersion = readNumber<int32_t>(streamInL); //throw UnexpectedEndOfStreamError
+ const int streamVersionR = readNumber<int32_t>(streamInR); //throw UnexpectedEndOfStreamError
+
+ if (streamVersion != streamVersionR)
+ throw FileError(_("Database file is corrupted:") + L"\n" + fmtPath(displayFilePathL) + L"\n" + fmtPath(displayFilePathR), L"Different stream formats");
+
+ //TODO: remove migration code at some time! 2017-02-01
+ if (streamVersion != 2 &&
+ streamVersion != DB_FORMAT_STREAM)
+ throw FileError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(displayFilePathL)), L"Unknown stream format");
+
+ //TODO: remove migration code at some time! 2017-02-01
+ if (streamVersion == 2)
+ {
+ const bool has1stPartL = readNumber<int8_t>(streamInL) != 0; //throw UnexpectedEndOfStreamError
+ const bool has1stPartR = readNumber<int8_t>(streamInR) != 0; //
+
+ if (has1stPartL == has1stPartR)
+ throw FileError(_("Database file is corrupted:") + L"\n" + fmtPath(displayFilePathL) + L"\n" + fmtPath(displayFilePathR), L"Second stream part missing");
+ if (has1stPartL != leadStreamLeft)
+ throw FileError(_("Database file is corrupted:") + L"\n" + fmtPath(displayFilePathL) + L"\n" + fmtPath(displayFilePathR), L"has1stPartL != leadStreamLeft");
+
+ MemoryStreamIn<ByteArray>& in1stPart = leadStreamLeft ? streamInL : streamInR;
+ MemoryStreamIn<ByteArray>& in2ndPart = leadStreamLeft ? streamInR : streamInL;
+
+ const size_t size1stPart = static_cast<size_t>(readNumber<uint64_t>(in1stPart));
+ const size_t size2ndPart = static_cast<size_t>(readNumber<uint64_t>(in2ndPart));
+
+ ByteArray tmpB;
+ tmpB.resize(size1stPart + size2ndPart); //throw bad_alloc
+ readArray(in1stPart, &*tmpB.begin(), size1stPart); //stream always non-empty
+ readArray(in2ndPart, &*tmpB.begin() + size1stPart, size2ndPart); //throw UnexpectedEndOfStreamError
+
+ const ByteArray tmpL = readContainer<ByteArray>(streamInL);
+ const ByteArray tmpR = readContainer<ByteArray>(streamInR);
+
+ auto output = std::make_shared<InSyncFolder>(InSyncFolder::DIR_STATUS_IN_SYNC);
+ StreamParserV2 parser(decompStream(tmpL),
+ decompStream(tmpR),
+ decompStream(tmpB));
+ parser.recurse(*output); //throw UnexpectedEndOfStreamError
+ return output;
+ }
+ else
+ {
+ MemoryStreamIn<ByteArray>& streamInPart1 = leadStreamLeft ? streamInL : streamInR;
+ MemoryStreamIn<ByteArray>& streamInPart2 = leadStreamLeft ? streamInR : streamInL;
+
+ const size_t sizePart1 = static_cast<size_t>(readNumber<uint64_t>(streamInPart1));
+ const size_t sizePart2 = static_cast<size_t>(readNumber<uint64_t>(streamInPart2));
+
+ ByteArray buf;
+ buf.resize(sizePart1 + sizePart2); //throw bad_alloc
+
+ if (sizePart1 > 0) readArray(streamInPart1, &*buf.begin(), sizePart1); //throw UnexpectedEndOfStreamError
+ if (sizePart2 > 0) readArray(streamInPart2, &*buf.begin() + sizePart1, sizePart2); //
+
+ MemoryStreamIn<ByteArray> streamIn(buf);
+ const ByteArray bufText = readContainer<ByteArray>(streamIn); //
+ const ByteArray bufSmallNum = readContainer<ByteArray>(streamIn); //throw UnexpectedEndOfStreamError
+ const ByteArray bufBigNum = readContainer<ByteArray>(streamIn); //
+
+ auto output = std::make_shared<InSyncFolder>(InSyncFolder::DIR_STATUS_IN_SYNC);
+ StreamParser parser(streamVersion,
+ decompStream(bufText),
+ decompStream(bufSmallNum),
+ decompStream(bufBigNum)); //throw FileError
+ if (leadStreamLeft)
+ parser.recurse<LEFT_SIDE>(*output); //throw UnexpectedEndOfStreamError
+ else
+ parser.recurse<RIGHT_SIDE>(*output); //throw UnexpectedEndOfStreamError
+ return output;
+ }
+ }
+ catch (const UnexpectedEndOfStreamError&)
+ {
+ throw FileError(_("Database file is corrupted:") + L"\n" + fmtPath(displayFilePathL) + L"\n" + fmtPath(displayFilePathR), L"Unexpected end of stream.");
+ }
+ catch (const std::bad_alloc& e)
+ {
+ throw FileError(_("Database file is corrupted:") + L"\n" + fmtPath(displayFilePathL) + L"\n" + fmtPath(displayFilePathR),
+ _("Out of memory.") + L" " + utfTo<std::wstring>(e.what()));
+ }
+ }
+
+private:
+ StreamParser(int streamVersion, const ByteArray& bufText, const ByteArray& bufSmallNumbers, const ByteArray& bufBigNumbers) :
+ streamVersion_(streamVersion),
+ streamInText_(bufText),
+ streamInSmallNum_(bufSmallNumbers),
+ streamInBigNum_(bufBigNumbers)
+ {
+ (void)streamVersion_; //clang: -Wunused-private-field
+ }
+
+ template <SelectedSide leadSide>
+ void recurse(InSyncFolder& container) //throw UnexpectedEndOfStreamError
+ {
+ size_t fileCount = readNumber<uint32_t>(streamInSmallNum_);
+ while (fileCount-- != 0)
+ {
+ const Zstring itemName = readUtf8(streamInText_);
+ const auto cmpVar = static_cast<CompareVariant>(readNumber<int32_t>(streamInSmallNum_));
+ const uint64_t fileSize = readNumber<uint64_t>(streamInSmallNum_);
+
+ const InSyncDescrFile dataL = readFileDescr(streamInBigNum_);
+ const InSyncDescrFile dataT = readFileDescr(streamInBigNum_);
+
+ container.addFile(itemName,
+ SelectParam<leadSide>::ref(dataL, dataT),
+ SelectParam<leadSide>::ref(dataT, dataL), cmpVar, fileSize);
+ }
+
+ size_t linkCount = readNumber<uint32_t>(streamInSmallNum_);
+ while (linkCount-- != 0)
+ {
+ const Zstring itemName = readUtf8(streamInText_);
+ const auto cmpVar = static_cast<CompareVariant>(readNumber<int32_t>(streamInSmallNum_));
+
+ const InSyncDescrLink dataL = readLinkDescr(streamInBigNum_);
+ const InSyncDescrLink dataT = readLinkDescr(streamInBigNum_);
+
+ container.addSymlink(itemName,
+ SelectParam<leadSide>::ref(dataL, dataT),
+ SelectParam<leadSide>::ref(dataT, dataL), cmpVar);
+ }
+
+ size_t dirCount = readNumber<uint32_t>(streamInSmallNum_);
+ while (dirCount-- != 0)
+ {
+ const Zstring itemName = readUtf8(streamInText_);
+ const auto status = static_cast<InSyncFolder::InSyncStatus>(readNumber<int32_t>(streamInSmallNum_));
+
+ InSyncFolder& dbFolder = container.addFolder(itemName, status);
+ recurse<leadSide>(dbFolder);
+ }
+ }
+
+ static Zstring readUtf8(MemoryStreamIn<ByteArray>& streamIn) { return utfTo<Zstring>(readContainer<Zbase<char>>(streamIn)); } //throw UnexpectedEndOfStreamError
+ //optional: use null-termiation: 5% overall size reduction
+ //optional: split into streamInText_/streamInSmallNum_: overall size increase! (why?)
+
+ static InSyncDescrFile readFileDescr(MemoryStreamIn<ByteArray>& streamIn) //throw UnexpectedEndOfStreamError
+ {
+ //attention: order of function argument evaluation is undefined! So do it one after the other...
+ const auto modTime = readNumber<int64_t>(streamIn); //throw UnexpectedEndOfStreamError
+ const AFS::FileId fileId = readContainer<Zbase<char>>(streamIn);
+
+ return InSyncDescrFile(modTime, fileId);
+ }
+
+ static InSyncDescrLink readLinkDescr(MemoryStreamIn<ByteArray>& streamIn) //throw UnexpectedEndOfStreamError
+ {
+ const auto modTime = readNumber<int64_t>(streamIn);
+ return InSyncDescrLink(modTime);
+ }
+
+ //TODO: remove migration code at some time! 2017-02-01
+ class StreamParserV2
+ {
+ public:
+ StreamParserV2(const ByteArray& bufferL,
+ const ByteArray& bufferR,
+ const ByteArray& bufferB) :
+ inputLeft_ (bufferL),
+ inputRight_(bufferR),
+ inputBoth_ (bufferB) {}
+
+ void recurse(InSyncFolder& container) //throw UnexpectedEndOfStreamError
+ {
+ size_t fileCount = readNumber<uint32_t>(inputBoth_);
+ while (fileCount-- != 0)
+ {
+ const Zstring itemName = readUtf8(inputBoth_);
+ const auto cmpVar = static_cast<CompareVariant>(readNumber<int32_t>(inputBoth_));
+ const uint64_t fileSize = readNumber<uint64_t>(inputBoth_);
+ const InSyncDescrFile dataL = readFileDescr(inputLeft_);
+ const InSyncDescrFile dataR = readFileDescr(inputRight_);
+ container.addFile(itemName, dataL, dataR, cmpVar, fileSize);
+ }
+
+ size_t linkCount = readNumber<uint32_t>(inputBoth_);
+ while (linkCount-- != 0)
+ {
+ const Zstring itemName = readUtf8(inputBoth_);
+ const auto cmpVar = static_cast<CompareVariant>(readNumber<int32_t>(inputBoth_));
+ InSyncDescrLink dataL = readLinkDescr(inputLeft_);
+ InSyncDescrLink dataR = readLinkDescr(inputRight_);
+ container.addSymlink(itemName, dataL, dataR, cmpVar);
+ }
+
+ size_t dirCount = readNumber<uint32_t>(inputBoth_);
+ while (dirCount-- != 0)
+ {
+ const Zstring itemName = readUtf8(inputBoth_);
+ const auto status = static_cast<InSyncFolder::InSyncStatus>(readNumber<int32_t>(inputBoth_));
+
+ InSyncFolder& dbFolder = container.addFolder(itemName, status);
+ recurse(dbFolder);
+ }
+ }
+
+ private:
+ MemoryStreamIn<ByteArray> inputLeft_; //data related to one side only
+ MemoryStreamIn<ByteArray> inputRight_; //
+ MemoryStreamIn<ByteArray> inputBoth_; //data concerning both sides
+ };
+
+ const int streamVersion_;
+ MemoryStreamIn<ByteArray> streamInText_; //
+ MemoryStreamIn<ByteArray> streamInSmallNum_; //data with bias to lead side
+ MemoryStreamIn<ByteArray> streamInBigNum_; //
+};
+
+//#######################################################################################################################################
+
+class LastSynchronousStateUpdater
+{
+ /*
+ 1. filter by file name does *not* create a new hierarchy, but merely gives a different *view* on the existing file hierarchy
+ => only update database entries matching this view!
+ 2. Symlink handling *does* create a new (asymmetric) hierarchy during comparison
+ => update all database entries!
+ */
+public:
+ static void execute(const BaseFolderPair& baseFolder, InSyncFolder& dbFolder)
+ {
+ LastSynchronousStateUpdater updater(baseFolder.getCompVariant(), baseFolder.getFilter());
+ updater.recurse(baseFolder, dbFolder);
+ }
+
+private:
+ LastSynchronousStateUpdater(CompareVariant activeCmpVar, const HardFilter& filter) :
+ filter_(filter),
+ activeCmpVar_(activeCmpVar) {}
+
+ void recurse(const ContainerObject& hierObj, InSyncFolder& dbFolder)
+ {
+ process(hierObj.refSubFiles (), hierObj.getPairRelativePath(), dbFolder.files);
+ process(hierObj.refSubLinks (), hierObj.getPairRelativePath(), dbFolder.symlinks);
+ process(hierObj.refSubFolders(), hierObj.getPairRelativePath(), dbFolder.folders);
+ }
+
+ template <class M, class V>
+ static V& mapAddOrUpdate(M& map, const Zstring& key, V&& value)
+ {
+#if defined ZEN_LINUX && !defined __cpp_lib_map_try_emplace
+ auto rv = map.emplace(key, value); //C++11 emplace will move r-value arguments => don't use std::forward!
+#else //C++17's map::try_emplace() is faster than map::emplace() if key is already existing
+ auto rv = map.try_emplace(key, std::forward<V>(value)); //and does NOT MOVE r-value arguments in this case!
+#endif
+ if (rv.second)
+ return rv.first->second;
+
+ return rv.first->second = std::forward<V>(value);
+ }
+
+ void process(const ContainerObject::FileList& currentFiles, const Zstring& parentRelPath, InSyncFolder::FileList& dbFiles)
+ {
+ std::unordered_set<const InSyncFile*> toPreserve; //referencing fixed-in-memory std::map elements
+
+ for (const FilePair& file : currentFiles)
+ if (!file.isPairEmpty())
+ {
+ if (file.getCategory() == FILE_EQUAL) //data in sync: write current state
+ {
+ //Caveat: If FILE_EQUAL, we *implicitly* assume equal left and right short names matching case: InSyncFolder's mapping tables use short name as a key!
+ //This makes us silently dependent from code in algorithm.h!!!
+ assert(file.getItemName<LEFT_SIDE>() == file.getItemName<RIGHT_SIDE>());
+ //this should be taken for granted:
+ assert(file.getFileSize<LEFT_SIDE>() == file.getFileSize<RIGHT_SIDE>());
+
+ //create or update new "in-sync" state
+ InSyncFile& dbFile = mapAddOrUpdate(dbFiles, file.getPairItemName(),
+ InSyncFile(InSyncDescrFile(file.getLastWriteTime< LEFT_SIDE>(),
+ file.getFileId < LEFT_SIDE>()),
+ InSyncDescrFile(file.getLastWriteTime<RIGHT_SIDE>(),
+ file.getFileId <RIGHT_SIDE>()),
+ activeCmpVar_,
+ file.getFileSize<LEFT_SIDE>()));
+ toPreserve.insert(&dbFile);
+ }
+ else //not in sync: preserve last synchronous state
+ {
+ auto it = dbFiles.find(file.getPairItemName());
+ if (it != dbFiles.end())
+ toPreserve.insert(&it->second);
+ }
+ }
+
+ //delete removed items (= "in-sync") from database
+ erase_if(dbFiles, [&](const InSyncFolder::FileList::value_type& v) -> bool
+ {
+ if (toPreserve.find(&v.second) != toPreserve.end())
+ return false;
+ //all items not existing in "currentFiles" have either been deleted meanwhile or been excluded via filter:
+ const Zstring& itemRelPath = AFS::appendPaths(parentRelPath, v.first, FILE_NAME_SEPARATOR);
+ return filter_.passFileFilter(itemRelPath);
+ //note: items subject to traveral errors are also excluded by this file filter here! see comparison.cpp, modified file filter for read errors
+ });
+ }
+
+ void process(const ContainerObject::SymlinkList& currentSymlinks, const Zstring& parentRelPath, InSyncFolder::SymlinkList& dbSymlinks)
+ {
+ std::unordered_set<const InSyncSymlink*> toPreserve;
+
+ for (const SymlinkPair& symlink : currentSymlinks)
+ if (!symlink.isPairEmpty())
+ {
+ if (symlink.getLinkCategory() == SYMLINK_EQUAL) //data in sync: write current state
+ {
+ assert(symlink.getItemName<LEFT_SIDE>() == symlink.getItemName<RIGHT_SIDE>());
+
+ //create or update new "in-sync" state
+ InSyncSymlink& dbSymlink = mapAddOrUpdate(dbSymlinks, symlink.getPairItemName(),
+ InSyncSymlink(InSyncDescrLink(symlink.getLastWriteTime<LEFT_SIDE>()),
+ InSyncDescrLink(symlink.getLastWriteTime<RIGHT_SIDE>()),
+ activeCmpVar_));
+ toPreserve.insert(&dbSymlink);
+ }
+ else //not in sync: preserve last synchronous state
+ {
+ auto it = dbSymlinks.find(symlink.getPairItemName());
+ if (it != dbSymlinks.end())
+ toPreserve.insert(&it->second);
+ }
+ }
+
+ //delete removed items (= "in-sync") from database
+ erase_if(dbSymlinks, [&](const InSyncFolder::SymlinkList::value_type& v) -> bool
+ {
+ if (toPreserve.find(&v.second) != toPreserve.end())
+ return false;
+ //all items not existing in "currentSymlinks" have either been deleted meanwhile or been excluded via filter:
+ const Zstring& itemRelPath = AFS::appendPaths(parentRelPath, v.first, FILE_NAME_SEPARATOR);
+ return filter_.passFileFilter(itemRelPath);
+ });
+ }
+
+ void process(const ContainerObject::FolderList& currentFolders, const Zstring& parentRelPath, InSyncFolder::FolderList& dbFolders)
+ {
+ std::unordered_set<const InSyncFolder*> toPreserve;
+
+ for (const FolderPair& folder : currentFolders)
+ if (!folder.isPairEmpty())
+ switch (folder.getDirCategory())
+ {
+ case DIR_EQUAL:
+ {
+ assert(folder.getItemName<LEFT_SIDE>() == folder.getItemName<RIGHT_SIDE>());
+
+ //update directory entry only (shallow), but do *not touch* exising child elements!!!
+ const Zstring& key = folder.getPairItemName();
+ auto insertResult = dbFolders.emplace(key, InSyncFolder(InSyncFolder::DIR_STATUS_IN_SYNC)); //get or create
+ auto it = insertResult.first;
+
+ InSyncFolder& dbFolder = it->second;
+ dbFolder.status = InSyncFolder::DIR_STATUS_IN_SYNC; //update immediate directory entry
+ toPreserve.insert(&dbFolder);
+ recurse(folder, dbFolder);
+ }
+ break;
+
+ case DIR_CONFLICT:
+ case DIR_DIFFERENT_METADATA:
+ //if DIR_DIFFERENT_METADATA and no old database entry yet: we have to insert a placeholder database entry:
+ //we cannot simply skip the whole directory, since sub-items might be in sync!
+ //Example: directories on left and right differ in case while sub-files are equal
+ {
+ //reuse last "in-sync" if available or insert strawman entry (do not try to update and thereby remove child elements!!!)
+ InSyncFolder& dbFolder = dbFolders.emplace(folder.getPairItemName(), InSyncFolder(InSyncFolder::DIR_STATUS_STRAW_MAN)).first->second;
+ toPreserve.insert(&dbFolder);
+ recurse(folder, dbFolder); //unconditional recursion without filter check! => no problem since "childItemMightMatch" is optional!!!
+ }
+ break;
+
+ //not in sync: reuse last synchronous state:
+ case DIR_LEFT_SIDE_ONLY:
+ case DIR_RIGHT_SIDE_ONLY:
+ {
+ auto it = dbFolders.find(folder.getPairItemName());
+ if (it != dbFolders.end())
+ {
+ toPreserve.insert(&it->second);
+ recurse(folder, it->second); //although existing sub-items cannot be in sync, items deleted on both sides *are* in-sync!!!
+ }
+ }
+ break;
+ }
+
+ //delete removed items (= "in-sync") from database
+ erase_if(dbFolders, [&](InSyncFolder::FolderList::value_type& v) -> bool
+ {
+ if (toPreserve.find(&v.second) != toPreserve.end())
+ return false;
+
+ const Zstring& itemRelPath = AFS::appendPaths(parentRelPath, v.first, FILE_NAME_SEPARATOR);
+ //if directory is not included in "currentDirs", it is either not existing anymore, in which case it should be deleted from database
+ //or it was excluded via filter and the database entry should be preserved
+
+ bool childItemMightMatch = true;
+ const bool passFilter = filter_.passDirFilter(itemRelPath, &childItemMightMatch);
+ if (!passFilter && childItemMightMatch)
+ dbSetEmptyState(v.second, appendSeparator(itemRelPath)); //child items might match, e.g. *.txt include filter!
+ return passFilter;
+ });
+ }
+
+ //delete all entries for removed folder (= "in-sync") from database
+ void dbSetEmptyState(InSyncFolder& dbFolder, const Zstring& parentRelPathPf)
+ {
+ erase_if(dbFolder.files, [&](const InSyncFolder::FileList ::value_type& v) { return filter_.passFileFilter(parentRelPathPf + v.first); });
+ erase_if(dbFolder.symlinks, [&](const InSyncFolder::SymlinkList::value_type& v) { return filter_.passFileFilter(parentRelPathPf + v.first); });
+
+ erase_if(dbFolder.folders, [&](InSyncFolder::FolderList::value_type& v)
+ {
+ const Zstring& itemRelPath = parentRelPathPf + v.first;
+
+ bool childItemMightMatch = true;
+ const bool passFilter = filter_.passDirFilter(itemRelPath, &childItemMightMatch);
+ if (!passFilter && childItemMightMatch)
+ dbSetEmptyState(v.second, appendSeparator(itemRelPath));
+ return passFilter;
+ });
+ }
+
+ const HardFilter& filter_; //filter used while scanning directory: generates view on actual files!
+ const CompareVariant activeCmpVar_;
+};
+
+
+struct StreamStatusNotifier
+{
+ StreamStatusNotifier(const std::wstring& msgPrefix, const std::function<void(const std::wstring& statusMsg)>& notifyStatus) :
+ msgPrefix_(msgPrefix), notifyStatus_(notifyStatus) {}
+
+ void operator()(int64_t bytesDelta) //throw X
+ {
+ bytesTotal_ += bytesDelta;
+ if (notifyStatus_) notifyStatus_(msgPrefix_ + L" (" + formatFilesizeShort(bytesTotal_) + L")"); //throw X
+ }
+
+private:
+ const std::wstring msgPrefix_;
+ int64_t bytesTotal_ = 0;
+ const std::function<void(const std::wstring& statusMsg)> notifyStatus_;
+};
+
+
+std::pair<DbStreams::const_iterator,
+ DbStreams::const_iterator> getCommonSession(const DbStreams& streamsLeft, const DbStreams& streamsRight, //throw FileError, FileErrorDatabaseNotExisting
+ const std::wstring& displayFilePathL, //used for diagnostics only
+ const std::wstring& displayFilePathR)
+{
+ auto itCommonL = streamsLeft .cend();
+ auto itCommonR = streamsRight.cend();
+
+ for (auto itL = streamsLeft.begin(); itL != streamsLeft.end(); ++itL)
+ {
+ auto itR = streamsRight.find(itL->first);
+ if (itR != streamsRight.end())
+ /* handle case when db file is loaded together with a former copy of itself:
+ - some streams may have been updated in the meantime => must not discard either db file!
+ - since db file was copied, multiple streams may have matching sessionID
+ => IGNORE all of them: one of them may be used later against other sync targets!
+ */
+ if (itL->second.isLeadStream != itR->second.isLeadStream)
+ {
+ if (itCommonL != streamsLeft.end())
+ throw FileError(_("Database file is corrupted:") + L"\n" + fmtPath(displayFilePathL) + L"\n" + fmtPath(displayFilePathR),
+ L"Multiple common sessions found."); //should not be possible (anymore)
+ itCommonL = itL;
+ itCommonR = itR;
+ }
+ }
+
+ if (itCommonL == streamsLeft.end())
+ throw FileErrorDatabaseNotExisting(_("Initial synchronization:") + L" \n" +
+ _("The database files do not yet contain information about the last synchronization."));
+
+ return { itCommonL, itCommonR };
+}
+}
+
+//#######################################################################################################################################
+
+std::shared_ptr<InSyncFolder> fff::loadLastSynchronousState(const BaseFolderPair& baseFolder, //throw FileError, FileErrorDatabaseNotExisting -> return value always bound!
+ const std::function<void(const std::wstring& statusMsg)>& notifyStatus)
+{
+ const AbstractPath dbPathLeft = getDatabaseFilePath< LEFT_SIDE>(baseFolder);
+ const AbstractPath dbPathRight = getDatabaseFilePath<RIGHT_SIDE>(baseFolder);
+
+ if (!baseFolder.isAvailable< LEFT_SIDE>() ||
+ !baseFolder.isAvailable<RIGHT_SIDE>())
+ {
+ //avoid race condition with directory existence check: reading sync.ffs_db may succeed although first dir check had failed => conflicts!
+ //https://sourceforge.net/tracker/?func=detail&atid=1093080&aid=3531351&group_id=234430
+ const AbstractPath filePath = !baseFolder.isAvailable<LEFT_SIDE>() ? dbPathLeft : dbPathRight;
+ throw FileErrorDatabaseNotExisting(_("Initial synchronization:") + L" \n" + //it could be due to a to-be-created target directory not yet existing => FileErrorDatabaseNotExisting
+ replaceCpy(_("Database file %x does not yet exist."), L"%x", fmtPath(AFS::getDisplayPath(filePath))));
+ }
+
+ StreamStatusNotifier notifyLoadL(replaceCpy(_("Loading file %x..."), L"%x", fmtPath(AFS::getDisplayPath(dbPathLeft) )), notifyStatus);
+ StreamStatusNotifier notifyLoadR(replaceCpy(_("Loading file %x..."), L"%x", fmtPath(AFS::getDisplayPath(dbPathRight))), notifyStatus);
+
+ //read file data: list of session ID + DirInfo-stream
+ const DbStreams streamsLeft = ::loadStreams(dbPathLeft, notifyLoadL); //throw FileError, FileErrorDatabaseNotExisting, X
+ const DbStreams streamsRight = ::loadStreams(dbPathRight, notifyLoadR); //
+
+ //find associated session: there can be at most one session within intersection of left and right ids
+ std::pair<DbStreams::const_iterator,
+ DbStreams::const_iterator> session = getCommonSession(streamsLeft, streamsRight, //throw FileError, FileErrorDatabaseNotExisting
+ AFS::getDisplayPath(dbPathLeft),
+ AFS::getDisplayPath(dbPathRight));
+
+ const bool leadStreamLeft = session.first->second.isLeadStream;
+ assert(session.first->second.isLeadStream != session.second->second.isLeadStream);
+ const ByteArray& streamL = session.first ->second.rawStream;
+ const ByteArray& streamR = session.second->second.rawStream;
+
+ return StreamParser::execute(leadStreamLeft, streamL, streamR, //throw FileError
+ AFS::getDisplayPath(dbPathLeft),
+ AFS::getDisplayPath(dbPathRight));
+}
+
+
+void fff::saveLastSynchronousState(const BaseFolderPair& baseFolder, const std::function<void(const std::wstring& statusMsg)>& notifyStatus) //throw FileError
+{
+ //transactional behaviour! write to tmp files first
+ const AbstractPath dbPathLeft = getDatabaseFilePath< LEFT_SIDE>(baseFolder);
+ const AbstractPath dbPathRight = getDatabaseFilePath<RIGHT_SIDE>(baseFolder);
+
+ const AbstractPath dbPathLeftTmp = getDatabaseFilePath< LEFT_SIDE>(baseFolder, true /*tempfile*/);
+ const AbstractPath dbPathRightTmp = getDatabaseFilePath<RIGHT_SIDE>(baseFolder, true /*tempfile*/);
+
+ StreamStatusNotifier notifyLoadL(replaceCpy(_("Loading file %x..."), L"%x", fmtPath(AFS::getDisplayPath(dbPathLeft) )), notifyStatus);
+ StreamStatusNotifier notifyLoadR(replaceCpy(_("Loading file %x..."), L"%x", fmtPath(AFS::getDisplayPath(dbPathRight))), notifyStatus);
+
+ StreamStatusNotifier notifySaveL(replaceCpy(_("Saving file %x..."), L"%x", fmtPath(AFS::getDisplayPath(dbPathLeft) )), notifyStatus);
+ StreamStatusNotifier notifySaveR(replaceCpy(_("Saving file %x..."), L"%x", fmtPath(AFS::getDisplayPath(dbPathRight))), notifyStatus);
+
+ //(try to) load old database files...
+ DbStreams streamsLeft; //list of session ID + DirInfo-stream
+ DbStreams streamsRight;
+
+ try { streamsLeft = ::loadStreams(dbPathLeft, notifyLoadL); }
+ catch (FileError&) {}
+ try { streamsRight = ::loadStreams(dbPathRight, notifyLoadR); }
+ catch (FileError&) {}
+ //if error occurs: just overwrite old file! User is already informed about issues right after comparing!
+
+ auto lastSyncState = std::make_shared<InSyncFolder>(InSyncFolder::DIR_STATUS_IN_SYNC);
+ auto itStreamOldL = streamsLeft .cend();
+ auto itStreamOldR = streamsRight.cend();
+ try
+ {
+ //find associated session: there can be at most one session within intersection of left and right ids
+ std::tie(itStreamOldL, itStreamOldR) = getCommonSession(streamsLeft, streamsRight, //throw FileError, FileErrorDatabaseNotExisting
+ AFS::getDisplayPath(dbPathLeft),
+ AFS::getDisplayPath(dbPathRight));
+
+ const bool leadStreamLeft = itStreamOldL->second.isLeadStream;
+
+ //load last synchrounous state
+ lastSyncState = StreamParser::execute(leadStreamLeft,
+ itStreamOldL->second.rawStream, //throw FileError
+ itStreamOldR->second.rawStream,
+ AFS::getDisplayPath(dbPathLeft),
+ AFS::getDisplayPath(dbPathRight));
+ }
+ catch (FileError&) {} //if error occurs: just overwrite old file! User is already informed about issues right after comparing!
+
+ //update last synchrounous state
+ LastSynchronousStateUpdater::execute(baseFolder, *lastSyncState);
+
+ //serialize again
+ SessionData sessionDataL = {};
+ SessionData sessionDataR = {};
+ sessionDataL.isLeadStream = true;
+ sessionDataR.isLeadStream = false;
+
+ StreamGenerator::execute(*lastSyncState, //throw FileError
+ AFS::getDisplayPath(dbPathLeft),
+ AFS::getDisplayPath(dbPathRight),
+ sessionDataL.rawStream,
+ sessionDataR.rawStream);
+
+ //check if there is some work to do at all
+ if (itStreamOldL != streamsLeft .end() && itStreamOldL->second == sessionDataL &&
+ itStreamOldR != streamsRight.end() && itStreamOldR->second == sessionDataR)
+ return; //some users monitor the *.ffs_db file with RTS => don't touch the file if it isnt't strictly needed
+
+ //erase old session data
+ if (itStreamOldL != streamsLeft.end())
+ streamsLeft.erase(itStreamOldL);
+ if (itStreamOldR != streamsRight.end())
+ streamsRight.erase(itStreamOldR);
+
+ //create new session data
+ const std::string sessionID = zen::generateGUID();
+
+ streamsLeft [sessionID] = std::move(sessionDataL);
+ streamsRight[sessionID] = std::move(sessionDataR);
+
+ //write (temp-) files as a transaction
+ saveStreams(streamsLeft, dbPathLeftTmp, notifySaveL); //throw FileError
+ auto guardTmpL = makeGuard<ScopeGuardRunMode::ON_FAIL>([&] { try { AFS::removeFilePlain(dbPathLeftTmp); } catch (FileError&) {} });
+ saveStreams(streamsRight, dbPathRightTmp, notifySaveR); //
+ auto guardTmpR = makeGuard<ScopeGuardRunMode::ON_FAIL>([&] { try { AFS::removeFilePlain(dbPathRightTmp); } catch (FileError&) {} });
+
+ //operation finished: rename temp files -> this should work (almost) transactionally:
+ //if there were no write access, creation of temp files would have failed
+ AFS::removeFileIfExists(dbPathLeft); //throw FileError
+ AFS::renameItem(dbPathLeftTmp, dbPathLeft); //throw FileError, (ErrorDifferentVolume)
+ guardTmpL.dismiss();
+
+ AFS::removeFileIfExists(dbPathRight); //
+ AFS::renameItem(dbPathRightTmp, dbPathRight); //
+ guardTmpR.dismiss();
+}
bgstack15