Skip to content

Commit cd41165

Browse files
committed
refactor(libstore): use ParsedURL for S3
1 parent 9e1f451 commit cd41165

File tree

1 file changed

+87
-36
lines changed

1 file changed

+87
-36
lines changed

src/libstore/filetransfer.cc

Lines changed: 87 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "nix/util/callback.hh"
99
#include "nix/util/signals.hh"
1010
#include "nix/store/store-reference.hh"
11+
#include "nix/util/url.hh"
1112

1213
#include "store-config-private.hh"
1314
#if NIX_WITH_S3_SUPPORT
@@ -147,7 +148,8 @@ struct curlFileTransfer : public FileTransfer
147148
// Handle S3 URLs with curl-based AWS SigV4 authentication
148149
if (hasPrefix(request.uri, "s3://")) {
149150
try {
150-
auto [httpsUri, params] = fileTransfer.convertS3ToHttpsUri(request.uri);
151+
auto s3Url = fileTransfer.parseS3Url(request.uri);
152+
auto httpsUri = s3Url.toHttpsUrl().to_string();
151153

152154
// Update the request URI to use HTTPS
153155
const_cast<FileTransferRequest &>(request).uri = httpsUri;
@@ -157,7 +159,7 @@ struct curlFileTransfer : public FileTransfer
157159
isS3Request = true;
158160

159161
// Get credentials
160-
std::string profile = getOr(params, "profile", "");
162+
std::string profile = s3Url.getProfile();
161163
auto credProvider = profile.empty() ? AwsCredentialProvider::createDefault()
162164
: AwsCredentialProvider::createProfile(profile);
163165

@@ -166,7 +168,7 @@ struct curlFileTransfer : public FileTransfer
166168
if (creds) {
167169
awsCredentials = creds->accessKeyId + ":" + creds->secretAccessKey;
168170

169-
std::string region = getOr(params, "region", "us-east-1");
171+
std::string region = s3Url.getRegion();
170172
std::string service = "s3";
171173
awsSigV4Provider = "aws:amz:" + region + ":" + service;
172174

@@ -176,7 +178,7 @@ struct curlFileTransfer : public FileTransfer
176178
requestHeaders, ("x-amz-security-token: " + *creds->sessionToken).c_str());
177179
}
178180

179-
debug("Using AWS SigV4 authentication for S3 request to %s", httpsUri);
181+
debug("Using AWS SigV4 authentication for S3 request to %s", httpsUri.c_str());
180182
} else {
181183
warn("Failed to obtain AWS credentials for S3 request %s", request.uri);
182184
}
@@ -865,41 +867,90 @@ struct curlFileTransfer : public FileTransfer
865867
}
866868

867869
#if NIX_WITH_S3_SUPPORT
868-
std::tuple<std::string, std::string, Store::Config::Params> parseS3Uri(std::string uri)
870+
/**
871+
* Parsed S3 URL with convenience methods for parameter access and HTTPS conversion
872+
*/
873+
struct S3Url
869874
{
870-
auto [path, params] = splitUriAndParams(uri);
875+
std::string bucket;
876+
std::string key;
877+
StringMap params;
871878

872-
auto slash = path.find('/', 5); // 5 is the length of "s3://" prefix
873-
if (slash == std::string::npos)
874-
throw nix::Error("bad S3 URI '%s'", path);
879+
std::string getProfile() const
880+
{
881+
return getOr(params, "profile", "");
882+
}
875883

876-
std::string bucketName(path, 5, slash - 5);
877-
std::string key(path, slash + 1);
884+
std::string getRegion() const
885+
{
886+
return getOr(params, "region", "us-east-1");
887+
}
878888

879-
return {bucketName, key, params};
880-
}
889+
std::string getScheme() const
890+
{
891+
return getOr(params, "scheme", "https");
892+
}
893+
894+
std::string getEndpoint() const
895+
{
896+
return getOr(params, "endpoint", "");
897+
}
898+
899+
/**
900+
* Convert S3 URL to HTTPS URL for use with curl's AWS SigV4 authentication
901+
*/
902+
ParsedURL toHttpsUrl() const
903+
{
904+
std::string region = getRegion();
905+
std::string endpoint = getEndpoint();
906+
std::string scheme = getScheme();
907+
908+
ParsedURL httpsUrl;
909+
httpsUrl.scheme = scheme;
910+
911+
if (!endpoint.empty()) {
912+
// Custom endpoint (e.g., MinIO, custom S3-compatible service)
913+
httpsUrl.authority = ParsedURL::Authority{.host = endpoint};
914+
httpsUrl.path = "/" + bucket + "/" + key;
915+
} else {
916+
// Standard AWS S3 endpoint
917+
httpsUrl.authority = ParsedURL::Authority{.host = "s3." + region + ".amazonaws.com"};
918+
httpsUrl.path = "/" + bucket + "/" + key;
919+
}
920+
921+
return httpsUrl;
922+
}
923+
};
881924

882925
/**
883-
* Convert S3 URI to HTTPS URI for use with curl's AWS SigV4 authentication
926+
* Parse S3 URI into structured S3Url object
884927
*/
885-
std::pair<std::string, Store::Config::Params> convertS3ToHttpsUri(const std::string & s3Uri)
928+
S3Url parseS3Url(const std::string & uri)
886929
{
887-
auto [bucketName, key, params] = parseS3Uri(s3Uri);
888-
889-
std::string region = getOr(params, "region", "us-east-1");
890-
std::string endpoint = getOr(params, "endpoint", "");
891-
std::string scheme = getOr(params, "scheme", "https");
892-
893-
std::string httpsUri;
894-
if (!endpoint.empty()) {
895-
// Custom endpoint (e.g., MinIO, custom S3-compatible service)
896-
httpsUri = scheme + "://" + endpoint + "/" + bucketName + "/" + key;
897-
} else {
898-
// Standard AWS S3 endpoint
899-
httpsUri = scheme + "://s3." + region + ".amazonaws.com/" + bucketName + "/" + key;
900-
}
930+
try {
931+
auto parsed = parseURL(uri);
901932

902-
return {httpsUri, params};
933+
if (parsed.scheme != "s3")
934+
throw nix::Error("URI scheme '%s' is not 's3'", parsed.scheme);
935+
936+
if (!parsed.authority || parsed.authority->host.empty())
937+
throw nix::Error("S3 URI missing bucket name");
938+
939+
std::string bucket = parsed.authority->host;
940+
std::string key = parsed.path;
941+
942+
// Remove leading slash from key if present
943+
if (!key.empty() && key[0] == '/') {
944+
key = key.substr(1);
945+
}
946+
947+
if (key.empty())
948+
throw nix::Error("S3 URI missing object key");
949+
950+
return S3Url{.bucket = bucket, .key = key, .params = parsed.query};
951+
} catch (BadURL & e) {
952+
throw nix::Error("invalid S3 URI '%s': %s", uri, e.what());
953+
}
903954
}
904955
#endif
905956

@@ -914,17 +965,17 @@ struct curlFileTransfer : public FileTransfer
914965
// Fall back to legacy S3Helper approach
915966
// FIXME: do this on a worker thread
916967
try {
917-
auto [bucketName, key, params] = parseS3Uri(request.uri);
968+
auto s3Parsed = this->parseS3Url(request.uri);
918969

919-
std::string profile = getOr(params, "profile", "");
920-
std::string region = getOr(params, "region", Aws::Region::US_EAST_1);
921-
std::string scheme = getOr(params, "scheme", "");
922-
std::string endpoint = getOr(params, "endpoint", "");
970+
std::string profile = getOr(s3Parsed.params, "profile", "");
971+
std::string region = getOr(s3Parsed.params, "region", Aws::Region::US_EAST_1);
972+
std::string scheme = getOr(s3Parsed.params, "scheme", "");
973+
std::string endpoint = getOr(s3Parsed.params, "endpoint", "");
923974

924975
S3Helper s3Helper(profile, region, scheme, endpoint);
925976

926977
// FIXME: implement ETag
927-
auto s3Res = s3Helper.getObject(bucketName, key);
978+
auto s3Res = s3Helper.getObject(s3Parsed.bucket, s3Parsed.key);
928979
FileTransferResult res;
929980
if (!s3Res.data)
930981
throw FileTransferError(NotFound, {}, "S3 object '%s' does not exist", request.uri);

0 commit comments

Comments
 (0)