nix

configuration files that power my machines
Log | Files | Refs | README | LICENSE

commit 2c482ec4e5641dfe53e8507c9c420aedcb4b12bf
parent 01b6006d3fe22e2db5a6d9551a947f6d7952edf4
Author: mtmn <miro@haravara.org>
Date:   Sat, 25 Apr 2026 23:46:21 +0200

dotfiles: add git-annex-init

Diffstat:
Amodules/mixins/dotfiles/bin/git-annex-init | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 190 insertions(+), 0 deletions(-)

diff --git a/modules/mixins/dotfiles/bin/git-annex-init b/modules/mixins/dotfiles/bin/git-annex-init @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +Initialize git-annex with an S3 special remote. + +Usage: + git-annex-init <name> <prefix> [options] + +Env overrides: + BUCKET, REGION, HOST, AWS_PROFILE, LARGE_FILES_THRESHOLD +""" + +import argparse +import logging +import os +import subprocess +import sys +from pathlib import Path + +DEFAULTS = { + "bucket": "mine", + "region": "us-east-1", + "host": "estri.saatana.cat", + "large_files_threshold": "100kb", + "aws_profile": "default", +} + +log = logging.getLogger("git-annex-init") + + +def run( + cmd: list[str], check: bool = True, capture: bool = False +) -> subprocess.CompletedProcess: + log.debug("$ %s", " ".join(cmd)) + return subprocess.run(cmd, check=check, capture_output=capture, text=True) + + +def run_ok(cmd: list[str]) -> bool: + return run(cmd, check=False, capture=True).returncode == 0 + + +def aws_credential(key: str, profile: str) -> str: + result = run(["aws", "configure", "get", key, "--profile", profile], capture=True) + value = result.stdout.strip() + if not value: + log.error("AWS credential '%s' not found in profile '%s'", key, profile) + sys.exit(1) + return value + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Initialize git-annex with an S3 special remote.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=( + "examples:\n" + " git-annex-init my-project annex/my-project/\n" + " git-annex-init my-project annex/my-project/ --dry-run\n" + " git-annex-init my-project annex/my-project/ --bucket other --region eu-west-1\n" + " BUCKET=other git-annex-init my-project annex/my-project/\n" + ), + ) + parser.add_argument("name", help="annex description (e.g. my-project)") + parser.add_argument("prefix", help="path inside bucket (e.g. annex/my-project/)") + parser.add_argument( + "--bucket", + default=os.environ.get("BUCKET", DEFAULTS["bucket"]), + help=f"S3 bucket name (env: BUCKET, default: {DEFAULTS['bucket']})", + ) + parser.add_argument( + "--region", + default=os.environ.get("REGION", DEFAULTS["region"]), + help=f"S3 region (env: REGION, default: {DEFAULTS['region']})", + ) + parser.add_argument( + "--host", + default=os.environ.get("HOST", DEFAULTS["host"]), + help=f"S3-compatible endpoint host (env: HOST, default: {DEFAULTS['host']})", + ) + parser.add_argument( + "--large-files", + default=os.environ.get( + "LARGE_FILES_THRESHOLD", DEFAULTS["large_files_threshold"] + ), + metavar="THRESHOLD", + help=f"size threshold for annex (env: LARGE_FILES_THRESHOLD, default: {DEFAULTS['large_files_threshold']})", + ) + parser.add_argument( + "--aws-profile", + default=os.environ.get("AWS_PROFILE", DEFAULTS["aws_profile"]), + help=f"AWS credentials profile (env: AWS_PROFILE, default: {DEFAULTS['aws_profile']})", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="print what would happen without executing", + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="show each command as it runs" + ) + args = parser.parse_args() + + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(message)s", + ) + + if args.dry_run: + log.info("Dry run — no changes will be made.") + + cwd = Path.cwd() + + # Credentials + log.info("Fetching AWS credentials from profile '%s'...", args.aws_profile) + os.environ["AWS_ACCESS_KEY_ID"] = aws_credential( + "aws_access_key_id", args.aws_profile + ) + os.environ["AWS_SECRET_ACCESS_KEY"] = aws_credential( + "aws_secret_access_key", args.aws_profile + ) + + if args.dry_run: + log.info( + "Would initialize git-annex for '%s' at s3://%s/%s", + args.name, + args.bucket, + args.prefix, + ) + sys.exit(0) + + # git init + if not (cwd / ".git").exists(): + log.info("Initializing git repository...") + run(["git", "init"]) + + # git-annex init + if not run_ok(["git", "annex", "status"]): + log.info("Initializing git-annex...") + run(["git", "annex", "init", args.name]) + run( + [ + "git", + "annex", + "config", + "--set", + "annex.largefiles", + f"largerthan={args.large_files}", + ] + ) + + # Stage and commit + log.info("Staging files...") + run(["git", "add", "."]) + run(["git", "annex", "add", "."]) + if not run_ok(["git", "diff", "--cached", "--quiet"]): + run(["git", "commit", "-m", "Initial commit"]) + + # S3 remote + if not run_ok(["git", "annex", "info", "s3"]): + log.info("Initializing S3 remote...") + run( + [ + "git", + "annex", + "initremote", + "s3", + "type=S3", + f"bucket={args.bucket}", + f"region={args.region}", + f"host={args.host}", + "requeststyle=path", + "encryption=none", + "storageclass=STANDARD_IA", + f"fileprefix={args.prefix}", + ] + ) + else: + log.info("Enabling existing S3 remote...") + run(["git", "annex", "enableremote", "s3"]) + + # Push + log.info("Copying files to S3...") + run(["git", "annex", "copy", "--to", "s3", "."]) + run(["git", "annex", "sync", "s3"]) + + log.info("Done. %s → s3://%s/%s", args.name, args.bucket, args.prefix) + log.info("Verify with: git annex whereis .") + + +if __name__ == "__main__": + main()