diff --git a/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProvider.java b/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProvider.java index f65549b69fec..cc6ba9a12495 100644 --- a/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProvider.java +++ b/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProvider.java @@ -48,6 +48,7 @@ import java.nio.file.DirectoryStream.Filter; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileStore; +import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.OpenOption; @@ -351,6 +352,15 @@ public boolean deleteIfExists(Path path) throws IOException { initStorage(); CloudStoragePath cloudPath = CloudStorageUtil.checkPath(path); if (cloudPath.seemsLikeADirectoryAndUsePseudoDirectories()) { + // if the "folder" is empty then we're fine, otherwise complain + // that we cannot act on folders. + try (DirectoryStream paths = Files.newDirectoryStream(path)) { + if (!paths.iterator().hasNext()) { + // "folder" isn't actually there in the first place, so: success! + // (we must return true so delete doesn't freak out) + return true; + } + } throw new CloudStoragePseudoDirectoryException(cloudPath); } return storage.delete(cloudPath.getBlobId()); diff --git a/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProviderTest.java b/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProviderTest.java index 970d60217e57..1fcf4487d95a 100644 --- a/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProviderTest.java +++ b/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProviderTest.java @@ -413,7 +413,6 @@ public void testDelete_dotDirNotNormalized_throwsIae() throws IOException { @Test public void testDelete_trailingSlash() throws IOException { - thrown.expect(CloudStoragePseudoDirectoryException.class); Files.delete(Paths.get(URI.create("gs://love/passion/"))); } @@ -442,7 +441,6 @@ public void testDeleteIfExists() throws IOException { @Test public void testDeleteIfExists_trailingSlash() throws IOException { - thrown.expect(CloudStoragePseudoDirectoryException.class); Files.deleteIfExists(Paths.get(URI.create("gs://love/passion/"))); } diff --git a/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemTest.java b/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemTest.java index d7d5b346c376..51607b6b7937 100644 --- a/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemTest.java +++ b/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemTest.java @@ -24,7 +24,9 @@ import com.google.common.testing.NullPointerTester; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -38,6 +40,9 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.FileVisitResult; +import java.nio.file.attribute.BasicFileAttributes; /** * Unit tests for {@link CloudStorageFileSystem}. @@ -55,6 +60,9 @@ public class CloudStorageFileSystemTest { + "The Heart-ache, and the thousand Natural shocks\n" + "That Flesh is heir to? 'Tis a consummation\n"; + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Before public void before() { CloudStorageFileSystemProvider.setStorageOptions(LocalStorageHelper.getOptions()); @@ -180,6 +188,117 @@ public void testMatcher() throws IOException { } } + @Test + public void testDeleteEmptyFolder() throws IOException { + try (FileSystem fs = CloudStorageFileSystem.forBucket("bucket")) { + List paths = new ArrayList<>(); + paths.add(fs.getPath("dir/angel")); + paths.add(fs.getPath("dir/dir2/another_angel")); + paths.add(fs.getPath("atroot")); + for (Path path : paths) { + Files.write(path, ALONE.getBytes(UTF_8)); + } + // we can delete non-existent folders, because they are not represented on disk anyways. + Files.delete(fs.getPath("ghost/")); + Files.delete(fs.getPath("dir/ghost/")); + Files.delete(fs.getPath("dir/dir2/ghost/")); + // likewise, deleteIfExists works. + Files.deleteIfExists(fs.getPath("ghost/")); + Files.deleteIfExists(fs.getPath("dir/ghost/")); + Files.deleteIfExists(fs.getPath("dir/dir2/ghost/")); + } + } + + @Test + public void testDeleteFullFolder() throws IOException { + thrown.expect(CloudStoragePseudoDirectoryException.class); + try (FileSystem fs = CloudStorageFileSystem.forBucket("bucket")) { + Files.write(fs.getPath("dir/angel"), ALONE.getBytes(UTF_8)); + // we cannot delete existing folders if they contain something + Files.delete(fs.getPath("dir/")); + } + } + + @Test + public void testDelete() throws IOException { + try (FileSystem fs = CloudStorageFileSystem.forBucket("bucket")) { + List paths = new ArrayList<>(); + paths.add(fs.getPath("dir/angel")); + paths.add(fs.getPath("dir/dir2/another_angel")); + paths.add(fs.getPath("atroot")); + for (Path path : paths) { + Files.write(path, ALONE.getBytes(UTF_8)); + } + Files.delete(fs.getPath("atroot")); + Files.delete(fs.getPath("dir/angel")); + Files.deleteIfExists(fs.getPath("dir/dir2/another_angel")); + + for (Path path : paths) { + assertThat(Files.exists(path)).isFalse(); + } + } + } + + @Test + public void testDeleteEmptiedFolder() throws IOException { + try (FileSystem fs = CloudStorageFileSystem.forBucket("bucket")) { + List paths = new ArrayList<>(); + paths.add(fs.getPath("dir/angel")); + paths.add(fs.getPath("dir/dir2/another_angel")); + for (Path path : paths) { + Files.write(path, ALONE.getBytes(UTF_8)); + } + Files.delete(fs.getPath("dir/angel")); + Files.deleteIfExists(fs.getPath("dir/dir2/another_angel")); + // delete folder (trailing slash is required) + Path dir2 = fs.getPath("dir/dir2/"); + Files.deleteIfExists(dir2); + Path dir = fs.getPath("dir/"); + Files.deleteIfExists(dir); + // We can't check Files.exists on a folder (since GCS fakes folders) + } + } + + @Test + public void testDeleteRecursive() throws IOException { + try (FileSystem fs = CloudStorageFileSystem.forBucket("bucket")) { + List paths = new ArrayList<>(); + paths.add(fs.getPath("atroot")); + paths.add(fs.getPath("dir/angel")); + paths.add(fs.getPath("dir/dir2/another_angel")); + paths.add(fs.getPath("dir/dir2/angel3")); + paths.add(fs.getPath("dir/dir3/cloud")); + for (Path path : paths) { + Files.write(path, ALONE.getBytes(UTF_8)); + } + + deleteRecursive(fs.getPath("dir/")); + assertThat(Files.exists(fs.getPath("dir/angel"))).isFalse(); + assertThat(Files.exists(fs.getPath("dir/dir3/cloud"))).isFalse(); + assertThat(Files.exists(fs.getPath("atroot"))).isTrue(); + } + } + + /** + * Delete the given directory and all of its contents if non-empty. + * @param directory the directory to delete + * @throws IOException + */ + private static void deleteRecursive(Path directory) throws IOException { + Files.walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } + private void assertMatches(FileSystem fs, PathMatcher matcher, String toMatch, boolean expected) { assertThat(matcher.matches(fs.getPath(toMatch).getFileName())).isEqualTo(expected); } diff --git a/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/it/ITGcsNio.java b/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/it/ITGcsNio.java index 602b2d7ff03f..7aba29c7cb37 100644 --- a/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/it/ITGcsNio.java +++ b/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/it/ITGcsNio.java @@ -44,10 +44,13 @@ import java.nio.channels.ReadableByteChannel; import java.nio.channels.SeekableByteChannel; import java.nio.file.FileSystem; +import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -354,6 +357,46 @@ public void testListFiles() throws IOException { } } + + @Test + public void testDeleteRecursive() throws IOException { + try (FileSystem fs = getTestBucket()) { + List paths = new ArrayList<>(); + paths.add(fs.getPath("Racine")); + paths.add(fs.getPath("playwrights/Moliere")); + paths.add(fs.getPath("playwrights/French/Corneille")); + for (Path path : paths) { + Files.write(path, FILE_CONTENTS, UTF_8); + } + deleteRecursive(fs.getPath("playwrights/")); + assertThat(Files.exists(fs.getPath("playwrights/Moliere"))).isFalse(); + assertThat(Files.exists(fs.getPath("playwrights/French/Corneille"))).isFalse(); + assertThat(Files.exists(fs.getPath("Racine"))).isTrue(); + Files.deleteIfExists(fs.getPath("Racine")); + assertThat(Files.exists(fs.getPath("Racine"))).isFalse(); + } + } + + /** + * Delete the given directory and all of its contents if non-empty. + * @param directory the directory to delete + * @throws IOException + */ + private static void deleteRecursive(Path directory) throws IOException { + Files.walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } + private int readFully(ReadableByteChannel chan, byte[] outputBuf) throws IOException { ByteBuffer buf = ByteBuffer.wrap(outputBuf); int sofar = 0;