commit 2c482ec4e5641dfe53e8507c9c420aedcb4b12bf
parent 01b6006d3fe22e2db5a6d9551a947f6d7952edf4
Author: mtmn <miro@haravara.org>
Date: Sat, 25 Apr 2026 23:46:21 +0200
dotfiles: add git-annex-init
Diffstat:
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()