| Matthias Andreas Benkard | 262110d | 2021-08-24 06:35:55 +0200 | [diff] [blame] | 1 | {-# LANGUAGE BlockArguments #-} |
| 2 | {-# LANGUAGE DuplicateRecordFields #-} |
| 3 | {-# LANGUAGE LambdaCase #-} |
| 4 | {-# LANGUAGE TypeOperators #-} |
| 5 | {-# LANGUAGE UndecidableInstances #-} |
| 6 | |
| 7 | module Mulkup.Bupstash (BupItem(..), Bupstash (..), bupPut, bupGc, bupList, bupRemove, BupFilter (..), runBupstash, bupItemUTCTime) where |
| 8 | |
| 9 | import Mulkup.Config (MulkupConfig (..)) |
| Matthias Andreas Benkard | b06f591 | 2026-05-25 17:32:24 +0200 | [diff] [blame^] | 10 | import Mulkup.Flags (Flags (..)) |
| Matthias Andreas Benkard | 262110d | 2021-08-24 06:35:55 +0200 | [diff] [blame] | 11 | import Mulkup.Prelude hiding (put) |
| 12 | import Optics |
| 13 | import Polysemy |
| 14 | import Polysemy.Reader (Reader, asks) |
| 15 | import Turtle hiding (err, x) |
| 16 | import Data.Aeson (FromJSON, eitherDecode) |
| 17 | import Polysemy.Error (Error, throw) |
| 18 | import Data.Text (pack, unpack) |
| 19 | import Mulkup.Logging |
| 20 | import Colog.Polysemy (Log) |
| 21 | import Colog (Message) |
| 22 | import Data.Time |
| 23 | import Data.Time.Clock.POSIX |
| 24 | |
| 25 | -- * API |
| 26 | |
| 27 | data BupItem = BupItem |
| 28 | { id :: Text, |
| 29 | unix_timestamp_millis :: Integer, |
| 30 | tags :: Map String String |
| 31 | } |
| 32 | deriving stock (Generic, Show) |
| 33 | deriving anyclass (FromJSON) |
| 34 | |
| 35 | data BupFilter = BupFilter |
| 36 | { labels :: [(Text, Text)], |
| 37 | minimumAge :: Maybe Text |
| 38 | } |
| 39 | |
| 40 | makeFieldLabelsNoPrefix ''BupFilter |
| 41 | |
| 42 | data Bupstash m a where |
| 43 | BupGc :: Bupstash m () |
| 44 | BupPut :: Text -> [Text] -> [(Text, Text)] -> Bupstash m () |
| 45 | BupList :: BupFilter -> Bupstash m [BupItem] |
| 46 | BupRemove :: [Text] -> Bupstash m () |
| 47 | |
| 48 | makeSem ''Bupstash |
| 49 | |
| 50 | -- * Smart Accessors |
| 51 | |
| 52 | bupItemUTCTime :: BupItem -> UTCTime |
| 53 | bupItemUTCTime item = |
| 54 | posixSecondsToUTCTime $ |
| 55 | secondsToNominalDiffTime $ |
| 56 | fromInteger (item ^. #unix_timestamp_millis) / 1000 |
| 57 | |
| 58 | -- * Implementation |
| 59 | |
| Matthias Andreas Benkard | b06f591 | 2026-05-25 17:32:24 +0200 | [diff] [blame^] | 60 | -- | Runs a 'Bupstash' using the "bupstash" CLI command. |
| 61 | runBupstash :: (Member (Error Text) r, Member (Log Message) r, Member (Embed IO) r, Member (Reader MulkupConfig) r, Member (Reader Flags) r) => Sem (Bupstash ': r) a -> Sem r a |
| Matthias Andreas Benkard | 262110d | 2021-08-24 06:35:55 +0200 | [diff] [blame] | 62 | runBupstash = interpret \case |
| 63 | BupGc -> |
| 64 | procs "bupstash" ["gc"] empty |
| 65 | |
| 66 | BupPut baseDir exclusions labels -> do |
| 67 | host <- getHost |
| Matthias Andreas Benkard | b06f591 | 2026-05-25 17:32:24 +0200 | [diff] [blame^] | 68 | verboseArg <- getVerboseArg |
| 69 | procs "bupstash" (["put", "--print-stats", "--xattrs"] ++ verboseArg ++ map exclusionArg exclusions ++ map labelArg labels ++ [labelArg ("host", host)] ++ [baseDir]) empty |
| Matthias Andreas Benkard | 262110d | 2021-08-24 06:35:55 +0200 | [diff] [blame] | 70 | |
| 71 | BupList bupFilter -> do |
| 72 | host <- getHost |
| 73 | out <- strict $ inproc "bupstash" (["list", "--format=jsonl1"] ++ filterArgs host bupFilter) empty |
| 74 | let parsedItems = map parseItem (patchLines (lines out)) |
| 75 | forM parsedItems \case |
| 76 | Left err -> do |
| 77 | let errtext = pack err |
| 78 | logError errtext |
| 79 | throw errtext |
| 80 | Right x -> |
| 81 | return x |
| 82 | |
| 83 | BupRemove ids -> do |
| Matthias Andreas Benkard | e1d0bbb | 2021-08-24 20:42:38 +0200 | [diff] [blame] | 84 | forM_ ids $ \id' -> do |
| 85 | procs "bupstash" ["rm", labelArg ("id", id')] empty |
| Matthias Andreas Benkard | 262110d | 2021-08-24 06:35:55 +0200 | [diff] [blame] | 86 | |
| 87 | where |
| 88 | getHost :: Member (Reader MulkupConfig) r => Sem r Text |
| 89 | getHost = |
| 90 | asks @MulkupConfig (^. #host) |
| 91 | |
| Matthias Andreas Benkard | b06f591 | 2026-05-25 17:32:24 +0200 | [diff] [blame^] | 92 | getVerboseArg :: (Member (Reader Flags) r) => Sem r [Text] |
| 93 | getVerboseArg = do |
| 94 | isVerbose <- asks @Flags (^. #verbose) |
| 95 | return $ if isVerbose then ["-v"] else [] |
| 96 | |
| Matthias Andreas Benkard | 262110d | 2021-08-24 06:35:55 +0200 | [diff] [blame] | 97 | -- | Fixes up the buggy two-line output that Buptash produces in |
| 98 | -- jsonl1 output mode. |
| 99 | -- |
| 100 | -- See: https://github.com/andrewchambers/bupstash/pull/241 |
| 101 | patchLines :: [Text] -> [Text] |
| 102 | patchLines = concatMap patchLine |
| 103 | where |
| 104 | patchLine :: Text -> [Text] |
| 105 | patchLine line |
| 106 | | line == "}" = |
| 107 | [] |
| 108 | | length (filter (== '}') $ unpack line) < length (filter (== '{') $ unpack line) = |
| 109 | [line <> "}"] |
| 110 | | otherwise = |
| 111 | [line] |
| 112 | |
| 113 | labelArg :: (Text, Text) -> Text |
| 114 | labelArg (key, value) = key <> "=" <> value |
| 115 | |
| 116 | exclusionArg :: Text -> Text |
| 117 | exclusionArg = ("--exclude=" <>) |
| 118 | |
| 119 | filterArgs :: Text -> BupFilter -> [Text] |
| 120 | filterArgs host (BupFilter labels minimumAge) = |
| 121 | [labelArg ("host", host)] ++ |
| 122 | concatMap (\label -> ["and", labelArg label]) labels ++ |
| 123 | concatMap (\x -> ["and", "older-than", x]) minimumAge |
| 124 | |
| 125 | parseItem :: Text -> Either String BupItem |
| 126 | parseItem = eitherDecode . encodeUtf8 |