blob: 8804593508a1845736cf0e10698e994ca4fb64de [file] [log] [blame]
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module Mulkup.Bupstash (BupItem(..), Bupstash (..), bupPut, bupGc, bupList, bupRemove, BupFilter (..), runBupstash, bupItemUTCTime) where
import Mulkup.Config (MulkupConfig (..))
import Mulkup.Prelude hiding (put)
import Optics
import Polysemy
import Polysemy.Reader (Reader, asks)
import Turtle hiding (err, x)
import Data.Aeson (FromJSON, eitherDecode)
import Polysemy.Error (Error, throw)
import Data.Text (pack, unpack)
import Mulkup.Logging
import Colog.Polysemy (Log)
import Colog (Message)
import Data.Time
import Data.Time.Clock.POSIX
-- * API
data BupItem = BupItem
{ id :: Text,
unix_timestamp_millis :: Integer,
tags :: Map String String
}
deriving stock (Generic, Show)
deriving anyclass (FromJSON)
data BupFilter = BupFilter
{ labels :: [(Text, Text)],
minimumAge :: Maybe Text
}
makeFieldLabelsNoPrefix ''BupFilter
data Bupstash m a where
BupGc :: Bupstash m ()
BupPut :: Text -> [Text] -> [(Text, Text)] -> Bupstash m ()
BupList :: BupFilter -> Bupstash m [BupItem]
BupRemove :: [Text] -> Bupstash m ()
makeSem ''Bupstash
-- * Smart Accessors
bupItemUTCTime :: BupItem -> UTCTime
bupItemUTCTime item =
posixSecondsToUTCTime $
secondsToNominalDiffTime $
fromInteger (item ^. #unix_timestamp_millis) / 1000
-- * Implementation
-- | Runs a 'Bupstash' using the “bupstash” CLI command.
runBupstash :: (Member (Error Text) r, Member (Log Message) r, Member (Embed IO) r, Member (Reader MulkupConfig) r) => Sem (Bupstash ': r) a -> Sem r a
runBupstash = interpret \case
BupGc ->
procs "bupstash" ["gc"] empty
BupPut baseDir exclusions labels -> do
host <- getHost
procs "bupstash" (["put", "--xattrs"] ++ map exclusionArg exclusions ++ map labelArg labels ++ [labelArg ("host", host)] ++ [baseDir]) empty
BupList bupFilter -> do
host <- getHost
out <- strict $ inproc "bupstash" (["list", "--format=jsonl1"] ++ filterArgs host bupFilter) empty
let parsedItems = map parseItem (patchLines (lines out))
forM parsedItems \case
Left err -> do
let errtext = pack err
logError errtext
throw errtext
Right x ->
return x
BupRemove ids -> do
procs "bupstash" ["rm", "--ids-from-stdin"] (select (map unsafeTextToLine ids))
where
getHost :: Member (Reader MulkupConfig) r => Sem r Text
getHost =
asks @MulkupConfig (^. #host)
-- | Fixes up the buggy two-line output that Buptash produces in
-- jsonl1 output mode.
--
-- See: https://github.com/andrewchambers/bupstash/pull/241
patchLines :: [Text] -> [Text]
patchLines = concatMap patchLine
where
patchLine :: Text -> [Text]
patchLine line
| line == "}" =
[]
| length (filter (== '}') $ unpack line) < length (filter (== '{') $ unpack line) =
[line <> "}"]
| otherwise =
[line]
labelArg :: (Text, Text) -> Text
labelArg (key, value) = key <> "=" <> value
exclusionArg :: Text -> Text
exclusionArg = ("--exclude=" <>)
filterArgs :: Text -> BupFilter -> [Text]
filterArgs host (BupFilter labels minimumAge) =
[labelArg ("host", host)] ++
concatMap (\label -> ["and", labelArg label]) labels ++
concatMap (\x -> ["and", "older-than", x]) minimumAge
parseItem :: Text -> Either String BupItem
parseItem = eitherDecode . encodeUtf8