1818 */
1919
2020#include < cstdlib>
21+ #include < memory>
2122#include < optional>
2223#include < string>
2324#include < string_view>
25+ #include < unordered_map>
26+ #include < utility>
27+ #include < vector>
2428
2529#include < arrow/filesystem/filesystem.h>
2630#if ICEBERG_S3_ENABLED
3640
3741namespace iceberg ::arrow {
3842
43+ #if ICEBERG_S3_ENABLED
44+
3945namespace {
4046
41- #if ICEBERG_S3_ENABLED
4247const std::string* FindProperty (
4348 const std::unordered_map<std::string, std::string>& properties,
4449 std::string_view key) {
@@ -74,6 +79,24 @@ Status EnsureS3Initialized() {
7479 return {};
7580}
7681
82+ // Splits any URI scheme off `endpoint` into `options.scheme`, returning the bare
83+ // host[:port] that Arrow's `endpoint_override` expects.
84+ std::string SplitEndpointScheme (std::string_view endpoint,
85+ ::arrow::fs::S3Options& options) {
86+ if (const auto pos = endpoint.find (" ://" ); pos != std::string_view::npos) {
87+ options.scheme = std::string (endpoint.substr (0 , pos));
88+ endpoint = endpoint.substr (pos + 3 );
89+ }
90+ return std::string (endpoint);
91+ }
92+
93+ bool IsS3FileIOCredentialPrefix (std::string_view prefix) {
94+ return prefix == " s3" || prefix.starts_with (" s3://" ) || prefix.starts_with (" s3a://" ) ||
95+ prefix.starts_with (" s3n://" );
96+ }
97+
98+ } // namespace
99+
77100// / \brief Configure S3Options from a properties map.
78101// /
79102// / \param properties The configuration properties map.
@@ -100,26 +123,21 @@ Result<::arrow::fs::S3Options> ConfigureS3Options(
100123 }
101124
102125 // Configure region
103- if (const auto * region = FindProperty (properties, S3Properties::kRegion );
126+ if (const auto * region = FindProperty (properties, S3Properties::kClientRegion );
104127 region != nullptr ) {
105128 options.region = *region;
106129 }
107130
108131 // Configure endpoint (for MinIO, LocalStack, etc.)
109132 if (const auto * endpoint = FindProperty (properties, S3Properties::kEndpoint );
110133 endpoint != nullptr ) {
111- options.endpoint_override = *endpoint;
112- } else {
113- // Fall back to AWS standard environment variables for endpoint override
114- const char * s3_endpoint_env = std::getenv (" AWS_ENDPOINT_URL_S3" );
115- if (s3_endpoint_env != nullptr ) {
116- options.endpoint_override = s3_endpoint_env;
117- } else {
118- const char * endpoint_env = std::getenv (" AWS_ENDPOINT_URL" );
119- if (endpoint_env != nullptr ) {
120- options.endpoint_override = endpoint_env;
121- }
122- }
134+ options.endpoint_override = SplitEndpointScheme (*endpoint, options);
135+ } else if (const char * s3_endpoint_env = std::getenv (" AWS_ENDPOINT_URL_S3" );
136+ s3_endpoint_env != nullptr ) {
137+ options.endpoint_override = SplitEndpointScheme (s3_endpoint_env, options);
138+ } else if (const char * endpoint_env = std::getenv (" AWS_ENDPOINT_URL" );
139+ endpoint_env != nullptr ) {
140+ options.endpoint_override = SplitEndpointScheme (endpoint_env, options);
123141 }
124142
125143 ICEBERG_ASSIGN_OR_RAISE (const auto path_style_access,
@@ -128,11 +146,11 @@ Result<::arrow::fs::S3Options> ConfigureS3Options(
128146 options.force_virtual_addressing = !*path_style_access;
129147 }
130148
131- // Configure SSL
149+ // Explicit `s3.ssl.enabled` overrides any endpoint-derived scheme.
132150 ICEBERG_ASSIGN_OR_RAISE (const auto ssl_enabled,
133151 ParseOptionalBool (properties, S3Properties::kSslEnabled ));
134- if (ssl_enabled.has_value () && !*ssl_enabled ) {
135- options.scheme = " http" ;
152+ if (ssl_enabled.has_value ()) {
153+ options.scheme = *ssl_enabled ? " https " : " http" ;
136154 }
137155
138156 // Configure timeouts
@@ -152,33 +170,160 @@ Result<::arrow::fs::S3Options> ConfigureS3Options(
152170
153171 return options;
154172}
155- #endif
156173
157- } // namespace
174+ namespace {
158175
159- Result<std::unique_ptr<FileIO >> MakeS3FileIO (
176+ Result<std::shared_ptr<::arrow::fs::FileSystem >> BuildArrowS3FileSystem (
160177 const std::unordered_map<std::string, std::string>& properties) {
161- #if ICEBERG_S3_ENABLED
162178 ICEBERG_RETURN_UNEXPECTED (EnsureS3Initialized ());
163-
164- // Configure S3 options from properties (uses default credentials if empty)
165179 ICEBERG_ASSIGN_OR_RAISE (auto options, ConfigureS3Options (properties));
166180 ICEBERG_ARROW_ASSIGN_OR_RETURN (auto fs, ::arrow::fs::S3FileSystem::Make (options));
181+ return std::shared_ptr<::arrow::fs::FileSystem>(std::move (fs));
182+ }
167183
168- return std::make_unique<ArrowFileSystemFileIO>(std::move (fs));
169- #else
170- return NotSupported (" Arrow S3 support is not enabled" );
171- #endif
184+ std::string CanonicalizeS3Scheme (std::string_view location) {
185+ for (std::string_view scheme : {" s3a://" , " s3n://" , " oss://" }) {
186+ if (location.starts_with (scheme)) {
187+ return std::string (" s3://" ).append (location.substr (scheme.size ()));
188+ }
189+ }
190+ return std::string (location);
191+ }
192+
193+ class ArrowS3FileIO final : public FileIO, public SupportsStorageCredentials {
194+ public:
195+ ArrowS3FileIO (std::shared_ptr<::arrow::fs::FileSystem> arrow_fs,
196+ std::unordered_map<std::string, std::string> default_properties)
197+ : default_file_io_(std::move(arrow_fs)),
198+ default_properties_ (std::move(default_properties)) {}
199+
200+ Result<std::unique_ptr<InputFile>> NewInputFile (std::string file_location) override ;
201+
202+ Result<std::unique_ptr<InputFile>> NewInputFile (std::string file_location,
203+ size_t length) override ;
204+
205+ Result<std::unique_ptr<OutputFile>> NewOutputFile (std::string file_location) override ;
206+
207+ Status DeleteFile (const std::string& file_location) override ;
208+
209+ Status DeleteFiles (const std::vector<std::string>& file_locations) override ;
210+
211+ Status SetStorageCredentials (
212+ const std::vector<StorageCredential>& storage_credentials) override ;
213+
214+ const std::vector<StorageCredential>& credentials () const override {
215+ return storage_credentials_;
216+ }
217+
218+ SupportsStorageCredentials* AsSupportsStorageCredentials () override { return this ; }
219+
220+ private:
221+ ArrowFileSystemFileIO& FileIOForPath (std::string_view location);
222+
223+ ArrowFileSystemFileIO default_file_io_;
224+ std::unordered_map<std::string, std::string> default_properties_;
225+ std::vector<StorageCredential> storage_credentials_;
226+ std::vector<std::pair<std::string, std::unique_ptr<ArrowFileSystemFileIO>>>
227+ file_io_by_prefix_;
228+ };
229+
230+ Status ArrowS3FileIO::SetStorageCredentials (
231+ const std::vector<StorageCredential>& storage_credentials) {
232+ std::vector<std::pair<std::string, std::unique_ptr<ArrowFileSystemFileIO>>>
233+ file_io_by_prefix;
234+ file_io_by_prefix.reserve (storage_credentials.size ());
235+ // TODO(gangwu): Refresh vended credentials via credentials.uri before tokens expire.
236+ for (const auto & credential : storage_credentials) {
237+ ICEBERG_RETURN_UNEXPECTED (credential.Validate ());
238+ if (!IsS3FileIOCredentialPrefix (credential.prefix )) {
239+ return NotSupported (
240+ " Storage credential prefix '{}' is unsupported by Arrow S3 FileIO" ,
241+ credential.prefix );
242+ }
243+ auto properties = default_properties_;
244+ for (const auto & [key, value] : credential.config ) {
245+ properties[key] = value;
246+ }
247+ ICEBERG_ASSIGN_OR_RAISE (auto fs, BuildArrowS3FileSystem (properties));
248+ file_io_by_prefix.emplace_back (
249+ CanonicalizeS3Scheme (credential.prefix ),
250+ std::make_unique<ArrowFileSystemFileIO>(std::move (fs)));
251+ }
252+ file_io_by_prefix_ = std::move (file_io_by_prefix);
253+ storage_credentials_ = storage_credentials;
254+ return {};
255+ }
256+
257+ ArrowFileSystemFileIO& ArrowS3FileIO::FileIOForPath (std::string_view location) {
258+ if (file_io_by_prefix_.empty ()) {
259+ return default_file_io_;
260+ }
261+ const std::string canonical = CanonicalizeS3Scheme (location);
262+ ArrowFileSystemFileIO* best = &default_file_io_;
263+ size_t best_len = 0 ;
264+ for (const auto & [prefix, file_io] : file_io_by_prefix_) {
265+ if (prefix.size () > best_len && canonical.starts_with (prefix)) {
266+ best = file_io.get ();
267+ best_len = prefix.size ();
268+ }
269+ }
270+ return *best;
271+ }
272+
273+ Result<std::unique_ptr<InputFile>> ArrowS3FileIO::NewInputFile (
274+ std::string file_location) {
275+ return FileIOForPath (file_location).NewInputFile (std::move (file_location));
276+ }
277+
278+ Result<std::unique_ptr<InputFile>> ArrowS3FileIO::NewInputFile (std::string file_location,
279+ size_t length) {
280+ return FileIOForPath (file_location).NewInputFile (std::move (file_location), length);
281+ }
282+
283+ Result<std::unique_ptr<OutputFile>> ArrowS3FileIO::NewOutputFile (
284+ std::string file_location) {
285+ return FileIOForPath (file_location).NewOutputFile (std::move (file_location));
286+ }
287+
288+ Status ArrowS3FileIO::DeleteFile (const std::string& file_location) {
289+ return FileIOForPath (file_location).DeleteFile (file_location);
290+ }
291+
292+ Status ArrowS3FileIO::DeleteFiles (const std::vector<std::string>& file_locations) {
293+ std::unordered_map<ArrowFileSystemFileIO*, std::vector<std::string>> locations_by_io;
294+ for (const auto & file_location : file_locations) {
295+ locations_by_io[&FileIOForPath (file_location)].push_back (file_location);
296+ }
297+ for (auto & [file_io, locations] : locations_by_io) {
298+ ICEBERG_RETURN_UNEXPECTED (file_io->DeleteFiles (locations));
299+ }
300+ return {};
301+ }
302+
303+ } // namespace
304+
305+ Result<std::unique_ptr<FileIO>> MakeS3FileIO (
306+ const std::unordered_map<std::string, std::string>& properties) {
307+ // Uses default credentials if properties are empty.
308+ ICEBERG_ASSIGN_OR_RAISE (auto fs, BuildArrowS3FileSystem (properties));
309+ return std::make_unique<ArrowS3FileIO>(std::move (fs), properties);
172310}
173311
174312Status FinalizeS3 () {
175- #if ICEBERG_S3_ENABLED
176313 auto status = ::arrow::fs::FinalizeS3 ();
177314 ICEBERG_ARROW_RETURN_NOT_OK (status);
178315 return {};
316+ }
317+
179318#else
319+
320+ Result<std::unique_ptr<FileIO>> MakeS3FileIO (
321+ [[maybe_unused]] const std::unordered_map<std::string, std::string>& properties) {
180322 return NotSupported (" Arrow S3 support is not enabled" );
181- #endif
182323}
183324
325+ Status FinalizeS3 () { return NotSupported (" Arrow S3 support is not enabled" ); }
326+
327+ #endif
328+
184329} // namespace iceberg::arrow
0 commit comments