Skip to content

Commit 7a80bb4

Browse files
authored
[FLINK-39115][s3] Support URI Handling Utility
1 parent 4eb8666 commit 7a80bb4

7 files changed

Lines changed: 307 additions & 191 deletions

File tree

flink-filesystems/flink-s3-fs-native/src/main/java/org/apache/flink/fs/s3native/NativeS3BulkCopyHelper.java

Lines changed: 4 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,6 @@
7878
* atomically moved into place after the stream has been fully copied. Cancellation through the
7979
* provided {@link ICloseableRegistry} aborts active S3 response streams, cancels pending futures,
8080
* stops the worker pool, and deletes incomplete temporary files.
81-
*
82-
* <p><b>TODO:</b> Consider extracting URI parsing logic to a shared S3UriUtils utility class to
83-
* consolidate S3 URI handling across the codebase.
8481
*/
8582
@Internal
8683
class NativeS3BulkCopyHelper {
@@ -185,8 +182,8 @@ public void copyFiles(
185182
for (int i = 0; i < requests.size(); i++) {
186183
PathsCopyingFileSystem.CopyRequest request = requests.get(i);
187184
String sourceUri = request.getSource().toUri().toString();
188-
if (isSupportedS3Scheme(request.getSource())
189-
&& isSupportedLocalScheme(request.getDestination())) {
185+
if (S3UriUtils.isSupportedS3Scheme(request.getSource())
186+
&& S3UriUtils.isSupportedLocalScheme(request.getDestination())) {
190187
copyFutures.add(copyS3ToLocal(request, downloadPool, cancellation));
191188
} else {
192189
throw new UnsupportedOperationException(
@@ -237,8 +234,8 @@ private CompletableFuture<Void> copyS3ToLocal(
237234
throws IOException {
238235

239236
String sourceUri = request.getSource().toUri().toString();
240-
String bucket = extractBucket(sourceUri);
241-
String key = extractKey(sourceUri);
237+
String bucket = S3UriUtils.extractBucketName(sourceUri);
238+
String key = S3UriUtils.extractKey(sourceUri);
242239
Path destination = new File(request.getDestination().getPath()).toPath().toAbsolutePath();
243240

244241
Path parent = destination.getParent();
@@ -388,16 +385,6 @@ private void waitForCopies(List<CompletableFuture<Void>> futures) throws IOExcep
388385
}
389386
}
390387

391-
static boolean isSupportedS3Scheme(org.apache.flink.core.fs.Path path) {
392-
String scheme = path.toUri().getScheme();
393-
return "s3".equalsIgnoreCase(scheme) || "s3a".equalsIgnoreCase(scheme);
394-
}
395-
396-
static boolean isSupportedLocalScheme(org.apache.flink.core.fs.Path path) {
397-
String scheme = path.toUri().getScheme();
398-
return scheme == null || "file".equalsIgnoreCase(scheme);
399-
}
400-
401388
private static void abortAndClose(ResponseInputStream<GetObjectResponse> stream) {
402389
try {
403390
stream.abort();
@@ -502,40 +489,4 @@ static boolean isConnectionPoolExhausted(Throwable throwable) {
502489
}
503490
return false;
504491
}
505-
506-
/**
507-
* Extracts the bucket name from an S3 URI.
508-
*
509-
* <p>Supports both s3:// and s3a:// schemes (s3a is normalized to s3).
510-
*
511-
* @param s3Uri the S3 URI
512-
* @return the bucket name
513-
*/
514-
String extractBucket(String s3Uri) {
515-
String uri = s3Uri.replaceFirst("s3a://", "s3://");
516-
int bucketStart = uri.indexOf("://") + 3;
517-
int bucketEnd = uri.indexOf("/", bucketStart);
518-
if (bucketEnd == -1) {
519-
return uri.substring(bucketStart);
520-
}
521-
return uri.substring(bucketStart, bucketEnd);
522-
}
523-
524-
/**
525-
* Extracts the object key from an S3 URI.
526-
*
527-
* <p>Supports both s3:// and s3a:// schemes (s3a is normalized to s3).
528-
*
529-
* @param s3Uri the S3 URI
530-
* @return the object key (empty string if no key in URI)
531-
*/
532-
String extractKey(String s3Uri) {
533-
String uri = s3Uri.replaceFirst("s3a://", "s3://");
534-
int bucketStart = uri.indexOf("://") + 3;
535-
int keyStart = uri.indexOf("/", bucketStart);
536-
if (keyStart == -1) {
537-
return "";
538-
}
539-
return uri.substring(keyStart + 1);
540-
}
541492
}

flink-filesystems/flink-s3-fs-native/src/main/java/org/apache/flink/fs/s3native/NativeS3FileSystem.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ public Path getHomeDirectory() {
205205
@Override
206206
public FileStatus getFileStatus(Path path) throws IOException {
207207
checkNotClosed();
208-
final String key = NativeS3ObjectOperations.extractKey(path);
208+
final String key = S3UriUtils.extractKey(path);
209209
final S3Client s3Client = clientProvider.getS3Client();
210210

211211
LOG.debug("Getting file status for s3://{}/{}", bucketName, key);
@@ -304,7 +304,7 @@ public BlockLocation[] getFileBlockLocations(FileStatus file, long start, long l
304304
@Override
305305
public FSDataInputStream open(Path path, int bufferSize) throws IOException {
306306
checkNotClosed();
307-
final String key = NativeS3ObjectOperations.extractKey(path);
307+
final String key = S3UriUtils.extractKey(path);
308308
final S3Client s3Client = clientProvider.getS3Client();
309309
final long fileSize = getFileStatus(path).getLen();
310310
return new NativeS3InputStream(s3Client, bucketName, key, fileSize, bufferSize);
@@ -313,7 +313,7 @@ public FSDataInputStream open(Path path, int bufferSize) throws IOException {
313313
@Override
314314
public FSDataInputStream open(Path path) throws IOException {
315315
checkNotClosed();
316-
final String key = NativeS3ObjectOperations.extractKey(path);
316+
final String key = S3UriUtils.extractKey(path);
317317
final S3Client s3Client = clientProvider.getS3Client();
318318
final long fileSize = getFileStatus(path).getLen();
319319
return new NativeS3InputStream(s3Client, bucketName, key, fileSize, readBufferSize);
@@ -337,7 +337,7 @@ public FSDataInputStream open(Path path) throws IOException {
337337
@Override
338338
public FileStatus[] listStatus(Path path) throws IOException {
339339
checkNotClosed();
340-
String key = NativeS3ObjectOperations.extractKey(path);
340+
String key = S3UriUtils.extractKey(path);
341341
if (!key.isEmpty() && !key.endsWith("/")) {
342342
key = key + "/";
343343
}
@@ -384,7 +384,7 @@ public FileStatus[] listStatus(Path path) throws IOException {
384384
@Override
385385
public boolean delete(Path path, boolean recursive) throws IOException {
386386
checkNotClosed();
387-
final String key = NativeS3ObjectOperations.extractKey(path);
387+
final String key = S3UriUtils.extractKey(path);
388388
final S3Client s3Client = clientProvider.getS3Client();
389389

390390
try {
@@ -452,7 +452,7 @@ public FSDataOutputStream create(Path path, WriteMode overwriteMode) throws IOEx
452452
}
453453
}
454454

455-
final String key = NativeS3ObjectOperations.extractKey(path);
455+
final String key = S3UriUtils.extractKey(path);
456456
return new NativeS3OutputStream(
457457
clientProvider.getS3Client(),
458458
bucketName,
@@ -471,8 +471,8 @@ public FSDataOutputStream create(Path path, WriteMode overwriteMode) throws IOEx
471471
@Override
472472
public boolean rename(Path src, Path dst) throws IOException {
473473
checkNotClosed();
474-
final String srcKey = NativeS3ObjectOperations.extractKey(src);
475-
final String dstKey = NativeS3ObjectOperations.extractKey(dst);
474+
final String srcKey = S3UriUtils.extractKey(src);
475+
final String dstKey = S3UriUtils.extractKey(dst);
476476
final S3Client s3Client = clientProvider.getS3Client();
477477

478478
final FileStatus srcStatus = getFileStatus(src);
@@ -519,8 +519,8 @@ public String generateEntropy() {
519519
@Override
520520
public boolean canCopyPaths(Path source, Path destination) {
521521
return bulkCopyHelper != null
522-
&& NativeS3BulkCopyHelper.isSupportedS3Scheme(source)
523-
&& NativeS3BulkCopyHelper.isSupportedLocalScheme(destination);
522+
&& S3UriUtils.isSupportedS3Scheme(source)
523+
&& S3UriUtils.isSupportedLocalScheme(destination);
524524
}
525525

526526
@Override
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.flink.fs.s3native;
20+
21+
import org.apache.flink.annotation.Internal;
22+
import org.apache.flink.core.fs.Path;
23+
24+
import static org.apache.flink.util.Preconditions.checkArgument;
25+
import static org.apache.flink.util.Preconditions.checkNotNull;
26+
27+
/** Shared S3 URI handling utilities. */
28+
@Internal
29+
public final class S3UriUtils {
30+
31+
private S3UriUtils() {}
32+
33+
private static final String S3 = "s3://";
34+
private static final String S3A = "s3a://";
35+
36+
/** Extracts the S3 object key from a Flink {@link Path}. */
37+
public static String extractKey(Path path) {
38+
String pathStr = path.toUri().getPath();
39+
if (pathStr.startsWith("/")) {
40+
pathStr = pathStr.substring(1);
41+
}
42+
return pathStr;
43+
}
44+
45+
/** Extracts the S3 bucket name from a Flink {@link Path}. */
46+
public static String extractBucketName(Path path) {
47+
return path.toUri().getHost();
48+
}
49+
50+
/**
51+
* Extracts the bucket name from a raw S3 URI string.
52+
*
53+
* @throws IllegalArgumentException if the scheme is not s3:// or s3a://
54+
*/
55+
public static String extractBucketName(String s3Uri) {
56+
String uri = requireSupportedScheme(s3Uri);
57+
int bucketEnd = uri.indexOf('/', S3.length());
58+
return bucketEnd == -1 ? uri.substring(S3.length()) : uri.substring(S3.length(), bucketEnd);
59+
}
60+
61+
/**
62+
* Extracts the S3 object key from a raw S3 URI string, or an empty string if the URI addresses
63+
* a bucket only.
64+
*
65+
* @throws IllegalArgumentException if the scheme is not s3:// or s3a://
66+
*/
67+
public static String extractKey(String s3Uri) {
68+
String uri = requireSupportedScheme(s3Uri);
69+
int keyStart = uri.indexOf('/', S3.length());
70+
return keyStart == -1 ? "" : uri.substring(keyStart + 1);
71+
}
72+
73+
/** Returns whether the path uses a supported S3 scheme ({@code s3} or {@code s3a}). */
74+
public static boolean isSupportedS3Scheme(Path path) {
75+
String scheme = path.toUri().getScheme();
76+
return "s3".equalsIgnoreCase(scheme) || "s3a".equalsIgnoreCase(scheme);
77+
}
78+
79+
/** Returns whether the path addresses the local file system ({@code file} or no scheme). */
80+
public static boolean isSupportedLocalScheme(Path path) {
81+
String scheme = path.toUri().getScheme();
82+
return scheme == null || "file".equalsIgnoreCase(scheme);
83+
}
84+
85+
private static String requireSupportedScheme(String s3Uri) {
86+
checkNotNull(s3Uri, "s3Uri must not be null");
87+
String uri = startsWithIgnoreCase(s3Uri, S3A) ? S3 + s3Uri.substring(S3A.length()) : s3Uri;
88+
checkArgument(
89+
startsWithIgnoreCase(uri, S3),
90+
"Unsupported S3 URI (expected s3:// or s3a:// scheme): %s",
91+
s3Uri);
92+
return uri;
93+
}
94+
95+
private static boolean startsWithIgnoreCase(String value, String prefix) {
96+
return value.regionMatches(true, 0, prefix, 0, prefix.length());
97+
}
98+
}

flink-filesystems/flink-s3-fs-native/src/main/java/org/apache/flink/fs/s3native/writer/NativeS3ObjectOperations.java

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
package org.apache.flink.fs.s3native.writer;
2020

2121
import org.apache.flink.annotation.Internal;
22-
import org.apache.flink.core.fs.Path;
2322
import org.apache.flink.fs.s3native.NativeS3FileIoUtils;
2423
import org.apache.flink.fs.s3native.S3EncryptionConfig;
2524
import org.apache.flink.fs.s3native.S3ExceptionUtils;
@@ -89,11 +88,6 @@
8988
* <li>SSE-C (customer-provided keys) via a KeyProvider interface
9089
* <li>Client-side encryption via an EncryptionHandler interface
9190
* </ul>
92-
*
93-
* <p><b>S3 URI Handling:</b> The {@link #extractKey(Path)} and {@link #extractBucketName(Path)}
94-
* methods expect URIs in the standard {@code s3://bucket/key} format. Other formats like path-style
95-
* ({@code https://s3.amazonaws.com/bucket/key}) or virtual-hosted-style ({@code
96-
* https://bucket.s3.amazonaws.com/key}) are not currently supported.
9791
*/
9892
@Internal
9993
public class NativeS3ObjectOperations {
@@ -454,44 +448,6 @@ public String getBucketName() {
454448
return bucketName;
455449
}
456450

457-
/**
458-
* Extracts the S3 object key from a Flink Path.
459-
*
460-
* <p>Expected URI format: {@code s3://bucket-name/path/to/object}
461-
*
462-
* <p><b>Limitations:</b> This method only supports the standard S3 URI format. Other URI
463-
* formats are NOT supported:
464-
*
465-
* <ul>
466-
* <li>{@code https://bucket.s3.amazonaws.com/path/to/object} (virtual-hosted style)
467-
* <li>{@code https://s3.amazonaws.com/bucket/path/to/object} (path style)
468-
* <li>{@code s3a://} or {@code s3n://} schemes (Hadoop-specific)
469-
* </ul>
470-
*
471-
* @param path the Flink Path with s3:// scheme
472-
* @return the object key (path portion without leading slash)
473-
*/
474-
public static String extractKey(Path path) {
475-
String pathStr = path.toUri().getPath();
476-
if (pathStr.startsWith("/")) {
477-
pathStr = pathStr.substring(1);
478-
}
479-
return pathStr;
480-
}
481-
482-
/**
483-
* Extracts the S3 bucket name from a Flink Path.
484-
*
485-
* <p>Expected URI format: {@code s3://bucket-name/path/to/object}
486-
*
487-
* @param path the Flink Path with s3:// scheme
488-
* @return the bucket name (host portion of the URI)
489-
* @see #extractKey(Path) for URI format limitations
490-
*/
491-
public static String extractBucketName(Path path) {
492-
return path.toUri().getHost();
493-
}
494-
495451
public static class UploadPartResult {
496452
private final int partNumber;
497453
private final String eTag;

flink-filesystems/flink-s3-fs-native/src/main/java/org/apache/flink/fs/s3native/writer/NativeS3RecoverableWriter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.apache.flink.core.fs.RecoverableFsDataOutputStream;
2323
import org.apache.flink.core.fs.RecoverableWriter;
2424
import org.apache.flink.core.io.SimpleVersionedSerializer;
25+
import org.apache.flink.fs.s3native.S3UriUtils;
2526

2627
import org.slf4j.Logger;
2728
import org.slf4j.LoggerFactory;
@@ -64,7 +65,7 @@ private NativeS3RecoverableWriter(
6465
@Override
6566
public RecoverableFsDataOutputStream open(Path path) throws IOException {
6667
checkNotClosed();
67-
String key = NativeS3ObjectOperations.extractKey(path);
68+
String key = S3UriUtils.extractKey(path);
6869
LOG.debug("Opening recoverable stream for key: {}", key);
6970

7071
String uploadId = s3AccessHelper.startMultiPartUpload(key);

0 commit comments

Comments
 (0)