diff --git a/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProvider.java b/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProvider.java index 4f22c9aeb89e..f1e35eed067f 100644 --- a/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProvider.java +++ b/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProvider.java @@ -353,7 +353,8 @@ private SeekableByteChannel newWriteChannel(Path path, Set throws IOException { initStorage(); CloudStoragePath cloudPath = CloudStorageUtil.checkPath(path); - if (cloudPath.seemsLikeADirectoryAndUsePseudoDirectories(null)) { + boolean allowSlash = options.contains(OptionAllowTrailingSlash.getInstance()); + if (!allowSlash && cloudPath.seemsLikeADirectoryAndUsePseudoDirectories(null)) { throw new CloudStoragePseudoDirectoryException(cloudPath); } BlobId file = cloudPath.getBlobId(); @@ -768,8 +769,16 @@ public A readAttributes( } CloudStorageObjectAttributes ret; ret = new CloudStorageObjectAttributes(blobInfo); - // if size is 0 it could be a folder - if (ret.size() == 0 && cloudPath.seemsLikeADirectoryAndUsePseudoDirectories(storage)) { + /* + There exists a file with this name, yes. But should we pretend it's a directory? + The web UI will allow the user to "create directories" by creating files + whose name ends in slash (and these files aren't always zero-size). + If we're set to use pseudo directories and the file name looks like a path, + then say it's a directory. We pass null to avoid trying to actually list files; + if the path doesn't end in "/" we'll truthfully say it's a file. Yes it may also be + a directory but we don't want to do a prefix search every time the user stats a file. + */ + if (cloudPath.seemsLikeADirectoryAndUsePseudoDirectories(null)) { @SuppressWarnings("unchecked") A result = (A) new CloudStoragePseudoDirectoryAttributes(cloudPath); return result; diff --git a/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageOptions.java b/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageOptions.java index db7d45be8397..2d0548a112e2 100644 --- a/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageOptions.java +++ b/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageOptions.java @@ -95,5 +95,13 @@ public static CloudStorageOption.OpenCopy withChannelReopen(int count) { return OptionMaxChannelReopens.create(count); } + /** + * Allows one to use trailing slashes in file names. You really shouldn't (this is here for tests + * only). + */ + static CloudStorageOption.Open allowTrailingSlash() { + return OptionAllowTrailingSlash.getInstance(); + } + private CloudStorageOptions() {} } diff --git a/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/OptionAllowTrailingSlash.java b/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/OptionAllowTrailingSlash.java new file mode 100644 index 000000000000..751222dd8cdb --- /dev/null +++ b/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/OptionAllowTrailingSlash.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.storage.contrib.nio; + +class OptionAllowTrailingSlash implements CloudStorageOption.Open { + + static OptionAllowTrailingSlash instance; + + private OptionAllowTrailingSlash() {}; + + public static synchronized OptionAllowTrailingSlash getInstance() { + if (null == instance) { + instance = new OptionAllowTrailingSlash(); + } + return instance; + } +} diff --git a/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProviderTest.java b/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProviderTest.java index 8056acd52b54..5b7401b881a9 100644 --- a/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProviderTest.java +++ b/google-cloud-clients/google-cloud-contrib/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProviderTest.java @@ -104,6 +104,13 @@ public void testSize_trailingSlash_returnsFakePseudoDirectorySize() throws Excep assertThat(Files.size(Paths.get(URI.create("gs://bucket/wat/")))).isEqualTo(1); } + @Test + public void test_trailingSlash_isFolder() throws Exception { + Path path = Paths.get(URI.create("gs://bucket/wat/")); + Files.write(path, SINGULARITY.getBytes(UTF_8), CloudStorageOptions.allowTrailingSlash()); + assertThat(Files.isDirectory(path)).isTrue(); + } + @Test public void testSize_trailingSlash_disablePseudoDirectories() throws Exception { try (CloudStorageFileSystem fs = forBucket("doodle", usePseudoDirectories(false))) { @@ -111,6 +118,7 @@ public void testSize_trailingSlash_disablePseudoDirectories() throws Exception { byte[] rapture = SINGULARITY.getBytes(UTF_8); Files.write(path, rapture); assertThat(Files.size(path)).isEqualTo(rapture.length); + Files.delete(path); } } @@ -119,6 +127,7 @@ public void testReadAllBytes() throws Exception { Path path = Paths.get(URI.create("gs://bucket/wat")); Files.write(path, SINGULARITY.getBytes(UTF_8)); assertThat(new String(Files.readAllBytes(path), UTF_8)).isEqualTo(SINGULARITY); + Files.delete(path); } @Test @@ -139,6 +148,7 @@ public void testNewByteChannelRead() throws Exception { buffer.rewind(); assertThat(input.read(buffer)).isEqualTo(-1); } + Files.delete(path); } @Test @@ -160,6 +170,7 @@ public void testNewByteChannelRead_seeking() throws Exception { assertThat(input.position()).isEqualTo(5); assertThat(new String(buffer.array(), UTF_8)).isEqualTo("hello"); } + Files.delete(path); } @Test @@ -667,6 +678,9 @@ public void testCopy_overwriteAttributes() throws Exception { attributes = Files.readAttributes(target2, CloudStorageFileAttributes.class); assertThat(attributes.mimeType()).hasValue("text/palfun"); assertThat(attributes.cacheControl()).hasValue("public; max-age=666"); + Files.delete(source); + Files.delete(target1); + Files.delete(target2); } @Test