From 2f13792040302899b6d7db00aa9c673dd470d460 Mon Sep 17 00:00:00 2001 From: Niels Basjes Date: Mon, 5 Dec 2022 20:59:57 +0100 Subject: [PATCH] feat: FileSystemProvider::readAttributes for basic and gcs views (#1066) --- .../nio/CloudStorageFileSystemProvider.java | 88 ++++++++++++++- .../CloudStorageFileSystemProviderTest.java | 101 ++++++++++++++++++ 2 files changed, 187 insertions(+), 2 deletions(-) diff --git a/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProvider.java b/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProvider.java index c25d1e7e84..b2a55fc696 100644 --- a/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProvider.java +++ b/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProvider.java @@ -64,6 +64,7 @@ import java.nio.file.attribute.FileAttributeView; import java.nio.file.spi.FileSystemProvider; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -71,6 +72,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.TreeMap; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import javax.inject.Singleton; @@ -839,11 +841,93 @@ whose name ends in slash (and these files aren't always zero-size). } @Override - public Map readAttributes(Path path, String attributes, LinkOption... options) { + public Map readAttributes(Path path, String attributes, LinkOption... options) + throws IOException { // TODO(#811): Java 7 NIO defines at least eleven string attributes we'd want to support // (eg. BasicFileAttributeView and PosixFileAttributeView), so rather than a partial // implementation we rely on the other overload for now. - throw new UnsupportedOperationException(); + + // Partial implementation for a few commonly used ones: basic, gcs + String[] split = attributes.split(":", 2); + if (split.length != 2) { + throw new UnsupportedOperationException(); + } + String view = split[0]; + List attributeNames = Arrays.asList(split[1].split(",")); + boolean allAttributes = attributeNames.size() == 1 && attributeNames.get(0).equals("*"); + + BasicFileAttributes fileAttributes; + + Map results = new TreeMap<>(); + switch (view) { + case "gcs": + fileAttributes = readAttributes(path, CloudStorageFileAttributes.class, options); + break; + case "basic": + fileAttributes = readAttributes(path, BasicFileAttributes.class, options); + break; + default: + throw new UnsupportedOperationException(); + } + + if (fileAttributes == null) { + throw new UnsupportedOperationException(); + } + + // BasicFileAttributes + if (allAttributes || attributeNames.contains("lastModifiedTime")) { + results.put("lastModifiedTime", fileAttributes.lastModifiedTime()); + } + if (allAttributes || attributeNames.contains("lastAccessTime")) { + results.put("lastAccessTime", fileAttributes.lastAccessTime()); + } + if (allAttributes || attributeNames.contains("creationTime")) { + results.put("creationTime", fileAttributes.creationTime()); + } + if (allAttributes || attributeNames.contains("isRegularFile")) { + results.put("isRegularFile", fileAttributes.isRegularFile()); + } + if (allAttributes || attributeNames.contains("isDirectory")) { + results.put("isDirectory", fileAttributes.isDirectory()); + } + if (allAttributes || attributeNames.contains("isSymbolicLink")) { + results.put("isSymbolicLink", fileAttributes.isSymbolicLink()); + } + if (allAttributes || attributeNames.contains("isOther")) { + results.put("isOther", fileAttributes.isOther()); + } + if (allAttributes || attributeNames.contains("size")) { + results.put("size", fileAttributes.size()); + } + + // CloudStorageFileAttributes + if (fileAttributes instanceof CloudStorageFileAttributes) { + CloudStorageFileAttributes cloudStorageFileAttributes = + (CloudStorageFileAttributes) fileAttributes; + if (allAttributes || attributeNames.contains("etag")) { + results.put("etag", cloudStorageFileAttributes.etag()); + } + if (allAttributes || attributeNames.contains("mimeType")) { + results.put("mimeType", cloudStorageFileAttributes.mimeType()); + } + if (allAttributes || attributeNames.contains("acl")) { + results.put("acl", cloudStorageFileAttributes.acl()); + } + if (allAttributes || attributeNames.contains("cacheControl")) { + results.put("cacheControl", cloudStorageFileAttributes.cacheControl()); + } + if (allAttributes || attributeNames.contains("contentEncoding")) { + results.put("contentEncoding", cloudStorageFileAttributes.contentEncoding()); + } + if (allAttributes || attributeNames.contains("contentDisposition")) { + results.put("contentDisposition", cloudStorageFileAttributes.contentDisposition()); + } + if (allAttributes || attributeNames.contains("userMetadata")) { + results.put("userMetadata", cloudStorageFileAttributes.userMetadata()); + } + } + + return results; } @Override diff --git a/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProviderTest.java b/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProviderTest.java index 9f264ea07d..4bc004e4d5 100644 --- a/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProviderTest.java +++ b/google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProviderTest.java @@ -16,6 +16,7 @@ package com.google.cloud.storage.contrib.nio; +import static com.google.cloud.storage.Acl.Role.OWNER; import static com.google.cloud.storage.contrib.nio.CloudStorageFileSystem.forBucket; import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; @@ -26,10 +27,17 @@ import static java.nio.file.StandardOpenOption.CREATE_NEW; import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.nio.file.StandardOpenOption.WRITE; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import com.google.cloud.storage.Acl; +import com.google.cloud.storage.Acl.User; import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper; import com.google.cloud.testing.junit4.MultipleAttemptsRule; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.testing.NullPointerTester; import java.io.IOException; @@ -49,10 +57,13 @@ import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.TreeMap; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -817,6 +828,96 @@ public void getUserAgentStartsWithCorrectToken() { .startsWith("gcloud-java-nio/"); } + @Test + public void testReadAttributes() throws IOException { + CloudStorageFileSystem fileSystem = forBucket("dummy"); + CloudStorageFileSystemProvider fileSystemProvider = spy(fileSystem.provider()); + + BasicFileAttributes attributesBasic = mock(BasicFileAttributes.class); + // BasicFileAttributes + when(attributesBasic.creationTime()).thenReturn(FileTime.fromMillis(1L)); + when(attributesBasic.lastModifiedTime()).thenReturn(FileTime.fromMillis(2L)); + when(attributesBasic.lastAccessTime()).thenReturn(FileTime.fromMillis(3L)); + when(attributesBasic.isRegularFile()).thenReturn(true); + when(attributesBasic.isDirectory()).thenReturn(true); + when(attributesBasic.isSymbolicLink()).thenReturn(true); + when(attributesBasic.isOther()).thenReturn(true); + when(attributesBasic.size()).thenReturn(42L); + + CloudStorageFileAttributes attributesGcs = mock(CloudStorageFileAttributes.class); + // BasicFileAttributes + when(attributesGcs.creationTime()).thenReturn(FileTime.fromMillis(1L)); + when(attributesGcs.lastModifiedTime()).thenReturn(FileTime.fromMillis(2L)); + when(attributesGcs.lastAccessTime()).thenReturn(FileTime.fromMillis(3L)); + when(attributesGcs.isRegularFile()).thenReturn(true); + when(attributesGcs.isDirectory()).thenReturn(true); + when(attributesGcs.isSymbolicLink()).thenReturn(true); + when(attributesGcs.isOther()).thenReturn(true); + when(attributesGcs.size()).thenReturn(42L); + + List acls = ImmutableList.of(Acl.newBuilder(new User("Foo"), OWNER).build()); + + // CloudStorageFileAttributes + when(attributesGcs.etag()).thenReturn(Optional.of("TheEtag")); + when(attributesGcs.mimeType()).thenReturn(Optional.of("TheMimeType")); + when(attributesGcs.acl()).thenReturn(Optional.of(acls)); + when(attributesGcs.cacheControl()).thenReturn(Optional.of("TheCacheControl")); + when(attributesGcs.contentEncoding()).thenReturn(Optional.of("TheContentEncoding")); + when(attributesGcs.contentDisposition()).thenReturn(Optional.of("TheContentDisposition")); + when(attributesGcs.userMetadata()).thenReturn(new TreeMap<>()); + + CloudStoragePath path1 = CloudStoragePath.getPath(fileSystem, "/"); + when(fileSystemProvider.readAttributes(path1, BasicFileAttributes.class)) + .thenReturn(attributesBasic); + when(fileSystemProvider.readAttributes(path1, CloudStorageFileAttributes.class)) + .thenReturn(attributesGcs); + + Map expectedBasic = new TreeMap<>(); + // BasicFileAttributes + expectedBasic.put("creationTime", FileTime.fromMillis(1L)); + expectedBasic.put("lastModifiedTime", FileTime.fromMillis(2L)); + expectedBasic.put("lastAccessTime", FileTime.fromMillis(3L)); + expectedBasic.put("isRegularFile", true); + expectedBasic.put("isDirectory", true); + expectedBasic.put("isSymbolicLink", true); + expectedBasic.put("isOther", true); + expectedBasic.put("size", 42L); + + assertEquals(expectedBasic, fileSystemProvider.readAttributes(path1, "basic:*")); + + Map expectedGcs = new TreeMap<>(expectedBasic); + // CloudStorageFileAttributes + expectedGcs.put("etag", Optional.of("TheEtag")); + expectedGcs.put("mimeType", Optional.of("TheMimeType")); + expectedGcs.put("acl", Optional.of(acls)); + expectedGcs.put("cacheControl", Optional.of("TheCacheControl")); + expectedGcs.put("contentEncoding", Optional.of("TheContentEncoding")); + expectedGcs.put("contentDisposition", Optional.of("TheContentDisposition")); + expectedGcs.put("userMetadata", new TreeMap<>()); + + assertEquals(expectedGcs, fileSystemProvider.readAttributes(path1, "gcs:*")); + + Map expectedSpecific = new TreeMap<>(); + expectedSpecific.put("lastModifiedTime", FileTime.fromMillis(2L)); + expectedSpecific.put("isSymbolicLink", true); + expectedSpecific.put("isOther", true); + + // Asking for attributes that should NOT be known because we ask for basic view ! + assertEquals( + expectedSpecific, + fileSystemProvider.readAttributes( + path1, "basic:lastModifiedTime,isSymbolicLink,isOther,etag,cacheControl")); + + // Add the attributes that are only known in gcs view + expectedSpecific.put("etag", Optional.of("TheEtag")); + expectedSpecific.put("cacheControl", Optional.of("TheCacheControl")); + + assertEquals( + expectedSpecific, + fileSystemProvider.readAttributes( + path1, "gcs:lastModifiedTime,isSymbolicLink,isOther,etag,cacheControl")); + } + private static CloudStorageConfiguration permitEmptyPathComponents(boolean value) { return CloudStorageConfiguration.builder().permitEmptyPathComponents(value).build(); }