Skip to content

Commit

Permalink
[#78] Add 'checkUpdates' function to Updater
Browse files Browse the repository at this point in the history
  • Loading branch information
vaclavsvejcar committed Sep 30, 2021
1 parent 4dab35c commit da77da9
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 22 deletions.
10 changes: 8 additions & 2 deletions embedded/default-global-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
## This is the global configuration file for Headroom.
## See https://github.com/vaclavsvejcar/headroom for more details.

## Whether Headroom should automatically check for available updates.
check-for-updates: true
## Configuration for Headroom Updater.
updates:

## Whether Headroom should automatically check for available updates.
check-for-updates: true

## How often (in days) should Headroom check for updates.
update-interval-days: 1
19 changes: 18 additions & 1 deletion src/Headroom/Configuration/GlobalConfig.hs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ and it's located in user's home directory.

module Headroom.Configuration.GlobalConfig
( GlobalConfig(..)
, UpdaterConfig(..)
, initGlobalConfigIfNeeded
, loadGlobalConfig
, parseGlobalConfig
)
where

Expand All @@ -44,9 +46,19 @@ import RIO.FilePath ( (</>) )

--------------------------------- DATA TYPES ---------------------------------

-- | Data type representing updater configuration.
data UpdaterConfig = UpdaterConfig
{ ucCheckForUpdates :: Bool -- ^ whether to check for updates
, ucUpdateIntervalDays :: Integer -- ^ how ofter check for updates
}
deriving (Eq, Generic, Show)

instance FromJSON UpdaterConfig where
parseJSON = genericParseJSON aesonOptions

-- | Data type representing global configuration options.
data GlobalConfig = GlobalConfig
{ gcCheckForUpdates :: Bool -- ^ whether to check for updates
{ gcUpdates :: UpdaterConfig -- ^ config for updater
}
deriving (Eq, Generic, Show)

Expand Down Expand Up @@ -76,6 +88,11 @@ loadGlobalConfig = do
Y.decodeThrow content


-- | Parses global configuration /YAML/ file.
parseGlobalConfig :: (MonadThrow m) => ByteString -> m GlobalConfig
parseGlobalConfig = Y.decodeThrow


------------------------------ PRIVATE FUNCTIONS -----------------------------

configPath :: FilePath
Expand Down
11 changes: 6 additions & 5 deletions src/Headroom/IO/KVStore.hs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ module Headroom.IO.KVStore
, ValueKey(..)
, StorePath(..)
-- * Public Functions
, mkKVStore
, sqliteKVStore
, valueKey
, getValue
, putValue
Expand Down Expand Up @@ -109,10 +109,11 @@ data KVStore m = KVStore


-- | Constructs the default 'KVStore' that uses /SQLite/ as a backend.
mkKVStore :: MonadIO m
=> StorePath -- ^ path of the store location
-> KVStore m -- ^ store instance
mkKVStore sp = KVStore { kvGetValue = getValue sp, kvPutValue = putValue sp }
sqliteKVStore :: MonadIO m
=> StorePath -- ^ path of the store location
-> KVStore m -- ^ store instance
sqliteKVStore sp =
KVStore { kvGetValue = getValue sp, kvPutValue = putValue sp }

-------------------------------- TYPE CLASSES --------------------------------

Expand Down
44 changes: 36 additions & 8 deletions src/Headroom/Updater.hs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ might be capable to update /Headroom/ binaries automatically.
-}

module Headroom.Updater
( fetchLatestVersion
( checkUpdates
, fetchLatestVersion
, parseLatestVersion
-- * Error Data Types
, UpdaterError(..)
Expand All @@ -32,10 +33,16 @@ where
import Data.Aeson ( Value(String) )
import qualified Data.Aeson as A
import Data.String.Interpolate ( iii )
import Data.Time ( UTCTime(utctDay) )
import Headroom.Configuration.GlobalConfig ( 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
)
Expand All @@ -46,10 +53,32 @@ import Lens.Micro.Aeson ( key )
import RIO
import qualified RIO.ByteString.Lazy as BL
import qualified RIO.Text as T
import Text.URI ( URI )
import RIO.Time ( diffDays
, getCurrentTime
)
import qualified Text.URI as URI


-- | Check whether newer version is available (if enabled by configuration).
checkUpdates :: (Has UpdaterConfig env, HasRIO KVStore env, HasRIO Network env)
=> RIO env (Maybe Version)
checkUpdates = do
KVStore {..} <- viewL
UpdaterConfig {..} <- viewL
now <- getCurrentTime
maybeLastCheckDate <- kvGetValue lastCheckDateKey
let today = utctDay now
shouldCheck = ucCheckForUpdates && case utctDay <$> maybeLastCheckDate of
Just lastCheck | diffDays lastCheck today > ucUpdateIntervalDays -> True
_ -> False
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
Expand All @@ -59,7 +88,11 @@ fetchLatestVersion = do
case A.decode (BL.fromStrict resp) of
Just json -> parseLatestVersion json
_ -> throwM $ CannotDetectVersion "cannot fetch response"
where handleError = throwM . CannotDetectVersion . T.pack . displayException
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.
Expand All @@ -73,11 +106,6 @@ parseLatestVersion json = case json ^? key "name" of
_ -> throwM $ CannotDetectVersion "cannot parse response"


latestVersionApiURI :: MonadThrow m => m URI
latestVersionApiURI = URI.mkURI
"https://api.github.com/repos/vaclavsvejcar/headroom/releases/latest"


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

-- | Error during processing updates.
Expand Down
8 changes: 7 additions & 1 deletion test/Headroom/Configuration/GlobalConfigSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}


module Headroom.Configuration.GlobalConfigSpec
( spec
)
Expand All @@ -16,6 +15,7 @@ import Headroom.Data.Has ( Has(..) )
import Headroom.Data.Lens ( suffixLenses
, suffixLensesFor
)
import Headroom.Embedded ( defaultGlobalConfig )
import Headroom.IO.FileSystem ( FileSystem(..)
, mkFileSystem
)
Expand All @@ -28,6 +28,7 @@ import RIO.FilePath ( (</>) )
import Test.Hspec



data TestEnv = TestEnv
{ envFileSystem :: FileSystem (RIO TestEnv)
}
Expand Down Expand Up @@ -78,3 +79,8 @@ spec = do
_ <- runRIO env' initGlobalConfigIfNeeded
result <- doesFileExist cfgPath
result `shouldBe` True


describe "parseGlobalConfig" $ do
it "parses embedded default config YAML" $ do
parseGlobalConfig defaultGlobalConfig `shouldSatisfy` isRight
2 changes: 1 addition & 1 deletion test/Headroom/IO/KvStoreSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ spec = do
let path = StorePath . T.pack $ dir </> "test-db.sqlite"
fstKey = valueKey @Text "fst-key"
sndKey = valueKey @Text "snd-key"
KVStore {..} = mkKVStore path
KVStore {..} = sqliteKVStore path
maybeFst <- kvGetValue fstKey
_ <- kvPutValue sndKey "foo"
_ <- kvPutValue sndKey "bar"
Expand Down
45 changes: 41 additions & 4 deletions test/Headroom/UpdaterSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}

Expand All @@ -15,14 +14,20 @@ where

import Data.Aeson ( Value )
import qualified Data.Aeson as A
import Data.String.Interpolate ( i )
import Headroom.Configuration.GlobalConfig ( UpdaterConfig(..) )
import Headroom.Data.Has ( Has(..) )
import Headroom.Data.Lens ( suffixLenses
, suffixLensesFor
)
import Headroom.IO.KVStore ( KVStore(..) )
import Headroom.IO.Network ( Network(..)
, NetworkError(..)
)
import Headroom.Meta.Version ( pvp )
import Headroom.Meta ( buildVersion )
import Headroom.Meta.Version ( printVersionP
, pvp
)
import Headroom.Updater
import RIO
import qualified RIO.ByteString as B
Expand All @@ -32,21 +37,47 @@ import RIO.Partial ( fromJust )
import Test.Hspec



data TestEnv = TestEnv
{ envNetwork :: Network (RIO TestEnv)
{ envKVStore :: KVStore (RIO TestEnv)
, envNetwork :: Network (RIO TestEnv)
, envUpdaterConfig :: UpdaterConfig
}

suffixLenses ''TestEnv
suffixLensesFor ["nDownloadContent"] ''Network

instance Has (KVStore (RIO TestEnv)) TestEnv where
hasLens = envKVStoreL

instance Has (Network (RIO TestEnv)) TestEnv where
hasLens = envNetworkL

instance Has UpdaterConfig TestEnv where
hasLens = envUpdaterConfigL


spec :: Spec
spec = do
let testFile = "test-data" </> "updater" </> "github-resp.json"

describe "checkUpdates" $ do
it "returns Nothing if current version is the latest" $ do
let json = [i|{"name": "#{printVersionP buildVersion}"}|]
env =
env0
& (envKVStoreL .~ kvStore')
& (envNetworkL . nDownloadContentL .~ nDownloadContent')
& (envUpdaterConfigL .~ updaterConfig')
nDownloadContent' = const . pure $ json
updaterConfig' = UpdaterConfig True 1
kvStore' = KVStore { kvGetValue = const . pure $ Nothing
, kvPutValue = const . const . pure $ ()
}
actual <- runRIO env checkUpdates
actual `shouldBe` Nothing


describe "fetchLatestVersion" $ do
it "gets latest version info" $ do
raw <- B.readFile testFile
Expand All @@ -61,11 +92,17 @@ spec = do
runRIO env fetchLatestVersion `shouldThrow` \case
(CannotDetectVersion _) -> True


describe "parseLatestVersion" $ do
it "parses latest version from raw JSON input" $ do
raw <- BL.readFile testFile
actual <- parseLatestVersion (fromJust . A.decode @Value $ raw)
actual `shouldBe` [pvp|0.4.2.0|]


env0 :: TestEnv
env0 = TestEnv { envNetwork = Network { nDownloadContent = undefined } }
env0 = TestEnv
{ envKVStore = KVStore { kvGetValue = undefined, kvPutValue = undefined }
, envNetwork = Network { nDownloadContent = undefined }
, envUpdaterConfig = undefined
}

0 comments on commit da77da9

Please sign in to comment.