-- | -- Module : Aura.State -- Copyright : (c) Colin Woodbury, 2012 - 2020 -- License : GPL3 -- Maintainer: Colin Woodbury -- -- Handle the saving and restoring of installed package states. module Aura.State ( PkgState(..) , saveState , restoreState , inState , readState , stateCache , getStateFiles ) where import Aura.Cache import Aura.Colour (red) import Aura.Core (Env(..), notify, report, warn) import Aura.IO import Aura.Languages import Aura.Pacman (pacman, pacmanLines) import Aura.Settings import Aura.Types import Aura.Utils (hush) import Data.Aeson import Data.Versions import RIO import qualified RIO.ByteString.Lazy as BL import RIO.Directory import RIO.FilePath import qualified RIO.List as L import RIO.List.Partial ((!!)) import qualified RIO.Map as M import qualified RIO.Map.Unchecked as M import qualified RIO.NonEmpty as NEL import qualified RIO.Text as T import RIO.Time import Text.Printf (printf) --- -- | All packages installed at some specific `ZonedTime`. Any "pinned" PkgState will -- never be deleted by `-Bc`. data PkgState = PkgState { timeOf :: !ZonedTime , pinnedOf :: !Bool , pkgsOf :: !(Map PkgName Versioning) } instance ToJSON PkgState where toJSON (PkgState t pnd ps) = object [ "time" .= t , "pinned" .= pnd , "packages" .= fmap prettyV ps ] instance FromJSON PkgState where parseJSON = withObject "PkgState" $ \v -> PkgState <$> v .: "time" <*> v .: "pinned" <*> fmap f (v .: "packages") where f = M.mapMaybe (hush . versioning) data StateDiff = StateDiff { _toAlter :: ![SimplePkg] , _toRemove :: ![PkgName] } -- | The default location of all saved states: \/var\/cache\/aura\/states stateCache :: FilePath stateCache = "/var/cache/aura/states" -- | Does a given package have an entry in a particular `PkgState`? inState :: SimplePkg -> PkgState -> Bool inState (SimplePkg n v) s = (Just v ==) . M.lookup n $ pkgsOf s rawCurrentState :: Environment -> IO [SimplePkg] rawCurrentState env = mapMaybe simplepkg' <$> pacmanLines env ["-Q"] currentState :: Environment -> IO PkgState currentState env = do pkgs <- rawCurrentState env time <- getZonedTime pure . PkgState time False . M.fromAscList $ map (\(SimplePkg n v) -> (n, v)) pkgs compareStates :: PkgState -> PkgState -> StateDiff compareStates old curr = tcar { _toAlter = olds old curr <> _toAlter tcar } where tcar = toChangeAndRemove old curr -- | All packages that were changed and newly installed. toChangeAndRemove :: PkgState -> PkgState -> StateDiff toChangeAndRemove old curr = uncurry StateDiff . M.foldrWithKey status ([], []) $ pkgsOf curr where status k v (d, r) = case M.lookup k (pkgsOf old) of Nothing -> (d, k : r) Just v' | v == v' -> (d, r) | otherwise -> (SimplePkg k v' : d, r) -- | Packages that were uninstalled since the last record. olds :: PkgState -> PkgState -> [SimplePkg] olds old curr = map (uncurry SimplePkg) . M.assocs $ M.difference (pkgsOf old) (pkgsOf curr) -- | The filepaths of every saved package state. getStateFiles :: IO [FilePath] getStateFiles = do createDirectoryIfMissing True stateCache L.sort . map (stateCache ) <$> listDirectory stateCache -- | Save a package state. -- In writing the first state file, the `states` directory is created automatically. saveState :: Settings -> IO () saveState ss = do state <- currentState $ envOf ss let filename = stateCache dotFormat (timeOf state) <.> "json" createDirectoryIfMissing True stateCache BL.writeFile filename $ encode state notify ss saveState_1 dotFormat :: ZonedTime -> String dotFormat (ZonedTime t _) = L.intercalate "." items where items = [ show ye , printf "%02d(%s)" mo (mnths !! (mo - 1)) , printf "%02d" da , printf "%02d" (todHour $ localTimeOfDay t) , printf "%02d" (todMin $ localTimeOfDay t) , printf "%02d" ((round . todSec $ localTimeOfDay t) :: Int) ] (ye, mo, da) = toGregorian $ localDay t mnths :: [String] mnths = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ] -- | Does its best to restore a state chosen by the user. restoreState :: RIO Env () restoreState = liftIO getStateFiles >>= maybe (throwM $ Failure restoreState_2) f . NEL.nonEmpty where f :: NonEmpty FilePath -> RIO Env () f sfs = do ss <- asks settings let pth = either id id . cachePathOf $ commonConfigOf ss mpast <- liftIO $ selectState sfs >>= readState case mpast of Nothing -> throwM $ Failure readState_1 Just past -> do curr <- liftIO . currentState $ envOf ss Cache cache <- liftIO $ cacheContents pth let StateDiff rein remo = compareStates past curr (okay, nope) = L.partition (`M.member` cache) rein traverse_ (report red restoreState_1 . fmap spName) $ NEL.nonEmpty nope reinstallAndRemove (mapMaybe (`M.lookup` cache) okay) remo selectState :: NonEmpty FilePath -> IO FilePath selectState = getSelection T.pack -- | Given a `FilePath` to a package state file, attempt to read and parse -- its contents. As of Aura 2.0, only state files in JSON format are accepted. readState :: FilePath -> IO (Maybe PkgState) readState = fmap decode . BL.readFile -- | `reinstalling` can mean true reinstalling, or just altering. reinstallAndRemove :: [PackagePath] -> [PkgName] -> RIO Env () reinstallAndRemove [] [] = asks settings >>= \ss -> warn ss reinstallAndRemove_1 reinstallAndRemove down remo | null remo = reinstall | null down = remove | otherwise = reinstall *> remove where remove = asks (envOf . settings) >>= \env -> liftIO . pacman env $ "-R" : asFlag remo reinstall = asks (envOf . settings) >>= \env -> liftIO . pacman env $ "-U" : map (T.pack . ppPath) down