You are viewing a plain text version of this content. The canonical link for it is here.
Posted to common-commits@hadoop.apache.org by cn...@apache.org on 2025/05/27 22:20:32 UTC

(hadoop) branch apache-github-HADOOP-19343 created (now 31e24808e6b)

This is an automated email from the ASF dual-hosted git repository.

cnauroth pushed a change to branch apache-github-HADOOP-19343
in repository https://gitbox.apache.org/repos/asf/hadoop.git


      at 31e24808e6b HADOOP-19343: GCS / Add implementation for create() API

This branch includes the following new commits:

     new 31e24808e6b HADOOP-19343: GCS / Add implementation for create() API

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



---------------------------------------------------------------------
To unsubscribe, e-mail: common-commits-unsubscribe@hadoop.apache.org
For additional commands, e-mail: common-commits-help@hadoop.apache.org


(hadoop) 01/01: HADOOP-19343: GCS / Add implementation for create() API

Posted by cn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

cnauroth pushed a commit to branch apache-github-HADOOP-19343
in repository https://gitbox.apache.org/repos/asf/hadoop.git

commit 31e24808e6bd7033804bf63ab2d710315c1c2a7b
Author: Arunkumar Chacko <ar...@google.com>
AuthorDate: Tue May 27 22:17:54 2025 +0000

    HADOOP-19343: GCS / Add implementation for create() API
    
    Closes #7656
    
    Signed-off-by: Chris Nauroth <cn...@apache.org>
---
 hadoop-project/pom.xml                             |   7 +-
 .../hadoop-gcp/dev-support/findbugs-exclude.xml    |  29 ++
 hadoop-tools/hadoop-gcp/pom.xml                    | 503 ++++++++++++++++++++
 .../src/config/checkstyle-suppressions.xml         |  26 ++
 .../java/org/apache/hadoop/fs/gs/Constants.java    |  31 ++
 .../org/apache/hadoop/fs/gs/CreateOptions.java     | 121 +++++
 .../apache/hadoop/fs/gs/ErrorTypeExtractor.java    |  60 +++
 .../java/org/apache/hadoop/fs/gs/FileInfo.java     | 203 +++++++++
 .../apache/hadoop/fs/gs/GoogleCloudStorage.java    | 261 +++++++++++
 .../gs/GoogleCloudStorageClientWriteChannel.java   | 116 +++++
 .../hadoop/fs/gs/GoogleCloudStorageFileSystem.java |  89 ++++
 .../hadoop/fs/gs/GoogleCloudStorageItemInfo.java   | 423 +++++++++++++++++
 .../hadoop/fs/gs/GoogleHadoopFileSystem.java       | 505 +++++++++++++++++++++
 .../fs/gs/GoogleHadoopFileSystemConfiguration.java |  75 +++
 .../hadoop/fs/gs/GoogleHadoopOutputStream.java     | 124 +++++
 .../hadoop/fs/gs/HadoopConfigurationProperty.java  |  90 ++++
 .../org/apache/hadoop/fs/gs/StorageResourceId.java | 328 +++++++++++++
 .../java/org/apache/hadoop/fs/gs/StringPaths.java  | 169 +++++++
 .../java/org/apache/hadoop/fs/gs/UriPaths.java     | 113 +++++
 .../hadoop/fs/gs/VerificationAttributes.java       |  68 +++
 .../java/org/apache/hadoop/fs/gs/package-info.java |  23 +
 .../apache/hadoop/fs/gs/TestStorageResourceId.java | 285 ++++++++++++
 .../org/apache/hadoop/fs/gs/TestStringPaths.java   | 164 +++++++
 .../java/org/apache/hadoop/fs/gs/TestUriPaths.java | 150 ++++++
 .../java/org/apache/hadoop/fs/gs/package-info.java |  22 +
 hadoop-tools/pom.xml                               |   1 +
 26 files changed, 3985 insertions(+), 1 deletion(-)

diff --git a/hadoop-project/pom.xml b/hadoop-project/pom.xml
index 248e17f02c5..b0c4cc832da 100644
--- a/hadoop-project/pom.xml
+++ b/hadoop-project/pom.xml
@@ -108,7 +108,7 @@
     <findbugs.version>3.0.5</findbugs.version>
     <dnsjava.version>3.6.1</dnsjava.version>
 
-    <guava.version>27.0-jre</guava.version>
+    <guava.version>33.1.0-jre</guava.version>
     <guice.version>5.1.0</guice.version>
 
     <bouncycastle.version>1.78.1</bouncycastle.version>
@@ -2142,6 +2142,11 @@
         <artifactId>failsafe</artifactId>
         <version>2.4.4</version>
       </dependency>
+      <dependency>
+      <groupId>com.google.cloud</groupId>
+      <artifactId>google-cloud-storage</artifactId>
+      <version>2.44.1</version>
+    </dependency>
     </dependencies>
   </dependencyManagement>
 
diff --git a/hadoop-tools/hadoop-gcp/dev-support/findbugs-exclude.xml b/hadoop-tools/hadoop-gcp/dev-support/findbugs-exclude.xml
new file mode 100644
index 00000000000..80be329bd6d
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/dev-support/findbugs-exclude.xml
@@ -0,0 +1,29 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You 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.
+-->
+<FindBugsFilter>
+  <!-- Redundant null check makes code clearer, future-proof here. -->
+  <Match>
+    <Class name="org.apache.hadoop.fs.gs.GoogleCloudStorage" />
+    <Method name="createItemInfoForBlob" />
+    <Bug pattern="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE" />
+  </Match>
+  <Match>
+    <Class name="org.apache.hadoop.fs.gs.GoogleCloudStorage" />
+    <Method name="createItemInfoForBlob" />
+    <Bug pattern="NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE" />
+  </Match>
+</FindBugsFilter>
diff --git a/hadoop-tools/hadoop-gcp/pom.xml b/hadoop-tools/hadoop-gcp/pom.xml
new file mode 100644
index 00000000000..d5744f1f97c
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/pom.xml
@@ -0,0 +1,503 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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. See accompanying LICENSE file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.hadoop</groupId>
+    <artifactId>hadoop-project</artifactId>
+    <version>3.5.0-SNAPSHOT</version>
+    <relativePath>../../hadoop-project</relativePath>
+  </parent>
+  <artifactId>hadoop-gcp</artifactId>
+  <version>3.5.0-SNAPSHOT</version>
+  <name>Apache Hadoop Google Cloud Platform support</name>
+  <description>
+    This module contains code to support integration with Google Cloud Platform.
+    It also declares the dependencies needed to work with Google Cloud Storage.
+  </description>
+  <packaging>jar</packaging>
+
+  <properties>
+    <file.encoding>UTF-8</file.encoding>
+    <downloadSources>true</downloadSources>
+    <hadoop.tmp.dir>${project.build.directory}/test</hadoop.tmp.dir>
+  </properties>
+
+  <profiles>
+    <profile>
+      <id>tests-off</id>
+      <activation>
+        <file>
+          <missing>src/test/resources/auth-keys.xml</missing>
+        </file>
+      </activation>
+      <properties>
+        <skipITs>true</skipITs>
+      </properties>
+    </profile>
+    <profile>
+      <id>tests-on</id>
+      <activation>
+        <file>
+          <exists>src/test/resources/auth-keys.xml</exists>
+        </file>
+      </activation>
+      <properties>
+        <skipITs>false</skipITs>
+      </properties>
+    </profile>
+    <profile>
+      <id>parallel-tests</id>
+      <activation>
+        <property>
+          <name>parallel-tests</name>
+        </property>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.hadoop</groupId>
+            <artifactId>hadoop-maven-plugins</artifactId>
+            <executions>
+              <execution>
+                <id>parallel-tests-createdir</id>
+                <goals>
+                  <goal>parallel-tests-createdir</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-surefire-plugin</artifactId>
+            <configuration>
+              <forkCount>${testsThreadCount}</forkCount>
+              <reuseForks>false</reuseForks>
+              <trimStackTrace>false</trimStackTrace>
+              <argLine>${maven-surefire-plugin.argLine} -DminiClusterDedicatedDirs=true</argLine>
+              <systemPropertyVariables>
+                <testsThreadCount>${testsThreadCount}</testsThreadCount>
+                <test.build.data>${test.build.data}/${surefire.forkNumber}</test.build.data>
+                <test.build.dir>${test.build.dir}/${surefire.forkNumber}</test.build.dir>
+                <hadoop.tmp.dir>${hadoop.tmp.dir}/${surefire.forkNumber}</hadoop.tmp.dir>
+                <test.unique.fork.id>job-${job.id}-fork-000${surefire.forkNumber}</test.unique.fork.id>
+              </systemPropertyVariables>
+            </configuration>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>default-integration-test</id>
+                <goals>
+                  <goal>integration-test</goal>
+                  <goal>verify</goal>
+                </goals>
+                <configuration>
+                  <forkCount>${testsThreadCount}</forkCount>
+                  <reuseForks>false</reuseForks>
+                  <argLine>${maven-surefire-plugin.argLine} -DminiClusterDedicatedDirs=true</argLine>
+                  <forkedProcessTimeoutInSeconds>${fs.gs.scale.test.timeout}</forkedProcessTimeoutInSeconds>
+                  <trimStackTrace>false</trimStackTrace>
+                  <systemPropertyVariables>
+                    <!-- Tell tests that they are being executed in parallel -->
+                    <test.parallel.execution>true</test.parallel.execution>
+                    <test.build.data>${test.build.data}/${surefire.forkNumber}</test.build.data>
+                    <test.build.dir>${test.build.dir}/${surefire.forkNumber}</test.build.dir>
+                    <hadoop.tmp.dir>${hadoop.tmp.dir}/${surefire.forkNumber}</hadoop.tmp.dir>
+
+                    <!-- Due to a Maven quirk, setting this to just -->
+                    <!-- surefire.forkNumber won't do the parameter -->
+                    <!-- substitution.  Putting a prefix in front of it like -->
+                    <!-- "fork-" makes it work. -->
+                    <test.unique.fork.id>job-${job.id}-fork-000${surefire.forkNumber}</test.unique.fork.id>
+                    <test.default.timeout>${test.integration.timeout}</test.default.timeout>
+                  </systemPropertyVariables>
+                  <includes>
+                    <include>**/ITest*.java</include>
+                  </includes>
+                </configuration>
+              </execution>
+              <execution>
+                <id>sequential-integration-tests</id>
+                <goals>
+                  <goal>integration-test</goal>
+                  <goal>verify</goal>
+                </goals>
+                <configuration>
+                  <forkedProcessTimeoutInSeconds>${fs.gs.scale.test.timeout}</forkedProcessTimeoutInSeconds>
+                  <trimStackTrace>false</trimStackTrace>
+                  <systemPropertyVariables>
+                    <!-- Tell tests that they are being executed sequentially -->
+                    <test.parallel.execution>false</test.parallel.execution>
+                    <test.unique.fork.id>job-${job.id}</test.unique.fork.id>
+                  </systemPropertyVariables>
+                  <!-- Do a sequential run for tests that cannot handle -->
+                  <!-- parallel execution. -->
+                  <includes>
+                    <include>**/ITest*.java</include>
+                  </includes>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+    <profile>
+      <id>sequential-tests</id>
+      <activation>
+        <property>
+          <name>!parallel-tests</name>
+        </property>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <executions>
+              <execution>
+                <goals>
+                  <goal>integration-test</goal>
+                  <goal>verify</goal>
+                </goals>
+                <configuration>
+                  <trimStackTrace>false</trimStackTrace>
+                  <systemPropertyVariables>
+                    <test.unique.fork.id>job-${job.id}</test.unique.fork.id>
+                  </systemPropertyVariables>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-shade-plugin</artifactId>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <transformers>
+                <transformer
+                    implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer"/>
+                <transformer
+                    implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer"/>
+                <transformer
+                    implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
+              </transformers>
+              <filters>
+
+                <filter>
+                  <artifact>com.google.auth:*</artifact>
+                  <includes>
+                    <include>**</include>
+                  </includes>
+                </filter>
+                <filter>
+                  <artifact>io.grpc:*</artifact>
+                  <includes>
+                    <include>**</include>
+                  </includes>
+                </filter>
+                <filter>
+                  <artifact>io.opencensus:*</artifact>
+                  <includes>
+                    <include>**</include>
+                  </includes>
+                </filter>
+                <filter>
+                  <artifact>*:*</artifact>
+                  <excludes>
+                    <exclude>*.json</exclude>
+                    <exclude>google/**</exclude>
+                    <exclude>grpc/**</exclude>
+                  </excludes>
+                </filter>
+              </filters>
+              <artifactSet>
+                <includes>
+                  <include>com.google.api</include>
+                  <include>com.google.api-client</include>
+                  <include>com.google.api.grpc</include>
+                  <include>com.google.apis</include>
+                  <include>com.google.auth</include>
+                  <include>com.google.cloud</include>
+                  <include>com.google.cloud.bigdataoss</include>
+                  <include>com.google.cloud.grpc</include>
+                  <include>com.google.cloud.http</include>
+                  <include>com.google.flogger</include>
+                  <include>com.google.code.gson</include>
+                  <include>com.google.guava</include>
+                  <include>com.google.http-client</include>
+                  <include>com.google.oauth-client</include>
+                  <include>com.google.protobuf</include>
+                  <include>com.google.re2j</include>
+                  <include>com.google.storage.v2</include>
+                  <include>com.lmax</include>
+                  <include>io.grpc</include>
+                  <include>io.opencensus</include>
+                  <include>io.perfmark</include>
+                  <include>org.apache.httpcomponents</include>
+                  <include>org.threeten:threetenbp</include>
+                </includes>
+              </artifactSet>
+              <minimizeJar>true</minimizeJar>
+              <relocations>
+                <relocation>
+                  <pattern>com</pattern>
+                  <shadedPattern>com.google.cloud.hadoop.repackaged.ossgcs.com</shadedPattern>
+                  <includes>
+                    <include>com.google.api.**</include>
+                    <include>com.google.api.gax.**</include>
+                    <include>com.google.auth.**</include>
+                    <include>com.google.cloud.*</include>
+                    <include>com.google.cloud.audit.**</include>
+                    <include>com.google.cloud.grpc.**</include>
+                    <include>com.google.cloud.hadoop.gcsio.**</include>
+                    <include>com.google.cloud.hadoop.util.**</include>
+                    <include>com.google.cloud.http.**</include>
+                    <include>com.google.cloud.monitoring.**</include>
+                    <include>com.google.cloud.spi.**</include>
+                    <include>com.google.cloud.storage.**</include>
+                    <include>com.google.common.**</include>
+                    <include>com.google.geo.**</include>
+                    <include>com.google.gson.**</include>
+                    <include>com.google.google.storage.**</include>
+                    <include>com.google.iam.**</include>
+                    <include>com.google.logging.**</include>
+                    <include>com.google.longrunning.**</include>
+                    <include>com.google.monitoring.**</include>
+                    <include>com.google.protobuf.**</include>
+                    <include>com.google.re2j.**</include>
+                    <include>com.google.rpc.**</include>
+                    <include>com.google.storage.**</include>
+                    <include>com.google.thirdparty.**</include>
+                    <include>com.google.type.**</include>
+                    <include>com.lmax.disruptor.**</include>
+                  </includes>
+                  <excludes>
+                    <exclude>com.google.cloud.hadoop.util.AccessTokenProvider</exclude>
+                    <exclude>com.google.cloud.hadoop.util.AccessTokenProvider$AccessToken</exclude>
+                    <exclude>com.google.cloud.hadoop.util.AccessTokenProvider$AccessTokenType</exclude>
+                    <exclude>com.google.cloud.hadoop.util.AccessBoundary</exclude>
+                    <exclude>com.google.cloud.hadoop.util.AccessBoundary$Action</exclude>
+                    <exclude>com.google.cloud.hadoop.util.AutoValue_AccessBoundary</exclude>
+                  </excludes>
+                </relocation>
+                <relocation>
+                  <pattern>org</pattern>
+                  <shadedPattern>com.google.cloud.hadoop.repackaged.ossgcs.org</shadedPattern>
+                  <includes>
+                    <include>org.apache.http.**</include>
+                    <include>org.threeten.**</include>
+                  </includes>
+                </relocation>
+
+                <relocation>
+                  <pattern>io.grpc.netty.shaded</pattern>
+                  <shadedPattern>
+                    com.google.cloud.hadoop.repackaged.ossgcs.io.grpc.netty.shaded
+                  </shadedPattern>
+                </relocation>
+                <relocation>
+                  <pattern>io</pattern>
+                  <shadedPattern>com.google.cloud.hadoop.repackaged.ossgcs.io</shadedPattern>
+                  <includes>
+                    <include>io.grpc.**</include>
+                    <include>io.opencensus.**</include>
+                    <include>io.perfmark.**</include>
+                  </includes>
+                </relocation>
+                <relocation>
+                  <pattern>META-INF/native/io_grpc_netty_shaded_</pattern>
+                  <shadedPattern>
+                    META-INF/native/com_google_cloud_hadoop_repackaged_gcs_io_grpc_netty_shaded_
+                  </shadedPattern>
+                </relocation>
+                <relocation>
+                  <pattern>META-INF/native/libio_grpc_netty_shaded_</pattern>
+                  <shadedPattern>
+                    META-INF/native/libcom_google_cloud_hadoop_repackaged_gcs_io_grpc_netty_shaded_
+                  </shadedPattern>
+                </relocation>
+              </relocations>
+              <shadedArtifactAttached>true</shadedArtifactAttached>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
+        <groupId>com.github.spotbugs</groupId>
+        <artifactId>spotbugs-maven-plugin</artifactId>
+        <configuration>
+          <xmlOutput>true</xmlOutput>
+          <excludeFilterFile>${basedir}/dev-support/findbugs-exclude.xml
+          </excludeFilterFile>
+          <effort>Max</effort>
+        </configuration>
+      </plugin>
+            <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-checkstyle-plugin</artifactId>
+          <configuration>
+            <suppressionsLocation>src/config/checkstyle-suppressions.xml</suppressionsLocation>
+          </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <forkedProcessTimeoutInSeconds>3600</forkedProcessTimeoutInSeconds>
+          <systemPropertyVariables>
+            <test.default.timeout>${test.integration.timeout}</test.default.timeout>
+          </systemPropertyVariables>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>banned-illegal-imports</id>
+            <phase>process-sources</phase>
+            <goals>
+              <goal>enforce</goal>
+            </goals>
+            <configuration>
+              <rules>
+                <restrictImports>
+                  <includeTestCode>false</includeTestCode>
+                  <reason>Restrict mapreduce imports to committer code</reason>
+                  <exclusions>
+
+                  </exclusions>
+                  <bannedImports>
+                    <bannedImport>org.apache.hadoop.mapreduce.**</bannedImport>
+                    <bannedImport>org.apache.hadoop.mapred.**</bannedImport>
+                  </bannedImports>
+                </restrictImports>
+                <restrictImports>
+                  <includeTestCode>false</includeTestCode>
+                  <reason>Restrict encryption client imports to encryption client factory</reason>
+                  <exclusions>
+
+                  </exclusions>
+                  <bannedImports>
+
+                  </bannedImports>
+                </restrictImports>
+              </rules>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+<dependencyManagement>
+        <dependencies>
+          <dependency>
+            <!--
+      We're using a specific Protobuf version to ensure compatibility with the
+      Google Cloud Storage (GCS) client. The GCS client often relies on
+      particular Long-Term Support (LTS) versions of Protobuf. When we upgrade
+      the GCS client, we'll likely need to update Protobuf too. To prevent
+      dependency conflicts, Protobuf will be shaded within the GCS connector's
+      fat JAR.
+      -->
+            <groupId>com.google.protobuf</groupId>
+            <artifactId>protobuf-java</artifactId>
+            <version>3.25.5</version>
+        </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-common</artifactId>
+      <scope>provided</scope>
+      <exclusions>
+        <exclusion>
+          <groupId>javax.servlet</groupId>
+          <artifactId>servlet-api</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>javax.enterprise</groupId>
+          <artifactId>cdi-api</artifactId>
+        </exclusion>
+        <!-- Exclude protobuf-java 2.5.0 -->
+        <exclusion>
+          <groupId>com.google.protobuf</groupId>
+          <artifactId>protobuf-java</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.platform</groupId>
+      <artifactId>junit-platform-launcher</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.vintage</groupId>
+      <artifactId>junit-vintage-engine</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.cloud</groupId>
+      <artifactId>google-cloud-storage</artifactId>
+    </dependency>
+        <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-engine</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
+
diff --git a/hadoop-tools/hadoop-gcp/src/config/checkstyle-suppressions.xml b/hadoop-tools/hadoop-gcp/src/config/checkstyle-suppressions.xml
new file mode 100644
index 00000000000..8c765bc97f9
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/config/checkstyle-suppressions.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suppressions PUBLIC
+        "-//Checkstyle//DTD SuppressionFilter Configuration 1.0//EN"
+        "https://checkstyle.org/dtds/suppressions_1_0.dtd">
+
+
+<!--
+  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.
+-->
+
+<suppressions>
+    <suppress checks="ParameterNumber"
+              files="org[\\/]apache[\\/]hadoop[\\/]fs[\\/]gs[\\/]GoogleCloudStorageItemInfo.java"/>
+    <suppress checks="ParameterNumber"
+              files="org[\\/]apache[\\/]hadoop[\\/]fs[\\/]gs[\\/]TestStorageResourceId.java"/>
+</suppressions>
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/Constants.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/Constants.java
new file mode 100644
index 00000000000..34434b2859a
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/Constants.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.hadoop.fs.gs;
+
+final class Constants {
+  private Constants() {}
+
+  // URI scheme for GCS.
+  static final String SCHEME = "gs";
+  static final String PATH_DELIMITER = "/";
+
+  static final String GCS_CONFIG_PREFIX = "fs.gs";
+
+  static final String BASE_KEY_PREFIX = "google.cloud";
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/CreateOptions.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/CreateOptions.java
new file mode 100644
index 00000000000..c9b44a1a481
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/CreateOptions.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.hadoop.fs.gs;
+
+import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkArgument;
+
+import org.apache.hadoop.thirdparty.com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * Options that can be specified when creating a file in the {@link GoogleCloudStorageFileSystem}.
+ */
+final class CreateOptions {
+  private final ImmutableMap<String, byte[]> attributes;
+  private final String contentType;
+  private final long overwriteGenerationId;
+  private final WriteMode mode;
+
+  private CreateOptions(CreateOperationOptionsBuilder builder) {
+    this.attributes = ImmutableMap.copyOf(builder.attributes);
+    this.contentType = builder.contentType;
+    this.overwriteGenerationId = builder.overwriteGenerationId;
+    this.mode = builder.writeMode;
+  }
+
+  boolean isOverwriteExisting() {
+    return this.mode == WriteMode.OVERWRITE;
+  }
+
+  enum WriteMode {
+    /**
+     * Creates a new file for write and fails if file already exists.
+     */
+    CREATE_NEW,
+    /**
+     * Creates a new file for write or overwrites an existing file if it already exists.
+     */
+    OVERWRITE
+  }
+
+  static CreateOperationOptionsBuilder builder() {
+    return new CreateOperationOptionsBuilder();
+  }
+
+  /**
+   * Extended attributes to set when creating a file.
+   */
+  ImmutableMap<String, byte[]> getAttributes() {
+    return attributes;
+  }
+
+  /**
+   * Content-type to set when creating a file.
+   */
+  @Nullable
+  String getContentType() {
+    return contentType;
+  }
+
+  /**
+   * Whether to overwrite an existing file with the same name.
+   */
+  WriteMode getWriteMode() {
+    return mode;
+  }
+
+  /**
+   * Generation of existing object to overwrite. Ignored if set to {@link
+   * StorageResourceId#UNKNOWN_GENERATION_ID}, but otherwise this is used instead of {@code
+   * overwriteExisting}, where 0 indicates no existing object, and otherwise an existing object will
+   * only be overwritten by the newly created file if its generation matches this provided
+   * generationId.
+   */
+  long getOverwriteGenerationId() {
+    return overwriteGenerationId;
+  }
+
+  static class CreateOperationOptionsBuilder {
+    private Map<String, byte[]> attributes = ImmutableMap.of();
+    private String contentType = "application/octet-stream";
+    private long overwriteGenerationId = StorageResourceId.UNKNOWN_GENERATION_ID;
+    private WriteMode writeMode = WriteMode.CREATE_NEW;
+
+    CreateOperationOptionsBuilder setWriteMode(WriteMode mode) {
+      this.writeMode = mode;
+      return this;
+    }
+
+    CreateOptions build() {
+      CreateOptions options = new CreateOptions(this);
+
+      checkArgument(!options.getAttributes().containsKey("Content-Type"),
+          "The Content-Type attribute must be set via the contentType option");
+      if (options.getWriteMode() != WriteMode.OVERWRITE) {
+        checkArgument(options.getOverwriteGenerationId() == StorageResourceId.UNKNOWN_GENERATION_ID,
+            "overwriteGenerationId is set to %s but it can be set only in OVERWRITE mode",
+            options.getOverwriteGenerationId());
+      }
+
+      return options;
+    }
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/ErrorTypeExtractor.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/ErrorTypeExtractor.java
new file mode 100644
index 00000000000..a4497734524
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/ErrorTypeExtractor.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 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 org.apache.hadoop.fs.gs;
+
+import io.grpc.Status;
+
+/**
+ * Implementation for {@link ErrorTypeExtractor} for exception specifically thrown from gRPC path.
+ */
+final class ErrorTypeExtractor {
+
+  enum ErrorType {
+    NOT_FOUND, OUT_OF_RANGE, ALREADY_EXISTS, FAILED_PRECONDITION, INTERNAL, RESOURCE_EXHAUSTED,
+    UNAVAILABLE, UNKNOWN
+  }
+
+  //  public static final ErrorTypeExtractor INSTANCE = new ErrorTypeExtractor();
+
+  private static final String BUCKET_ALREADY_EXISTS_MESSAGE =
+      "FAILED_PRECONDITION: Your previous request to create the named bucket succeeded and you "
+          + "already own it.";
+
+  private ErrorTypeExtractor() {
+  }
+
+  static ErrorType getErrorType(Exception error) {
+    switch (Status.fromThrowable(error).getCode()) {
+    case NOT_FOUND:
+      return ErrorType.NOT_FOUND;
+    case OUT_OF_RANGE:
+      return ErrorType.OUT_OF_RANGE;
+    case ALREADY_EXISTS:
+      return ErrorType.ALREADY_EXISTS;
+    case FAILED_PRECONDITION:
+      return ErrorType.FAILED_PRECONDITION;
+    case RESOURCE_EXHAUSTED:
+      return ErrorType.RESOURCE_EXHAUSTED;
+    case INTERNAL:
+      return ErrorType.INTERNAL;
+    case UNAVAILABLE:
+      return ErrorType.UNAVAILABLE;
+    default:
+      return ErrorType.UNKNOWN;
+    }
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/FileInfo.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/FileInfo.java
new file mode 100644
index 00000000000..df8d63f5eec
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/FileInfo.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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 org.apache.hadoop.fs.gs;
+
+import java.net.URI;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Contains information about a file or a directory.
+ *
+ * <p>Note: This class wraps GoogleCloudStorageItemInfo, adds file system specific information and
+ * hides bucket/object specific information.
+ */
+final class FileInfo {
+
+  // Info about the root path.
+  static final FileInfo ROOT_INFO =
+      new FileInfo(GoogleCloudStorageFileSystem.GCSROOT, GoogleCloudStorageItemInfo.ROOT_INFO);
+
+  // Path of this file or directory.
+  private final URI path;
+
+  // Information about the underlying GCS item.
+  private final GoogleCloudStorageItemInfo itemInfo;
+
+  /**
+   * Constructs an instance of FileInfo.
+   *
+   * @param itemInfo Information about the underlying item.
+   */
+  private FileInfo(URI path, GoogleCloudStorageItemInfo itemInfo) {
+    this.itemInfo = itemInfo;
+
+    // Construct the path once.
+    this.path = path;
+  }
+
+  /**
+   * Gets the path of this file or directory.
+   */
+  URI getPath() {
+    return path;
+  }
+
+  /**
+   * Indicates whether this item is a directory.
+   */
+  boolean isDirectory() {
+    return itemInfo.isDirectory();
+  }
+
+  /**
+   * Indicates whether this item is an inferred directory.
+   */
+  boolean isInferredDirectory() {
+    return itemInfo.isInferredDirectory();
+  }
+
+  /**
+   * Indicates whether this instance has information about the unique, shared root of the underlying
+   * storage system.
+   */
+  boolean isGlobalRoot() {
+    return itemInfo.isGlobalRoot();
+  }
+
+  /**
+   * Gets creation time of this item.
+   *
+   * <p>Time is expressed as milliseconds since January 1, 1970 UTC.
+   */
+  long getCreationTime() {
+    return itemInfo.getCreationTime();
+  }
+
+  /**
+   * Gets the size of this file or directory.
+   *
+   * <p>For files, size is in number of bytes. For directories size is 0. For items that do not
+   * exist, size is -1.
+   */
+  long getSize() {
+    return itemInfo.getSize();
+  }
+
+  /**
+   * Gets the modification time of this file if one is set, otherwise the value of {@link
+   * #getCreationTime()} is returned.
+   *
+   * <p>Time is expressed as milliseconds since January 1, 1970 UTC.
+   */
+  long getModificationTime() {
+    return itemInfo.getModificationTime();
+  }
+
+  /**
+   * Retrieve file attributes for this file.
+   *
+   * @return A map of file attributes
+   */
+  Map<String, byte[]> getAttributes() {
+    return itemInfo.getMetadata();
+  }
+
+  /**
+   * Indicates whether this file or directory exists.
+   */
+  boolean exists() {
+    return itemInfo.exists();
+  }
+
+  /**
+   * Returns CRC32C checksum of the file or {@code null}.
+   */
+  byte[] getCrc32cChecksum() {
+    VerificationAttributes verificationAttributes = itemInfo.getVerificationAttributes();
+    return verificationAttributes == null ? null : verificationAttributes.getCrc32c();
+  }
+
+  /**
+   * Returns MD5 checksum of the file or {@code null}.
+   */
+  byte[] getMd5Checksum() {
+    VerificationAttributes verificationAttributes = itemInfo.getVerificationAttributes();
+    return verificationAttributes == null ? null : verificationAttributes.getMd5hash();
+  }
+
+  /**
+   * Gets information about the underlying item.
+   */
+  GoogleCloudStorageItemInfo getItemInfo() {
+    return itemInfo;
+  }
+
+  /**
+   * Gets string representation of this instance.
+   */
+  @Override
+  public String toString() {
+    return getPath() + (exists() ?
+        ": created on: " + Instant.ofEpochMilli(getCreationTime()) :
+        ": exists: no");
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof FileInfo)) {
+      return false;
+    }
+    FileInfo fileInfo = (FileInfo) o;
+    return Objects.equals(path, fileInfo.path) && Objects.equals(itemInfo, fileInfo.itemInfo);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(path, itemInfo);
+  }
+
+  /**
+   * Handy factory method for constructing a FileInfo from a GoogleCloudStorageItemInfo while
+   * potentially returning a singleton instead of really constructing an object for cases like ROOT.
+   */
+  static FileInfo fromItemInfo(GoogleCloudStorageItemInfo itemInfo) {
+    if (itemInfo.isRoot()) {
+      return ROOT_INFO;
+    }
+    URI path = UriPaths.fromResourceId(itemInfo.getResourceId(), /* allowEmptyObjectName= */ true);
+    return new FileInfo(path, itemInfo);
+  }
+
+  /**
+   * Handy factory method for constructing a list of FileInfo from a list of
+   * GoogleCloudStorageItemInfo.
+   */
+  static List<FileInfo> fromItemInfos(List<GoogleCloudStorageItemInfo> itemInfos) {
+    List<FileInfo> fileInfos = new ArrayList<>(itemInfos.size());
+    for (GoogleCloudStorageItemInfo itemInfo : itemInfos) {
+      fileInfos.add(fromItemInfo(itemInfo));
+    }
+    return fileInfos;
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleCloudStorage.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleCloudStorage.java
new file mode 100644
index 00000000000..9c15962b7ef
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleCloudStorage.java
@@ -0,0 +1,261 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.hadoop.fs.gs;
+
+import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.*;
+import static org.apache.hadoop.thirdparty.com.google.common.base.Strings.isNullOrEmpty;
+
+import com.google.cloud.storage.*;
+import org.apache.hadoop.thirdparty.com.google.common.collect.ImmutableList;
+import org.apache.hadoop.thirdparty.com.google.common.collect.Maps;
+import org.apache.hadoop.thirdparty.com.google.common.io.BaseEncoding;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.nio.channels.WritableByteChannel;
+import java.nio.file.FileAlreadyExistsException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A wrapper around <a href="https://github.com/googleapis/java-storage">Google cloud storage
+ * client</a>.
+ */
+class GoogleCloudStorage {
+  public static final Logger LOG = LoggerFactory.getLogger(GoogleHadoopFileSystem.class);
+  static final List<Storage.BlobField> BLOB_FIELDS =
+      ImmutableList.of(Storage.BlobField.BUCKET, Storage.BlobField.CONTENT_ENCODING,
+          Storage.BlobField.CONTENT_TYPE, Storage.BlobField.CRC32C, Storage.BlobField.GENERATION,
+          Storage.BlobField.METADATA, Storage.BlobField.MD5HASH, Storage.BlobField.METAGENERATION,
+          Storage.BlobField.NAME, Storage.BlobField.SIZE, Storage.BlobField.TIME_CREATED,
+          Storage.BlobField.UPDATED);
+  private final Storage storage;
+  private final GoogleHadoopFileSystemConfiguration configuration;
+
+  /**
+   * Having an instance of gscImpl to redirect calls to Json client while new client implementation
+   * is in WIP.
+   */
+  GoogleCloudStorage(GoogleHadoopFileSystemConfiguration configuration) throws IOException {
+    // TODO: Set projectId
+    // TODO: Set credentials
+    this.storage = StorageOptions.newBuilder().build().getService();
+    this.configuration = configuration;
+  }
+
+  public WritableByteChannel create(final StorageResourceId resourceId, final CreateOptions options)
+      throws IOException {
+    LOG.trace("create({})", resourceId);
+
+    checkArgument(resourceId.isStorageObject(), "Expected full StorageObject id, got %s",
+        resourceId);
+    // Update resourceId if generationId is missing
+    StorageResourceId resourceIdWithGeneration = resourceId;
+    if (!resourceId.hasGenerationId()) {
+      resourceIdWithGeneration =
+          new StorageResourceId(resourceId.getBucketName(), resourceId.getObjectName(),
+              getWriteGeneration(resourceId, options.isOverwriteExisting()));
+    }
+
+    return new GoogleCloudStorageClientWriteChannel(storage, resourceIdWithGeneration, options);
+  }
+
+  /**
+   * Gets the object generation for a write operation
+   *
+   * <p>making getItemInfo call even if overwrite is disabled to fail fast in case file is existing.
+   *
+   * @param resourceId object for which generation info is requested
+   * @param overwrite  whether existing object should be overwritten
+   * @return the generation of the object
+   * @throws IOException if the object already exists and cannot be overwritten
+   */
+  private long getWriteGeneration(StorageResourceId resourceId, boolean overwrite)
+      throws IOException {
+    LOG.trace("getWriteGeneration({}, {})", resourceId, overwrite);
+    GoogleCloudStorageItemInfo info = getItemInfo(resourceId);
+    if (!info.exists()) {
+      return 0L;
+    }
+    if (info.exists() && overwrite) {
+      long generation = info.getContentGeneration();
+      checkState(generation != 0, "Generation should not be 0 for an existing item");
+      return generation;
+    }
+
+    throw new FileAlreadyExistsException(String.format("Object %s already exists.", resourceId));
+  }
+
+  public void close() {
+    try {
+      storage.close();
+    } catch (Exception e) {
+      LOG.warn("Error occurred while closing the storage client", e);
+    }
+  }
+
+  public GoogleCloudStorageItemInfo getItemInfo(StorageResourceId resourceId) throws IOException {
+    LOG.trace("getItemInfo({})", resourceId);
+
+    // Handle ROOT case first.
+    if (resourceId.isRoot()) {
+      return GoogleCloudStorageItemInfo.ROOT_INFO;
+    }
+    GoogleCloudStorageItemInfo itemInfo = null;
+
+    if (resourceId.isBucket()) {
+      Bucket bucket = getBucket(resourceId.getBucketName());
+      if (bucket != null) {
+        itemInfo = createItemInfoForBucket(resourceId, bucket);
+      } else {
+        LOG.debug("getBucket({}): not found", resourceId.getBucketName());
+      }
+    } else {
+      Blob blob = getBlob(resourceId);
+      if (blob != null) {
+        itemInfo = createItemInfoForBlob(resourceId, blob);
+      } else {
+        LOG.debug("getObject({}): not found", resourceId);
+      }
+    }
+
+    if (itemInfo == null) {
+      itemInfo = GoogleCloudStorageItemInfo.createNotFound(resourceId);
+    }
+    LOG.debug("getItemInfo: {}", itemInfo);
+    return itemInfo;
+  }
+
+  /**
+   * Gets the bucket with the given name.
+   *
+   * @param bucketName name of the bucket to get
+   * @return the bucket with the given name or null if bucket not found
+   * @throws IOException if the bucket exists but cannot be accessed
+   */
+  @Nullable
+  private Bucket getBucket(String bucketName) throws IOException {
+    LOG.debug("getBucket({})", bucketName);
+    checkArgument(!isNullOrEmpty(bucketName), "bucketName must not be null or empty");
+    try {
+      return storage.get(bucketName);
+    } catch (StorageException e) {
+      if (ErrorTypeExtractor.getErrorType(e) == ErrorTypeExtractor.ErrorType.NOT_FOUND) {
+        return null;
+      }
+      throw new IOException("Error accessing Bucket " + bucketName, e);
+    }
+  }
+
+  private static GoogleCloudStorageItemInfo createItemInfoForBlob(StorageResourceId resourceId,
+      Blob blob) {
+    checkArgument(resourceId != null, "resourceId must not be null");
+    checkArgument(blob != null, "object must not be null");
+    checkArgument(resourceId.isStorageObject(),
+        "resourceId must be a StorageObject. resourceId: %s", resourceId);
+    checkArgument(resourceId.getBucketName().equals(blob.getBucket()),
+        "resourceId.getBucketName() must equal object.getBucket(): '%s' vs '%s'",
+        resourceId.getBucketName(), blob.getBucket());
+    checkArgument(resourceId.getObjectName().equals(blob.getName()),
+        "resourceId.getObjectName() must equal object.getName(): '%s' vs '%s'",
+        resourceId.getObjectName(), blob.getName());
+
+    Map<String, byte[]> decodedMetadata =
+        blob.getMetadata() == null ? null : decodeMetadata(blob.getMetadata());
+
+    byte[] md5Hash = null;
+    byte[] crc32c = null;
+
+    if (!isNullOrEmpty(blob.getCrc32c())) {
+      crc32c = BaseEncoding.base64().decode(blob.getCrc32c());
+    }
+
+    if (!isNullOrEmpty(blob.getMd5())) {
+      md5Hash = BaseEncoding.base64().decode(blob.getMd5());
+    }
+
+    return GoogleCloudStorageItemInfo.createObject(resourceId,
+        blob.getCreateTimeOffsetDateTime() == null ?
+            0 :
+            blob.getCreateTimeOffsetDateTime().toInstant().toEpochMilli(),
+        blob.getUpdateTimeOffsetDateTime() == null ?
+            0 :
+            blob.getUpdateTimeOffsetDateTime().toInstant().toEpochMilli(),
+        blob.getSize() == null ? 0 : blob.getSize(), blob.getContentType(),
+        blob.getContentEncoding(), decodedMetadata,
+        blob.getGeneration() == null ? 0 : blob.getGeneration(),
+        blob.getMetageneration() == null ? 0 : blob.getMetageneration(),
+        new VerificationAttributes(md5Hash, crc32c));
+  }
+
+  static Map<String, byte[]> decodeMetadata(Map<String, String> metadata) {
+    return Maps.transformValues(metadata, GoogleCloudStorage::decodeMetadataValues);
+  }
+
+  @Nullable
+  private static byte[] decodeMetadataValues(String value) {
+    try {
+      return BaseEncoding.base64().decode(value);
+    } catch (IllegalArgumentException iae) {
+      LOG.error("Failed to parse base64 encoded attribute value {}", value, iae);
+      return null;
+    }
+  }
+
+  /**
+   * Gets the object with the given resourceId.
+   *
+   * @param resourceId identifies a StorageObject
+   * @return the object with the given name or null if object not found
+   * @throws IOException if the object exists but cannot be accessed
+   */
+  @Nullable
+  Blob getBlob(StorageResourceId resourceId) throws IOException {
+    checkArgument(resourceId.isStorageObject(), "Expected full StorageObject id, got %s",
+        resourceId);
+    String bucketName = resourceId.getBucketName();
+    String objectName = resourceId.getObjectName();
+    Blob blob;
+    try {
+      blob = storage.get(BlobId.of(bucketName, objectName),
+          Storage.BlobGetOption.fields(BLOB_FIELDS.toArray(new Storage.BlobField[0])));
+    } catch (StorageException e) {
+      throw new IOException("Error accessing " + resourceId, e);
+    }
+    return blob;
+  }
+
+  private static GoogleCloudStorageItemInfo createItemInfoForBucket(StorageResourceId resourceId,
+      Bucket bucket) {
+    checkArgument(resourceId != null, "resourceId must not be null");
+    checkArgument(bucket != null, "bucket must not be null");
+    checkArgument(resourceId.isBucket(), "resourceId must be a Bucket. resourceId: %s", resourceId);
+    checkArgument(resourceId.getBucketName().equals(bucket.getName()),
+        "resourceId.getBucketName() must equal bucket.getName(): '%s' vs '%s'",
+        resourceId.getBucketName(), bucket.getName());
+
+    return GoogleCloudStorageItemInfo.createBucket(resourceId,
+        bucket.asBucketInfo().getCreateTimeOffsetDateTime().toInstant().toEpochMilli(),
+        bucket.asBucketInfo().getUpdateTimeOffsetDateTime().toInstant().toEpochMilli(),
+        bucket.getLocation(),
+        bucket.getStorageClass() == null ? null : bucket.getStorageClass().name());
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleCloudStorageClientWriteChannel.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleCloudStorageClientWriteChannel.java
new file mode 100644
index 00000000000..7956b6f0a82
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleCloudStorageClientWriteChannel.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.hadoop.fs.gs;
+
+import com.google.cloud.storage.BlobId;
+import com.google.cloud.storage.BlobInfo;
+import com.google.cloud.storage.BlobWriteSession;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.Storage.BlobWriteOption;
+import com.google.cloud.storage.StorageException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implements WritableByteChannel to provide write access to GCS via java-storage client.
+ */
+class GoogleCloudStorageClientWriteChannel implements WritableByteChannel {
+  private static final Logger LOG =
+      LoggerFactory.getLogger(GoogleCloudStorageClientWriteChannel.class);
+
+  private final StorageResourceId resourceId;
+  private WritableByteChannel writableByteChannel;
+
+  GoogleCloudStorageClientWriteChannel(final Storage storage,
+      final StorageResourceId resourceId, final CreateOptions createOptions) throws IOException {
+    this.resourceId = resourceId;
+    BlobWriteSession blobWriteSession = getBlobWriteSession(storage, resourceId, createOptions);
+    try {
+      this.writableByteChannel = blobWriteSession.open();
+    } catch (StorageException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private static BlobInfo getBlobInfo(final StorageResourceId resourceId,
+      final CreateOptions createOptions) {
+    BlobInfo blobInfo = BlobInfo.newBuilder(
+            BlobId.of(resourceId.getBucketName(), resourceId.getObjectName(),
+                resourceId.getGenerationId())).setContentType(createOptions.getContentType())
+        //                    .setMetadata(encodeMetadata(createOptions.getMetadata())) // TODO:
+        .build();
+    return blobInfo;
+  }
+
+  private static BlobWriteSession getBlobWriteSession(final Storage storage,
+      final StorageResourceId resourceId, final CreateOptions createOptions) {
+    return storage.blobWriteSession(getBlobInfo(resourceId, createOptions),
+        generateWriteOptions(createOptions));
+  }
+
+  private static BlobWriteOption[] generateWriteOptions(final CreateOptions createOptions) {
+    List<BlobWriteOption> blobWriteOptions = new ArrayList<>();
+
+    blobWriteOptions.add(BlobWriteOption.disableGzipContent());
+    blobWriteOptions.add(BlobWriteOption.generationMatch());
+
+    //TODO: Enable KMS and checksum
+    return blobWriteOptions.toArray(new BlobWriteOption[blobWriteOptions.size()]);
+  }
+
+  @Override
+  public boolean isOpen() {
+    return writableByteChannel != null && writableByteChannel.isOpen();
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (!isOpen()) {
+        return;
+      }
+
+      writableByteChannel.close();
+    } catch (Exception e) {
+      throw new IOException(
+          String.format("Upload failed for '%s'. reason=%s", resourceId, e.getMessage()), e);
+    } finally {
+      writableByteChannel = null;
+    }
+  }
+
+  private int writeInternal(final ByteBuffer byteBuffer) throws IOException {
+    int bytesWritten = writableByteChannel.write(byteBuffer);
+    LOG.trace("{} bytes were written out of provided buffer of capacity {}", bytesWritten,
+        byteBuffer.limit());
+    return bytesWritten;
+  }
+
+  @Override
+  public int write(final ByteBuffer src) throws IOException {
+    return writeInternal(src);
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleCloudStorageFileSystem.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleCloudStorageFileSystem.java
new file mode 100644
index 00000000000..e411f22eb39
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleCloudStorageFileSystem.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.hadoop.fs.gs;
+
+import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.*;
+import static org.apache.hadoop.fs.gs.Constants.SCHEME;
+
+import com.google.auth.Credentials;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.channels.WritableByteChannel;
+
+/**
+ * Provides FS semantics over GCS based on Objects API.
+ */
+class GoogleCloudStorageFileSystem {
+  private static final Logger LOG = LoggerFactory.getLogger(StorageResourceId.class);
+
+  // URI of the root path.
+  static final URI GCSROOT = URI.create(SCHEME + ":/");
+
+  // GCS access instance.
+  private GoogleCloudStorage gcs;
+
+  private static GoogleCloudStorage createCloudStorage(
+      final GoogleHadoopFileSystemConfiguration configuration, final Credentials credentials)
+      throws IOException {
+    checkNotNull(configuration, "configuration must not be null");
+
+    return new GoogleCloudStorage(configuration);
+  }
+
+  GoogleCloudStorageFileSystem(final GoogleHadoopFileSystemConfiguration configuration,
+      final Credentials credentials) throws IOException {
+    gcs = createCloudStorage(configuration, credentials);
+  }
+
+  WritableByteChannel create(final URI path, final CreateOptions createOptions)
+      throws IOException {
+    LOG.trace("create(path: {}, createOptions: {})", path, createOptions);
+    checkNotNull(path, "path could not be null");
+    StorageResourceId resourceId =
+        StorageResourceId.fromUriPath(path, /* allowEmptyObjectName=*/ true);
+
+    if (resourceId.isDirectory()) {
+      throw new IOException(
+          String.format("Cannot create a file whose name looks like a directory: '%s'",
+              resourceId));
+    }
+
+    if (createOptions.getOverwriteGenerationId() != StorageResourceId.UNKNOWN_GENERATION_ID) {
+      resourceId = new StorageResourceId(resourceId.getBucketName(), resourceId.getObjectName(),
+          createOptions.getOverwriteGenerationId());
+    }
+
+    return gcs.create(resourceId, createOptions);
+  }
+
+  void close() {
+    if (gcs == null) {
+      return;
+    }
+    LOG.trace("close()");
+    try {
+      gcs.close();
+    } finally {
+      gcs = null;
+    }
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleCloudStorageItemInfo.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleCloudStorageItemInfo.java
new file mode 100644
index 00000000000..887e68b05f9
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleCloudStorageItemInfo.java
@@ -0,0 +1,423 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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 org.apache.hadoop.fs.gs;
+
+import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkArgument;
+import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull;
+
+import org.apache.hadoop.thirdparty.com.google.common.annotations.VisibleForTesting;
+import org.apache.hadoop.thirdparty.com.google.common.collect.ImmutableMap;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Contains information about an item in Google Cloud Storage.
+ */
+final class GoogleCloudStorageItemInfo {
+  // Info about the root of GCS namespace.
+  public static final GoogleCloudStorageItemInfo ROOT_INFO =
+      new GoogleCloudStorageItemInfo(StorageResourceId.ROOT,
+          /* creationTime= */ 0,
+          /* modificationTime= */ 0,
+          /* size= */ 0,
+          /* location= */ null,
+          /* storageClass= */ null,
+          /* contentType= */ null,
+          /* contentEncoding= */ null,
+          /* metadata= */ null,
+          /* contentGeneration= */ 0,
+          /* metaGeneration= */ 0,
+          /* verificationAttributes= */ null);
+
+  /**
+   * Factory method for creating a GoogleCloudStorageItemInfo for a bucket.
+   *
+   * @param resourceId       Resource ID that identifies a bucket
+   * @param creationTime     Time when a bucket was created (milliseconds since January 1, 1970
+   *                         UTC).
+   * @param modificationTime Time when a bucket was last modified (milliseconds since January 1,
+   *                         1970 UTC).
+   * @param location         Location of a bucket.
+   * @param storageClass     Storage class of a bucket.
+   */
+  static GoogleCloudStorageItemInfo createBucket(StorageResourceId resourceId,
+      long creationTime, long modificationTime, String location, String storageClass) {
+    checkNotNull(resourceId, "resourceId must not be null");
+    checkArgument(resourceId.isBucket(), "expected bucket but got '%s'", resourceId);
+    return new GoogleCloudStorageItemInfo(resourceId, creationTime, modificationTime,
+        /* size= */ 0, location, storageClass,
+        /* contentType= */ null,
+        /* contentEncoding= */ null,
+        /* metadata= */ null,
+        /* contentGeneration= */ 0,
+        /* metaGeneration= */ 0,
+        /* verificationAttributes= */ null);
+  }
+
+  /**
+   * Factory method for creating a GoogleCloudStorageItemInfo for an object.
+   *
+   * @param resourceId   identifies either root, a Bucket, or a StorageObject
+   * @param creationTime Time when object was created (milliseconds since January 1, 1970
+   *                     UTC).
+   * @param size         Size of the given object (number of bytes) or -1 if the object
+   *                     does not exist.
+   * @param metadata     User-supplied object metadata for this object.
+   */
+  static GoogleCloudStorageItemInfo createObject(StorageResourceId resourceId,
+      long creationTime, long modificationTime, long size, String contentType,
+      String contentEncoding, Map<String, byte[]> metadata, long contentGeneration,
+      long metaGeneration, VerificationAttributes verificationAttributes) {
+    checkNotNull(resourceId, "resourceId must not be null");
+    checkArgument(
+        !resourceId.isRoot(),
+        "expected object or directory but got '%s'", resourceId);
+    checkArgument(
+        !resourceId.isBucket(),
+        "expected object or directory but got '%s'", resourceId);
+    return new GoogleCloudStorageItemInfo(resourceId, creationTime, modificationTime, size,
+        /* location= */ null,
+        /* storageClass= */ null, contentType, contentEncoding, metadata, contentGeneration,
+        metaGeneration, verificationAttributes);
+  }
+
+  /**
+   * Factory method for creating a "found" GoogleCloudStorageItemInfo for an inferred directory.
+   *
+   * @param resourceId Resource ID that identifies an inferred directory
+   */
+  static GoogleCloudStorageItemInfo createInferredDirectory(StorageResourceId resourceId) {
+    return new GoogleCloudStorageItemInfo(resourceId,
+        /* creationTime= */ 0,
+        /* modificationTime= */ 0,
+        /* size= */ 0,
+        /* location= */ null,
+        /* storageClass= */ null,
+        /* contentType= */ null,
+        /* contentEncoding= */ null,
+        /* metadata= */ null,
+        /* contentGeneration= */ 0,
+        /* metaGeneration= */ 0,
+        /* verificationAttributes= */ null);
+  }
+
+  /**
+   * Factory method for creating a "not found" GoogleCloudStorageItemInfo for a bucket or an object.
+   *
+   * @param resourceId Resource ID that identifies an inferred directory
+   */
+  static GoogleCloudStorageItemInfo createNotFound(StorageResourceId resourceId) {
+    return new GoogleCloudStorageItemInfo(resourceId,
+        /* creationTime= */ 0,
+        /* modificationTime= */ 0,
+        /* size= */ -1,
+        /* location= */ null,
+        /* storageClass= */ null,
+        /* contentType= */ null,
+        /* contentEncoding= */ null,
+        /* metadata= */ null,
+        /* contentGeneration= */ 0,
+        /* metaGeneration= */ 0,
+        /* verificationAttributes= */ null);
+  }
+
+  // The Bucket and maybe StorageObject names of the GCS "item" referenced by this object. Not
+  // null.
+  private final StorageResourceId resourceId;
+
+  // Creation time of this item.
+  // Time is expressed as milliseconds since January 1, 1970 UTC.
+  private final long creationTime;
+
+  // Modification time of this item.
+  // Time is expressed as milliseconds since January 1, 1970 UTC.
+  private final long modificationTime;
+
+  // Size of an object (number of bytes).
+  // Size is -1 for items that do not exist.
+  private final long size;
+
+  // Location of this item.
+  private final String location;
+
+  // Storage class of this item.
+  private final String storageClass;
+
+  // Content-Type of this item
+  private final String contentType;
+
+  private final String contentEncoding;
+
+  // User-supplied metadata.
+  private final Map<String, byte[]> metadata;
+
+  private final long contentGeneration;
+
+  private final long metaGeneration;
+
+  private final VerificationAttributes verificationAttributes;
+
+  private GoogleCloudStorageItemInfo(StorageResourceId resourceId, long creationTime,
+      long modificationTime, long size, String location, String storageClass, String contentType,
+      String contentEncoding, Map<String, byte[]> metadata, long contentGeneration,
+      long metaGeneration, VerificationAttributes verificationAttributes) {
+    this.resourceId = checkNotNull(resourceId, "resourceId must not be null");
+    this.creationTime = creationTime;
+    this.modificationTime = modificationTime;
+    this.size = size;
+    this.location = location;
+    this.storageClass = storageClass;
+    this.contentType = contentType;
+    this.contentEncoding = contentEncoding;
+    this.metadata = (metadata == null) ? ImmutableMap.of() : metadata;
+    this.contentGeneration = contentGeneration;
+    this.metaGeneration = metaGeneration;
+    this.verificationAttributes = verificationAttributes;
+  }
+
+  /**
+   * Gets bucket name of this item.
+   */
+  String getBucketName() {
+    return resourceId.getBucketName();
+  }
+
+  /**
+   * Gets object name of this item.
+   */
+  String getObjectName() {
+    return resourceId.getObjectName();
+  }
+
+  /**
+   * Gets the resourceId that holds the (possibly null) bucketName and objectName of this object.
+   */
+  StorageResourceId getResourceId() {
+    return resourceId;
+  }
+
+  /**
+   * Gets creation time of this item.
+   *
+   * <p>Time is expressed as milliseconds since January 1, 1970 UTC.
+   */
+  long getCreationTime() {
+    return creationTime;
+  }
+
+  /**
+   * Gets modification time of this item.
+   *
+   * <p>Time is expressed as milliseconds since January 1, 1970 UTC.
+   */
+  long getModificationTime() {
+    return modificationTime;
+  }
+
+  /**
+   * Gets size of this item (number of bytes). Returns -1 if the object does not exist.
+   */
+  long getSize() {
+    return size;
+  }
+
+  /**
+   * Gets location of this item.
+   *
+   * <p>Note: Location is only supported for buckets. The value is always null for objects.
+   */
+  String getLocation() {
+    return location;
+  }
+
+  /**
+   * Gets storage class of this item.
+   *
+   * <p>Note: Storage-class is only supported for buckets. The value is always null for objects.
+   */
+  String getStorageClass() {
+    return storageClass;
+  }
+
+  /**
+   * Gets the content-type of this item, or null if unknown or inapplicable.
+   *
+   * <p>Note: content-type is only supported for objects, and will always be null for buckets.
+   */
+  String getContentType() {
+    return contentType;
+  }
+
+  /**
+   * Gets the content-encoding of this item, or null if unknown or inapplicable.
+   *
+   * <p>Note: content-encoding is only supported for objects, and will always be null for buckets.
+   */
+  String getContentEncoding() {
+    return contentEncoding;
+  }
+
+  /**
+   * Gets user-supplied metadata for this item.
+   *
+   * <p>Note: metadata is only supported for objects. This value is always an empty map for buckets.
+   */
+  Map<String, byte[]> getMetadata() {
+    return metadata;
+  }
+
+  /**
+   * Indicates whether this item is a bucket. Root is not considered to be a bucket.
+   */
+  boolean isBucket() {
+    return resourceId.isBucket();
+  }
+
+  /**
+   * Indicates whether this item refers to the GCS root (gs://).
+   */
+  boolean isRoot() {
+    return resourceId.isRoot();
+  }
+
+  /**
+   * Indicates whether this instance has information about the unique, shared root of the underlying
+   * storage system.
+   */
+  boolean isGlobalRoot() {
+    return isRoot() && exists();
+  }
+
+  /**
+   * Indicates whether {@code itemInfo} is a directory.
+   */
+  boolean isDirectory() {
+    return isGlobalRoot() || isBucket() || resourceId.isDirectory();
+  }
+
+  /**
+   * Indicates whether {@code itemInfo} is an inferred directory.
+   */
+  boolean isInferredDirectory() {
+    return creationTime == 0 && modificationTime == 0 && size == 0 && contentGeneration == 0
+        && metaGeneration == 0;
+  }
+
+  /**
+   * Get the content generation of the object.
+   */
+  long getContentGeneration() {
+    return contentGeneration;
+  }
+
+  /**
+   * Get the meta generation of the object.
+   */
+  long getMetaGeneration() {
+    return metaGeneration;
+  }
+
+  /**
+   * Get object validation attributes.
+   */
+  VerificationAttributes getVerificationAttributes() {
+    return verificationAttributes;
+  }
+
+  /**
+   * Indicates whether this item exists.
+   */
+  boolean exists() {
+    return size >= 0;
+  }
+
+  /**
+   * Helper for checking logical equality of metadata maps, checking equality of keySet() between
+   * this.metadata and otherMetadata, and then using Arrays.equals to compare contents of
+   * corresponding byte arrays.
+   */
+  @VisibleForTesting
+  public boolean metadataEquals(Map<String, byte[]> otherMetadata) {
+    if (metadata == otherMetadata) {
+      // Fast-path for common cases where the same actual default metadata instance may be
+      // used in
+      // multiple different item infos.
+      return true;
+    }
+    // No need to check if other `metadata` is not null,
+    // because previous `if` checks if both of them are null.
+    if (metadata == null || otherMetadata == null) {
+      return false;
+    }
+    if (!metadata.keySet().equals(otherMetadata.keySet())) {
+      return false;
+    }
+
+    // Compare each byte[] with Arrays.equals.
+    for (Map.Entry<String, byte[]> metadataEntry : metadata.entrySet()) {
+      if (!Arrays.equals(metadataEntry.getValue(), otherMetadata.get(metadataEntry.getKey()))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Gets string representation of this instance.
+   */
+  @Override
+  public String toString() {
+    return exists() ?
+        String.format("%s: created on: %s", resourceId, Instant.ofEpochMilli(creationTime)) :
+        String.format("%s: exists: no", resourceId);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof GoogleCloudStorageItemInfo) {
+      GoogleCloudStorageItemInfo other = (GoogleCloudStorageItemInfo) obj;
+      return resourceId.equals(other.resourceId) && creationTime == other.creationTime
+          && modificationTime == other.modificationTime && size == other.size && Objects.equals(
+          location, other.location) && Objects.equals(storageClass, other.storageClass)
+          && Objects.equals(verificationAttributes, other.verificationAttributes)
+          && metaGeneration == other.metaGeneration && contentGeneration == other.contentGeneration
+          && metadataEquals(other.getMetadata());
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = 1;
+    result = prime * result + resourceId.hashCode();
+    result = prime * result + (int) creationTime;
+    result = prime * result + (int) modificationTime;
+    result = prime * result + (int) size;
+    result = prime * result + Objects.hashCode(location);
+    result = prime * result + Objects.hashCode(storageClass);
+    result = prime * result + Objects.hashCode(verificationAttributes);
+    result = prime * result + (int) metaGeneration;
+    result = prime * result + (int) contentGeneration;
+    result = prime * result + metadata.entrySet().stream()
+        .mapToInt(e -> Objects.hash(e.getKey()) + Arrays.hashCode(e.getValue())).sum();
+    return result;
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleHadoopFileSystem.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleHadoopFileSystem.java
new file mode 100644
index 00000000000..1c2fc19d2b5
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleHadoopFileSystem.java
@@ -0,0 +1,505 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.hadoop.fs.gs;
+
+import static org.apache.hadoop.fs.gs.Constants.GCS_CONFIG_PREFIX;
+import static org.apache.hadoop.fs.gs.GoogleHadoopFileSystemConfiguration.GCS_WORKING_DIRECTORY;
+
+import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkArgument;
+import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkState;
+import static org.apache.hadoop.thirdparty.com.google.common.base.Strings.isNullOrEmpty;
+
+import com.google.auth.oauth2.GoogleCredentials;
+import org.apache.hadoop.thirdparty.com.google.common.base.Ascii;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URI;
+import java.util.EnumSet;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.*;
+import org.apache.hadoop.fs.permission.FsPermission;
+import org.apache.hadoop.security.ProviderUtils;
+import org.apache.hadoop.util.Progressable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * GoogleHadoopFileSystem is rooted in a single bucket at initialization time; in this case, Hadoop
+ * paths no longer correspond directly to general GCS paths, and all Hadoop operations going through
+ * this FileSystem will never touch any GCS bucket other than the bucket on which this FileSystem is
+ * rooted.
+ *
+ * <p>This implementation sacrifices a small amount of cross-bucket interoperability in favor of
+ * more straightforward FileSystem semantics and compatibility with existing Hadoop applications. In
+ * particular, it is not subject to bucket-naming constraints, and files are allowed to be placed in
+ * root.
+ */
+public class GoogleHadoopFileSystem extends FileSystem {
+
+  public static final Logger LOG = LoggerFactory.getLogger(GoogleHadoopFileSystem.class);
+
+  /**
+   * URI scheme for GoogleHadoopFileSystem.
+   */
+  private static final String SCHEME = Constants.SCHEME;
+
+  /**
+   * Default value of replication factor.
+   */
+  static final short REPLICATION_FACTOR_DEFAULT = 3;
+
+  // TODO: Take this from config
+  private static final int PERMISSIONS_TO_REPORT = 700;
+
+  /**
+   * The URI the File System is passed in initialize.
+   */
+  private URI initUri;
+
+  /**
+   * Default block size. Note that this is the size that is reported to Hadoop FS clients. It does
+   * not modify the actual block size of an underlying GCS object, because GCS JSON API does not
+   * allow modifying or querying the value. Modifying this value allows one to control how many
+   * mappers are used to process a given file.
+   */
+  private long defaultBlockSize = GoogleHadoopFileSystemConfiguration.BLOCK_SIZE.getDefault();
+
+  // The bucket the file system is rooted in used for default values of:
+  // -- working directory
+  // -- user home directories (only for Hadoop purposes).
+  private Path fsRoot;
+
+  /**
+   * Current working directory; overridden in initialize() if {@link
+   * GoogleHadoopFileSystemConfiguration#GCS_WORKING_DIRECTORY} is set.
+   */
+  private Path workingDirectory;
+  private GoogleCloudStorageFileSystem gcsFs;
+  private boolean isClosed;
+  private FsPermission reportedPermissions;
+
+  public GoogleHadoopFileSystemConfiguration getFileSystemConfiguration() {
+    return fileSystemConfiguration;
+  }
+
+  private GoogleHadoopFileSystemConfiguration fileSystemConfiguration;
+
+  @Override
+  public void initialize(final URI path, Configuration config) throws IOException {
+    LOG.trace("initialize(path: {}, config: {})", path, config);
+
+    checkArgument(path != null, "path must not be null");
+    checkArgument(config != null, "config must not be null");
+    checkArgument(path.getScheme() != null, "scheme of path must not be null");
+    checkArgument(path.getScheme().equals(getScheme()), "URI scheme not supported: {}", path);
+
+    config =
+        ProviderUtils.excludeIncompatibleCredentialProviders(config, GoogleHadoopFileSystem.class);
+    super.initialize(path, config);
+
+    initUri = path;
+
+    // Set this configuration as the default config for this instance; configure()
+    // will perform some file-system-specific adjustments, but the original should
+    // be sufficient (and is required) for the delegation token binding initialization.
+    setConf(config);
+
+    this.reportedPermissions = new FsPermission(PERMISSIONS_TO_REPORT);
+
+    initializeFsRoot();
+
+    this.fileSystemConfiguration = new GoogleHadoopFileSystemConfiguration(config);
+    initializeWorkingDirectory(fileSystemConfiguration);
+    initializeGcsFs(fileSystemConfiguration);
+  }
+
+  private void initializeFsRoot() {
+    String rootBucket = initUri.getAuthority();
+    checkArgument(rootBucket != null, "No bucket specified in GCS URI: {}", initUri);
+    // Validate root bucket name
+    URI rootUri = UriPaths.fromStringPathComponents(rootBucket, /* objectName= */
+        null, /* allowEmptyObjectName= */ true);
+    fsRoot = new Path(rootUri);
+    LOG.trace("Configured FS root: '{}'", fsRoot);
+  }
+
+  private void initializeWorkingDirectory(final GoogleHadoopFileSystemConfiguration config) {
+    String configWorkingDirectory = config.getWorkingDirectory();
+    if (isNullOrEmpty(configWorkingDirectory)) {
+      LOG.warn("No working directory configured, using default: '{}'", workingDirectory);
+    }
+    // Use the public method to ensure proper behavior of normalizing and resolving the new
+    // working directory relative to the initial filesystem-root directory.
+    setWorkingDirectory(
+        isNullOrEmpty(configWorkingDirectory) ? fsRoot : new Path(configWorkingDirectory));
+    LOG.trace("Configured working directory: {} = {}", GCS_WORKING_DIRECTORY.getKey(),
+        getWorkingDirectory());
+  }
+
+  private synchronized void initializeGcsFs(final GoogleHadoopFileSystemConfiguration config)
+      throws IOException {
+    this.gcsFs = createGcsFs(config);
+  }
+
+  private GoogleCloudStorageFileSystem createGcsFs(final GoogleHadoopFileSystemConfiguration config)
+      throws IOException {
+    GoogleCredentials credentials = getCredentials(config);
+    return new GoogleCloudStorageFileSystem(config, credentials);
+  }
+
+  private GoogleCredentials getCredentials(GoogleHadoopFileSystemConfiguration config)
+      throws IOException {
+    return getCredentials(config, GCS_CONFIG_PREFIX);
+  }
+
+  public static GoogleCredentials getCredentials(GoogleHadoopFileSystemConfiguration config,
+      String... keyPrefixesVararg) throws IOException {
+    return GoogleCredentials.getApplicationDefault(); // TODO: Add other Auth mechanisms
+  }
+
+  @Override
+  protected void checkPath(final Path path) {
+    LOG.trace("checkPath(path: {})", path);
+    // Validate scheme
+    URI uri = path.toUri();
+
+    String scheme = uri.getScheme();
+    if (scheme != null && !scheme.equalsIgnoreCase(getScheme())) {
+      throw new IllegalArgumentException(
+          String.format("Wrong scheme: {}, in path: {}, expected scheme: {}", scheme, path,
+              getScheme()));
+    }
+
+    String bucket = uri.getAuthority();
+    String rootBucket = fsRoot.toUri().getAuthority();
+
+    // Bucket-less URIs will be qualified later
+    if (bucket == null || bucket.equals(rootBucket)) {
+      return;
+    }
+
+    throw new IllegalArgumentException(
+        String.format("Wrong bucket: {}, in path: {}, expected bucket: {}", bucket, path,
+            rootBucket));
+  }
+
+  /**
+   * Validates that GCS path belongs to this file system. The bucket must match the root bucket
+   * provided at initialization time.
+   */
+  Path getHadoopPath(final URI gcsPath) {
+    LOG.trace("getHadoopPath(gcsPath: {})", gcsPath);
+
+    // Handle root. Delegate to getGcsPath on "gs:/" to resolve the appropriate gs://<bucket> URI.
+    if (gcsPath.equals(getGcsPath(fsRoot))) {
+      return fsRoot;
+    }
+
+    StorageResourceId resourceId = StorageResourceId.fromUriPath(gcsPath, true);
+
+    checkArgument(!resourceId.isRoot(), "Missing authority in gcsPath '{}'", gcsPath);
+    String rootBucket = fsRoot.toUri().getAuthority();
+    checkArgument(resourceId.getBucketName().equals(rootBucket),
+        "Authority of URI '{}' doesn't match root bucket '{}'", resourceId.getBucketName(),
+        rootBucket);
+
+    Path hadoopPath = new Path(fsRoot,
+        new Path(/* schema= */ null, /* authority= */ null, resourceId.getObjectName()));
+    LOG.trace("getHadoopPath(gcsPath: {}): {}", gcsPath, hadoopPath);
+    return hadoopPath;
+  }
+
+  /**
+   * Translates a "gs:/" style hadoopPath (or relative path which is not fully-qualified) into the
+   * appropriate GCS path which is compatible with the underlying GcsFs.
+   */
+  URI getGcsPath(final Path hadoopPath) {
+    LOG.trace("getGcsPath(hadoopPath: {})", hadoopPath);
+
+    // Convert to fully qualified absolute path; the Path object will call back to get our current
+    // workingDirectory as part of fully resolving the path.
+    Path resolvedPath = makeQualified(hadoopPath);
+
+    String objectName = resolvedPath.toUri().getPath();
+    if (objectName != null && resolvedPath.isAbsolute()) {
+      // Strip off leading '/' because GoogleCloudStorageFileSystem.getPath appends it explicitly
+      // between bucket and objectName.
+      objectName = objectName.substring(1);
+    }
+
+    // Construct GCS path URI
+    String rootBucket = fsRoot.toUri().getAuthority();
+    URI gcsPath =
+        UriPaths.fromStringPathComponents(rootBucket, objectName, /* allowEmptyObjectName= */ true);
+    LOG.trace("getGcsPath(hadoopPath: {}): {}", hadoopPath, gcsPath);
+    return gcsPath;
+  }
+
+  @Override
+  public String getScheme() {
+    return SCHEME;
+  }
+
+  @Override
+  public FSDataInputStream open(final Path path, final int bufferSize) throws IOException {
+    LOG.trace("open({})", path);
+    throw new UnsupportedOperationException(path.toString());
+  }
+
+  @Override
+  public FSDataOutputStream create(Path hadoopPath, FsPermission permission, boolean overwrite,
+      int bufferSize, short replication, long blockSize, Progressable progress) throws IOException {
+    checkArgument(hadoopPath != null, "hadoopPath must not be null");
+    checkArgument(replication > 0, "replication must be a positive integer: %s", replication);
+    checkArgument(blockSize > 0, "blockSize must be a positive integer: %s", blockSize);
+
+    System.out.println(String.format("create(%s)", hadoopPath));
+    checkOpen();
+
+    LOG.trace("create(hadoopPath: {}, overwrite: {}, bufferSize: {} [ignored])", hadoopPath,
+        overwrite, bufferSize);
+
+    CreateOptions.WriteMode writeMode =
+        overwrite ? CreateOptions.WriteMode.OVERWRITE : CreateOptions.WriteMode.CREATE_NEW;
+    FSDataOutputStream response = new FSDataOutputStream(
+        new GoogleHadoopOutputStream(this, getGcsPath(hadoopPath),
+            CreateOptions.builder().setWriteMode(writeMode).build(), statistics), statistics);
+
+    return response;
+  }
+
+  @Override
+  public FSDataOutputStream createNonRecursive(Path hadoopPath, FsPermission permission,
+      EnumSet<CreateFlag> flags, int bufferSize, short replication, long blockSize,
+      Progressable progress) throws IOException {
+    throw new UnsupportedOperationException(hadoopPath.toString());
+  }
+
+  @Override
+  public FSDataOutputStream append(final Path path, final int i, final Progressable progressable)
+      throws IOException {
+    throw new UnsupportedOperationException(path.toString());
+  }
+
+  @Override
+  public boolean rename(final Path path, final Path path1) throws IOException {
+    LOG.trace("rename({}, {})", path, path1);
+    throw new UnsupportedOperationException(path.toString());
+  }
+
+  @Override
+  public boolean delete(final Path path, final boolean recursive) throws IOException {
+    LOG.trace("delete({}, {})", path, recursive);
+    throw new UnsupportedOperationException(path.toString());
+  }
+
+  @Override
+  public FileStatus[] listStatus(final Path path) throws FileNotFoundException, IOException {
+    checkArgument(path != null, "hadoopPath must not be null");
+
+    checkOpen();
+
+    LOG.trace("listStatus(hadoopPath: {})", path);
+    throw new UnsupportedOperationException(path.toString());
+  }
+
+  /**
+   * Overridden to make root its own parent. This is POSIX compliant, but more importantly guards
+   * against poor directory accounting in the PathData class of Hadoop 2's FsShell.
+   */
+  @Override
+  public Path makeQualified(final Path path) {
+    Path qualifiedPath = super.makeQualified(path);
+
+    URI uri = qualifiedPath.toUri();
+
+    checkState("".equals(uri.getPath()) || qualifiedPath.isAbsolute(),
+        "Path '{}' must be fully qualified.", qualifiedPath);
+
+    Path result;
+    String upath = uri.getPath();
+
+    // Strip initial '..'s to make root is its own parent.
+    int i = 0;
+    while (upath.startsWith("/../", i)) {
+      // Leave a preceding slash, so path is still absolute.
+      i += 3;
+    }
+    if (i == upath.length() || upath.substring(i).equals("/..")) {
+      // Allow a Path of gs://someBucket to map to gs://someBucket/
+      result = new Path(uri.getScheme(), uri.getAuthority(), "/");
+    } else if (i == 0) {
+      result = qualifiedPath;
+    } else {
+      result = new Path(uri.getScheme(), uri.getAuthority(), upath.substring(i));
+    }
+
+    LOG.trace("makeQualified(path: {}): {}", path, result);
+    return result;
+  }
+
+  /**
+   * Returns a URI of the root of this FileSystem.
+   */
+  @Override
+  public URI getUri() {
+    return fsRoot.toUri();
+  }
+
+  /**
+   * The default port is listed as -1 as an indication that ports are not used.
+   */
+  @Override
+  protected int getDefaultPort() {
+    int result = -1;
+    LOG.trace("getDefaultPort(): %d", result);
+    return result;
+  }
+
+  @Override
+  public boolean hasPathCapability(final Path path, final String capability) {
+    checkNotNull(path, "path must not be null");
+    checkArgument(!isNullOrEmpty(capability), "capability must not be null or empty string for {}",
+        path);
+    switch (Ascii.toLowerCase(capability)) {
+    case CommonPathCapabilities.FS_APPEND:
+    case CommonPathCapabilities.FS_CONCAT:
+      return false;
+    default:
+      return false;
+    }
+  }
+
+  /**
+   * Gets the current working directory.
+   *
+   * @return The current working directory.
+   */
+  @Override
+  public Path getWorkingDirectory() {
+    LOG.trace("getWorkingDirectory(): {}", workingDirectory);
+    return workingDirectory;
+  }
+
+  @Override
+  public boolean mkdirs(final Path path, final FsPermission fsPermission) throws IOException {
+    LOG.trace("mkdirs({})", path);
+    throw new UnsupportedOperationException(path.toString());
+  }
+
+//  /**
+//   * Gets the default replication factor.
+//   */
+//  @Override
+//  public short getDefaultReplication() {
+//    return REPLICATION_FACTOR_DEFAULT;
+//  }
+
+  @Override
+  public FileStatus getFileStatus(final Path path) throws IOException {
+    checkArgument(path != null, "path must not be null");
+
+    checkOpen();
+
+    URI gcsPath = getGcsPath(path);
+
+    LOG.trace("getFileStatus(): {}", gcsPath);
+
+    throw new UnsupportedOperationException(path.toString());
+  }
+
+  /**
+   * Returns home directory of the current user.
+   *
+   * <p>Note: This directory is only used for Hadoop purposes. It is not the same as a user's OS
+   * home directory.
+   */
+  @Override
+  public Path getHomeDirectory() {
+    Path result = new Path(fsRoot, "user/" + System.getProperty("user.name"));
+    LOG.trace("getHomeDirectory(): {}", result);
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * <p>Returns the service if delegation tokens are configured, otherwise, null.
+   */
+  @Override
+  public String getCanonicalServiceName() {
+    // TODO: Add delegation token support
+    return null;
+  }
+
+  /**
+   * Gets GCS FS instance.
+   */
+  GoogleCloudStorageFileSystem getGcsFs() {
+    return gcsFs;
+  }
+
+  /**
+   * Assert that the FileSystem has been initialized and not close()d.
+   */
+  private void checkOpen() throws IOException {
+    if (isClosed) {
+      throw new IOException("GoogleHadoopFileSystem has been closed or not initialized.");
+    }
+  }
+
+  @Override
+  public void close() throws IOException {
+    LOG.trace("close()");
+    if (isClosed) {
+      return;
+    }
+
+    super.close();
+
+    getGcsFs().close();
+
+    this.isClosed = true;
+  }
+
+  @Override
+  public long getUsed() throws IOException {
+    long result = super.getUsed();
+    LOG.trace("getUsed(): {}", result);
+    return result;
+  }
+
+//  @Override
+//  public long getDefaultBlockSize() {
+//    LOG.trace("getDefaultBlockSize(): {}", defaultBlockSize);
+//    return defaultBlockSize;
+//  }
+
+  @Override
+  public void setWorkingDirectory(final Path hadoopPath) {
+    checkArgument(hadoopPath != null, "hadoopPath must not be null");
+    URI gcsPath = UriPaths.toDirectory(getGcsPath(hadoopPath));
+    workingDirectory = getHadoopPath(gcsPath);
+    LOG.trace("setWorkingDirectory(hadoopPath: {}): {}", hadoopPath, workingDirectory);
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleHadoopFileSystemConfiguration.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleHadoopFileSystemConfiguration.java
new file mode 100644
index 00000000000..16d940b16f4
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleHadoopFileSystemConfiguration.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.hadoop.fs.gs;
+
+import static java.lang.Math.toIntExact;
+
+import org.apache.hadoop.conf.Configuration;
+
+/**
+ * This class provides a configuration for the {@link GoogleHadoopFileSystem} implementations.
+ */
+class GoogleHadoopFileSystemConfiguration {
+  /**
+   * Configuration key for default block size of a file.
+   *
+   * <p>Note that this is the size that is reported to Hadoop FS clients. It does not modify the
+   * actual block size of an underlying GCS object, because GCS JSON API does not allow modifying or
+   * querying the value. Modifying this value allows one to control how many mappers are used to
+   * process a given file.
+   */
+  public static final HadoopConfigurationProperty<Long> BLOCK_SIZE =
+      new HadoopConfigurationProperty<>("fs.gs.block.size", 64 * 1024 * 1024L);
+
+  /**
+   * Configuration key for GCS project ID. Default value: none
+   */
+  public static final HadoopConfigurationProperty<String> GCS_PROJECT_ID =
+      new HadoopConfigurationProperty<>("fs.gs.project.id");
+
+  /**
+   * Configuration key for initial working directory of a GHFS instance. Default value: '/'
+   */
+  public static final HadoopConfigurationProperty<String> GCS_WORKING_DIRECTORY =
+      new HadoopConfigurationProperty<>("fs.gs.working.dir", "/");
+
+  /**
+   * Configuration key for setting write buffer size.
+   */
+  public static final HadoopConfigurationProperty<Long> GCS_OUTPUT_STREAM_BUFFER_SIZE =
+      new HadoopConfigurationProperty<>("fs.gs.outputstream.buffer.size", 8L * 1024 * 1024);
+
+  private final String workingDirectory;
+
+  public int getOutStreamBufferSize() {
+    return outStreamBufferSize;
+  }
+
+  private final int outStreamBufferSize;
+
+  GoogleHadoopFileSystemConfiguration(Configuration config) {
+    this.workingDirectory = GCS_WORKING_DIRECTORY.get(config, config::get);
+    this.outStreamBufferSize =
+        toIntExact(GCS_OUTPUT_STREAM_BUFFER_SIZE.get(config, config::getLongBytes));
+  }
+
+  public String getWorkingDirectory() {
+    return this.workingDirectory;
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleHadoopOutputStream.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleHadoopOutputStream.java
new file mode 100644
index 00000000000..747d9f001c5
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/GoogleHadoopOutputStream.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.hadoop.fs.gs;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.channels.Channels;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.WritableByteChannel;
+import javax.annotation.Nonnull;
+
+import org.apache.hadoop.fs.FileAlreadyExistsException;
+import org.apache.hadoop.fs.FileSystem;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class GoogleHadoopOutputStream extends OutputStream {
+  public static final Logger LOG = LoggerFactory.getLogger(StorageResourceId.class);
+
+  private final GoogleHadoopFileSystem ghfs;
+
+  // Path of the file to write to.
+  private final URI dstGcsPath;
+
+  private OutputStream outputStream;
+
+  // Statistics tracker provided by the parent GoogleHadoopFileSystem for recording
+  // numbers of bytes written.
+  private final FileSystem.Statistics statistics;
+
+  /**
+   * Constructs an instance of GoogleHadoopOutputStream object.
+   *
+   * @param ghfs              Instance of {@link GoogleHadoopFileSystem}.
+   * @param dstGcsPath        Path of the file to write to.
+   * @param statistics        File system statistics object.
+   * @param createFileOptions options for file creation
+   * @throws IOException if an IO error occurs.
+   */
+  GoogleHadoopOutputStream(GoogleHadoopFileSystem ghfs, URI dstGcsPath,
+      CreateOptions createFileOptions, FileSystem.Statistics statistics) throws IOException {
+    LOG.trace("GoogleHadoopOutputStream(gcsPath: {}, createFileOptions: {})", dstGcsPath,
+        createFileOptions);
+    this.ghfs = ghfs;
+    this.dstGcsPath = dstGcsPath;
+    this.statistics = statistics;
+
+    this.outputStream = createOutputStream(ghfs.getGcsFs(), dstGcsPath, createFileOptions,
+        ghfs.getFileSystemConfiguration());
+  }
+
+  private static OutputStream createOutputStream(GoogleCloudStorageFileSystem gcsfs, URI gcsPath,
+      CreateOptions options, GoogleHadoopFileSystemConfiguration fileSystemConfiguration)
+      throws IOException {
+    WritableByteChannel channel;
+    try {
+      channel = gcsfs.create(gcsPath, options);
+    } catch (java.nio.file.FileAlreadyExistsException e) {
+
+      throw (FileAlreadyExistsException) new FileAlreadyExistsException(
+          String.format("'%s' already exists", gcsPath)).initCause(e);
+    }
+    OutputStream outputStream = Channels.newOutputStream(channel);
+    int bufferSize = fileSystemConfiguration.getOutStreamBufferSize();
+    return bufferSize > 0 ? new BufferedOutputStream(outputStream, bufferSize) : outputStream;
+  }
+
+  @Override
+  public void write(int b) throws IOException {
+    throwIfNotOpen();
+    outputStream.write(b);
+    statistics.incrementBytesWritten(1);
+    statistics.incrementWriteOps(1);
+  }
+
+  @Override
+  public void write(@Nonnull byte[] b, int offset, int len) throws IOException {
+    throwIfNotOpen();
+    outputStream.write(b, offset, len);
+    statistics.incrementBytesWritten(len);
+    statistics.incrementWriteOps(1);
+  }
+
+  @Override
+  public void close() throws IOException {
+    LOG.trace("close(): final destination: {}", dstGcsPath);
+
+    if (outputStream == null) {
+      LOG.trace("close(): Ignoring; stream already closed.");
+      return;
+    }
+
+    try {
+      outputStream.close();
+    } finally {
+      outputStream = null;
+    }
+  }
+
+  private void throwIfNotOpen() throws IOException {
+    if (outputStream == null) {
+      throw new ClosedChannelException();
+    }
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/HadoopConfigurationProperty.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/HadoopConfigurationProperty.java
new file mode 100644
index 00000000000..9360290a09c
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/HadoopConfigurationProperty.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.hadoop.fs.gs;
+
+import org.apache.hadoop.thirdparty.com.google.common.collect.ImmutableList;
+
+import java.util.List;
+import java.util.function.BiFunction;
+
+import org.apache.hadoop.conf.Configuration;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Hadoop configuration property.
+ */
+class HadoopConfigurationProperty<T> {
+  private static final Logger LOG = LoggerFactory.getLogger(HadoopConfigurationProperty.class);
+
+  private final String key;
+  private final List<String> deprecatedKeys;
+  private final T defaultValue;
+
+  private List<String> keyPrefixes = ImmutableList.of("");
+
+  HadoopConfigurationProperty(String key) {
+    this(key, null);
+  }
+
+  HadoopConfigurationProperty(String key, T defaultValue, String... deprecatedKeys) {
+    this.key = key;
+    this.deprecatedKeys =
+        deprecatedKeys == null ? ImmutableList.of() : ImmutableList.copyOf(deprecatedKeys);
+    this.defaultValue = defaultValue;
+  }
+
+  String getKey() {
+    return key;
+  }
+
+  T getDefault() {
+    return defaultValue;
+  }
+
+  T get(Configuration config, BiFunction<String, T, T> getterFn) {
+    String lookupKey = getLookupKey(config, key, (c, k) -> c.get(k) != null);
+    return logProperty(lookupKey, getterFn.apply(lookupKey, defaultValue));
+  }
+
+  private String getLookupKey(Configuration config, String lookupKey,
+      BiFunction<Configuration, String, Boolean> checkFn) {
+    for (String prefix : keyPrefixes) {
+      String prefixedKey = prefix + lookupKey;
+      if (checkFn.apply(config, prefixedKey)) {
+        return prefixedKey;
+      }
+      for (String deprecatedKey : deprecatedKeys) {
+        String prefixedDeprecatedKey = prefix + deprecatedKey;
+        if (checkFn.apply(config, prefixedDeprecatedKey)) {
+          LOG.warn("Using deprecated key '{}', use '{}' key instead.", prefixedDeprecatedKey,
+              prefixedKey);
+          return prefixedDeprecatedKey;
+        }
+      }
+    }
+    return keyPrefixes.get(0) + lookupKey;
+  }
+
+  private static <S> S logProperty(String key, S value) {
+    LOG.trace("{} = {}", key, value);
+    return value;
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/StorageResourceId.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/StorageResourceId.java
new file mode 100644
index 00000000000..5935564feed
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/StorageResourceId.java
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.hadoop.fs.gs;
+
+import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkArgument;
+import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.hadoop.thirdparty.com.google.common.base.Strings.isNullOrEmpty;
+import static org.apache.hadoop.fs.gs.Constants.SCHEME;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.URI;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Data struct representing either a GCS StorageObject, a GCS Bucket or the GCS root (gs://). If
+ * both bucketName and objectName are null, the StorageResourceId refers to GCS root (gs://). If
+ * bucketName is non-null, and objectName is null, then this refers to a GCS Bucket. Otherwise, if
+ * bucketName and objectName are both non-null, this refers to a GCS StorageObject.
+ */
+class StorageResourceId {
+
+  private static final Logger LOG = LoggerFactory.getLogger(StorageResourceId.class);
+
+  // The generationId used to denote "unknown"; if given to a method expecting generationId
+  // constraints, the method may perform extra low-level GETs to determine an existing
+  // generationId
+  // if idempotency constraints require doing so.
+  static final long UNKNOWN_GENERATION_ID = -1L;
+
+  // Pattern that parses out bucket and object names.
+  // Given 'gs://foo-bucket/foo/bar/baz', matcher.group(x) will return:
+  // 0 = gs://foo-bucket/foo/bar/baz
+  // 1 = foo-bucket/foo/bar/baz
+  // 2 = foo-bucket
+  // 3 = /foo/bar/baz
+  // 4 = foo/bar/baz
+  // Groups 2 and 4 can be used to create an instance.
+  private static final Pattern GCS_PATH_PATTERN = Pattern.compile("gs://(([^/]+)(/(.+)?)?)?");
+
+  // The singleton instance identifying the GCS root (gs://). Both getObjectName() and
+  // getBucketName() will return null.
+  static final StorageResourceId ROOT = new StorageResourceId();
+
+  // Bucket name of this storage resource to be used with the Google Cloud Storage API.
+  private final String bucketName;
+
+  // Object name of this storage resource to be used with the Google Cloud Storage API.
+  private final String objectName;
+
+  // Human-readable String to be returned by toString(); kept as 'final' member for efficiency.
+  private final String stringPath;
+
+  // The generationId to be used with precondition checks when using this StorageResourceId
+  // as an identifier for mutation requests.
+  private final long generationId;
+
+  /**
+   * Constructor for a StorageResourceId that refers to the GCS root (gs://). Private because all
+   * external users should just use the singleton StorageResourceId.ROOT.
+   */
+  private StorageResourceId() {
+    this.bucketName = null;
+    this.objectName = null;
+    this.stringPath = StringPaths.fromComponents(bucketName, objectName);
+    this.generationId = UNKNOWN_GENERATION_ID;
+  }
+
+  /**
+   * Constructor for a StorageResourceId representing a Bucket; {@code getObjectName()} will return
+   * null for a StorageResourceId that represents a Bucket.
+   *
+   * @param bucketName The bucket name of the resource. Must be non-empty and non-null.
+   */
+  StorageResourceId(String bucketName) {
+    checkArgument(!isNullOrEmpty(bucketName), "bucketName must not be null or empty");
+
+    this.bucketName = bucketName;
+    this.objectName = null;
+    this.stringPath = StringPaths.fromComponents(bucketName, objectName);
+    this.generationId = UNKNOWN_GENERATION_ID;
+  }
+
+  /**
+   * Constructor for a StorageResourceId representing a full StorageObject, including bucketName and
+   * objectName.
+   *
+   * @param bucketName The bucket name of the resource. Must be non-empty and non-null.
+   * @param objectName The object name of the resource. Must be non-empty and non-null.
+   */
+  StorageResourceId(String bucketName, String objectName) {
+    checkArgument(!isNullOrEmpty(bucketName), "bucketName must not be null or empty");
+    checkArgument(!isNullOrEmpty(objectName), "objectName must not be null or empty");
+
+    this.bucketName = bucketName;
+    this.objectName = objectName;
+    this.stringPath = StringPaths.fromComponents(bucketName, objectName);
+    this.generationId = UNKNOWN_GENERATION_ID;
+  }
+
+  /**
+   * Constructor for a StorageResourceId representing a full StorageObject, including bucketName and
+   * objectName.
+   *
+   * @param bucketName   The bucket name of the resource. Must be non-empty and non-null.
+   * @param objectName   The object name of the resource. Must be non-empty and non-null.
+   * @param generationId The generationId to be used with precondition checks when using this
+   *                     StorageResourceId as an identifier for mutation requests.
+   */
+  StorageResourceId(String bucketName, String objectName, long generationId) {
+    checkArgument(!isNullOrEmpty(bucketName), "bucketName must not be null or empty");
+    checkArgument(!isNullOrEmpty(objectName), "objectName must not be null or empty");
+
+    this.bucketName = bucketName;
+    this.objectName = objectName;
+    this.stringPath = StringPaths.fromComponents(bucketName, objectName);
+    this.generationId = generationId;
+  }
+
+  /**
+   * Constructor for a StorageResourceId representing a full StorageObject, including bucketName and
+   * objectName.
+   *
+   * @param bucketName   The bucket name of the resource. Must be non-empty and non-null.
+   * @param generationId The generationId to be used with precondition checks when using this
+   *                     StorageResourceId as an identifier for mutation requests.
+   */
+  StorageResourceId(String bucketName, long generationId) {
+    checkArgument(!isNullOrEmpty(bucketName), "bucketName must not be null or empty");
+    this.bucketName = bucketName;
+    this.objectName = null;
+    this.stringPath = StringPaths.fromComponents(bucketName, objectName);
+    this.generationId = generationId;
+  }
+
+  /**
+   * Returns true if this StorageResourceId represents a GCS StorageObject; if true, both {@code
+   * getBucketName} and {@code getObjectName} will be non-empty and non-null.
+   */
+  boolean isStorageObject() {
+    return bucketName != null && objectName != null;
+  }
+
+  /**
+   * Returns true if this StorageResourceId represents a GCS Bucket; if true, then {@code
+   * getObjectName} will return null.
+   */
+  boolean isBucket() {
+    return bucketName != null && objectName == null;
+  }
+
+  /**
+   * Returns true if this StorageResourceId represents the GCS root (gs://); if true, then both
+   * {@code getBucketName} and {@code getObjectName} will be null.
+   */
+  boolean isRoot() {
+    return bucketName == null && objectName == null;
+  }
+
+  /**
+   * Indicates if this StorageResourceId corresponds to a 'directory'; similar to {@link
+   * FileInfo#isDirectory} except deals entirely with pathnames instead of also checking for
+   * exists() to be true on a corresponding GoogleCloudStorageItemInfo.
+   */
+  boolean isDirectory() {
+    return isRoot() || isBucket() || StringPaths.isDirectoryPath(objectName);
+  }
+
+  /**
+   * Gets the bucket name component of this resource identifier.
+   */
+  String getBucketName() {
+    return bucketName;
+  }
+
+  /**
+   * Gets the object name component of this resource identifier.
+   */
+  String getObjectName() {
+    return objectName;
+  }
+
+  /**
+   * The generationId to be used with precondition checks when using this StorageResourceId as an
+   * identifier for mutation requests. The generationId is *not* used when determining equals() or
+   * hashCode().
+   */
+  long getGenerationId() {
+    return generationId;
+  }
+
+  /**
+   * Returns true if generationId is not UNKNOWN_GENERATION_ID.
+   */
+  boolean hasGenerationId() {
+    return generationId != UNKNOWN_GENERATION_ID;
+  }
+
+  /**
+   * Returns a string of the form {@code gs://<bucketName>/<objectName>}.
+   */
+  @Override
+  public String toString() {
+    return stringPath;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof StorageResourceId) {
+      StorageResourceId other = (StorageResourceId) obj;
+      return Objects.equals(bucketName, other.bucketName) && Objects.equals(objectName,
+          other.objectName);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return stringPath.hashCode();
+  }
+
+  /**
+   * Converts StorageResourceId instance to look like a directory path. If the path already looks
+   * like a directory path then this call is a no-op.
+   *
+   * @return A resourceId with a directory path corresponding to the given resourceId.
+   */
+  StorageResourceId toDirectoryId() {
+    if (isStorageObject() && !StringPaths.isDirectoryPath(getObjectName())) {
+      return new StorageResourceId(getBucketName(), StringPaths.toDirectoryPath(getObjectName()));
+    }
+    return this;
+  }
+
+  /**
+   * Parses {@link StorageResourceId} from specified string.
+   */
+  static StorageResourceId fromStringPath(String path) {
+    return fromStringPath(path, UNKNOWN_GENERATION_ID);
+  }
+
+  /**
+   * Parses {@link StorageResourceId} from specified string and generationId.
+   */
+  static StorageResourceId fromStringPath(String path, long generationId) {
+    checkArgument(path != null, "path must not be null");
+
+    Matcher matcher = GCS_PATH_PATTERN.matcher(path);
+    checkArgument(matcher.matches(), "'%s' is not a valid GCS object name.", path);
+
+    String bucketName = matcher.group(2);
+    String relativePath = matcher.group(4);
+    if (bucketName == null) {
+      checkArgument(generationId == UNKNOWN_GENERATION_ID,
+          "Cannot specify generationId '%s' for root object '%s'", generationId, path);
+      return ROOT;
+    } else if (relativePath != null) {
+      return new StorageResourceId(bucketName, relativePath, generationId);
+    }
+    checkArgument(generationId == UNKNOWN_GENERATION_ID,
+        "Cannot specify generationId '%s' for bucket '%s'", generationId, path);
+    return new StorageResourceId(bucketName);
+  }
+
+  /**
+   * Validates the given URI and if valid, returns the associated StorageResourceId.
+   *
+   * @param path                 The GCS URI to validate.
+   * @param allowEmptyObjectName If true, a missing object name is not considered invalid.
+   * @return a StorageResourceId that may be the GCS root, a Bucket, or a StorageObject.
+   */
+  static StorageResourceId fromUriPath(URI path, boolean allowEmptyObjectName) {
+    return fromUriPath(path, allowEmptyObjectName, UNKNOWN_GENERATION_ID);
+  }
+
+  /**
+   * Validates the given URI and if valid, returns the associated StorageResourceId.
+   *
+   * @param path                 The GCS URI to validate.
+   * @param allowEmptyObjectName If true, a missing object name is not considered invalid.
+   * @param generationId         The generationId to be used with precondition checks when
+   *                             using this
+   * @return a StorageResourceId that may be the GCS root, a Bucket, or a StorageObject.
+   */
+  static StorageResourceId fromUriPath(URI path, boolean allowEmptyObjectName,
+      long generationId) {
+    LOG.trace("fromUriPath('{}', {})", path, allowEmptyObjectName);
+    checkNotNull(path);
+
+    if (!SCHEME.equals(path.getScheme())) {
+      throw new IllegalArgumentException(
+          String.format("GCS path supports only '%s' scheme, instead got '%s' from '%s'.", SCHEME,
+              path.getScheme(), path));
+    }
+
+    if (path.equals(GoogleCloudStorageFileSystem.GCSROOT)) {
+      return ROOT;
+    }
+
+    String bucketName = StringPaths.validateBucketName(path.getAuthority());
+    // Note that we're using getPath here instead of rawPath, etc. This is because it is assumed
+    // that the path was properly encoded in getPath (or another similar method):
+    String objectName = StringPaths.validateObjectName(path.getPath(), allowEmptyObjectName);
+
+    return isNullOrEmpty(objectName) ?
+        new StorageResourceId(bucketName, generationId) :
+        new StorageResourceId(bucketName, objectName, generationId);
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/StringPaths.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/StringPaths.java
new file mode 100644
index 00000000000..80682c3ed2a
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/StringPaths.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.hadoop.fs.gs;
+
+import static org.apache.hadoop.thirdparty.com.google.common.base.Strings.isNullOrEmpty;
+import static org.apache.hadoop.fs.gs.Constants.PATH_DELIMITER;
+
+import org.apache.hadoop.thirdparty.com.google.common.base.CharMatcher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility methods for String GCS paths.
+ */
+final class StringPaths {
+
+  public static final Logger LOG = LoggerFactory.getLogger(StorageResourceId.class);
+
+  private StringPaths() {
+  }
+
+  // 14x faster (20ns vs 280ns) than "^[a-z0-9_.-]+$" regex
+  private static final CharMatcher BUCKET_NAME_CHAR_MATCHER = CharMatcher.ascii()
+      .and(CharMatcher.inRange('0', '9').or(CharMatcher.inRange('a', 'z'))
+          .or(CharMatcher.anyOf("_.-")))
+      .precomputed();
+
+  /**
+   * Validate the given bucket name to make sure that it can be used as a part of a file system
+   * path.
+   *
+   * <p>Note: this is not designed to duplicate the exact checks that GCS would perform on the
+   * server side. We make some checks that are relevant to using GCS as a file system.
+   *
+   * @param bucketName Bucket name to check.
+   */
+  static String validateBucketName(String bucketName) {
+    // If the name ends with '/', remove it.
+    bucketName = toFilePath(bucketName);
+
+    if (isNullOrEmpty(bucketName)) {
+      throw new IllegalArgumentException("GCS bucket name cannot be empty.");
+    }
+
+    if (!BUCKET_NAME_CHAR_MATCHER.matchesAllOf(bucketName)) {
+      throw new IllegalArgumentException(String.format(
+          "Invalid GCS bucket name '%s': bucket name must contain only 'a-z0-9_.-' characters.",
+          bucketName));
+    }
+
+    return bucketName;
+  }
+
+  /**
+   * Validate the given object name to make sure that it can be used as a part of a file system
+   * path.
+   *
+   * <p>Note: this is not designed to duplicate the exact checks that GCS would perform on the
+   * server side. We make some checks that are relevant to using GCS as a file system.
+   *
+   * @param objectName           Object name to check.
+   * @param allowEmptyObjectName If true, a missing object name is not considered invalid.
+   */
+  static String validateObjectName(String objectName, boolean allowEmptyObjectName) {
+    LOG.trace("validateObjectName('{}', {})", objectName, allowEmptyObjectName);
+
+    if (isNullOrEmpty(objectName) || objectName.equals(PATH_DELIMITER)) {
+      if (allowEmptyObjectName) {
+        objectName = "";
+      } else {
+        throw new IllegalArgumentException(String.format(
+            "GCS path must include non-empty object name [objectName='%s',"
+                + " allowEmptyObjectName=%s]", objectName, allowEmptyObjectName));
+      }
+    }
+
+    // We want objectName to look like a traditional file system path,
+    // therefore, disallow objectName with consecutive '/' chars.
+    for (int i = 0; i < (objectName.length() - 1); i++) {
+      if (objectName.charAt(i) == '/' && objectName.charAt(i + 1) == '/') {
+        throw new IllegalArgumentException(
+            String.format("GCS path must not have consecutive '/' characters: '%s'", objectName));
+      }
+    }
+
+    // Remove leading '/' if it exists.
+    if (objectName.startsWith(PATH_DELIMITER)) {
+      objectName = objectName.substring(1);
+    }
+
+    LOG.trace("validateObjectName -> '{}'", objectName);
+    return objectName;
+  }
+
+  /**
+   * Helper for standardizing the way various human-readable messages in logs/exceptions that refer
+   * to a bucket/object pair.
+   */
+  public static String fromComponents(String bucketName, String objectName) {
+    if (bucketName == null && objectName != null) {
+      throw new IllegalArgumentException(
+          String.format("Invalid bucketName/objectName pair: gs://%s/%s", "<null>", objectName));
+    }
+    // TODO(user): Unify this method with other methods that convert bucketName/objectName
+    // to a URI; maybe use the single slash for compatibility.
+    StringBuilder result = new StringBuilder("gs://");
+    if (bucketName != null) {
+      result.append(bucketName);
+    }
+    if (objectName != null) {
+      result.append('/').append(objectName);
+    }
+    return result.toString();
+  }
+
+  /**
+   * Indicates whether the given object name looks like a directory path.
+   *
+   * @param path Name of the object to inspect.
+   * @return Whether the given object name looks like a directory path.
+   */
+  public static boolean isDirectoryPath(String path) {
+    return !isNullOrEmpty(path) && path.endsWith(PATH_DELIMITER);
+  }
+
+  /**
+   * Converts the given object name to look like a file path. If the object name already looks like
+   * a file path then this call is a no-op.
+   *
+   * <p>If the object name is null or empty, it is returned as-is.
+   *
+   * @param path Name of the object to inspect.
+   * @return File path for the given path.
+   */
+  public static String toFilePath(String path) {
+    return !isNullOrEmpty(path) && isDirectoryPath(path) ?
+        path.substring(0, path.length() - 1) :
+        path;
+  }
+
+  /**
+   * Converts the given object name to look like a directory path. If the object name already looks
+   * like a directory path then this call is a no-op.
+   *
+   * <p>If the object name is null or empty, it is returned as-is.
+   *
+   * @param path Name of the object to inspect.
+   * @return Directory path for the given path.
+   */
+  static String toDirectoryPath(String path) {
+    return isNullOrEmpty(path) || isDirectoryPath(path) ? path : path + PATH_DELIMITER;
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/UriPaths.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/UriPaths.java
new file mode 100644
index 00000000000..30e13cb33cb
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/UriPaths.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.hadoop.fs.gs;
+
+import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.hadoop.fs.gs.Constants.PATH_DELIMITER;
+import static org.apache.hadoop.fs.gs.Constants.SCHEME;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * Utility methods for URI GCS paths.
+ */
+final class UriPaths {
+
+  private UriPaths() {
+  }
+
+  /**
+   * Converts the given path to look like a directory path. If the path already looks like a
+   * directory path then this call is a no-op.
+   *
+   * @param path Path to convert.
+   * @return Directory path for the given path.
+   */
+  public static URI toDirectory(URI path) {
+    StorageResourceId resourceId = StorageResourceId.fromUriPath(path, true);
+
+    if (resourceId.isStorageObject() && !resourceId.isDirectory()) {
+      resourceId = resourceId.toDirectoryId();
+      path = fromResourceId(resourceId, /* allowEmptyObjectName= */ false);
+    }
+    return path;
+  }
+
+  /**
+   * Gets the parent directory of the given path.
+   *
+   * @param path Path to convert.
+   * @return Path of parent directory of the given item or null for root path.
+   */
+  public static URI getParentPath(URI path) {
+    checkNotNull(path);
+
+    // Root path has no parent.
+    if (path.equals(GoogleCloudStorageFileSystem.GCSROOT)) {
+      return null;
+    }
+
+    StorageResourceId resourceId = StorageResourceId.fromUriPath(path, true);
+
+    if (resourceId.isBucket()) {
+      return GoogleCloudStorageFileSystem.GCSROOT;
+    }
+
+    String objectName = resourceId.getObjectName();
+    int index = StringPaths.isDirectoryPath(objectName) ?
+        objectName.lastIndexOf(PATH_DELIMITER, objectName.length() - 2) :
+        objectName.lastIndexOf(PATH_DELIMITER);
+    return index < 0 ?
+        fromStringPathComponents(resourceId.getBucketName(), /* objectName= */
+            null, /* allowEmptyObjectName= */ true) :
+        fromStringPathComponents(resourceId.getBucketName(), objectName.substring(0, index + 1),
+            /* allowEmptyObjectName= */ false);
+  }
+
+  /**
+   * Constructs and returns full path for the given bucket and object names.
+   */
+  public static URI fromResourceId(StorageResourceId resourceId, boolean allowEmptyObjectName) {
+    return fromStringPathComponents(resourceId.getBucketName(), resourceId.getObjectName(),
+        allowEmptyObjectName);
+  }
+
+  /**
+   * Constructs and returns full path for the given bucket and object names.
+   */
+  public static URI fromStringPathComponents(String bucketName, String objectName,
+      boolean allowEmptyObjectName) {
+    if (allowEmptyObjectName && bucketName == null && objectName == null) {
+      return GoogleCloudStorageFileSystem.GCSROOT;
+    }
+
+    String authority = StringPaths.validateBucketName(bucketName);
+    String path = PATH_DELIMITER + StringPaths.validateObjectName(objectName, allowEmptyObjectName);
+
+    try {
+      return new URI(SCHEME, authority, path,
+          /* query= */ null,
+          /* fragment= */ null);
+    } catch (URISyntaxException e) {
+      throw new IllegalArgumentException(
+          String.format("Invalid bucket name (%s) or object name (%s)", bucketName, objectName), e);
+    }
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/VerificationAttributes.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/VerificationAttributes.java
new file mode 100644
index 00000000000..4155482fc7d
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/VerificationAttributes.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * 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 org.apache.hadoop.fs.gs;
+
+import java.util.Arrays;
+import javax.annotation.Nullable;
+
+/**
+ * GCS provided validation attributes for a single object.
+ */
+class VerificationAttributes {
+  private final byte[] md5hash;
+  private final byte[] crc32c;
+
+  VerificationAttributes(@Nullable byte[] md5hash, @Nullable byte[] crc32c) {
+    this.md5hash = md5hash;
+    this.crc32c = crc32c;
+  }
+
+  /**
+   * MD5 hash of an object, if available.
+   */
+  @Nullable
+  byte[] getMd5hash() {
+    return md5hash;
+  }
+
+  /**
+   * CRC32c checksum of an object, if available.
+   */
+  @Nullable
+  byte[] getCrc32c() {
+    return crc32c;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof VerificationAttributes)) {
+      return false;
+    }
+    VerificationAttributes that = (VerificationAttributes) o;
+    return Arrays.equals(md5hash, that.md5hash) && Arrays.equals(crc32c, that.crc32c);
+  }
+
+  @Override
+  public int hashCode() {
+    int result = Arrays.hashCode(md5hash);
+    result = 31 * result + Arrays.hashCode(crc32c);
+    return result;
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/package-info.java b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/package-info.java
new file mode 100644
index 00000000000..eedfb7822ac
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/main/java/org/apache/hadoop/fs/gs/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Google Cloud Storage Filesystem.
+ */
+
+package org.apache.hadoop.fs.gs;
\ No newline at end of file
diff --git a/hadoop-tools/hadoop-gcp/src/test/java/org/apache/hadoop/fs/gs/TestStorageResourceId.java b/hadoop-tools/hadoop-gcp/src/test/java/org/apache/hadoop/fs/gs/TestStorageResourceId.java
new file mode 100644
index 00000000000..e0a39b2d7e4
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/test/java/org/apache/hadoop/fs/gs/TestStorageResourceId.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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 org.apache.hadoop.fs.gs;
+
+import java.net.URI;
+
+import org.junit.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class TestStorageResourceId {
+  @Test
+  public void testConstructorInvalid() {
+    assertThrows(IllegalArgumentException.class, () -> {
+      new StorageResourceId(null);
+    });
+
+    assertThrows(IllegalArgumentException.class, () -> {
+      new StorageResourceId("");
+    });
+
+    assertThrows(IllegalArgumentException.class, () -> {
+      new StorageResourceId(null, null);
+    });
+
+    assertThrows(IllegalArgumentException.class, () -> {
+      new StorageResourceId("foo", null);
+    });
+
+    assertThrows(IllegalArgumentException.class, () -> {
+      new StorageResourceId("", null);
+    });
+
+    assertThrows(IllegalArgumentException.class, () -> {
+      new StorageResourceId(null, null, 0L);
+    });
+
+    assertThrows(IllegalArgumentException.class, () -> {
+      new StorageResourceId("foo", null, 0L);
+    });
+
+    assertThrows(IllegalArgumentException.class, () -> {
+      new StorageResourceId("", null, 0L);
+    });
+
+    assertThrows(IllegalArgumentException.class, () -> {
+      new StorageResourceId(null, 0L);
+    });
+
+    assertThrows(IllegalArgumentException.class, () -> {
+      new StorageResourceId("", 0L);
+    });
+  }
+
+  @Test
+  public void testFromStringPathInvalid() {
+    assertThrows(IllegalArgumentException.class, () -> {
+      StorageResourceId.fromStringPath(null);
+    });
+
+    assertThrows(IllegalArgumentException.class, () -> {
+      StorageResourceId.fromStringPath("");
+    });
+
+    assertThrows(IllegalArgumentException.class, () -> {
+      StorageResourceId.fromStringPath("foo");
+    });
+
+    assertThrows(IllegalArgumentException.class, () -> {
+      StorageResourceId.fromStringPath("/foo/bar");
+    });
+
+    assertThrows(IllegalArgumentException.class, () -> {
+      StorageResourceId.fromStringPath("gs:///foo/bar");
+    });
+  }
+
+  @Test
+  public void testConstructor() {
+    String bucketName = "testbucketname";
+    String objectName = "a/b/c.txt";
+
+    verify(new StorageResourceId(bucketName), bucketName,
+        StorageResourceId.UNKNOWN_GENERATION_ID, null, false,
+        true, true, false, false);
+
+    verify(new StorageResourceId(bucketName, objectName), bucketName,
+        StorageResourceId.UNKNOWN_GENERATION_ID, objectName, false,
+        false, false, true, false);
+
+    long genId = System.currentTimeMillis();
+    verify(new StorageResourceId(bucketName, objectName, genId), bucketName,
+        genId, objectName, true,
+        false, false, true, false);
+
+    verify(new StorageResourceId(bucketName, genId), bucketName,
+        genId, null, true,
+        true, true, false, false);
+  }
+
+  @Test
+  public void testEqualsBucket() {
+    StorageResourceId bucket1 = new StorageResourceId("test-bucket");
+    StorageResourceId bucket2 = new StorageResourceId("test-bucket");
+    assertTrue(bucket1.equals(bucket2));
+    assertEquals(bucket1.hashCode(), bucket2.hashCode());
+  }
+
+  @Test
+  public void testEqualsObject() {
+    StorageResourceId obj1 = new StorageResourceId("test-bucket", "test-object");
+    StorageResourceId obj2 = new StorageResourceId("test-bucket", "test-object");
+    assertTrue(obj1.equals(obj2));
+    assertEquals(obj1.hashCode(), obj2.hashCode());
+  }
+
+  @Test
+  public void testEqualsDifferentBucket() {
+    StorageResourceId bucket1 = new StorageResourceId("test-bucket");
+    StorageResourceId bucket2 = new StorageResourceId("other-bucket");
+    assertFalse(bucket1.equals(bucket2));
+  }
+
+  @Test
+  public void testEqualsDifferentObject() {
+    StorageResourceId obj1 = new StorageResourceId("test-bucket", "test-object");
+    StorageResourceId obj2 = new StorageResourceId("test-bucket", "other-object");
+    assertFalse(obj1.equals(obj2));
+  }
+
+  @Test
+  public void testToDirectoryIdFromFile() {
+    StorageResourceId fileId = new StorageResourceId("my-bucket", "path/to/file.txt");
+    StorageResourceId dirId = fileId.toDirectoryId();
+
+    assertNotSame(fileId, dirId); // Should return a new instance
+    assertTrue(dirId.isDirectory());
+    assertEquals("my-bucket", dirId.getBucketName());
+    assertEquals("path/to/file.txt/", dirId.getObjectName());
+    assertEquals(fileId.getGenerationId(), dirId.getGenerationId());
+  }
+
+  @Test
+  public void testToDirectoryIdFromDirectoryObject() {
+    StorageResourceId dirIdOriginal = new StorageResourceId("my-bucket", "path/to/dir/");
+    StorageResourceId dirIdConverted = dirIdOriginal.toDirectoryId();
+
+    assertSame(dirIdOriginal, dirIdConverted); // Should return the same instance
+    assertTrue(dirIdConverted.isDirectory());
+    assertEquals("path/to/dir/", dirIdConverted.getObjectName());
+  }
+
+  @Test
+  public void testToDirectoryIdFromBucket() {
+    StorageResourceId bucketId = new StorageResourceId("my-bucket");
+    StorageResourceId convertedId = bucketId.toDirectoryId();
+    assertSame(bucketId, convertedId);
+    assertTrue(convertedId.isBucket());
+  }
+
+  @Test
+  public void testFromStringPathRoot() {
+    StorageResourceId id = StorageResourceId.fromStringPath("gs://");
+    assertTrue(id.isRoot());
+  }
+
+  @Test
+  public void testFromStringPathBucket() {
+    StorageResourceId id = StorageResourceId.fromStringPath("gs://my-bucket");
+    assertTrue(id.isBucket());
+    assertEquals("my-bucket", id.getBucketName());
+    assertNull(id.getObjectName());
+    assertEquals(StorageResourceId.UNKNOWN_GENERATION_ID, id.getGenerationId());
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {
+      "gs://my-bucket/object",
+      "gs://my-bucket/folder/file.txt",
+      "gs://my-bucket/folder/"
+  })
+  public void testFromStringPathObject(String path) {
+    String expectedBucket = path.split("/")[2];
+    String expectedObject =
+        path.substring(path.indexOf(expectedBucket) + expectedBucket.length() + 1);
+
+    StorageResourceId id = StorageResourceId.fromStringPath(path);
+    assertTrue(id.isStorageObject());
+    assertEquals(expectedBucket, id.getBucketName());
+    assertEquals(expectedObject, id.getObjectName());
+    assertEquals(StorageResourceId.UNKNOWN_GENERATION_ID, id.getGenerationId());
+  }
+
+  @Test
+  public void testFromStringPathObjectWithGenerationId() {
+    long genId = 12345L;
+    StorageResourceId id = StorageResourceId.fromStringPath("gs://my-bucket/object.txt", genId);
+    assertTrue(id.isStorageObject());
+    assertEquals("my-bucket", id.getBucketName());
+    assertEquals("object.txt", id.getObjectName());
+    assertEquals(genId, id.getGenerationId());
+    assertTrue(id.hasGenerationId());
+  }
+
+  @Test
+  public void testFromUriPathBucket() throws Exception {
+    URI uri = new URI("gs://my-bucket");
+    StorageResourceId id = StorageResourceId.fromUriPath(uri, true);
+    assertTrue(id.isBucket());
+    assertEquals("my-bucket", id.getBucketName());
+    assertNull(id.getObjectName());
+  }
+
+  @Test
+  public void testFromUriPathObject() throws Exception {
+    URI uri = new URI("gs://my-bucket/path/to/file.txt");
+    StorageResourceId id = StorageResourceId.fromUriPath(uri, false);
+    assertTrue(id.isStorageObject());
+    assertEquals("my-bucket", id.getBucketName());
+    assertEquals("path/to/file.txt", id.getObjectName());
+  }
+
+  @Test
+  public void testFromUriPathObjectWithGenerationId() throws Exception {
+    URI uri = new URI("gs://my-bucket/object.txt");
+    long genId = 54321L;
+    StorageResourceId id = StorageResourceId.fromUriPath(uri, false, genId);
+    assertTrue(id.isStorageObject());
+    assertEquals("my-bucket", id.getBucketName());
+    assertEquals("object.txt", id.getObjectName());
+    assertEquals(genId, id.getGenerationId());
+    assertTrue(id.hasGenerationId());
+  }
+
+  @Test
+  public void testFromUriPathBucketWithGenerationId() throws Exception {
+    assertThrows(IllegalArgumentException.class, () -> {
+      URI uri = new URI("gs://my-bucket");
+      long genId = 54321L;
+      StorageResourceId.fromUriPath(uri, false, genId);
+    });
+  }
+
+  private static void verify(
+      StorageResourceId id,
+      String bucketName,
+      long generationId,
+      String objectName,
+      boolean hasGenerationId,
+      boolean isBucket,
+      boolean isDirectory,
+      boolean isStorageObject,
+      boolean isRoot) {
+    assertEquals(bucketName, id.getBucketName());
+    assertEquals(generationId, id.getGenerationId());
+    assertEquals(objectName, id.getObjectName());
+    assertEquals(hasGenerationId, id.hasGenerationId());
+    assertEquals(isBucket, id.isBucket());
+    assertEquals(isDirectory, id.isDirectory());
+    assertEquals(isStorageObject, id.isStorageObject());
+    assertEquals(isRoot, id.isRoot());
+  }
+}
diff --git a/hadoop-tools/hadoop-gcp/src/test/java/org/apache/hadoop/fs/gs/TestStringPaths.java b/hadoop-tools/hadoop-gcp/src/test/java/org/apache/hadoop/fs/gs/TestStringPaths.java
new file mode 100644
index 00000000000..16234e0ce1d
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/test/java/org/apache/hadoop/fs/gs/TestStringPaths.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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 org.apache.hadoop.fs.gs;
+
+import org.junit.Test;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+public class TestStringPaths {
+  @Test
+  public void testValidateBucketNameValid() {
+    assertEquals("my-bucket", StringPaths.validateBucketName("my-bucket"));
+    assertEquals("my.bucket", StringPaths.validateBucketName("my.bucket"));
+    assertEquals("my_bucket", StringPaths.validateBucketName("my_bucket"));
+    assertEquals("bucket123", StringPaths.validateBucketName("bucket123"));
+    assertEquals("a", StringPaths.validateBucketName("a"));
+    assertEquals("long-bucket-name-with-numbers-123",
+        StringPaths.validateBucketName("long-bucket-name-with-numbers-123"));
+  }
+
+  @Test
+  public void testValidateBucketNameEndsWithSlash() {
+    assertEquals("my-bucket", StringPaths.validateBucketName("my-bucket/"));
+    assertEquals("another-bucket", StringPaths.validateBucketName("another-bucket/"));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testValidateBucketNameEmpty() {
+    StringPaths.validateBucketName("");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testValidateBucketNameNull() {
+    StringPaths.validateBucketName(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testValidateBucketNameInvalidChars() {
+    StringPaths.validateBucketName("my bucket"); // Space
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testValidateBucketNameInvalidChars2() {
+    StringPaths.validateBucketName("my@bucket"); // @ symbol
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testValidateBucketNameUpperCase() {
+    StringPaths.validateBucketName("MyBucket"); // Uppercase
+  }
+
+  @Test
+  public void testValidateObjectNameValid() {
+    assertEquals("path/to/object",
+        StringPaths.validateObjectName("path/to/object", false));
+    assertEquals("object", StringPaths.validateObjectName("object", false));
+    assertEquals("dir/",
+        StringPaths.validateObjectName("dir/", false)); // Still valid after validation
+    assertEquals("", StringPaths.validateObjectName("/", true)); // Slash becomes empty if allowed
+    assertEquals("", StringPaths.validateObjectName("", true));
+  }
+
+  @Test
+  public void testValidateObjectNameLeadingSlash() {
+    assertEquals("path/to/object", StringPaths.validateObjectName("/path/to/object", false));
+    assertEquals("object", StringPaths.validateObjectName("/object", false));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testValidateObjectNameEmptyNotAllowed() {
+    StringPaths.validateObjectName("", false);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testValidateObjectNameNullNotAllowed() {
+    StringPaths.validateObjectName(null, false);
+  }
+
+  @Test
+  public void testValidateObjectNameEmptyAllowed() {
+    assertEquals("", StringPaths.validateObjectName("", true));
+    assertEquals("", StringPaths.validateObjectName(null, true));
+    assertEquals("", StringPaths.validateObjectName("/", true)); // Single slash becomes empty
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testValidateObjectNameConsecutiveSlashes() {
+    StringPaths.validateObjectName("path//to/object", false);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testValidateObjectNameConsecutiveSlashesAtStart() {
+    StringPaths.validateObjectName("//path/to/object", false);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testValidateObjectNameConsecutiveSlashesAtEnd() {
+    StringPaths.validateObjectName("path/to/object//", false);
+  }
+
+  @Test
+  public void testFromComponentsValid() {
+    assertEquals("gs://my-bucket/path/to/object",
+        StringPaths.fromComponents("my-bucket", "path/to/object"));
+    assertEquals("gs://my-bucket/dir/", StringPaths.fromComponents("my-bucket", "dir/"));
+    assertEquals("gs://my-bucket/", StringPaths.fromComponents("my-bucket", ""));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testFromComponentsNullBucketNonNullObject() {
+    StringPaths.fromComponents(null, "path/to/object");
+  }
+
+  @Test
+  public void testFromComponentsNullBucketAndObject() {
+    assertEquals("gs://", StringPaths.fromComponents(null, null));
+  }
+
+  @Test
+  public void testIsDirectoryPath() {
+    assertTrue(StringPaths.isDirectoryPath("dir/"));
+    assertTrue(StringPaths.isDirectoryPath("path/to/dir/"));
+    assertFalse(StringPaths.isDirectoryPath("file.txt"));
+    assertFalse(StringPaths.isDirectoryPath("path/to/file.txt"));
+    assertFalse(StringPaths.isDirectoryPath(""));
+    assertFalse(StringPaths.isDirectoryPath(null));
+  }
+
+  @Test
+  public void testToFilePath() {
+    assertEquals("path/to/file", StringPaths.toFilePath("path/to/file/"));
+    assertEquals("file.txt", StringPaths.toFilePath("file.txt"));
+    assertEquals("dir", StringPaths.toFilePath("dir/"));
+    assertEquals("", StringPaths.toFilePath(""));
+    assertNull(StringPaths.toFilePath(null));
+  }
+
+  // --- Tests for toDirectoryPath ---
+
+  @Test
+  public void testToDirectoryPath() {
+    assertEquals("path/to/dir/", StringPaths.toDirectoryPath("path/to/dir"));
+    assertEquals("dir/", StringPaths.toDirectoryPath("dir/"));
+    assertEquals("file/", StringPaths.toDirectoryPath("file"));
+    assertEquals("", StringPaths.toDirectoryPath(""));
+    assertNull(StringPaths.toDirectoryPath(null));
+  }
+}
\ No newline at end of file
diff --git a/hadoop-tools/hadoop-gcp/src/test/java/org/apache/hadoop/fs/gs/TestUriPaths.java b/hadoop-tools/hadoop-gcp/src/test/java/org/apache/hadoop/fs/gs/TestUriPaths.java
new file mode 100644
index 00000000000..fe93a28dc43
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/test/java/org/apache/hadoop/fs/gs/TestUriPaths.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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 org.apache.hadoop.fs.gs;
+
+import java.net.URI;
+
+import org.junit.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TestUriPaths {
+  @Test
+  public void testToDirectoryFile() throws Exception {
+    URI fileUri = new URI("gs://my-bucket/path/to/file.txt");
+    URI expectedDirUri = new URI("gs://my-bucket/path/to/file.txt/");
+    // Temporarily override the behavior for testing purposes
+    // This is not a clean mocking strategy for static methods, but demonstrates the test intent.
+    // In a real environment, you'd use PowerMock or refactor.
+    URI result = UriPaths.toDirectory(fileUri);
+    assertEquals(expectedDirUri, result);
+  }
+
+  @Test
+  public void testToDirectoryAlreadyDirectory() throws Exception {
+    URI dirUri = new URI("gs://my-bucket/path/to/dir/");
+    URI result = UriPaths.toDirectory(dirUri);
+    assertEquals(dirUri, result);
+  }
+
+  @Test
+  public void testToDirectoryRootBucket() throws Exception {
+    URI bucketUri = new URI("gs://my-bucket");
+    URI result = UriPaths.toDirectory(bucketUri);
+    assertEquals(bucketUri, result); // Buckets are implicitly directories
+  }
+
+  @Test
+  public void testGetParentPathFile() throws Exception {
+    URI uri = new URI("gs://my-bucket/path/to/file.txt");
+    URI expectedParent = new URI("gs://my-bucket/path/to/");
+    assertEquals(expectedParent, UriPaths.getParentPath(uri));
+  }
+
+  @Test
+  public void testGetParentPathDirectory() throws Exception {
+    URI uri = new URI("gs://my-bucket/path/to/dir/");
+    URI expectedParent = new URI("gs://my-bucket/path/to/");
+    assertEquals(expectedParent, UriPaths.getParentPath(uri));
+  }
+
+  @Test
+  public void testGetParentPathObjectAtBucketRoot() throws Exception {
+    URI uri = new URI("gs://my-bucket/file.txt");
+    URI expectedParent = new URI("gs://my-bucket/");
+    assertEquals(expectedParent, UriPaths.getParentPath(uri));
+  }
+
+  @Test
+  public void testGetParentPathDirectoryAtBucketRoot() throws Exception {
+    URI uri = new URI("gs://my-bucket/dir/");
+    URI expectedParent = new URI("gs://my-bucket/");
+    assertEquals(expectedParent, UriPaths.getParentPath(uri));
+  }
+
+  @Test
+  public void testGetParentPathBucket() throws Exception {
+    URI uri = new URI("gs://my-bucket");
+    assertEquals(GoogleCloudStorageFileSystem.GCSROOT, UriPaths.getParentPath(uri));
+  }
+
+  @Test
+  public void testFromResourceIdObject() throws Exception {
+    StorageResourceId resourceId = new StorageResourceId("my-bucket", "path/to/object");
+    URI expectedUri = new URI("gs://my-bucket/path/to/object");
+    assertEquals(expectedUri, UriPaths.fromResourceId(resourceId, false));
+  }
+
+  @Test
+  public void testFromResourceIdDirectory() throws Exception {
+    StorageResourceId resourceId = new StorageResourceId("my-bucket", "path/to/dir/");
+    URI expectedUri = new URI("gs://my-bucket/path/to/dir/");
+    assertEquals(expectedUri, UriPaths.fromResourceId(resourceId, false));
+  }
+
+  @Test
+  public void testFromResourceIdBucket() throws Exception {
+    StorageResourceId resourceId = new StorageResourceId("my-bucket");
+    URI expectedUri = new URI("gs://my-bucket/");
+    assertEquals(expectedUri, UriPaths.fromResourceId(resourceId, true));
+  }
+
+  @Test
+  public void testFromResourceIdEmptyObjectAllowed() throws Exception {
+    StorageResourceId resourceId = new StorageResourceId("my-bucket");
+    URI expectedUri = new URI("gs://my-bucket/");
+    assertEquals(expectedUri, UriPaths.fromResourceId(resourceId, true));
+  }
+
+  @Test
+  public void testFromResourceIdNullObjectAllowed() throws Exception {
+    StorageResourceId resourceId = new StorageResourceId("my-bucket");
+    URI expectedUri = new URI("gs://my-bucket/");
+    assertEquals(expectedUri, UriPaths.fromResourceId(resourceId, true));
+  }
+
+  @Test
+  public void testFromStringPathComponentsValid() throws Exception {
+    assertEquals(new URI("gs://my-bucket/path/to/object"),
+        UriPaths.fromStringPathComponents("my-bucket", "path/to/object", false));
+    assertEquals(new URI("gs://my-bucket/path/to/dir/"),
+        UriPaths.fromStringPathComponents("my-bucket", "path/to/dir/", false));
+    assertEquals(new URI("gs://my-bucket/"),
+        UriPaths.fromStringPathComponents("my-bucket", null, true));
+    assertEquals(new URI("gs://my-bucket/"),
+        UriPaths.fromStringPathComponents("my-bucket", "", true));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testFromStringPathComponentsNullBucketNameNotAllowed() {
+    UriPaths.fromStringPathComponents(null, "object", false);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testFromStringPathComponentsEmptyObjectNameNotAllowed() {
+    UriPaths.fromStringPathComponents("my-bucket", "", false);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testFromStringPathComponentsConsecutiveSlashes() {
+    UriPaths.fromStringPathComponents("my-bucket", "path//to/object", false);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testFromStringPathComponentsInvalidBucketName() {
+    UriPaths.fromStringPathComponents("MyBucket", "object", false); // Uppercase
+  }
+}
\ No newline at end of file
diff --git a/hadoop-tools/hadoop-gcp/src/test/java/org/apache/hadoop/fs/gs/package-info.java b/hadoop-tools/hadoop-gcp/src/test/java/org/apache/hadoop/fs/gs/package-info.java
new file mode 100644
index 00000000000..fe289cc6d3d
--- /dev/null
+++ b/hadoop-tools/hadoop-gcp/src/test/java/org/apache/hadoop/fs/gs/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Google Cloud Storage Filesystem tests.
+ */
+package org.apache.hadoop.fs.gs;
\ No newline at end of file
diff --git a/hadoop-tools/pom.xml b/hadoop-tools/pom.xml
index 8c1256a177c..70df9b02d0f 100644
--- a/hadoop-tools/pom.xml
+++ b/hadoop-tools/pom.xml
@@ -53,6 +53,7 @@
     <module>hadoop-fs2img</module>
     <module>hadoop-benchmark</module>
     <module>hadoop-compat-bench</module>
+    <module>hadoop-gcp</module>
   </modules>
 
   <build>


---------------------------------------------------------------------
To unsubscribe, e-mail: common-commits-unsubscribe@hadoop.apache.org
For additional commands, e-mail: common-commits-help@hadoop.apache.org