tools

various tools I have been using throughout the years
Log | Files | Refs | README | LICENSE

commit ac9d608a2ce57e13fd4e43ee82f4b59f144c718b
parent 58e44f1e772628c10b2528a110e96c44597266df
Author: mtmn <miro@haravara.org>
Date:   Sat,  9 May 2026 22:18:06 +0200

backup: remove annex, shadow backups guard

Diffstat:
MREADME.md | 2+-
Mbackup/README.md | 22++++++++++------------
Mbackup/backup.cabal | 2+-
Mbackup/backup.hs | 267++++++++++++++++++++++++++++++++++++++-----------------------------------------
4 files changed, 139 insertions(+), 154 deletions(-)

diff --git a/README.md b/README.md @@ -5,7 +5,7 @@ Various tools I have been using throughout the years. | tool | description | |---|---| | [aaltomuoto](aaltomuoto/) | frequency-colored audio waveform generator | -| [backup](backup/) | backup orchestrator — git, plakar, S3 | +| [backup](backup/) | terminal backup orchestrator | | [bandeno](bandeno/) | Bandcamp JSON Feed proxy | | [diffamer](diffamer/) | line-file syncer with rsync and coloured diff | | [diggah](diggah/) | find files modified within a time range, locally or over SSH | diff --git a/backup/README.md b/backup/README.md @@ -11,25 +11,23 @@ cabal build ## Usage ```sh -backup <type> [--push] +backup [-t <target>] [--push] [--quiet] [--list] ``` -### Types +### Targets -| Type | Description | -|------|-------------| -| `push` | Commit and push all git repos | +| Target | Description | +|--------|-------------| | `git` | Commit all git repos (push with `--push`) | -| `annex` | Sync all git-annex repos | -| `local` | Snapshot `$HOME` to the local plakar store | -| `plakar` | Snapshot all configured directories to the remote plakar store | -| `mobius` | Sync all configured directories to S3 | -| `ls` | List all configured backup targets | -| `<name>` | Run backup for a specific named target | +| `plakar` | Snapshot to both plakar repos (`s3` and `hoo`) | +| `mobius` | Sync all mobius targets to S3 | +| `<backend>:<name>[:<sub>]` | Run a single target, e.g. `git:nix`, `plakar:s3`, `plakar:hoo`, `plakar:s3:src`, `mobius:www` | ### Options | Flag | Description | |------|-------------| -| `--push` | Push after committing (for git targets) | +| `-p`, `--push` | Push after committing (for git targets) | +| `-q`, `--quiet` | Suppress command output (default: stream output) | +| `-l`, `--list` | List all configured backup targets and exit | | `-h`, `--help` | Show help | diff --git a/backup/backup.cabal b/backup/backup.cabal @@ -1,4 +1,4 @@ -cabal-version: 3.16 +cabal-version: 3.0 name: backup version: 0.1.0 build-type: Simple diff --git a/backup/backup.hs b/backup/backup.hs @@ -6,12 +6,12 @@ import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Text qualified as T import Data.Time (defaultTimeLocale, formatTime, getCurrentTime) +import System.Environment (getArgs, withArgs) import Turtle hiding (sort) data Config = Config { cfgHome :: Text , cfgGitDirs :: Map Text Text - , cfgGitAnnexDirs :: Map Text Text , cfgPlakarDirs :: Map Text Text , cfgMobiusDirs :: Map Text Text } @@ -29,7 +29,6 @@ mkConfig h = , ("releases", h <> "/src/haravara.org/releases") , ("bandcamp", h <> "/misc/music/backlog/_todo") ] - , cfgGitAnnexDirs = Map.empty , cfgPlakarDirs = Map.fromList [ ("src", h <> "/src") @@ -50,7 +49,9 @@ mkConfig h = -- Logging ts :: IO Text -ts = T.pack . formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S%z" <$> getCurrentTime +ts = + T.pack . formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S%z" + <$> getCurrentTime logMsg :: Text -> IO () logMsg msg = ts >>= \t -> echo (unsafeTextToLine $ "[" <> t <> "]: " <> msg) @@ -63,169 +64,155 @@ fatal msg = do -- Process helpers +run :: Bool -> Text -> [Text] -> IO ExitCode +run verbose cmd args + | verbose = proc cmd args stdin + | otherwise = fst <$> procStrict cmd args empty + gitPrefix :: Text -> [Text] gitPrefix dir = ["--git-dir=" <> dir <> "/.git", "--work-tree=" <> dir] -gitSilent :: Text -> [Text] -> IO ExitCode -gitSilent dir args = fst <$> procStrict "git" (gitPrefix dir <> args) empty +git :: Bool -> Text -> [Text] -> IO ExitCode +git verbose dir args = run verbose "git" (gitPrefix dir <> args) gitCapture :: Text -> [Text] -> IO (ExitCode, Text) gitCapture dir args = procStrict "git" (gitPrefix dir <> args) empty -gitLoud :: Text -> [Text] -> IO ExitCode -gitLoud dir args = proc "git" (gitPrefix dir <> args) stdin - -runSilent :: Text -> [Text] -> IO ExitCode -runSilent cmd args = fst <$> procStrict cmd args empty - -requireCmd :: Text -> IO () -requireCmd cmd = - which (T.unpack cmd) >>= maybe (fatal $ cmd <> " not found") (const $ return ()) - ok :: ExitCode -> Bool ok = (== ExitSuccess) +expect :: Text -> ExitCode -> IO () +expect msg ec = unless (ok ec) $ fatal msg + -- Core logic -syncRepos :: Text -> Bool -> Bool -> IO () -syncRepos dir push annex = do - stageEc <- gitSilent dir $ if annex then ["annex", "add", "."] else ["add", "-A"] - unless (ok stageEc) $ fatal $ "Failed to stage changes in " <> dir +syncRepos :: Text -> Bool -> IO () +syncRepos dir push = do + git False dir ["add", "-A"] + >>= expect ("Failed to stage changes in " <> dir) - diffEc <- gitSilent dir ["diff", "--cached", "--quiet"] + diffEc <- git False dir ["diff", "--cached", "--quiet"] if ok diffEc then logMsg $ "Nothing to commit in " <> dir else do - (_, changed) <- gitCapture dir ["diff", "--cached", "--name-only"] - commitEc <- gitLoud dir ["commit", "-m", T.unwords . T.lines $ changed] - unless (ok commitEc) $ fatal $ "Failed to commit changes in " <> dir + (changedEc, changed) <- + gitCapture dir ["diff", "--cached", "--name-only"] + expect ("Failed to list staged changes in " <> dir) changedEc + git True dir ["commit", "-m", T.unwords . T.lines $ changed] + >>= expect ("Failed to commit changes in " <> dir) logMsg $ "Committed changes in " <> dir - when push $ - if annex - then do - logMsg $ "Syncing annex in " <> dir - syncEc <- gitLoud dir ["annex", "sync", "--content"] - unless (ok syncEc) $ fatal $ "Failed to sync annex in " <> dir - logMsg $ "Successfully synced annex in " <> dir - else do - logMsg $ "Pushing changes from " <> dir - pushEc <- gitLoud dir ["push"] - unless (ok pushEc) $ fatal $ "Failed to push changes from " <> dir - logMsg $ "Successfully pushed changes from " <> dir - -plakarSync :: Config -> Text -> IO () -plakarSync cfg name = case Map.lookup name (cfgPlakarDirs cfg) of - Nothing -> fatal $ "Unknown plakar target: " <> name - Just dir -> do - logMsg $ "Syncing plakar/" <> name <> " from " <> dir - ec <- - runSilent - "plakar" - [ "at" - , "@estri" - , "backup" - , "--ignore" - , "**/.local/share/containers*" - , "--ignore" - , "**/Steam*" - , "--ignore" - , "**/mnt*" - , "--ignore" - , "**/.git*" - , dir - ] - unless (ok ec) $ fatal $ "plakar failed for " <> name - logMsg $ "Done plakar/" <> name - -mobiusSync :: Config -> Text -> IO () -mobiusSync cfg name = case Map.lookup name (cfgMobiusDirs cfg) of - Nothing -> fatal $ "Unknown mobius target: " <> name - Just dir -> do - logMsg $ "Syncing mobius/" <> name <> " from " <> dir - ec <- - runSilent - "aws" - [ "s3" - , "sync" - , "--exclude" - , "*/.git/*" - , "--no-follow-symlinks" - , dir - , "s3://mobius/" <> name - ] - unless (ok ec) $ fatal $ "aws s3 sync failed for " <> name - logMsg $ "Done mobius/" <> name - -backup :: Config -> Text -> Bool -> IO () -backup cfg btype push = case btype of - "push" -> do + when push $ do + logMsg $ "Pushing changes from " <> dir + git True dir ["push"] >>= expect ("Failed to push changes from " <> dir) + logMsg $ "Done pushing " <> dir + +runPlakar :: Bool -> Text -> [Text] -> Text -> IO ExitCode +runPlakar verbose repo ignores dir = + run verbose "plakar" $ + ["at", repo, "backup"] + <> concatMap (\p -> ["--ignore", p]) ignores + <> [dir] + +s3Ignores :: [Text] +s3Ignores = ["**/.local/share/containers*", "**/Steam*", "**/mnt*", "**/.git*"] + +hooIgnores :: [Text] +hooIgnores = ["**/.cache*", "**/Steam*", "**/mnt*", "**/.git*"] + +plakarS3 :: Bool -> Text -> Text -> IO () +plakarS3 verbose name dir = do + logMsg $ "Syncing plakar/s3/" <> name <> " from " <> dir + runPlakar verbose "@estri" s3Ignores dir + >>= expect ("plakar @s3 failed for " <> name) + logMsg $ "Done plakar/s3/" <> name + +plakarHoo :: Bool -> Text -> IO () +plakarHoo verbose dir = do + logMsg $ "Syncing plakar/hoo from " <> dir + runPlakar verbose "@hoo" hooIgnores dir + >>= expect "plakar @hoo failed" + logMsg "Done plakar/hoo" + +mobiusFilters :: Text -> [Text] +mobiusFilters "www" = + ["--exclude", "*", "--include", "*.html", "--include", "*.md"] +mobiusFilters _ = ["--exclude", "*/.git/*"] + +mobiusSync :: Bool -> Text -> Text -> IO () +mobiusSync verbose name dir = do + logMsg $ "Syncing mobius/" <> name <> " from " <> dir + let args = + ["s3", "sync"] + <> mobiusFilters name + <> ["--no-follow-symlinks", dir, "s3://mobius/" <> name] + run verbose "aws" args + >>= expect ("aws s3 sync failed for " <> name) + logMsg $ "Done mobius/" <> name + +listTargets :: Config -> IO () +listTargets cfg = do + let listKeys indent = + mapM_ (echo . unsafeTextToLine . (indent <>)) . sort . Map.keys + echo "git" + listKeys " " (cfgGitDirs cfg) + echo "plakar" + echo " hoo" + echo " s3" + listKeys " " (cfgPlakarDirs cfg) + echo "mobius" + listKeys " " (cfgMobiusDirs cfg) + +backup :: Config -> Text -> Bool -> Bool -> IO () +backup cfg target push verbose = case T.splitOn ":" target of + ["git"] -> forM_ (Map.toList (cfgGitDirs cfg)) $ \(name, dir) -> - logMsg name >> syncRepos dir True False - forM_ (Map.toList (cfgGitAnnexDirs cfg)) $ \(name, dir) -> - logMsg (name <> " (annex)") >> syncRepos dir True True - "git" -> - forM_ (Map.toList (cfgGitDirs cfg)) $ \(name, dir) -> - logMsg name >> syncRepos dir push False - "annex" -> - forM_ (Map.toList (cfgGitAnnexDirs cfg)) $ \(name, dir) -> - logMsg (name <> " (annex)") >> syncRepos dir push True - "ls" -> do - echo "git" - mapM_ (echo . unsafeTextToLine . (" " <>)) . sort . Map.keys $ cfgGitDirs cfg - echo "git-annex" - mapM_ (echo . unsafeTextToLine . (" " <>)) . sort . Map.keys $ cfgGitAnnexDirs cfg - echo "plakar" - mapM_ (echo . unsafeTextToLine . (" " <>)) . sort . Map.keys $ cfgPlakarDirs cfg - echo "mobius" - mapM_ (echo . unsafeTextToLine . (" " <>)) . sort . Map.keys $ cfgMobiusDirs cfg - "local" -> do - requireCmd "plakar" - logMsg "Running plakar" - ec <- - runSilent - "plakar" - [ "at" - , "@hoo" - , "backup" - , "--ignore" - , "**/.cache*" - , "--ignore" - , "**/Steam*" - , "--ignore" - , "**/mnt*" - , "--ignore" - , "**/.git*" - , cfgHome cfg - ] - unless (ok ec) $ fatal "plakar local backup failed" - logMsg "Done plakar local" - "plakar" -> - forM_ (Map.keys (cfgPlakarDirs cfg)) $ plakarSync cfg - "mobius" -> - forM_ (Map.keys (cfgMobiusDirs cfg)) $ mobiusSync cfg - _ -> case ( Map.lookup btype (cfgGitDirs cfg) - , Map.lookup btype (cfgGitAnnexDirs cfg) - , Map.lookup btype (cfgPlakarDirs cfg) - ) of - (Just dir, _, _) -> syncRepos dir push False - (_, Just dir, _) -> syncRepos dir push True - (_, _, Just _) -> plakarSync cfg btype - _ -> fatal $ "Unknown backup type '" <> btype <> "'" + logMsg name >> syncRepos dir push + ["plakar"] -> do + forM_ (Map.toList (cfgPlakarDirs cfg)) $ uncurry (plakarS3 verbose) + plakarHoo verbose (cfgHome cfg) + ["mobius"] -> + forM_ (Map.toList (cfgMobiusDirs cfg)) $ uncurry (mobiusSync verbose) + ["git", name] -> case Map.lookup name (cfgGitDirs cfg) of + Just dir -> syncRepos dir push + Nothing -> fatal $ "Unknown git target: " <> name + ["plakar", "s3"] -> + forM_ (Map.toList (cfgPlakarDirs cfg)) $ uncurry (plakarS3 verbose) + ["plakar", "hoo"] -> plakarHoo verbose (cfgHome cfg) + ["plakar", "s3", name] -> case Map.lookup name (cfgPlakarDirs cfg) of + Just dir -> plakarS3 verbose name dir + Nothing -> fatal $ "Unknown plakar/s3 dir: " <> name + ["mobius", name] -> case Map.lookup name (cfgMobiusDirs cfg) of + Just dir -> mobiusSync verbose name dir + Nothing -> fatal $ "Unknown mobius target: " <> name + _ -> fatal $ "Unknown target: " <> target data Args = Args - { argBackupType :: Text + { argTarget :: Maybe Text , argPush :: Bool + , argQuiet :: Bool + , argList :: Bool } argsParser :: Parser Args argsParser = - Args - <$> argText "backup_type" "Type of backup (push, ls, git, annex, local, plakar, mobius, <name>)" - <*> switch "push" 'p' "Push changes after commit (for Git items)" + let targetHelp = "git, plakar, mobius, <backend>:<name>[:<sub>]" + in Args + <$> optional (optText "target" 't' targetHelp) + <*> switch "push" 'p' "Push changes after commit" + <*> switch "quiet" 'q' "Suppress command output" + <*> switch "list" 'l' "List all configured backup targets" main :: IO () main = do - Args btype pushFlag <- options "Backup manager" argsParser - h <- need "HOME" >>= maybe (die "HOME not set") return - backup (mkConfig h) btype pushFlag + rawArgs <- getArgs + let effArgs = if null rawArgs then ["--help"] else rawArgs + Args mtarget pushFlag quietFlag listFlag <- + withArgs effArgs $ options "" argsParser + h <- need "HOME" >>= maybe (fatal "HOME not set") pure + let cfg = mkConfig h + if listFlag + then listTargets cfg + else case mtarget of + Just target -> backup cfg target pushFlag (not quietFlag) + Nothing -> fatal "no target given (use --list to list targets)"