winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }