FileStateCache.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "FileStateCache.h"
  9. #include "native/utilities/assetUtils.h"
  10. #include <AssetProcessor_Traits_Platform.h>
  11. #include <AzToolsFramework/Asset/AssetUtils.h>
  12. #include <QDir>
  13. #include <QDateTime>
  14. #include <QTimeZone>
  15. namespace AssetProcessor
  16. {
  17. // Note that the file state cache operates on the assumption that it is automatically loaded and kept
  18. // up to date by the file scanner (initially) and the file watcher (thereafter). This is why all these
  19. // functions do not check the physical device for the file state, but rather rely on the cache.
  20. bool FileStateCache::GetFileInfo(const QString& absolutePath, FileStateInfo* foundFileInfo) const
  21. {
  22. AZ_Assert(!m_fileInfoMap.empty(), "FileStateCache::GetFileInfo called before cache is initialized!");
  23. LockGuardType scopeLock(m_mapMutex);
  24. auto itr = m_fileInfoMap.find(PathToKey(absolutePath));
  25. if (itr != m_fileInfoMap.end())
  26. {
  27. if (foundFileInfo)
  28. {
  29. *foundFileInfo = itr.value();
  30. }
  31. return true;
  32. }
  33. return false;
  34. }
  35. bool FileStateCache::Exists(const QString& absolutePath) const
  36. {
  37. LockGuardType scopeLock(m_mapMutex);
  38. AZ_Assert(!m_fileInfoMap.empty(), "FileStateCache::Exists called before cache is initialized!");
  39. return GetFileInfo(absolutePath, nullptr);
  40. }
  41. void FileStateCache::WarmUpCache(const AssetFileInfo& existingInfo, const FileHash hash)
  42. {
  43. LockGuardType scopeLock(m_mapMutex);
  44. QString key = PathToKey(existingInfo.m_filePath);
  45. m_fileInfoMap[key] = FileStateInfo(existingInfo);
  46. // it is possible to update the cache so that the info is known, but the hash is not.
  47. if (hash == InvalidFileHash)
  48. {
  49. m_fileHashMap.remove(key);
  50. }
  51. else
  52. {
  53. m_fileHashMap[key] = hash;
  54. }
  55. }
  56. bool FileStateCache::GetHash(const QString& absolutePath, FileHash* foundHash)
  57. {
  58. AZ_Assert(!m_fileInfoMap.empty(), "FileStateCache::Exists called before cache is initialized!");
  59. LockGuardType scopeLock(m_mapMutex);
  60. auto fileInfoItr = m_fileInfoMap.find(PathToKey(absolutePath));
  61. if(fileInfoItr == m_fileInfoMap.end())
  62. {
  63. // No info on this file, return false
  64. return false;
  65. }
  66. auto itr = m_fileHashMap.find(PathToKey(absolutePath));
  67. if (itr != m_fileHashMap.end())
  68. {
  69. *foundHash = itr.value();
  70. return true;
  71. }
  72. // There's no hash stored yet or its been invalidated, calculate it
  73. *foundHash = AssetUtilities::GetFileHash(absolutePath.toUtf8().constData(), true);
  74. m_fileHashMap[PathToKey(absolutePath)] = *foundHash;
  75. return true;
  76. }
  77. void FileStateCache::RegisterForDeleteEvent(AZ::Event<FileStateInfo>::Handler& handler)
  78. {
  79. handler.Connect(m_deleteEvent);
  80. }
  81. void FileStateCache::AddInfoSet(QSet<AssetFileInfo> infoSet)
  82. {
  83. LockGuardType scopeLock(m_mapMutex);
  84. for (const AssetFileInfo& info : infoSet)
  85. {
  86. m_fileInfoMap[PathToKey(info.m_filePath)] = FileStateInfo(info);
  87. }
  88. }
  89. void FileStateCache::AddFile(const QString& absolutePath)
  90. {
  91. QFileInfo fileInfo(absolutePath);
  92. LockGuardType scopeLock(m_mapMutex);
  93. AddOrUpdateFileInternal(fileInfo);
  94. InvalidateHash(absolutePath);
  95. if(fileInfo.isDir())
  96. {
  97. ScanFolder(absolutePath);
  98. }
  99. }
  100. void FileStateCache::UpdateFile(const QString& absolutePath)
  101. {
  102. QFileInfo fileInfo(absolutePath);
  103. LockGuardType scopeLock(m_mapMutex);
  104. AddOrUpdateFileInternal(fileInfo);
  105. InvalidateHash(absolutePath);
  106. }
  107. void FileStateCache::RemoveFile(const QString& absolutePath)
  108. {
  109. LockGuardType scopeLock(m_mapMutex);
  110. auto itr = m_fileInfoMap.find(PathToKey(absolutePath));
  111. if (itr != m_fileInfoMap.end())
  112. {
  113. m_deleteEvent.Signal(itr.value());
  114. bool isDirectory = itr.value().m_isDirectory;
  115. QString parentPath = itr.value().m_absolutePath;
  116. m_fileInfoMap.erase(itr);
  117. if (isDirectory)
  118. {
  119. for (itr = m_fileInfoMap.begin(); itr != m_fileInfoMap.end(); )
  120. {
  121. if (itr.value().m_absolutePath.startsWith(parentPath))
  122. {
  123. itr = m_fileInfoMap.erase(itr);
  124. continue;
  125. }
  126. ++itr;
  127. }
  128. }
  129. }
  130. InvalidateHash(absolutePath);
  131. }
  132. void FileStateCache::InvalidateHash(const QString& absolutePath)
  133. {
  134. m_keyCache = {}; // Clear the key cache, its only really intended to help speedup the startup phase
  135. auto fileHashItr = m_fileHashMap.find(PathToKey(absolutePath));
  136. if (fileHashItr != m_fileHashMap.end())
  137. {
  138. m_fileHashMap.erase(fileHashItr);
  139. }
  140. }
  141. //////////////////////////////////////////////////////////////////////////
  142. QString FileStateCache::PathToKey(const QString& absolutePath) const
  143. {
  144. auto cached = m_keyCache.find(absolutePath);
  145. if (cached != m_keyCache.end())
  146. {
  147. return cached.value();
  148. }
  149. QString normalized = AssetUtilities::NormalizeFilePath(absolutePath);
  150. // Its possible for this API to be called on a case sensitive and case-insensitive file system for files
  151. // with the wrong case. For example, a source asset might have another source asset listed in its dependency json
  152. // but with incorrect case. If it were to call "Exists" or "GetFileInfo" with the wrong case, it would fail even
  153. // though the file actually does exist, and its a case insensitive system. The API contract for this class demands
  154. // that it act as if case-insensitive, so the map MUST be lowercase.
  155. normalized = normalized.toLower();
  156. m_keyCache[absolutePath] = normalized;
  157. return normalized;
  158. }
  159. void FileStateCache::AddOrUpdateFileInternal(QFileInfo fileInfo)
  160. {
  161. m_fileInfoMap[PathToKey(fileInfo.absoluteFilePath())] = FileStateInfo(fileInfo.absoluteFilePath(), fileInfo.lastModified(), fileInfo.size(), fileInfo.isDir());
  162. }
  163. void FileStateCache::ScanFolder(const QString& absolutePath)
  164. {
  165. QDir inputFolder(absolutePath);
  166. QFileInfoList entries = inputFolder.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Files);
  167. for (const QFileInfo& entry : entries)
  168. {
  169. AddOrUpdateFileInternal(entry);
  170. if (entry.isDir())
  171. {
  172. ScanFolder(entry.absoluteFilePath());
  173. }
  174. }
  175. }
  176. //////////////////////////////////////////////////////////////////////////
  177. static void PopulateFileInfoFromFileIO(const char* absolutePath, FileStateInfo* fileInfo, AZ::IO::FileIOBase* fileIO)
  178. {
  179. if ((!fileInfo) || (!fileIO))
  180. {
  181. return;
  182. }
  183. AZ::u64 modTime = AZ::IO::FileTimeToMSecsSincePosixEpoch(fileIO->ModificationTime(absolutePath));
  184. AZ::u64 fileSize = 0;
  185. fileIO->Size(absolutePath, fileSize);
  186. bool isDir = fileIO->IsDirectory(absolutePath);
  187. *fileInfo = FileStateInfo(absolutePath, QDateTime::fromMSecsSinceEpoch(modTime, QTimeZone::utc()), fileSize, isDir);
  188. }
  189. bool FileStatePassthrough::GetFileInfo(const QString& absolutePath, FileStateInfo* foundFileInfo) const
  190. {
  191. // note that this interface is also used against dummy file systems in unit tests
  192. // which means it cannot rely on Qt / QFileInfo or other operations that would use the actual file system
  193. AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance();
  194. AZ_Assert(fileIO, "A file IO system must be installed in order to get file info for a file.");
  195. if (!fileIO)
  196. {
  197. return false;
  198. }
  199. AZStd::string absolutePathStr(absolutePath.toUtf8().constData()); // cache to avoid utf8 encoding multiple times
  200. bool fileExists = fileIO->Exists(absolutePathStr.c_str());
  201. if (fileExists)
  202. {
  203. // on a case-sensitive file system, the existence of the file means that the entire path and file
  204. // name is already correct and we can early out.
  205. if constexpr (ASSETPROCESSOR_TRAIT_CASE_SENSITIVE_FILESYSTEM)
  206. {
  207. PopulateFileInfoFromFileIO(absolutePathStr.c_str(), foundFileInfo, fileIO);
  208. return true;
  209. }
  210. }
  211. else if constexpr (!ASSETPROCESSOR_TRAIT_CASE_SENSITIVE_FILESYSTEM)
  212. {
  213. // if the file does NOT exist and its a case-insensitive file system
  214. // it means that it wont exist with any casing. We can early out here.
  215. return false;
  216. }
  217. // On case-insensitive systems where the file was found, or
  218. // on case-sensitive systems where the file was NOT found,
  219. // it is necessary to consult the actual file directory, since in the former case, the file may be found
  220. // but will potentially have the wrong case, but in the latter case, the file may not be found because it does
  221. // exist with different case. Note that file operations like QFileInfo will not correct the case, we MUST
  222. // consult the directory table to find the actual case of the file since that is the only place the information
  223. // is recorded.
  224. AZ::IO::Path correctedPath(absolutePath.toUtf8().constData());
  225. correctedPath.MakePreferred();
  226. AZStd::string rootPath = correctedPath.RootPath().Native();
  227. AZStd::string correctedPathStr = correctedPath.Native().substr(rootPath.size());
  228. if (AzToolsFramework::AssetUtils::UpdateFilePathToCorrectCase(rootPath.c_str(), correctedPathStr, true))
  229. {
  230. QString reassembledPath = QString::fromUtf8((AZ::IO::Path(rootPath) / correctedPathStr).Native().c_str());
  231. PopulateFileInfoFromFileIO(reassembledPath.toUtf8().constData(), foundFileInfo, fileIO);
  232. return true;
  233. }
  234. return false;
  235. }
  236. bool FileStatePassthrough::Exists(const QString& absolutePath) const
  237. {
  238. return GetFileInfo(absolutePath, nullptr);
  239. }
  240. bool FileStatePassthrough::GetHash(const QString& absolutePath, FileHash* foundHash)
  241. {
  242. FileStateInfo fileInfo;
  243. if(!GetFileInfo(absolutePath, &fileInfo))
  244. {
  245. return false;
  246. }
  247. *foundHash = AssetUtilities::GetFileHash(fileInfo.m_absolutePath.toUtf8().constData(), true);
  248. return true;
  249. }
  250. void FileStatePassthrough::RegisterForDeleteEvent(AZ::Event<FileStateInfo>::Handler& handler)
  251. {
  252. handler.Connect(m_deleteEvent);
  253. }
  254. void FileStatePassthrough::SignalDeleteEvent(const QString& absolutePath) const
  255. {
  256. FileStateInfo info;
  257. if (GetFileInfo(absolutePath, &info))
  258. {
  259. m_deleteEvent.Signal(info);
  260. }
  261. }
  262. bool FileStateInfo::operator==(const FileStateInfo& rhs) const
  263. {
  264. return m_absolutePath == rhs.m_absolutePath
  265. && m_modTime == rhs.m_modTime
  266. && m_fileSize == rhs.m_fileSize
  267. && m_isDirectory == rhs.m_isDirectory;
  268. }
  269. } // namespace AssetProcessor