Skip to content

…Create nonexistent parent directories in AsyncResponseTransformer#toF #3018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/bugfix-AWSSDKforJavav2-5fdaf2b.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"category": "AWS SDK for Java v2",
"contributor": "",
"type": "bugfix",
"description": "`AsyncResponseTransformer#toFile` and `ResponseTransformer#toFile` will now create all nonexistent parent directories if they don't exist instead of throwing NoSuchFileException`"
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public interface AsyncResponseTransformer<ResponseT, ResultT> {
/**
* Creates an {@link AsyncResponseTransformer} that writes all the content to the given file. In the event of an error,
* the SDK will attempt to delete the file (whatever has been written to it so far). If the file already exists, an
* exception will be thrown.
* exception will be thrown. All nonexistent parent directories will be created.
*
* @param path Path to file to write to.
* @param <ResponseT> Pojo Response type.
Expand All @@ -124,7 +124,7 @@ static <ResponseT> AsyncResponseTransformer<ResponseT, ResponseT> toFile(Path pa
/**
* Creates an {@link AsyncResponseTransformer} that writes all the content to the given file. In the event of an error,
* the SDK will attempt to delete the file (whatever has been written to it so far). If the file already exists, an
* exception will be thrown.
* exception will be thrown. All nonexistent parent directories will be created.
*
* @param file File to write to.
* @param <ResponseT> Pojo Response type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
import software.amazon.awssdk.core.async.SdkPublisher;
import software.amazon.awssdk.core.exception.SdkClientException;

/**
* {@link AsyncResponseTransformer} that writes the data to the specified file.
Expand Down Expand Up @@ -71,9 +72,26 @@ public void onResponse(ResponseT response) {

@Override
public void onStream(SdkPublisher<ByteBuffer> publisher) {
// onStream may be called multiple times so reset the file channel every time
this.fileChannel = invokeSafely(() -> createChannel(path));
publisher.subscribe(new FileSubscriber(this.fileChannel, path, cf, this::exceptionOccurred));
try {
createParentDirectoriesIfNeeded(path);
Copy link
Contributor Author

@zoewangg zoewangg Feb 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably have to clean up directories if it fails. Will update the PR

Copy link
Contributor

@Bennett-Lynch Bennett-Lynch Feb 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like it would be non-trivial to determine how many levels of parent directories were created and thus need to be cleaned up?

E.g., if /x/y/z/ exists but we try to create file /x/y/z/a/b/c/file, we would presumably only want to delete /a/b/c.

// onStream may be called multiple times so reset the file channel every time
this.fileChannel = invokeSafely(() -> createChannel(path));
publisher.subscribe(new FileSubscriber(this.fileChannel, path, cf, this::exceptionOccurred));
} catch (Throwable exception) {
exceptionOccurred(exception);
}

}

private static void createParentDirectoriesIfNeeded(Path destinationPath) {
Path parentDirectory = destinationPath.getParent();
try {
if (parentDirectory != null) {
Files.createDirectories(parentDirectory);
}
} catch (IOException e) {
throw SdkClientException.create("Failed to create parent directories for " + destinationPath, e);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ default boolean needsConnectionLeftOpen() {

/**
* Creates a response transformer that writes all response content to the specified file. If the file already exists
* then a {@link java.nio.file.FileAlreadyExistsException} will be thrown.
* then a {@link java.nio.file.FileAlreadyExistsException} will be thrown. All nonexistent parent directories will be created.
*
* @param path Path to file to write to.
* @param <ResponseT> Type of unmarshalled response POJO.
Expand All @@ -103,6 +103,11 @@ static <ResponseT> ResponseTransformer<ResponseT, ResponseT> toFile(Path path) {
return (resp, in) -> {
try {
InterruptMonitor.checkInterrupted();
Path parentDirectory = path.getParent();
if (parentDirectory != null) {
Files.createDirectories(parentDirectory);
}

Files.copy(in, path);
return resp;
} catch (IOException copyException) {
Expand Down Expand Up @@ -135,7 +140,7 @@ static <ResponseT> ResponseTransformer<ResponseT, ResponseT> toFile(Path path) {

/**
* Creates a response transformer that writes all response content to the specified file. If the file already exists
* then a {@link java.nio.file.FileAlreadyExistsException} will be thrown.
* then a {@link java.nio.file.FileAlreadyExistsException} will be thrown. All nonexistent parent directories will be created.
*
* @param file File to write to.
* @param <ResponseT> Type of unmarshalled response POJO.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ public void cancel() {
assertThat(prepareFuture.isCompletedExceptionally()).isTrue();
}

@Test
public void parentDirectoryNotExists_shouldCreate() {
Path testPath = testFs.getPath("test/test_file.txt");
FileAsyncResponseTransformer xformer = new FileAsyncResponseTransformer(testPath);

CompletableFuture prepareFuture = xformer.prepare();

xformer.onResponse(new Object());
xformer.onStream(new TestPublisher());

assertThat(prepareFuture.isCompletedExceptionally()).isFalse();
}

@Test
public void synchronousPublisher_shouldNotHang() throws Exception {
List<CompletableFuture> futures = new ArrayList<>();
Expand Down
5 changes: 5 additions & 0 deletions services/s3/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,10 @@
<artifactId>eventstream</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.jimfs</groupId>
<artifactId>jimfs</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@
import static org.junit.Assert.assertEquals;
import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName;

import com.google.common.jimfs.Jimfs;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import org.junit.AfterClass;
import org.junit.BeforeClass;
Expand All @@ -45,7 +48,7 @@
import software.amazon.awssdk.utils.ImmutableMap;

public class GetObjectAsyncIntegrationTest extends S3IntegrationTestBase {

private static FileSystem testFs;
private static final String BUCKET = temporaryBucketName(GetObjectAsyncIntegrationTest.class);

private static final String KEY = "some-key";
Expand All @@ -60,6 +63,7 @@ public class GetObjectAsyncIntegrationTest extends S3IntegrationTestBase {
@BeforeClass
public static void setupFixture() throws IOException {
createBucket(BUCKET);
testFs = Jimfs.newFileSystem();
file = new RandomTempFile(10_000);
s3Async.putObject(PutObjectRequest.builder()
.bucket(BUCKET)
Expand All @@ -70,9 +74,10 @@ public static void setupFixture() throws IOException {
}

@AfterClass
public static void tearDownFixture() {
public static void tearDownFixture() throws IOException {
deleteBucketAndAllContents(BUCKET);
file.delete();
testFs.close();
}

@Test
Expand All @@ -87,6 +92,13 @@ public void toFile() throws Exception {
}
}

@Test
public void toFile_parentDirectoriesDoNotExist_shouldSucceed() throws Exception {
Path path = testFs.getPath(UUID.randomUUID().toString(), "test1", "test2", "test.txt");
s3Async.getObject(getObjectRequest, path).join();
assertThat(path).hasSameBinaryContentAs(file.toPath());
}

@Test
public void dumpToString() throws IOException {
String returned = s3Async.getObject(getObjectRequest, AsyncResponseTransformer.toBytes()).join().asUtf8String();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@
import static org.junit.Assert.assertEquals;
import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName;

import com.google.common.jimfs.Jimfs;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.util.Properties;
import java.util.UUID;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
Expand All @@ -39,7 +42,7 @@
import software.amazon.awssdk.testutils.RandomTempFile;

public class GetObjectIntegrationTest extends S3IntegrationTestBase {

private static FileSystem testFs;
private static final String BUCKET = temporaryBucketName(GetObjectIntegrationTest.class);

private static final String KEY = "some-key";
Expand All @@ -55,6 +58,7 @@ public class GetObjectIntegrationTest extends S3IntegrationTestBase {
@BeforeClass
public static void setupFixture() throws IOException {
createBucket(BUCKET);
testFs = Jimfs.newFileSystem();
file = new RandomTempFile(10_000);
s3.putObject(PutObjectRequest.builder()
.bucket(BUCKET)
Expand All @@ -63,9 +67,10 @@ public static void setupFixture() throws IOException {
}

@AfterClass
public static void tearDownFixture() {
public static void tearDownFixture() throws IOException {
deleteBucketAndAllContents(BUCKET);
file.delete();
testFs.close();
}

@Test
Expand Down Expand Up @@ -98,6 +103,13 @@ public void toFile() throws Exception {
}
}

@Test
public void toFile_parentDirectoriesDoNotExist_shouldSucceed() throws Exception {
Path path = testFs.getPath(UUID.randomUUID().toString(), "test1", "test2", "test.txt");
s3.getObject(getObjectRequest, path);
assertThat(path).hasSameBinaryContentAs(file.toPath());
}

@Test
public void toOutputStream() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Expand Down