packages feed

headroom-0.4.3.0: src/Headroom/Updater.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE NoImplicitPrelude   #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE QuasiQuotes         #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE TypeApplications    #-}

{-|
Module      : Headroom.Updater
Description : Update Manager for Headroom
Copyright   : (c) 2019-2022 Vaclav Svejcar
License     : BSD-3-Clause
Maintainer  : vaclav.svejcar@gmail.com
Stability   : experimental
Portability : POSIX

/Update Manager (Updater)/ is responsible for fetching data about latest version
of /Headroom/ and informing user about available updates. In future versions, it
might be capable to update /Headroom/ binaries automatically.
-}

module Headroom.Updater
  ( checkUpdates
  , fetchLatestVersion
  , parseLatestVersion
    -- * Error Data Types
  , UpdaterError(..)
  )
where

import           Data.Aeson                          ( Value(String) )
import qualified Data.Aeson                         as A
import           Data.String.Interpolate             ( iii )
import           Data.Time                           ( UTCTime(utctDay) )
import           Headroom.Config.Global              ( UpdaterConfig(..) )
import           Headroom.Data.Has                   ( Has(..)
                                                     , HasRIO
                                                     )
import           Headroom.IO.KVStore                 ( KVStore(..)
                                                     , valueKey
                                                     )
import           Headroom.IO.Network                 ( Network(..) )
import           Headroom.Meta                       ( buildVersion )
import           Headroom.Meta.Version               ( Version
                                                     , parseVersion
                                                     )
import           Headroom.Types                      ( fromHeadroomError
                                                     , toHeadroomError
                                                     )
import           Lens.Micro.Aeson                    ( key )
import           RIO
import qualified RIO.ByteString.Lazy                as BL
import qualified RIO.Text                           as T
import           RIO.Time                            ( diffDays
                                                     , getCurrentTime
                                                     )
import qualified Text.URI                           as URI


-- | Check whether newer version is available (if enabled by configuration).
checkUpdates :: (HasRIO KVStore env, HasRIO Network env)
             => UpdaterConfig
             -> RIO env (Maybe Version)
checkUpdates UpdaterConfig {..} = do
  KVStore {..}       <- viewL
  now                <- getCurrentTime
  maybeLastCheckDate <- kvGetValue lastCheckDateKey
  let today       = utctDay now
      shouldCheck = ucCheckForUpdates && case utctDay <$> maybeLastCheckDate of
        Just lastCheck
          | abs (diffDays lastCheck today) > ucUpdateIntervalDays -> True
          | otherwise -> False
        Nothing -> True
  when shouldCheck $ kvPutValue lastCheckDateKey now
  if shouldCheck then isNewer <$> fetchLatestVersion else pure Nothing
 where
  lastCheckDateKey = valueKey @UTCTime "updater/last-check-date"
  isNewer version | version > buildVersion = Just version
                  | otherwise              = Nothing


-- | Fetches and parses latest version from update server.
fetchLatestVersion :: (HasRIO Network env) => RIO env Version
fetchLatestVersion = do
  Network {..} <- viewL
  apiURI       <- latestVersionApiURI
  resp         <- catchAny (nDownloadContent apiURI) handleError
  case A.decode (BL.fromStrict resp) of
    Just json -> parseLatestVersion json
    _         -> throwM $ CannotDetectVersion "cannot fetch response"
 where
  handleError         = throwM . CannotDetectVersion . T.pack . displayException
  latestVersionApiURI = URI.mkURI
    "https://api.github.com/repos/vaclavsvejcar/headroom/releases/latest"



-- | Parses latest version number from /GitHub/ API response.
parseLatestVersion :: MonadThrow m
                   => Value     -- ^ raw JSON response from /GitHub/
                   -> m Version -- ^ parsed version
parseLatestVersion json = case json ^? key "name" of
  Just (String rawValue) -> case parseVersion rawValue of
    Just version -> pure version
    _            -> throwM $ CannotDetectVersion "cannot parse version"
  _ -> throwM $ CannotDetectVersion "cannot parse response"


---------------------------------  ERROR TYPES  --------------------------------

-- | Error during processing updates.
data UpdaterError = CannotDetectVersion Text
  deriving (Eq, Show, Typeable)

instance Exception UpdaterError where
  displayException = displayException'
  toException      = toHeadroomError
  fromException    = fromHeadroomError


displayException' :: UpdaterError -> String
displayException' = \case
  CannotDetectVersion reason -> [iii|
    Cannot get latest Headroom version from update servers, reason: #{reason}.
  |]