diff --git a/doc/man/task-sync.5.in b/doc/man/task-sync.5.in index a6fb8ffeb..6d2312f83 100644 --- a/doc/man/task-sync.5.in +++ b/doc/man/task-sync.5.in @@ -219,6 +219,26 @@ default profile, or an instance profile, set $ task config sync.aws.default_credentials true .fi +.SS S3-Compatible Storage + +To synchronize with an S3-compatible storage service other than AWS, configure +the service's endpoint URL in addition to the region, bucket, credentials, and +encryption secret described above: + +.nf + $ task config sync.aws.endpoint_url +.fi + +Some S3-compatible services require path-style bucket URLs. To enable them, set: + +.nf + $ task config sync.aws.force_path_style true +.fi + +The endpoint URL must include the scheme, such as \(oqhttp://\(cq or +\(oqhttps://\(cq. Consult the storage service's documentation for the region and +URL style it requires. + .SS Local Synchronization In order to take advantage of synchronization's side effect of saving disk diff --git a/src/Context.cpp b/src/Context.cpp index d04624d51..10409288e 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -263,6 +263,8 @@ std::string configurationDefaults = "#sync.aws.secret_access_key # secret_access_key for AWS sync\n" "#sync.aws.profile # profile name for AWS sync\n" "#sync.aws.default_credentials # use default credentials for AWS sync\n" + "#sync.aws.endpoint_url # endpoint URL for S3-compatible sync\n" + "#sync.aws.force_path_style=0 # use path-style S3 URLs\n" "#sync.gcp.credential_path # Path to JSON file containing credentials to " "authenticate GCP Sync\n" "#sync.gcp.bucket # Bucket for sync to GCP\n" diff --git a/src/commands/CmdShow.cpp b/src/commands/CmdShow.cpp index 888eb5482..3276efe00 100644 --- a/src/commands/CmdShow.cpp +++ b/src/commands/CmdShow.cpp @@ -199,6 +199,8 @@ int CmdShow::execute(std::string& output) { " sync.aws.access_key_id" " sync.aws.bucket" " sync.aws.default_credentials" + " sync.aws.endpoint_url" + " sync.aws.force_path_style" " sync.aws.profile" " sync.aws.region" " sync.aws.secret_access_key" diff --git a/src/commands/CmdSync.cpp b/src/commands/CmdSync.cpp index f9ea37ca1..6e966286a 100644 --- a/src/commands/CmdSync.cpp +++ b/src/commands/CmdSync.cpp @@ -99,6 +99,9 @@ int CmdSync::execute(std::string& output) { Context::getContext().config.get("sync.aws.secret_access_key"); std::string aws_default_credentials = Context::getContext().config.get("sync.aws.default_credentials"); + std::string aws_endpoint_url = Context::getContext().config.get("sync.aws.endpoint_url"); + bool aws_force_path_style = + Context::getContext().config.getBoolean("sync.aws.force_path_style"); if (aws_region == "") { throw std::string("sync.aws.region is required"); } @@ -128,14 +131,15 @@ int CmdSync::execute(std::string& output) { } if (using_profile) { - replica->sync_to_aws_with_profile(aws_region, aws_bucket, aws_profile, encryption_secret, - avoid_snapshots); + replica->sync_to_aws_with_profile(aws_region, aws_bucket, aws_profile, aws_endpoint_url, + aws_force_path_style, encryption_secret, avoid_snapshots); } else if (using_creds) { - replica->sync_to_aws_with_access_key(aws_region, aws_bucket, aws_access_key_id, - aws_secret_access_key, encryption_secret, - avoid_snapshots); + replica->sync_to_aws_with_access_key( + aws_region, aws_bucket, aws_access_key_id, aws_secret_access_key, aws_endpoint_url, + aws_force_path_style, encryption_secret, avoid_snapshots); } else { - replica->sync_to_aws_with_default_creds(aws_region, aws_bucket, encryption_secret, + replica->sync_to_aws_with_default_creds(aws_region, aws_bucket, aws_endpoint_url, + aws_force_path_style, encryption_secret, avoid_snapshots); } diff --git a/src/taskchampion-cpp/src/lib.rs b/src/taskchampion-cpp/src/lib.rs index a12bdf062..cb9012e76 100644 --- a/src/taskchampion-cpp/src/lib.rs +++ b/src/taskchampion-cpp/src/lib.rs @@ -187,6 +187,8 @@ mod ffi { region: String, bucket: String, profile_name: String, + endpoint_url: String, + force_path_style: bool, encryption_secret: &CxxString, avoid_snapshots: bool, ) -> Result<()>; @@ -198,6 +200,8 @@ mod ffi { bucket: String, access_key_id: String, secret_access_key: String, + endpoint_url: String, + force_path_style: bool, encryption_secret: &CxxString, avoid_snapshots: bool, ) -> Result<()>; @@ -207,6 +211,8 @@ mod ffi { &mut self, region: String, bucket: String, + endpoint_url: String, + force_path_style: bool, encryption_secret: &CxxString, avoid_snapshots: bool, ) -> Result<()>; @@ -909,6 +915,8 @@ impl Replica { region: String, bucket: String, profile_name: String, + endpoint_url: String, + force_path_style: bool, encryption_secret: &CxxString, avoid_snapshots: bool, ) -> Result<(), CppError> { @@ -918,8 +926,8 @@ impl Replica { bucket, credentials: tc::server::AwsCredentials::Profile { profile_name }, encryption_secret: encryption_secret.as_bytes().to_vec(), - endpoint_url: None, - force_path_style: false, + endpoint_url: (!endpoint_url.is_empty()).then_some(endpoint_url), + force_path_style, } .into_server() .await?; @@ -933,6 +941,8 @@ impl Replica { bucket: String, access_key_id: String, secret_access_key: String, + endpoint_url: String, + force_path_style: bool, encryption_secret: &CxxString, avoid_snapshots: bool, ) -> Result<(), CppError> { @@ -945,8 +955,8 @@ impl Replica { secret_access_key, }, encryption_secret: encryption_secret.as_bytes().to_vec(), - endpoint_url: None, - force_path_style: false, + endpoint_url: (!endpoint_url.is_empty()).then_some(endpoint_url), + force_path_style, } .into_server() .await?; @@ -958,6 +968,8 @@ impl Replica { &mut self, region: String, bucket: String, + endpoint_url: String, + force_path_style: bool, encryption_secret: &CxxString, avoid_snapshots: bool, ) -> Result<(), CppError> { @@ -967,8 +979,8 @@ impl Replica { bucket, credentials: tc::server::AwsCredentials::Default, encryption_secret: encryption_secret.as_bytes().to_vec(), - endpoint_url: None, - force_path_style: false, + endpoint_url: (!endpoint_url.is_empty()).then_some(endpoint_url), + force_path_style, } .into_server() .await?; diff --git a/test/show.test.py b/test/show.test.py index f3fdceb79..fbf3b8571 100755 --- a/test/show.test.py +++ b/test/show.test.py @@ -73,6 +73,16 @@ def test_show_no_unrecognized(self): "Your .taskrc file contains these unrecognized variables:\n foo", out ) + def test_show_s3_compatible_settings(self): + """Verify S3-compatible storage settings are recognized""" + self.t.config("sync.aws.endpoint_url", "https://minio.example.test") + self.t.config("sync.aws.force_path_style", "true") + code, out, err = self.t("show sync.aws") + self.assertIn("sync.aws.endpoint_url", out) + self.assertIn("https://minio.example.test", out) + self.assertIn("sync.aws.force_path_style", out) + self.assertIn("true", out) + class TestShowHelperCommand(TestCase): def setUp(self):