/* Copyright (C) 2012 Sebastian Herbord. All rights reserved. This file is part of Mod Organizer. Mod Organizer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Mod Organizer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ #include "directoryentry.h" #include "originconnection.h" #include "filesorigin.h" #include "fileentry.h" #include "envfs.h" #include "util.h" #include "windows_error.h" #include #include namespace MOShared { using namespace MOBase; const int MAXPATH_UNICODE = 32767; template void elapsedImpl(std::chrono::nanoseconds& out, F&& f) { if constexpr (DirectoryStats::EnableInstrumentation) { const auto start = std::chrono::high_resolution_clock::now(); f(); const auto end = std::chrono::high_resolution_clock::now(); out += (end - start); } else { f(); } } // elapsed() is not optimized out when EnableInstrumentation is false even // though it's equivalent that this macro #define elapsed(OUT, F) (F)(); //#define elapsed(OUT, F) elapsedImpl(OUT, F); static bool SupportOptimizedFind() { // large fetch and basic info for FindFirstFileEx is supported on win server 2008 r2, win 7 and newer OSVERSIONINFOEX versionInfo; versionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); versionInfo.dwMajorVersion = 6; versionInfo.dwMinorVersion = 1; ULONGLONG mask = ::VerSetConditionMask( ::VerSetConditionMask(0, VER_MAJORVERSION, VER_GREATER_EQUAL), VER_MINORVERSION, VER_GREATER_EQUAL); return (::VerifyVersionInfo(&versionInfo, VER_MAJORVERSION | VER_MINORVERSION, mask) == TRUE); } static bool DirCompareByName(const DirectoryEntry *lhs, const DirectoryEntry *rhs) { return _wcsicmp(lhs->getName().c_str(), rhs->getName().c_str()) < 0; } DirectoryStats::DirectoryStats() { std::memset(this, 0, sizeof(DirectoryStats)); } DirectoryStats& DirectoryStats::operator+=(const DirectoryStats& o) { dirTimes += o.dirTimes; fileTimes += o.fileTimes; sortTimes += o.sortTimes; subdirLookupTimes += o.subdirLookupTimes; addDirectoryTimes += o.addDirectoryTimes; filesLookupTimes += o.filesLookupTimes; addFileTimes += o.addFileTimes; addOriginToFileTimes += o.addOriginToFileTimes; addFileToOriginTimes += o.addFileToOriginTimes; addFileToRegisterTimes += o.addFileToRegisterTimes; originExists += o.originExists; originCreate += o.originCreate; originsNeededEnabled += o.originsNeededEnabled; subdirExists += o.subdirExists; subdirCreate += o.subdirCreate; fileExists += o.fileExists; fileCreate += o.fileCreate; filesInsertedInRegister += o.filesInsertedInRegister; filesAssignedInRegister += o.filesAssignedInRegister; return *this; } std::string DirectoryStats::csvHeader() { QStringList sl = { "dirTimes", "fileTimes", "sortTimes", "subdirLookupTimes", "addDirectoryTimes", "filesLookupTimes", "addFileTimes", "addOriginToFileTimes", "addFileToOriginTimes", "addFileToRegisterTimes", "originExists", "originCreate", "originsNeededEnabled", "subdirExists", "subdirCreate", "fileExists", "fileCreate", "filesInsertedInRegister", "filesAssignedInRegister"}; return sl.join(",").toStdString(); } std::string DirectoryStats::toCsv() const { QStringList oss; auto s = [](auto ns) { return ns.count() / 1000.0 / 1000.0 / 1000.0; }; oss << QString::number(s(dirTimes)) << QString::number(s(fileTimes)) << QString::number(s(sortTimes)) << QString::number(s(subdirLookupTimes)) << QString::number(s(addDirectoryTimes)) << QString::number(s(filesLookupTimes)) << QString::number(s(addFileTimes)) << QString::number(s(addOriginToFileTimes)) << QString::number(s(addFileToOriginTimes)) << QString::number(s(addFileToRegisterTimes)) << QString::number(originExists) << QString::number(originCreate) << QString::number(originsNeededEnabled) << QString::number(subdirExists) << QString::number(subdirCreate) << QString::number(fileExists) << QString::number(fileCreate) << QString::number(filesInsertedInRegister) << QString::number(filesAssignedInRegister); return oss.join(",").toStdString(); } DirectoryEntry::DirectoryEntry( std::wstring name, DirectoryEntry *parent, int originID) : m_OriginConnection(new OriginConnection), m_Name(std::move(name)), m_Parent(parent), m_Populated(false), m_TopLevel(true) { m_FileRegister.reset(new FileRegister(m_OriginConnection)); m_Origins.insert(originID); } DirectoryEntry::DirectoryEntry( std::wstring name, DirectoryEntry *parent, int originID, boost::shared_ptr fileRegister, boost::shared_ptr originConnection) : m_FileRegister(fileRegister), m_OriginConnection(originConnection), m_Name(std::move(name)), m_Parent(parent), m_Populated(false), m_TopLevel(false) { m_Origins.insert(originID); } DirectoryEntry::~DirectoryEntry() { clear(); } void DirectoryEntry::clear() { for (auto itor=m_SubDirectories.rbegin(); itor!=m_SubDirectories.rend(); ++itor) { delete *itor; } m_Files.clear(); m_FilesLookup.clear(); m_SubDirectories.clear(); m_SubDirectoriesLookup.clear(); } void DirectoryEntry::addFromOrigin( const std::wstring &originName, const std::wstring &directory, int priority, DirectoryStats& stats) { env::DirectoryWalker walker; addFromOrigin(walker, originName, directory, priority, stats); } void DirectoryEntry::addFromOrigin( env::DirectoryWalker& walker, const std::wstring &originName, const std::wstring &directory, int priority, DirectoryStats& stats) { FilesOrigin &origin = createOrigin(originName, directory, priority, stats); if (!directory.empty()) { addFiles(walker, origin, directory, stats); } m_Populated = true; } void DirectoryEntry::addFromList( const std::wstring &originName, const std::wstring &directory, env::Directory& root, int priority, DirectoryStats& stats) { stats = {}; FilesOrigin &origin = createOrigin(originName, directory, priority, stats); addDir(origin, root, stats); } void DirectoryEntry::addDir( FilesOrigin& origin, env::Directory& d, DirectoryStats& stats) { elapsed(stats.dirTimes, [&]{ for (auto& sd : d.dirs) { auto* sdirEntry = getSubDirectory(sd, true, stats, origin.getID()); sdirEntry->addDir(origin, sd, stats); } }); elapsed(stats.fileTimes, [&]{ for (auto& f : d.files) { insert(f, origin, L"", -1, stats); } }); elapsed(stats.sortTimes, [&]{ std::sort( m_SubDirectories.begin(), m_SubDirectories.end(), &DirCompareByName); }); m_Populated = true; } void DirectoryEntry::addFromBSA( const std::wstring &originName, std::wstring &directory, const std::wstring &fileName, int priority, int order) { DirectoryStats dummy; FilesOrigin &origin = createOrigin(originName, directory, priority, dummy); WIN32_FILE_ATTRIBUTE_DATA fileData; if (::GetFileAttributesExW(fileName.c_str(), GetFileExInfoStandard, &fileData) == 0) { throw windows_error(QObject::tr("failed to determine file time").toStdString()); } FILETIME now; ::GetSystemTimeAsFileTime(&now); const double clfSecondsPer100ns = 100. * 1.E-9; ((ULARGE_INTEGER *)&now)->QuadPart -= ((double)5) / clfSecondsPer100ns; size_t namePos = fileName.find_last_of(L"\\/"); if (namePos == std::wstring::npos) { namePos = 0; } else { ++namePos; } if (!containsArchive(fileName.substr(namePos)) || ::CompareFileTime(&fileData.ftLastWriteTime, &now) > 0) { BSA::Archive archive; BSA::EErrorCode res = archive.read(ToString(fileName, false).c_str(), false); if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) { std::ostringstream stream; stream << QObject::tr("invalid bsa file: ").toStdString() << ToString(fileName, false) << " error code " << res << " - " << ::GetLastError(); throw std::runtime_error(stream.str()); } addFiles(origin, archive.getRoot(), fileData.ftLastWriteTime, fileName.substr(namePos), order); m_Populated = true; } } void DirectoryEntry::propagateOrigin(int origin) { { std::scoped_lock lock(m_OriginsMutex); m_Origins.insert(origin); } if (m_Parent != nullptr) { m_Parent->propagateOrigin(origin); } } bool DirectoryEntry::originExists(const std::wstring &name) const { return m_OriginConnection->exists(name); } FilesOrigin &DirectoryEntry::getOriginByID(int ID) const { return m_OriginConnection->getByID(ID); } FilesOrigin &DirectoryEntry::getOriginByName(const std::wstring &name) const { return m_OriginConnection->getByName(name); } const FilesOrigin* DirectoryEntry::findOriginByID(int ID) const { return m_OriginConnection->findByID(ID); } int DirectoryEntry::anyOrigin() const { bool ignore; for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) { FileEntryPtr entry = m_FileRegister->getFile(iter->second); if ((entry.get() != nullptr) && !entry->isFromArchive()) { return entry->getOrigin(ignore); } } // if we got here, no file directly within this directory is a valid indicator for a mod, thus // we continue looking in subdirectories for (DirectoryEntry *entry : m_SubDirectories) { int res = entry->anyOrigin(); if (res != -1){ return res; } } return *(m_Origins.begin()); } std::vector DirectoryEntry::getFiles() const { std::vector result; for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) { result.push_back(m_FileRegister->getFile(iter->second)); } return result; } DirectoryEntry *DirectoryEntry::findSubDirectory( const std::wstring &name, bool alreadyLowerCase) const { SubDirectoriesLookup::const_iterator itor; if (alreadyLowerCase) { itor = m_SubDirectoriesLookup.find(name); } else { itor = m_SubDirectoriesLookup.find(ToLowerCopy(name)); } if (itor == m_SubDirectoriesLookup.end()) { return nullptr; } return itor->second; } DirectoryEntry *DirectoryEntry::findSubDirectoryRecursive(const std::wstring &path) { return getSubDirectoryRecursive(path, false, -1); } const FileEntryPtr DirectoryEntry::findFile( const std::wstring &name, bool alreadyLowerCase) const { FilesLookup::const_iterator iter; if (alreadyLowerCase) { iter = m_FilesLookup.find(FileKey(name)); } else { iter = m_FilesLookup.find(FileKey(ToLowerCopy(name))); } if (iter != m_FilesLookup.end()) { return m_FileRegister->getFile(iter->second); } else { return FileEntryPtr(); } } const FileEntryPtr DirectoryEntry::findFile(const FileKey& key) const { auto iter = m_FilesLookup.find(key); if (iter != m_FilesLookup.end()) { return m_FileRegister->getFile(iter->second); } else { return FileEntryPtr(); } } bool DirectoryEntry::hasFile(const std::wstring& name) const { return m_Files.contains(ToLowerCopy(name)); } bool DirectoryEntry::containsArchive(std::wstring archiveName) { for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) { FileEntryPtr entry = m_FileRegister->getFile(iter->second); if (entry->isFromArchive(archiveName)) { return true; } } return false; } const FileEntryPtr DirectoryEntry::searchFile( const std::wstring &path, const DirectoryEntry **directory) const { if (directory != nullptr) { *directory = nullptr; } if ((path.length() == 0) || (path == L"*")) { // no file name -> the path ended on a (back-)slash if (directory != nullptr) { *directory = this; } return FileEntryPtr(); } const size_t len = path.find_first_of(L"\\/"); if (len == std::string::npos) { // no more path components auto iter = m_Files.find(ToLowerCopy(path)); if (iter != m_Files.end()) { return m_FileRegister->getFile(iter->second); } else if (directory != nullptr) { DirectoryEntry *temp = findSubDirectory(path); if (temp != nullptr) { *directory = temp; } } } else { // file is in a subdirectory, recurse into the matching subdirectory std::wstring pathComponent = path.substr(0, len); DirectoryEntry *temp = findSubDirectory(pathComponent); if (temp != nullptr) { if (len >= path.size()) { log::error(QObject::tr("unexpected end of path").toStdString()); return FileEntryPtr(); } return temp->searchFile(path.substr(len + 1), directory); } } return FileEntryPtr(); } void DirectoryEntry::removeFile(FileEntry::Index index) { removeFileFromList(index); } bool DirectoryEntry::removeFile(const std::wstring &filePath, int *origin) { size_t pos = filePath.find_first_of(L"\\/"); if (pos == std::string::npos) { return this->remove(filePath, origin); } std::wstring dirName = filePath.substr(0, pos); std::wstring rest = filePath.substr(pos + 1); DirectoryEntry *entry = getSubDirectoryRecursive(dirName, false); if (entry != nullptr) { return entry->removeFile(rest, origin); } else { return false; } } void DirectoryEntry::removeDir(const std::wstring &path) { size_t pos = path.find_first_of(L"\\/"); if (pos == std::string::npos) { for (auto iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { DirectoryEntry *entry = *iter; if (CaseInsensitiveEqual(entry->getName(), path)) { entry->removeDirRecursive(); removeDirectoryFromList(iter); delete entry; break; } } } else { std::wstring dirName = path.substr(0, pos); std::wstring rest = path.substr(pos + 1); DirectoryEntry *entry = getSubDirectoryRecursive(dirName, false); if (entry != nullptr) { entry->removeDir(rest); } } } bool DirectoryEntry::remove(const std::wstring &fileName, int *origin) { const auto lcFileName = ToLowerCopy(fileName); auto iter = m_Files.find(lcFileName); bool b = false; if (iter != m_Files.end()) { if (origin != nullptr) { FileEntryPtr entry = m_FileRegister->getFile(iter->second); if (entry.get() != nullptr) { bool ignore; *origin = entry->getOrigin(ignore); } } b = m_FileRegister->removeFile(iter->second); } return b; } bool DirectoryEntry::hasContentsFromOrigin(int originID) const { return m_Origins.find(originID) != m_Origins.end(); } FilesOrigin &DirectoryEntry::createOrigin( const std::wstring &originName, const std::wstring &directory, int priority, DirectoryStats& stats) { auto r = m_OriginConnection->getOrCreate( originName, directory, priority, m_FileRegister, m_OriginConnection, stats); if (r.second) { ++stats.originCreate; } else { ++stats.originExists; } return r.first; } void DirectoryEntry::removeFiles(const std::set &indices) { removeFilesFromList(indices); } FileEntryPtr DirectoryEntry::insert( std::wstring_view fileName, FilesOrigin &origin, FILETIME fileTime, std::wstring_view archive, int order, DirectoryStats& stats) { std::wstring fileNameLower = ToLowerCopy(fileName); FileEntryPtr fe; FileKey key(std::move(fileNameLower)); { std::unique_lock lock(m_FilesMutex); FilesLookup::iterator itor; elapsed(stats.filesLookupTimes, [&]{ itor = m_FilesLookup.find(key); }); if (itor != m_FilesLookup.end()) { lock.unlock(); ++stats.fileExists; fe = m_FileRegister->getFile(itor->second); } else { ++stats.fileCreate; fe = m_FileRegister->createFile( std::wstring(fileName.begin(), fileName.end()), this, stats); elapsed(stats.addFileTimes, [&] { addFileToList(std::move(key.value), fe->getIndex()); }); // fileNameLower has moved from this point } } elapsed(stats.addOriginToFileTimes, [&]{ fe->addOrigin(origin.getID(), fileTime, archive, order); }); elapsed(stats.addFileToOriginTimes, [&]{ origin.addFile(fe->getIndex()); }); return fe; } FileEntryPtr DirectoryEntry::insert( env::File& file, FilesOrigin &origin, std::wstring_view archive, int order, DirectoryStats& stats) { FileEntryPtr fe; { std::unique_lock lock(m_FilesMutex); FilesMap::iterator itor; elapsed(stats.filesLookupTimes, [&]{ itor = m_Files.find(file.lcname); }); if (itor != m_Files.end()) { lock.unlock(); ++stats.fileExists; fe = m_FileRegister->getFile(itor->second); } else { ++stats.fileCreate; fe = m_FileRegister->createFile(std::move(file.name), this, stats); // file.name has been moved from this point elapsed(stats.addFileTimes, [&]{ addFileToList(std::move(file.lcname), fe->getIndex()); }); // file.lcname has been moved from this point } } elapsed(stats.addOriginToFileTimes, [&]{ fe->addOrigin(origin.getID(), file.lastModified, archive, order); }); elapsed(stats.addFileToOriginTimes, [&]{ origin.addFile(fe->getIndex()); }); return fe; } struct DirectoryEntry::Context { FilesOrigin& origin; DirectoryStats& stats; std::stack current; }; void DirectoryEntry::addFiles( env::DirectoryWalker& walker, FilesOrigin &origin, const std::wstring& path, DirectoryStats& stats) { Context cx = {origin, stats}; cx.current.push(this); walker.forEachEntry(path, &cx, [](void* pcx, std::wstring_view path) { onDirectoryStart((Context*)pcx, path); }, [](void* pcx, std::wstring_view path) { onDirectoryEnd((Context*)pcx, path); }, [](void* pcx, std::wstring_view path, FILETIME ft) { onFile((Context*)pcx, path, ft); } ); { std::scoped_lock lock(m_SubDirMutex); std::sort( m_SubDirectories.begin(), m_SubDirectories.end(), &DirCompareByName); } } void DirectoryEntry::onDirectoryStart(Context* cx, std::wstring_view path) { elapsed(cx->stats.dirTimes, [&] { auto* sd = cx->current.top()->getSubDirectory( path, true, cx->stats, cx->origin.getID()); cx->current.push(sd); }); } void DirectoryEntry::onDirectoryEnd(Context* cx, std::wstring_view path) { elapsed(cx->stats.dirTimes, [&] { auto* current = cx->current.top(); { std::scoped_lock lock(current->m_SubDirMutex); std::sort( current->m_SubDirectories.begin(), current->m_SubDirectories.end(), &DirCompareByName); } cx->current.pop(); }); } void DirectoryEntry::onFile(Context* cx, std::wstring_view path, FILETIME ft) { elapsed(cx->stats.fileTimes, [&]{ cx->current.top()->insert(path, cx->origin, ft, L"", -1, cx->stats); }); } void DirectoryEntry::addFiles( FilesOrigin &origin, BSA::Folder::Ptr archiveFolder, FILETIME &fileTime, const std::wstring &archiveName, int order) { DirectoryStats dummy; // add files for (unsigned int fileIdx = 0; fileIdx < archiveFolder->getNumFiles(); ++fileIdx) { BSA::File::Ptr file = archiveFolder->getFile(fileIdx); auto f = insert( ToWString(file->getName(), true), origin, fileTime, archiveName, order, dummy); if (f) { if (file->getUncompressedFileSize() > 0) { f->setFileSize(file->getFileSize(), file->getUncompressedFileSize()); } else { f->setFileSize(file->getFileSize(), FileEntry::NoFileSize); } } } // recurse into subdirectories for (unsigned int folderIdx = 0; folderIdx < archiveFolder->getNumSubFolders(); ++folderIdx) { BSA::Folder::Ptr folder = archiveFolder->getSubFolder(folderIdx); DirectoryEntry *folderEntry = getSubDirectoryRecursive( ToWString(folder->getName(), true), true, origin.getID()); folderEntry->addFiles(origin, folder, fileTime, archiveName, order); } } DirectoryEntry *DirectoryEntry::getSubDirectory( std::wstring_view name, bool create, DirectoryStats& stats, int originID) { std::wstring nameLc = ToLowerCopy(name); std::scoped_lock lock(m_SubDirMutex); SubDirectoriesLookup::iterator itor; elapsed(stats.subdirLookupTimes, [&] { itor = m_SubDirectoriesLookup.find(nameLc); }); if (itor != m_SubDirectoriesLookup.end()) { ++stats.subdirExists; return itor->second; } if (create) { ++stats.subdirCreate; auto* entry = new DirectoryEntry( std::wstring(name.begin(), name.end()), this, originID, m_FileRegister, m_OriginConnection); elapsed(stats.addDirectoryTimes, [&] { addDirectoryToList(entry, std::move(nameLc)); // nameLc is moved from this point }); return entry; } else { return nullptr; } } DirectoryEntry *DirectoryEntry::getSubDirectory( env::Directory& dir, bool create, DirectoryStats& stats, int originID) { SubDirectoriesLookup::iterator itor; std::scoped_lock lock(m_SubDirMutex); elapsed(stats.subdirLookupTimes, [&] { itor = m_SubDirectoriesLookup.find(dir.lcname); }); if (itor != m_SubDirectoriesLookup.end()) { ++stats.subdirExists; return itor->second; } if (create) { ++stats.subdirCreate; auto* entry = new DirectoryEntry( std::move(dir.name), this, originID, m_FileRegister, m_OriginConnection); // dir.name is moved from this point elapsed(stats.addDirectoryTimes, [&]{ addDirectoryToList(entry, std::move(dir.lcname)); }); // dir.lcname is moved from this point return entry; } else { return nullptr; } } DirectoryEntry *DirectoryEntry::getSubDirectoryRecursive( const std::wstring &path, bool create, int originID) { if (path.length() == 0) { // path ended with a backslash? return this; } const size_t pos = path.find_first_of(L"\\/"); DirectoryStats dummy; if (pos == std::wstring::npos) { return getSubDirectory(path, create, dummy); } else { DirectoryEntry *nextChild = getSubDirectory( path.substr(0, pos), create, dummy, originID); if (nextChild == nullptr) { return nullptr; } else { return nextChild->getSubDirectoryRecursive( path.substr(pos + 1), create, originID); } } } void DirectoryEntry::removeDirRecursive() { while (!m_Files.empty()) { m_FileRegister->removeFile(m_Files.begin()->second); } m_FilesLookup.clear(); for (DirectoryEntry *entry : m_SubDirectories) { entry->removeDirRecursive(); delete entry; } m_SubDirectories.clear(); m_SubDirectoriesLookup.clear(); } void DirectoryEntry::addDirectoryToList(DirectoryEntry* e, std::wstring nameLc) { m_SubDirectories.push_back(e); m_SubDirectoriesLookup.emplace(std::move(nameLc), e); } void DirectoryEntry::removeDirectoryFromList(SubDirectories::iterator itor) { const auto* entry = *itor; { auto itor2 = std::find_if( m_SubDirectoriesLookup.begin(), m_SubDirectoriesLookup.end(), [&](auto&& d) { return (d.second == entry); }); if (itor2 == m_SubDirectoriesLookup.end()) { log::error("entry {} not in sub directories map", entry->getName()); } else { m_SubDirectoriesLookup.erase(itor2); } } m_SubDirectories.erase(itor); } void DirectoryEntry::removeFileFromList(FileEntry::Index index) { auto removeFrom = [&](auto& list) { auto iter = std::find_if( list.begin(), list.end(), [&index](auto&& pair) { return (pair.second == index); } ); if (iter == list.end()) { auto f = m_FileRegister->getFile(index); if (f) { log::error( "can't remove file '{}', not in directory entry '{}'", f->getName(), getName()); } else { log::error( "can't remove file with index {}, not in directory entry '{}' and " "not in register", index, getName()); } } else { list.erase(iter); } }; removeFrom(m_FilesLookup); removeFrom(m_Files); } void DirectoryEntry::removeFilesFromList(const std::set& indices) { for (auto iter = m_Files.begin(); iter != m_Files.end();) { if (indices.find(iter->second) != indices.end()) { iter = m_Files.erase(iter); } else { ++iter; } } for (auto iter = m_FilesLookup.begin(); iter != m_FilesLookup.end();) { if (indices.find(iter->second) != indices.end()) { iter = m_FilesLookup.erase(iter); } else { ++iter; } } } void DirectoryEntry::addFileToList( std::wstring fileNameLower, FileEntry::Index index) { m_FilesLookup.emplace(fileNameLower, index); m_Files.emplace(std::move(fileNameLower), index); // fileNameLower has been moved from this point } struct DumpFailed : public std::runtime_error { using runtime_error::runtime_error; }; void DirectoryEntry::dump(const std::wstring& file) const { try { std::FILE* f = nullptr; auto e = _wfopen_s(&f, file.c_str(), L"wb"); if (e != 0 || !f) { throw DumpFailed(fmt::format( "failed to open, {} ({})", std::strerror(e), e)); } Guard g([&]{ std::fclose(f); }); dump(f, L"Data"); } catch(DumpFailed& e) { log::error( "failed to write list to '{}': {}", QString::fromStdWString(file).toStdString(), e.what()); } } void DirectoryEntry::dump(std::FILE* f, const std::wstring& parentPath) const { { std::scoped_lock lock(m_FilesMutex); for (auto&& index : m_Files) { const auto file = m_FileRegister->getFile(index.second); if (!file) { continue; } if (file->isFromArchive()) { // TODO: don't list files from archives. maybe make this an option? continue; } const auto& o = m_OriginConnection->getByID(file->getOrigin()); const auto path = parentPath + L"\\" + file->getName(); const auto line = path + L"\t(" + o.getName() + L")\r\n"; const auto lineu8 = MOShared::ToString(line, true); if (std::fwrite(lineu8.data(), lineu8.size(), 1, f) != 1) { const auto e = errno; throw DumpFailed(fmt::format( "failed to write, {} ({})", std::strerror(e), e)); } } } { std::scoped_lock lock(m_SubDirMutex); for (auto&& d : m_SubDirectories) { const auto path = parentPath + L"\\" + d->m_Name; d->dump(f, path); } } } } // namespace MOShared