FileCache.cpp (9818B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "Public/winget/FileCache.h" 5 #include <AppInstallerDownloader.h> 6 #include <AppInstallerLogging.h> 7 #include <AppInstallerStrings.h> 8 9 namespace AppInstaller::Caching 10 { 11 namespace anon 12 { 13 std::string_view GetNameForType(FileCache::Type type) 14 { 15 switch (type) 16 { 17 case FileCache::Type::IndexV1_Manifest: return "V1_M"; 18 case FileCache::Type::IndexV2_PackageVersionData: return "V2_PVD"; 19 case FileCache::Type::IndexV2_Manifest: return "V2_M"; 20 case FileCache::Type::Icon: return "Icon"; 21 #ifndef AICLI_DISABLE_TEST_HOOKS 22 case FileCache::Type::Tests: return "Tests"; 23 #endif 24 } 25 26 THROW_HR(E_UNEXPECTED); 27 } 28 29 std::unique_ptr<std::stringstream> GetUpstreamFile(const std::string& basePath, const std::string& relativePath, const Utility::SHA256::HashBuffer& expectedHash) 30 { 31 // Until signed files are implemented, fail on an empty hash 32 THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE, expectedHash.empty()); 33 34 std::string fullPath = basePath; 35 if (fullPath.back() != '/') 36 { 37 fullPath += '/'; 38 } 39 fullPath += relativePath; 40 41 if (Utility::IsUrlRemote(fullPath)) 42 { 43 auto result = std::make_unique<std::stringstream>(); 44 45 AICLI_LOG(Core, Verbose, << "Getting upstream file from remote: " << fullPath); 46 ProgressCallback emptyCallback; 47 48 constexpr int MaxRetryCount = 2; 49 constexpr std::chrono::seconds maximumWaitTimeAllowed = 10s; 50 for (int retryCount = 0; retryCount < MaxRetryCount; ++retryCount) 51 { 52 try 53 { 54 auto downloadResult = Utility::DownloadToStream(fullPath, *result, Utility::DownloadType::Manifest, emptyCallback); 55 56 if (!expectedHash.empty() && 57 !Utility::SHA256::AreEqual(expectedHash, downloadResult.Sha256Hash)) 58 { 59 AICLI_LOG(Core, Verbose, << "Invalid hash from [" << fullPath << "]: expected [" << Utility::SHA256::ConvertToString(expectedHash) << "], got [" << Utility::SHA256::ConvertToString(downloadResult.Sha256Hash) << "]"); 60 THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE); 61 } 62 63 break; 64 } 65 catch (const Utility::ServiceUnavailableException& sue) 66 { 67 if (retryCount < MaxRetryCount - 1) 68 { 69 auto waitSecondsForRetry = sue.RetryAfter(); 70 if (waitSecondsForRetry > maximumWaitTimeAllowed) 71 { 72 throw; 73 } 74 75 // TODO: Get real progress callback to allow cancelation. 76 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(waitSecondsForRetry); 77 Sleep(static_cast<DWORD>(ms.count())); 78 } 79 else 80 { 81 throw; 82 } 83 } 84 catch (...) 85 { 86 if (retryCount < MaxRetryCount - 1) 87 { 88 AICLI_LOG(Core, Verbose, << "Getting upstream file failed, waiting a bit and retrying: " << fullPath); 89 Sleep(500); 90 } 91 else 92 { 93 throw; 94 } 95 } 96 } 97 98 return result; 99 } 100 else 101 { 102 AICLI_LOG(Core, Verbose, << "Getting upstream file from local: " << fullPath); 103 std::ifstream fileStream{ fullPath, std::ios_base::in | std::ios_base::binary }; 104 std::string fileContents = Utility::ReadEntireStream(fileStream); 105 106 auto fileContentsHash = Utility::SHA256::ComputeHash(fileContents); 107 108 if (expectedHash.empty() || Utility::SHA256::AreEqual(expectedHash, fileContentsHash)) 109 { 110 return std::make_unique<std::stringstream>(std::move(fileContents)); 111 } 112 else 113 { 114 THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE); 115 } 116 } 117 } 118 } 119 120 FileCache::Details::Details(FileCache::Type type, std::string identifier) : 121 Type(type), Identifier(std::move(identifier)) 122 { 123 switch (type) 124 { 125 case Type::IndexV1_Manifest: 126 case Type::IndexV2_PackageVersionData: 127 case Type::IndexV2_Manifest: 128 case Type::Icon: 129 #ifndef AICLI_DISABLE_TEST_HOOKS 130 case Type::Tests: 131 #endif 132 BasePath = Runtime::PathName::Temp; 133 break; 134 default: 135 THROW_HR(E_UNEXPECTED); 136 } 137 } 138 139 std::filesystem::path FileCache::Details::GetCachePath() const 140 { 141 std::filesystem::path result = Runtime::GetPathTo(BasePath); 142 result /= "cache"; 143 result /= anon::GetNameForType(Type); 144 result /= Utility::ConvertToUTF16(Identifier); 145 return result; 146 } 147 148 FileCache::FileCache(Type type, std::string identifier, std::vector<std::string> sources) : 149 m_details(type, std::move(identifier)), m_sources(std::move(sources)) 150 { 151 m_cacheBase = m_details.GetCachePath(); 152 } 153 154 const FileCache::Details& FileCache::GetDetails() const 155 { 156 return m_details; 157 } 158 159 std::unique_ptr<std::istream> FileCache::GetFile(const std::filesystem::path& relativePath, const Utility::SHA256::HashBuffer& expectedHash) const 160 { 161 std::filesystem::path cachedFilePath = m_cacheBase / relativePath; 162 163 // Check cache for matching file 164 try 165 { 166 if (std::filesystem::is_regular_file(cachedFilePath)) 167 { 168 AICLI_LOG(Core, Verbose, << "Reading cached file [" << cachedFilePath << "]"); 169 170 std::ifstream fileStream{ cachedFilePath, std::ios_base::in | std::ios_base::binary }; 171 std::string fileContents = Utility::ReadEntireStream(fileStream); 172 173 auto fileContentsHash = Utility::SHA256::ComputeHash(fileContents); 174 175 if (Utility::SHA256::AreEqual(expectedHash, fileContentsHash)) 176 { 177 return std::make_unique<std::istringstream>(std::move(fileContents)); 178 } 179 else 180 { 181 AICLI_LOG(Core, Verbose, << "Removing cached file [" << cachedFilePath << "] due to hash mismatch; expected [" << 182 Utility::SHA256::ConvertToString(expectedHash) << "] but was [" << Utility::SHA256::ConvertToString(fileContentsHash) << "]"); 183 } 184 } 185 186 std::filesystem::remove_all(cachedFilePath); 187 } 188 catch (...) 189 { 190 LOG_CAUGHT_EXCEPTION_MSG("Error while attempting to read cached file"); 191 } 192 193 // Making it here means that we do not have a cached file or it needed to be updated and was removed. 194 auto result = GetUpstreamFile(relativePath.u8string(), expectedHash); 195 196 // GetUpstreamFile only returns with a successfully verified hash, we just need to write the file out. 197 // Only log failures as caching is an optimization. 198 try 199 { 200 std::filesystem::create_directories(cachedFilePath.parent_path()); 201 202 AICLI_LOG(Core, Verbose, << "Writing cached file [" << cachedFilePath << "]"); 203 std::ofstream fileStream{ cachedFilePath, std::ios_base::out | std::ios_base::binary | std::ios_base::trunc }; 204 LOG_LAST_ERROR_IF(fileStream.fail()); 205 fileStream << result->str() << std::flush; 206 LOG_LAST_ERROR_IF(fileStream.fail()); 207 } 208 catch (...) 209 { 210 LOG_CAUGHT_EXCEPTION_MSG("Error while attempting to write cached file"); 211 } 212 213 return result; 214 } 215 216 std::unique_ptr<std::stringstream> FileCache::GetUpstreamFile(std::string relativePath, const Utility::SHA256::HashBuffer& expectedHash) const 217 { 218 // Replace backslashes with forward slashes for HTTP requests (since local can handle them). 219 Utility::FindAndReplace(relativePath, "\\", "/"); 220 221 std::exception_ptr firstException; 222 223 for (const auto& upstream : m_sources) 224 { 225 try 226 { 227 return anon::GetUpstreamFile(upstream, relativePath, expectedHash); 228 } 229 catch(...) 230 { 231 LOG_CAUGHT_EXCEPTION_MSG("GetUpstreamFile failed on source: %hs", upstream.c_str()); 232 if (!firstException) 233 { 234 firstException = std::current_exception(); 235 } 236 } 237 } 238 239 if (firstException) 240 { 241 std::rethrow_exception(firstException); 242 } 243 244 // Somewhat arbitrary error that should only happen if no upstream sources provided. 245 THROW_HR(E_NOT_SET); 246 } 247 }