From dd97bf1666728a2e4c6b165c6dd93bc56c0b3f8b Mon Sep 17 00:00:00 2001 From: kigland Date: Sun, 1 Mar 2026 16:38:37 +0800 Subject: [PATCH] feat: add endpoint_url support to s3_sync for MinIO compatibility (#435) Add an optional endpoint_url parameter to s3_sync and ensure_bucket_exists. When not provided, falls back to the AWS_ENDPOINT_URL environment variable. This enables S3-compatible services like MinIO without requiring callers to manage the env var themselves. --- src/art/utils/s3.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/art/utils/s3.py b/src/art/utils/s3.py index a96acf19d..e18fac2a2 100644 --- a/src/art/utils/s3.py +++ b/src/art/utils/s3.py @@ -68,6 +68,7 @@ async def s3_sync( destination: str, *, profile: Optional[str] = None, + endpoint_url: Optional[str] = None, verbose: bool = False, delete: bool = False, exclude: list[ExcludableOption] | None = None, @@ -84,17 +85,27 @@ async def s3_sync( source: The *from* path. Can be a local path or an ``s3://`` URI. destination: The *to* path. Can be a local path or an ``s3://`` URI. profile: Optional AWS profile name to pass to the CLI. + endpoint_url: Optional custom endpoint URL for S3-compatible services + (e.g. MinIO). Falls back to the ``AWS_ENDPOINT_URL`` environment + variable when not provided. verbose: When *True*, the output of the AWS CLI is streamed to the calling process; otherwise it is suppressed. + delete: When *True*, delete files in *destination* that don't exist in + *source*. exclude: List of directories to exclude from sync. Raises: S3SyncError: If the *aws s3 sync* command exits with a non‑zero status. """ + if endpoint_url is None: + endpoint_url = os.environ.get("AWS_ENDPOINT_URL") + cmd: list[str] = ["aws"] if profile: cmd += ["--profile", profile] + if endpoint_url: + cmd += ["--endpoint-url", endpoint_url] cmd += ["s3"] # use cp for files, sync for directories @@ -125,15 +136,22 @@ async def s3_sync( async def ensure_bucket_exists( - s3_bucket: str | None = None, profile: str | None = None + s3_bucket: str | None = None, + profile: str | None = None, + endpoint_url: str | None = None, ) -> None: if s3_bucket is None: s3_bucket = os.environ["BACKUP_BUCKET"] + if endpoint_url is None: + endpoint_url = os.environ.get("AWS_ENDPOINT_URL") + # Check if bucket exists cmd = ["aws"] if profile: cmd += ["--profile", profile] + if endpoint_url: + cmd += ["--endpoint-url", endpoint_url] cmd += ["s3api", "head-bucket", "--bucket", s3_bucket] result = await asyncio.create_subprocess_exec(*cmd, stdout=DEVNULL, stderr=DEVNULL) @@ -147,6 +165,8 @@ async def ensure_bucket_exists( cmd = ["aws"] if profile: cmd += ["--profile", profile] + if endpoint_url: + cmd += ["--endpoint-url", endpoint_url] cmd += ["s3api", "create-bucket", "--bucket", s3_bucket] result = await asyncio.create_subprocess_exec(*cmd)