Repository: manoss96/fluke (language: python)

Issue:
30 create filter param in methods Dir.transfer to



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e11df61..9f304eb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,6 +20,30 @@ jobs:
     steps:
       - uses: actions/checkout@v3
 
+      - name: Set up fake SSH server
+        run: |
+          docker run -d \
+          -p 2222:2222 \
+          --env PUID=$(id -u $(whoami)) \
+          --env PGID=$(id -g $(whoami)) \
+          --env PASSWORD_ACCESS=true \
+          --env USER_NAME=test \
+          --env USER_PASSWORD=test \
+          --mount type=bind,\
+          src=${{ github.workspace }}/tests/test_files,\
+          dst=/tests/test_files \
+          linuxserver/openssh-server:amd64-version-9.3_p2-r0
+
+      - name: Set up fake GCS server
+        run: |
+          docker run -d \
+          -p 4443:4443 \
+          --mount type=bind,\
+          src=${{ github.workspace }}/tests/test_files,\
+          dst=/data/bucket/tests/test_files \
+          fsouza/fake-gcs-server:1.47.0 \
+          -external-url https://127.0.0.1:4443
+
       - name: Set up Python ${{ matrix.python-version }}
         uses: actions/setup-python@v4
         with:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14973af..1dbf5d3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,81 @@
 # Change Log
 All notable changes to this project will be documented in this file.
 
+## [0.5.0] - 2023/08/20
+
+### Added
+
+- There now exist classes ``fluke.storage.GCPStorageFile``
+  and ``fluke.storage.GCPStorageDir`` in order to interact
+  with Google Cloud Storage buckets. Furthermore, a class
+  ``fluke.auth.GCPAuth`` has been added in order to be able
+  to authenticate with GCP.
+  (https://github.com/manoss96/fluke/issues/10)
+
+- Added a new dependency: ``google-cloud-storage==2.10.0``
+  (https://github.com/manoss96/fluke/issues/10)
+
+- Class constructors ``fluke.storage.{AmazonS3File,AzureBlobFile,GCPStorageFile}``
+  now receive a ``load_metadata`` parameter that can be used in order to load
+  an object's metadata during instantiation.
+  (https://github.com/manoss96/fluke/issues/46)
+
+- Added method ``cat`` to *File* API that can be used
+  in order to print the contents of a file as text.
+  (https://github.com/manoss96/fluke/issues/58)
+
+### Changed
+
+- Methods ``Dir/File.transfer_to`` will now throw an ``InvalidChunkSizeError``
+  exceptions if a chunk size that is not supported by the "destination"
+  directory has been specified.
+  (https://github.com/manoss96/fluke/issues/48)
+
+- Method ``Dir.ls`` will now print a prettier output when
+  parameter ``recursively`` has been set to ``True``.
+  (https://github.com/manoss96/fluke/issues/50)
+
+- Class constructor ``fluke.auth.RemoteAuth`` no longer receives
+  string parameters ``public_key`` and ``key_type``. Instead, it
+  receives a single ``public_key`` parameter of type
+  ``RemoteAuth.PublicKey`` which can be generated via one of
+  the ``RemoteAuth.PublicKey`` class' "generate" methods.
+  (https://github.com/manoss96/fluke/issues/53)
+
+- Any contents of ``fluke.storage.RemoteDir`` will now be
+  displayed sorted by an ascending order no matter the
+  underlying remote system.
+  (https://github.com/manoss96/fluke/issues/53)
+
+- ``fluke.storage.RemoteDir`` will not follow any symbolic
+  links from now on.
+  (https://github.com/manoss96/fluke/issues/60)
+
+
+### Fixed
+
+- Fixed issue where referencing the root directory ``/``
+  through either ``fluke.storage.LocalDir`` or
+  ``fluke.storage.RemoteDir`` resulted in an
+  ``InvalidPathError`` exception being raised.
+  (https://github.com/manoss96/fluke/issues/52)
+
+- Fixed issue where it was impossible to establish a connection
+  to a remote host if their public key had been previously
+  added to the system's "known_hosts" file, though their
+  public key had been changed since then.
+  (https://github.com/manoss96/fluke/issues/53)
+
+- Fixed bug that occurred when caching the root directory
+  of any storage system.
+  (https://github.com/manoss96/fluke/issues/56)
+
+- Fixed bug where activating the cache on ``fluke.storage.RemoteDir``
+  would sometimes result in getting different contents due to
+  symbolic linked being followed.
+  (https://github.com/manoss96/fluke/issues/60)
+  
+
 ## [0.4.1] - 2023/07/25
 
 ### Changed
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f3a0e5c..e46feec 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,28 +12,113 @@ you should be able to set up a separate development environment just for Fluke.
 fastest way to do this would be the following:
 
 1. Make sure that Python 3.9 or later is installed on your machine.
-2. Either clone or download the "fluke" repository.
-3. Create a virtual environment just for Fluke (by running ``python3 -m venv env``)
-4. Install all dependencies (you can find a ``requirements.txt`` file in ``docs/source/``)
+2. Create a virtual environment just for Fluke (by running ``python3 -m venv env``) and activate it.
+3. Either clone or download the Fluke Github repository.
+4. Navigate to the repository and run ``pip install -e .[tests]``.
+   This results in fluke being installed in development mode,
+   which means that any Python script that imports from Fluke
+   will be referencing the modules within the cloned/downloaded
+   repository. You can read more about "development mode" in
+   [Local project installs](https://pip.pypa.io/en/stable/topics/local-project-installs/).
 
-At this point you may want to create a simple script, e.g. ``main.py``
-and place it directly within the cloned repo. In this script, you are
-free to import from "fluke" so that you can experiment by executing
-various bits of code. Just make sure that, when running your script,
-your working directory is the "fluke" repo, and you are good to go!
 
-Running the tests
+Mocking out remote entities
+-------------------------------------
+
+Interacting with various remote services and resources is a big part
+of Fluke. It is only natural that you want to test your code on without
+having to actually pay for access to these resources. Here's what
+you can do in order to test each non-local component:
+
+- **Remote Server**: In order to test access to a remote file system,
+  you may simply run an SSH server as a Docker container. Fluke uses
+  the [linuxserver/openssh-server](https://hub.docker.com/r/linuxserver/openssh-server)
+  Docker image.
+
+- **Amazon S3 / Amazon SQS**: In order to test these services you can utilize
+  the [moto](https://pypi.org/project/moto/) package, which, if you followed
+  the steps in "Setting up a development environment", should already be
+  installed to your Python virtual environment.
+
+- **Google Cloud Storage**: In order to mock out this service, you'll
+  want to run a dummy GCS server locally. The easiest way to do this
+  would be to run such a server as a Docker container. Fluke uses the
+  [fsouza/fake-gcs-server](https://hub.docker.com/r/fsouza/fake-gcs-server)
+  Docker image.
+
+- **Azure Blob Storage / Azure Queue Storage**: Unfortunately, there is not a way
+  of mocking out Azure services as of yet, though it is being looked into.
+  For now, an Azure storage account is required in order to truly test out
+  any code that interacts with these services.
+
+Testing
 -------------------------------------
 For a pull request to be merged, it is important that it passes all required
-tests. In order to ensure that, you can run the tests yourself by executing
-the following command:
+tests. In this section we'll see how to do this.
+
+### Prerequisites
+
+First things first, Fluke mocks out certain resources by using Docker
+containers. More specifically, it mocks out an SSH server so as to test
+classes ``fluke.storage.{RemoteFile,RemoteDir}``, as well as a GCS server
+for classes ``fluke.storage.{GCPStorageFile,GCPStorageDir}``. Therefore,
+in order to run all tests successfully, you need to have these services
+running as Docker containers on your system. Assuming that you have Docker
+installed, you can achieve this by executing the following commands:
+
+- **SSH Server**:
+  ```
+  docker run -d \
+  -p 2222:2222 \
+  --env PUID=$(id -u $(whoami)) \
+  --env PGID=$(id -g $(whoami)) \
+  --env PASSWORD_ACCESS=true \
+  --env USER_NAME=test \
+  --env USER_PASSWORD=test \
+  --mount type=bind,\
+  src={PATH_TO_FLUKE_REPO}/tests/test_files,\
+  dst=/tests/test_files \
+  linuxserver/openssh-server:amd64-version-9.3_p2-r0
+  ``````
+  This command will pull the ``linuxserver/openssh-server`` image from Docker Hub
+  and start it as a Docker container in the background, while at the same time
+  mapping its port `2222` to your local port `2222` and passing all required
+  environmental variables. Furthermore, it mounts the Fluke repository's ``tests/test_files``
+  folder into the container. Before you execute the above command, make sure that you replace
+  ``{PATH_TO_FLUKE_REPO}`` with the actual path of your local copy of the Fluke repository.
+
+- **GCS Server**:
+  ```
+  docker run -d \
+  -p 4443:4443 \
+  --mount type=bind,\
+  src={PATH_TO_FLUKE_REPO}/tests/test_files,\
+  dst=/data/bucket/tests/test_files \
+  fsouza/fake-gcs-server:1.47.0 \
+  -external-url https://127.0.0.1:4443
+  ``````
+  This command will pull the ``fsouza/fake-gcs-server`` image from Docker Hub
+  and start it as a Docker container in the background, while at the same time
+  mapping its port `4443` to your local port `4443`, as well as mounting the
+  Fluke repository's ``tests/test_files`` folder into the container, thereby
+  creating a dummy bucket called ``bucket`` which will be containing this folder.
+  Before you execute the above command, make sure that you replace ``{PATH_TO_FLUKE_REPO}``
+  with the actual path of your local copy of the Fluke repository.
+
+
+### Running the tests
+
+After you have all necessary Docker containers up and running,
+you may run the tests by executing the following command:
 ```
 python3 -m unittest discover tests
 ```
-You are also able to specify the testing modules that
+You are also able to specify the test module/class/method
 you wish to be executed:
 ```
 python3 -m unittest tests.test_storage
+python3 -m unittest tests.test_storage.TestRemoteDir
+python3 -m unittest tests.test_storage.TestRemoteDir.test_constructor
 ```
 Finally, you can get the test coverage by executing the following commands:
 ```
diff --git a/README.md b/README.md
index 5b02d2e..53be995 100644
--- a/README.md
+++ b/README.md
@@ -8,20 +8,21 @@
 <!-- What is Fluke? -->
 ## What is Fluke?
 
-Fluke is a Python package that is to be used as a higher-level API to
-cloud services that primarily relate to object storage and message queues.
-Fluke manages to hide away much of the complexity that derives from working
-with said services, aiding you in completing your tasks fast and hassle-free!
-Fluke achieves this by:
+Fluke is a Python package that acts as a higher-level API to
+cloud services that primarily relate to object storage and
+messaging. Fluke manages to hide away much of the complexity
+that derives from working with said services, aiding you in
+completing your tasks fast and hassle-free! Fluke achieves this by:
 
-* Treating object storage services as traditional file systems,
-  unifying the two under a single *File/Dir* API, through which
-  you are able to manage your data no matter where they reside,
-  be it a local/remote file system or a bucket in the cloud.
+* Wrapping object storage services within a *File/Dir* API
+  inspired by the more familiar file storage, through which
+  you are able to interact with your data no matter where
+  they reside, be it the local file system, a remote server,
+  or a bucket in the cloud.
 
-* Greatly reducing the intricacies of interacting with message queues
-  by thinking of them as mere data structures that support three elementary
-  operations, that is, push/peek/poll.
+* Greatly reducing the intricacies of working with message
+  queues by viewing them as mere data structures that support
+  three elementary operations, that is, *push*, *peek*, and *poll*.
 
 
 <!-- Installation -->
@@ -40,13 +41,14 @@ pip install fluke-api
 
 In this example, we will be using Fluke in order to:
 
-1. Poll an Amazon SQS queue every minute for new messages. Each of these messages contains the
-   path of a newly uploaded file to an Amazon S3 bucket.
-2. Use the content of these messages in order to locate and access said files within the bucket.
-3. If a file's metadata field ``transfer`` has been set to ``True``, then transfer it to a remote server.
+1. Poll an Amazon SQS queue every minute for new messages. Each of these messages
+   contains the path of a newly uploaded file to an Amazon S3 bucket.
+2. Use said messages in order to locate the corresponding files and transfer
+   them to a remote server.
 
 First things first, we need to be able to authenticate with both AWS
-and the remote server. In order to achieve this, we will be importing from ``fluke.auth``:
+and the remote server. In order to achieve this, we will be importing
+from ``fluke.auth``:
 
 ```python
 from fluke.auth import AWSAuth, RemoteAuth
@@ -80,11 +82,7 @@ with (
 ):
     for batch in queue.poll(polling_frequency=60):
         for msg in batch:
-            file = bucket.get_file(path=msg)
-            file.load_metadata()
-            metadata = file.get_metadata()
-            if bool(metadata['transfer']):
-                file.transfer_to(dst=rmt_dir)
+            bucket.get_file(path=msg).transfer_to(dst=rmt_dir)
 ```
 
 And that's basically it!
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 4e332c7..be66f54 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -22,7 +22,7 @@
 author = 'Manos Stoumpos'
 
 # The full version, including alpha/beta/rc tags
-release = '0.4.1'
+release = '0.5.0'
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst
index cda39c8..734f883 100644
--- a/docs/source/getting_started.rst
+++ b/docs/source/getting_started.rst
@@ -4,20 +4,21 @@
 What is Fluke?
 *******************
 
-Fluke is a Python package that is to be used as a higher-level API to
-cloud services that primarily relate to object storage and message queues.
-Fluke manages to hide away much of the complexity that derives from working
-with said services, aiding you in completing your tasks fast and hassle-free!
-Fluke achieves this by:
-
-* Treating object storage services as traditional file systems,
-  unifying the two under a single *File/Dir* API, through which
-  you are able to manage your data no matter where they reside,
-  be it a local/remote file system or a bucket in the cloud.
-
-* Greatly reducing the intricacies of interacting with message queues
-  by thinking of them as mere data structures that support three elementary
-  operations, that is, push/peek/poll.
+Fluke is a Python package that acts as a higher-level API to
+cloud services that primarily relate to object storage and
+messaging. Fluke manages to hide away much of the complexity
+that derives from working with said services, aiding you in
+completing your tasks fast and hassle-free! Fluke achieves this by:
+
+* Wrapping object storage services within a *File/Dir* API
+  inspired by the more familiar file storage, through which
+  you are able to interact with your data no matter where
+  they reside, be it the local file system, a remote server,
+  or a bucket in the cloud.
+  
+* Greatly reducing the intricacies of working with message
+  queues by viewing them as mere data structures that support
+  three elementary operations, that is, *push*, *peek*, and *poll*.
 
 *******************
 Installation
@@ -37,10 +38,10 @@ Usage example
 
 In this example, we will be using Fluke in order to:
 
-1. Poll an Amazon SQS queue every minute for new messages. Each of these messages contains the
-   path of a newly uploaded file to an Amazon S3 bucket.
-2. Use the content of these messages in order to locate and access said files within the bucket.
-3. If a file's metadata field ``transfer`` has been set to ``True``, then transfer it to a remote server.
+1. Poll an Amazon SQS queue every minute for new messages. Each of these messages
+   contains the path of a newly uploaded file to an Amazon S3 bucket.
+2. Use said messages in order to locate the corresponding files and transfer
+   them to a remote server.
 
 First things first, we need to be able to authenticate with both AWS
 and the remote server. In order to achieve this, we will be importing from
@@ -78,17 +79,13 @@ necessary resources in order to perform the data transfer:
       AmazonS3Dir(auth=aws_auth, bucket='bucket') as bucket,
       RemoteDir(auth=rmt_auth, path='/home/user/dir', create_if_missing=True) as rmt_dir
   ):
-    for batch in queue.poll(polling_frequency=60):
-        for msg in batch:
-            file = bucket.get_file(path=msg)
-            file.load_metadata()
-            metadata = file.get_metadata()
-            if bool(metadata['transfer']):
-                file.transfer_to(dst=rmt_dir)
+      for batch in queue.poll(polling_frequency=60):
+          for msg in batch:
+              bucket.get_file(path=msg).transfer_to(dst=rmt_dir)
 
 And that's basically it!
 
 You can learn more about Fluke by going through its detailed
-`User Guide <user_guide/handling_auth.html>`_, or by visiting
+`User Guide <user_guide/authentication.html>`_, or by visiting
 Fluke on `Github <https://github.com/manoss96/fluke>`_
 in order to check out the source code itself.
\ No newline at end of file
diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt
index b83ce22..0086bc3 100644
--- a/docs/source/requirements.txt
+++ b/docs/source/requirements.txt
@@ -11,5 +11,6 @@ azure-identity==1.13.0
 azure-storage-blob==12.17.0
 azure-storage-queue==12.6.0
 boto3==1.28.0
+google-cloud-storage==2.10.0
 paramiko==3.2.0
 tqdm==4.65.0
\ No newline at end of file
diff --git a/docs/source/user_guide/authentication.rst b/docs/source/user_guide/authentication.rst
index 2193331..7cb6a84 100644
--- a/docs/source/user_guide/authentication.rst
+++ b/docs/source/user_guide/authentication.rst
@@ -7,8 +7,8 @@ Authentication
 Authentication is the process by which a user verifies their identity. All remote
 resources require some form of authentication during which a user must identify
 themselves so that they are granted access to said resources. Fluke itself
-manages authentication through the `fluke.auth <../documentation/auth.html>`_ module.
-The general idea is that you must first initialize some *Auth* instance,
+manages authentication through the `fluke.auth <../documentation/auth.html>`_
+module. The general idea is that you must first initialize some *Auth* instance,
 which is then provided to the resource with which you wish to interact.
 For instance, in the code snippet below we use the
 `RemoteAuth <../documentation/auth.html#fluke.auth.RemoteAuth>`_
@@ -149,7 +149,55 @@ request access to:
 
 
 ==========================================
-Authenticating with a remote machine
+Authenticating with GCP
+==========================================
+
+When it comes to authenticating with GCP, there
+exist two different approaches. These are via Application
+Default Credentials (ADC), as well as, via a service
+account key.
+
+---------------------------------------------------
+Authenicating via application default credentials
+---------------------------------------------------
+
+After having
+`set up your application default credentials <https://cloud.google.com/docs/authentication/provide-credentials-adc>`_,
+which results in a JSON file that contains all required credentials being generated,
+you just need to invoke method `GCPAuth.from_application_default_credentials <../documentation/auth.html#fluke.auth.GCPAuth.from_application_default_credentials>`_ 
+and provide it with the path pointing to said file along with the ID of the
+project to which the resource you wish to access belongs:
+
+.. code-block:: python
+
+  from fluke.auth import GCPAuth
+
+  auth = GCPAuth.from_application_default_credentials(
+    project_id='your_project_id',
+    credentials='path/to/adc/file.json'
+  )
+
+
+---------------------------------------------------
+Authenicating via a service account key
+---------------------------------------------------
+
+Similarily, `creating a service account key <https://cloud.google.com/iam/docs/keys-create-delete>`_
+also results in a JSON file containing the service account key credentials.
+In this case, you just need the path pointing to this file as a service account
+key will already be tied to one and only GCP project:
+
+.. code-block:: python
+
+  from fluke.auth import GCPAuth
+
+  auth = GCPAuth.from_service_account_key(
+    credentials='path/to/sak/file.json'
+  )
+
+
+==========================================
+Authenticating with a remote SSH server
 ==========================================
 
 There are two ways for a user to authenticate themselves while
@@ -222,9 +270,9 @@ that is, hosts which are currently listed under your machine's
 to an unknown host, an exception will be thrown, in which case
 you have two options:
 
-#. If you are aware of the host's public key and its type, then you may
-   include this information into the ``RemoteAuth`` instance via
-   parameters ``public_key`` and ``key_type``:
+#. If you are aware of the host's public key, then you may
+   include this information into the ``RemoteAuth`` instance
+   by generating a key via ``RemoteAuth.PublicKey``:
 
    .. code-block:: python
 
@@ -234,8 +282,7 @@ you have two options:
        hostname='host',
        username='user',
        password='pwd',
-       public_key='PUBLIC_RSA_KEY',
-       key_type=RemoteAuth.KeyType.SSH_RSA
+       public_key=RemoteAuth.PublicKey.generate_ssh_rsa_key('PUBLIC_RSA_KEY')
      )
 
 #. Alternatively, you may simply set parameter ``verify_host`` to ``False``:
diff --git a/docs/source/user_guide/file_and_object_storage.rst b/docs/source/user_guide/file_and_object_storage.rst
index 2076e96..d82e8ee 100644
--- a/docs/source/user_guide/file_and_object_storage.rst
+++ b/docs/source/user_guide/file_and_object_storage.rst
@@ -21,17 +21,19 @@ in order to interact with data:
 #. **File API**: Used for handling either individual files in a file system,
    be it local or remote, or objects in the cloud. This API includes:
    
-     * `LocalFile <../documentation/storage.html#fluke.storage.LocalFile>`_
-     * `RemoteFile <../documentation/storage.html#fluke.storage.RemoteFile>`_
      * `AmazonS3File <../documentation/storage.html#fluke.storage.AmazonS3File>`_
      * `AzureBlobFile <../documentation/storage.html#fluke.storage.AzureBlobFile>`_
+     * `GCPStorageFile <../documentation/storage.html#fluke.storage.GCPStorageFile>`_
+     * `LocalFile <../documentation/storage.html#fluke.storage.LocalFile>`_
+     * `RemoteFile <../documentation/storage.html#fluke.storage.RemoteFile>`_
 
 #. **Dir API**: Used for handling entire directories of files/objects. This API includes:
 
-     * `LocalDir <../documentation/storage.html#fluke.storage.LocalDir>`_
-     * `RemoteDir <../documentation/storage.html#fluke.storage.RemoteDir>`_
      * `AmazonS3Dir <../documentation/storage.html#fluke.storage.AmazonS3Dir>`_
      * `AzureBlobDir <../documentation/storage.html#fluke.storage.AzureBlobDir>`_
+     * `GCPStorageDir <../documentation/storage.html#fluke.storage.GCPStorageDir>`_
+     * `LocalDir <../documentation/storage.html#fluke.storage.LocalDir>`_
+     * `RemoteDir <../documentation/storage.html#fluke.storage.RemoteDir>`_
 
 ----------------------------------------
 Accessing local entities
@@ -490,7 +492,11 @@ assigned to it. Let's do just that and see what happens:
   from fluke.storage import AmazonS3File
 
   # Gain access to 'file.txt' on Amazon S3 and print its metadata.
-  with AmazonS3File(auth=AWSAuth(**aws_credentials), bucket='bucket', path='dir/file.txt') as aws_obj:
+  with AmazonS3File(
+    auth=AWSAuth(**aws_credentials),
+    bucket='bucket',
+    path='dir/file.txt'
+  ) as aws_obj:
       print(aws_obj.get_metadata())
 
 By executing the above code, we get the following output:
@@ -510,10 +516,30 @@ Similarly, invoking an instance's ``get_metadata`` method won't fetch the object
 though it will search for any metadata we may have assigned to it locally.
 
 So how can we inspect the actual metadata of an object? This can be easily done
-by invoking an instance's ``load_metadata`` method, which goes on to fetch
-the object's actual metadata via HTTP and store them locally. Thus, going back to our
-example, we would be able to display the object's true metadata if we would just
-add the aforementioned line of code:
+by setting the constructor's parameter ``load_metadata``. On top of opening a
+connection to the file, setting this parameter to ``True`` will result in
+the object's actual metadata being fetched via HTTPS and stored in memory.
+Thus, going back to our example, we would be able to display the object's
+metadata if we would just set the aformentioned parameter:
+
+.. code-block:: python
+
+  from fluke.auth import AWSAuth
+  from fluke.storage import AmazonS3File
+
+  # Gain access to 'file.txt' on Amazon S3 and print its metadata.
+  with AmazonS3File(
+    auth=AWSAuth(**aws_credentials),
+    bucket='bucket',
+    path='dir/file.txt',
+    load_metadata=True
+  ) as aws_obj:
+      # Then print it.
+      print(aws_obj.get_metadata())
+
+Note that even if you don't set ``load_metadata`` to ``True`` during the
+file's instantiation, you can always invoke method ``load_metadata``
+manually at a later time and achieve the same result:
 
 .. code-block:: python
 
@@ -521,8 +547,13 @@ add the aforementioned line of code:
   from fluke.storage import AmazonS3File
 
   # Gain access to 'file.txt' on Amazon S3 and print its metadata.
-  with AmazonS3File(auth=AWSAuth(**aws_credentials), bucket='bucket', path='dir/file.txt') as aws_obj:
-      # Load metadata first.
+  with AmazonS3File(
+    auth=AWSAuth(**aws_credentials),
+    bucket='bucket',
+    path='dir/file.txt',
+    load_metadata=False
+  ) as aws_obj:
+      # Load metadata.
       aws_obj.load_metadata()
       # Then print it.
       print(aws_obj.get_metadata())
@@ -539,8 +570,8 @@ the transfer. Nevertheless, if no metadata have been assigned to a file through
 ``set_metadata``, then ``transfer_to`` will actually go on to fetch any potentially
 existing metadata associated with the file so that it may assign them to the resulting file.
 This means that in the following example, any actual metadata associated with *file.txt*
-will actually be carried over from Amazon S3 to Azure despite the fact that ``load_metadata``
-has not been invoked:
+will actually be carried over from Amazon S3 to Azure Blob Storage despite the fact that
+they have not been loaded beforehand.
 
 .. code-block:: python
 
@@ -548,12 +579,20 @@ has not been invoked:
   from fluke.storage import AmazonS3File, AzureBlobFile
 
   with (
-      AmazonS3File(auth=AWSAuth(**aws_credentials), bucket='bucket', path='dir/file.txt') as aws_obj,
-      AzureBlobDir(auth=AzureAuth.from_service_principal(**azr_credentials), container='container', path='file.txt') as azr_dir
+      AmazonS3File(
+        auth=AWSAuth(**aws_credentials),
+        bucket='bucket',
+        path='dir/file.txt'
+      ) as aws_obj,
+      AzureBlobDir(
+        auth=AzureAuth.from_service_principal(**azr_credentials),
+        container='container',
+        path='file.txt'
+      ) as azr_dir
   ):
       aws_obj.transfer_to(dst=azr_dir, include_metadata=True)
 
-..  _speeding-things-up-with-caching:
+..  _caching_data:
 
 ========================================
 Catching data
@@ -587,15 +626,15 @@ setting parameter ``cache`` to ``True`` during its instantiation:
   auth = AWSAuth(**aws_credentials)
 
   with AmazonS3Dir(auth=auth, bucket='bucket', path='dir', cache=True) as s3_dir:
-    # Fetch metadata via HTTP.
-    t = time.time()
+    # Fetch metadata via HTTPS.
+    t = time.perf_counter()
     s3_dir.load_metadata()
-    print(f"Fetched metadata in {time.time() - t:.2f} seconds!")
+    print(f"Fetched metadata in {time.perf_counter() - t:.2f} seconds!")
 
     # Fetch metadata from cache.
-    t = time.time()
+    t = time.perf_counter()
     s3_dir.load_metadata()
-    print(f"Fetched metadata in {time.time() - t:.2f} seconds!")
+    print(f"Fetched metadata in {time.perf_counter() - t:.2f} seconds!")
 
 Executing the above code block outputs the following:
 
@@ -656,9 +695,9 @@ them. Consider the following example:
   # Access an AWS S3 directory and render it "cacheable".
   with AmazonS3Dir(auth=aws_auth, bucket="bucket", path='dir', cache=True) as s3_dir:
     # Fetch the directory's total size and time it.
-    t = time.time()
+    t = time.perf_counter()
     _ = s3_dir.get_size(recursively=True)
-    print(f"Fetched size in {time.time() - t:.2f} seconds!")
+    print(f"Fetched size in {time.perf_counter() - t:.2f} seconds!")
 
     # Now purge the directory's cache.
     s3_dir.purge()
@@ -668,9 +707,9 @@ them. Consider the following example:
     _ = s3_dir.get_subdir('subdir').get_size(recursively=True)
 
     # Fetch the directory's total size and time it again.
-    t = time.time()
+    t = time.perf_counter()
     _ = s3_dir.get_size(recursively=True)
-    print(f"Fetched size in {time.time() - t:.2f} seconds!")
+    print(f"Fetched size in {time.perf_counter() - t:.2f} seconds!")
 
 The above code produces the following output when executed:
 
diff --git a/fluke/_cache.py b/fluke/_cache.py
index fbe405d..8c5d227 100644
--- a/fluke/_cache.py
+++ b/fluke/_cache.py
@@ -1,14 +1,13 @@
+from enum import Enum as _Enum
+from typing import Any as _Any
 from typing import Optional as _Optional
 from typing import Iterator as _Iterator
 from typing import Callable as _Callable
-from typing import Union as _Union
 
+from ._helper import infer_separator
 
-from ._helper import join_paths as _join_paths
-from ._helper import infer_separator as _infer_sep
 
-
-class Cache():
+class FileCache():
     '''
     A class whose instances represent cached \
     information about a file.
@@ -54,118 +53,143 @@ def set_metadata(self, metadata: dict[str, str]) -> None:
         self.__metadata = dict(metadata)
 
 
-class CacheManager():
+class DirCache():
     '''
-    A class used in managing ``Cache`` instances.
+    A class whose instances represent cached \
+    information about a directory.
+
+    :param str | None path: The path of the directory \
+        being cached. If ``None``, then the empty path \
+        is inferred.
     '''
 
-    def __init__(self):
+    class State(_Enum):
+        NOT_TRAVERSED = 0
+        TOP_LEVEL_TRAVERSED = 1
+        RECURSIVELY_TRAVERSED = 2
+
+
+    def __init__(self, path: _Optional[str]):
+        '''
+        A class whose instances represent cached \
+        information about a directory.
+
+        :param str | None path: The path of the directory \
+            being cached. If ``None``, then the empty path \
+            is inferred.
+        '''
+        if path is None:
+            self.__sep = '/'
+            self.__has_sep_root = False
+        else:
+            self.__sep = infer_separator(path)
+            self.__has_sep_root = path.startswith(self.__sep)
+        self.purge()
+
+
+    def remove_sep_prefix(func: _Callable) -> _Any:
+        '''
+        A decorator function used for removing \
+        any seperator prefixes from paths provided \
+        to the functions being decorated.
         '''
-        A class used in managing ``Cache`` instances.
-        '''
-        # __cache explained:
-        #
-        #   - __cache[file] --> Cache
-        #   - __cache[dir] --> list[int, dict[str, dict | Cache]]
-        #       - int:
-        #           - 0 --> dir has not been traversed
-        #           - 1 --> dir has been traversed top-level
-        #           - 2 --> dir has traversed recursively
-        #       - dict[str, dict | Cache]:
-        #           - A __cache-like dictionary.
-        #
-        #  Example:
-        #
-        #  {
-        #    'file1.txt': Cache(),
-        #    'dir' : [
-        #      1,
-        #      {
-        #        'file2.txt': Cache(),
-        #        'subdir': [0, {}]
-        #      }
-        #    ]
-        #  }
-        self.__cache: dict[str, _Union[list[int, dict], Cache]] = [0, dict()]
+        def wrapper(*args, **kwargs):
+            if (path := kwargs.get('path', None)) is not None:
+                kwargs['path'] = path.lstrip(args[0].__sep)
+            else:
+                args_new = []
+                for i, arg in enumerate(args):
+                    if i == 1:
+                        arg = arg.lstrip(args[0].__sep)
+                    args_new.append(arg)
+                args = tuple(args_new)
+            return func(*args, **kwargs)
+        return wrapper
 
 
     def purge(self) -> None:
         '''
-        Purges cache.
+        Purges the cache.
         '''
-        self.__cache = [0, dict()]
+        self.__state = __class__.State.NOT_TRAVERSED
+        self.__subdirs: dict[str, DirCache] = dict()
+        self.__files: dict[str, FileCache] = dict()
 
 
-    def get_size(self, file_path: str) -> _Optional[int]:
+    @remove_sep_prefix
+    def get_size(self, path: str) -> _Optional[int]:
         '''
         Returns the file's cached size if it exists, \
         else returns ``None``.
 
-        :param str file_path: The file's absolute path.
+        :param str path: The file's absolute path.
         '''
-        if (cache := self.__get_file_cache_ref(
-                file_path=file_path,
-                sep=_infer_sep(path=file_path),
-                create_if_missing=False
-        )) is not None:
-            return cache.get_size()
+        return (cache.get_size()
+        if (cache := self.__get_file_cache(path)) is not None
+        else None)
     
 
-    def cache_size(self, file_path: str, size: int) -> None:
+    @remove_sep_prefix
+    def cache_size(self, path: str, size: int) -> None:
         '''
         Caches the provided size.
 
-        :param str file_path: The file's absolute path.
+        :param str path: The file's absolute path.
         :param int size: The file's size.
+
+        :note: This method goes on to creates any \
+            necessary ``FileCache/DirCache`` instances \
+            if they do not already exist.
         '''
-        self.__get_file_cache_ref(
-            file_path=file_path,
-            sep=_infer_sep(path=file_path),
-            create_if_missing=True).set_size(size)
+        self.__create_file_cache(path).set_size(size)
     
 
-    def get_metadata(self, file_path: str) -> _Optional[dict[str, str]]:
+    @remove_sep_prefix
+    def get_metadata(self, path: str) -> _Optional[int]:
         '''
         Returns the file's cached metadata if they exist, \
         else returns ``None``.
 
-        :param str file_path: The file's absolute path.
+        :param str path: The file's absolute path.
         '''
-        if (cache := self.__get_file_cache_ref(
-                file_path=file_path,
-                sep=_infer_sep(path=file_path),
-                create_if_missing=False
-        )) is not None:
-            return cache.get_metadata()
-        
+        return (cache.get_metadata()
+        if (cache := self.__get_file_cache(path)) is not None
+        else None)
+    
 
-    def cache_metadata(self, file_path: str, metadata: dict[str, str]) -> None:
+    @remove_sep_prefix
+    def cache_metadata(
+        self,
+        path: str,
+        metadata: dict[str, str]
+    ) -> None:
         '''
         Caches the provided metadata.
 
-        :param str file_path: The file's absolute path.
-        :param dict[str, str]: The file's metadata.
+        :param str path: The file's absolute path.
+        :param dict[str, str] metadata: The file's metadata.
+
+        :note: This method goes on to creates any \
+            necessary ``FileCache/DirCache`` instances \
+            if they do not already exist.
         '''
-        self.__get_file_cache_ref(
-            file_path=file_path,
-            sep=_infer_sep(path=file_path),
-            create_if_missing=True).set_metadata(metadata)
+        self.__create_file_cache(path).set_metadata(metadata)
 
 
+    @remove_sep_prefix
     def get_content_iterator(
         self,
-        dir_path: str,
+        path: str,
         recursively: bool,
         include_dirs: bool
     ) -> _Optional[_Iterator[str]]:
         '''
-        Returns an iterator capable of going through all ``Cache`` \
+        Returns an iterator capable of going through all ``FileCache`` \
         instances' keys, either within the ordinary cache or the top-level \
         cache depending on the value of ``recursively``. Returns ``None`` \
-        if no ``Cache`` instances exist for the requested cache type.
+        if no ``FileCache`` instances exist for the requested cache type.
 
-        :param str dir_path: The absolute path of the directory \
-            whose contents are to be iterated.
+        :param path: The directory's absolute path.
         :param bool recursively: Indicates whether to iterate \
             instances recursively by looking in the ordinary \
             cache, or not by looking in the top-level cache.
@@ -173,55 +197,54 @@ def get_content_iterator(
             any directories when ``recursively`` has been set \
             to ``False``.
         '''
-        sep = _infer_sep(path=dir_path)
-
-        dir_cache = self.__get_dir_cache_ref(
-            dir_path=dir_path,
-            sep=sep,
-            create_if_missing=False)
-        
-        if dir_cache is None:
-            return None
+        if path == '':
+            cache = self
         else:
-            n = dir_cache[0]
-            if not ((n == 2) or (n == 1 and not recursively)):
-                return None
-
-        def iterate_recursively(dir_path: str) -> _Iterator[str]:
-            '''
-            Iterates the specified directory's \
-            contents recursively.
-
-            :param str dir_path: The absolute path \
-                of the directory in question.
-            '''
-            for name in (dir_cache := self.__get_dir_cache_ref(
-                dir_path=dir_path,
-                sep=sep,
-                create_if_missing=False
-            )[1]):
-                abs_path = _join_paths(sep, dir_path, name)
-                if isinstance(dir_cache[name], Cache):
-                    yield abs_path
-                else:
-                    yield from iterate_recursively(
-                        dir_path=abs_path)
-                    
-        if recursively:
-            return (fp for fp in iterate_recursively(dir_path=dir_path))
-        else:
-            dir_cache = self.__get_dir_cache_ref(
-                dir_path=dir_path,
-                sep=sep,
-                create_if_missing=False)[1]
-            return (_join_paths(sep, dir_path, name) for name in filter(
-                    lambda name: isinstance(dir_cache[name], Cache) or include_dirs,
-                    dir_cache))
+            cache = self.__get_dir_cache(path=path)
+
+        if (
+            cache is None or
+            cache.__state == __class__.State.NOT_TRAVERSED or
+            (
+                recursively and
+                cache.__state != __class__.State.RECURSIVELY_TRAVERSED
+            )
+        ):
+            return None
     
+        def iterate_contents(
+            dc: DirCache,
+            recursively: bool
+        ) -> _Iterator[str]:
+            if recursively:
+                for entity in dc.__files:
+                        yield entity
+                for subdir in dc.__subdirs.values():
+                    for entity in iterate_contents(subdir, recursively):
+                        yield entity
+            else:
+                for entity in (
+                    dc.__files | (
+                        dc.__subdirs
+                        if include_dirs
+                        else dict()
+                )):
+                    yield entity
+
+        iterator = iterate_contents(cache, recursively)
+
+        if self.__has_sep_root:
+            iterator = map(
+                lambda path: self.__sep + path,
+                iterator)
+
+        return sorted(iterator)
+
 
+    @remove_sep_prefix
     def cache_contents(
         self,
-        dir_path: str,
+        path: str,
         iterator: _Iterator[str],
         recursively: bool,
         is_file: _Callable[[str], bool]
@@ -230,6 +253,7 @@ def cache_contents(
         Goes through the provided iterator \
         in order to cache its contents.
 
+        :param str path: The directory's absolute path.
         :param Iterator[str] iterator: An iterator through \
             which a directory's contents can be traversed.
         :param bool recursively: Indicates whether the provided \
@@ -238,137 +262,188 @@ def cache_contents(
             receives a string path and returns a value indicating \
             whether said path corresponds to a file or a directory.
         '''
+        if path != '':
+            cache = self.__create_dir_cache(path=path)
+        else:
+            cache = self
 
-        # Grab the cache corresponding to the directory.
-        # Create it if it does not already exist.
-        sep = _infer_sep(path=dir_path)
+        if cache.__state == __class__.State.NOT_TRAVERSED:
+            if recursively:
+                cache.__state = __class__.State.RECURSIVELY_TRAVERSED
+            else:
+                cache.__state = __class__.State.TOP_LEVEL_TRAVERSED
+        elif cache.__state == __class__.State.TOP_LEVEL_TRAVERSED and recursively:
+            cache.__state = __class__.State.RECURSIVELY_TRAVERSED
+
+        # NOTE: Cache contents using top-level
+        #       ``DirCache`` instance.
+        for ep in sorted(iterator):
+            if is_file(ep):
+                self.__create_file_cache(ep.lstrip(self.__sep))
+            else:
+                self.__create_dir_cache(ep.lstrip(self.__sep))
 
-        dir_cache = self.__get_dir_cache_ref(
-            dir_path=dir_path,
-            sep=sep,
-            create_if_missing=True)
-        
-        # Go through the directory's contents
-        # recursively, and cache them.
-        if recursively:
-            subdirs = set()
-            for path in sorted(iterator):
-                # This will cache its contents.
-                _ = self.__get_file_cache_ref(
-                    file_path=path,
-                    sep=sep,
-                    create_if_missing=True)
-                # Meanwhile, gather all subdirectories.
-                *parent_dirs, _ = path.split(sep)
-                for i, dir in enumerate(parent_dirs):
-                    if dir == '':
-                        continue
-                    subdirs.add(f"{sep.join(parent_dirs[:i])}{sep+dir+sep}".removeprefix(sep))
-            # Mark every subdir as recursively traversed as well.
-            for subdir in subdirs:
-                self.__get_dir_cache_ref(
-                    dir_path=subdir,
-                    sep=sep,
-                    create_if_missing=False)[0] = 2
-        else:
-            for path in sorted(iterator):
-                rel_path = path.removeprefix(dir_path)
-                if rel_path not in dir_cache[1]:
-                    dir_cache[1].update({rel_path:
-                        Cache() if is_file(path) else [0, dict()]})
 
-        # Set directory as traversed.
-        dir_cache[0] = 2 if recursively else 1
+    @classmethod
+    def _create_dir_cache(
+        cls,
+        sep: str,
+        has_sep_root: bool
+    ) -> 'DirCache':
+        '''
+        Creates and returns a ``DirCache`` instance.
 
+        :param str sep: The path separator used.
+        :param has_sep_root: Indicates whether the \
+            underlying storage system being cached \
+            assumes a root directory whose name is \
+            the separator.
+        '''
+        instance = cls.__new__(cls)
+        instance.__sep = sep
+        instance.__has_sep_root = has_sep_root
+        instance.purge()
+        return instance
+    
 
-    def __get_file_cache_ref(
+    def __get_file_cache(
         self,
-        file_path: str,
-        sep: str,
-        create_if_missing: bool
-    ) -> Cache:
-        '''
-        Returns a reference to the ``Cache`` instance \
-        that corresponds to the specified file. If said \
-        instance does not exist and ``create_if_missing`` \
-        has been set to ``True``, then this method goes on \
-        to create and return it, else returns ``None``.
-
-        :param str file_path: The absolute path to the file \
-            in question.
-        :param str sep: The path's separator.
-        :param bool create_if_missing: Read description.
-        '''
-
-        # Remove any separator existing at the start
-        # of the path and split by said separator
-        *parent_dirs, file_name = (file_path
-            .removeprefix(sep)
-            .split(sep))
-
-        cache = self.__cache[1]
-
-        for dir in parent_dirs:
-            dir += sep
-            if dir not in cache:
-                if create_if_missing:
-                    cache.update({dir: [0, dict()]})
-                else:
-                    return None
-            cache = cache[dir][1]
+        path: str,
+        level: int = 1
+    ) -> _Optional[FileCache]:
+        '''
+        Returns the ``FileCache`` instance that \
+        corresponds to the provided path, or ``None``
+        if no such instance exists.
+
+        :param str path: The file's absolute path.
+        :param int level: A helping variable that prevents \
+            this method from recursing indefinitely. Defaults \
+            to ``1``.
+
+        :note: The provided path must have had any leading \
+            separators removed prior to being passes to this \
+            method.
+        '''
+        entities = path.split(self.__sep)
+
+        if len(entities) == level:
+            # Return ``FileCache`` instance.
+            return self.__files.get(path, None)
         
-        if file_name not in cache:
-            if create_if_missing:
-                cache.update({file_name: Cache()})
-            else:
-                return None
+        # Else construct ``DirCache`` path,
+        # and search if such cache exists.
+        current = self.__sep.join(entities[:level]) + self.__sep
+        return (
+            self.__subdirs[current].__get_file_cache(path, level+1)
+            if current in self.__subdirs
+            else None)
+    
 
-        return cache[file_name]
+    def __get_dir_cache(
+        self,
+        path: str,
+        level: int = 1
+    ) -> _Optional['DirCache']:
+        '''
+        Returns the ``DirCache`` instance that \
+        corresponds to the provided path, or ``None``
+        if no such instance exists.
+
+        :param str path: The directory's absolute path.
+        :param int level: A helping variable that prevents \
+            this method from recursing indefinitely. Defaults \
+            to ``1``.
+
+        :note: The provided path must have had any leading \
+            separators removed prior to being passes to this \
+            method.
+        '''
+        if path == '':
+            return self
+        
+        entities = path.split(self.__sep)
+        current = self.__sep.join(entities[:level]) + self.__sep
+
+        if len(entities) - 1 == level:
+            return self.__subdirs.get(current, None)
+        
+        return (
+            self.__subdirs[current].__get_dir_cache(path, level+1)
+            if current in self.__subdirs
+            else None)
     
 
-    def __get_dir_cache_ref(
+    def __create_file_cache(
         self,
-        dir_path: str,
-        sep: str,
-        create_if_missing: bool
-    ) -> _Optional[list[int, dict]]:
-        '''
-        Returns a reference to the list that corresponds \
-        to the specified directory's cache. If said directory \
-        does not exist and ``create_if_missing`` has been set \
-        to ``True``, then this method goes on to create and \
-        return it, else returns ``None``.
-
-        :param str dir_path: The absolute path to the file \
-            in question.
-        :param str sep: The path's separator.
-        :param bool create_if_missing: Read description.
-        '''
-
-        # Remove any left/right separator.
-        dir_path = (dir_path
-            .removeprefix(sep)
-            .removesuffix(sep))
-
-        *parent_dirs, dir_name = map(
-            lambda name: name + sep,
-            dir_path.split(sep))
-
-        cache = self.__cache[1]
-
-        for dir in parent_dirs:
-            if dir not in cache:
-                if create_if_missing:
-                    cache.update({dir: [0, dict()]})
-                else:
-                    return None
-            cache = cache[dir][1]
+        path: str,
+        level: int = 1
+    ) -> FileCache:
+        '''
+        Creates a ``FileCache`` instance for the provided \
+        path, along with any necessary ``DirCache`` instances. \
+        Returns the created ``FileCache`` instance.
+
+        :param str path: The file's absolute path.
+        :param int level: A helping variable that prevents \
+            this method from recursing indefinitely. Defaults \
+            to ``1``.
+
+        :note: The provided path must have had any leading \
+            separators removed prior to being passes to this \
+            method.
+        '''
+        entities = path.split(self.__sep)
+
+        if len(entities) == level:
+            if path not in self.__files:
+                self.__files.update({path: FileCache()})
+            return self.__files[path]
+        
+        current = self.__sep.join(entities[:level]) + self.__sep
+
+        if current not in self.__subdirs:
+            self.__subdirs.update({
+                current: DirCache._create_dir_cache(
+                    sep=self.__sep,
+                    has_sep_root=self.__has_sep_root)
+            })
+        
+        return self.__subdirs[current].__create_file_cache(path, level+1)
         
-        if dir_name not in cache:
-            if create_if_missing:
-                cache.update({dir_name: [0, dict()]})
-            else:
-                return None
 
-        return cache[dir_name]
-    
\ No newline at end of file
+    def __create_dir_cache(
+        self,
+        path: str,
+        level: int = 1
+    ) -> 'DirCache':
+        '''
+        Creates a ``DirCache`` instance for the provided \
+        path, along with any other ``DirCache`` instances \
+        that are deemed as necessary. Returns the created \
+        ``DirCache`` instance.
+
+        :param str path: The directory's absolute path.
+        :param int level: A helping variable that prevents \
+            this method from recursing indefinitely. Defaults \
+            to ``1``.
+
+        :note: The provided path must have had any leading \
+            separators removed prior to being passes to this \
+            method.
+        '''        
+        entities = path.split(self.__sep)
+        current = self.__sep.join(entities[:level]) + self.__sep
+
+        if current not in self.__subdirs:
+            self.__subdirs.update({
+                current: DirCache._create_dir_cache(
+                    sep=self.__sep,
+                    has_sep_root=self.__has_sep_root)
+            })
+
+        if len(entities) - 1 == level:
+            return self.__subdirs.get(path, None)
+        
+        return self.__subdirs[current].__create_dir_cache(
+            path=path, level=level+1)
\ No newline at end of file
diff --git a/fluke/_exceptions.py b/fluke/_exceptions.py
index 07db0b5..e8839de 100644
--- a/fluke/_exceptions.py
+++ b/fluke/_exceptions.py
@@ -99,7 +99,8 @@ def __init__(self, val: _Any):
 class BucketNotFoundError(Exception):
     '''
     This exception is thrown whenever the user provides \
-    an Amazon S3 bucket that does not exist.
+    an Amazon S3 / Google Cloud Storage bucket that does \
+    not exist.
 
     :param str bucket: The name of the bucket that \
         was provided by the user.
@@ -108,7 +109,8 @@ class BucketNotFoundError(Exception):
     def __init__(self, bucket: str):
         '''
         This exception is thrown whenever the user provides \
-        an Amazon S3 bucket that does not exist.
+        an Amazon S3 / Google Cloud Storage bucket that does \
+        not exist.
 
         :param str bucket: The name of the bucket that \
             was provided by the user.
@@ -138,25 +140,6 @@ def __init__(self, container: str):
         super().__init__(msg)
 
 
-class UnknownKeyTypeError(Exception):
-    '''
-    This exception is thrown whenever the user provides \
-    an unknown public key type.
-
-    :param str key_type: The type of the key that was provided.
-    '''
-
-    def __init__(self, key_type: str):
-        '''
-        This exception is thrown whenever the user provides \
-        an unknown public key type.
-
-        :param str key_type: The type of the key that was provided.
-        '''
-        msg = f'Key type "{key_type}" is not supported.'
-        super().__init__(msg)
-
-
 class OverwriteError(Exception):
     '''
     This exception is thrown whenever the user tries \
@@ -178,4 +161,24 @@ def __init__(self, file_path: str):
         '''
         msg = f'There already exists file "{file_path}". '
         msg += 'Try setting "overwrite" to "True".'
+        super().__init__(msg)
+
+
+class InvalidChunkSizeError(Exception):
+    '''
+    This exception is thrown whenever the user tries \
+    to upload a file to the cloud in chunks, whose size \
+    is not supported by the cloud provider.
+
+    :param str msg: The message to be displayed.
+    '''
+
+    def __init__(self, msg: str):
+        '''
+        This exception is thrown whenever the user tries \
+        to upload a file to the cloud in chunks, whose size \
+        is not supported by the cloud provider.
+
+        :param str msg: The message to be displayed.
+        '''
         super().__init__(msg)
\ No newline at end of file
diff --git a/fluke/_handlers.py b/fluke/_handlers.py
index f252b6c..8b8e115 100644
--- a/fluke/_handlers.py
+++ b/fluke/_handlers.py
@@ -1,6 +1,7 @@
 import os as _os
 from abc import ABC as _ABC
 from abc import abstractmethod as _absmethod
+from base64 import decodebytes as _decodebytes
 from typing import Iterator as _Iterator
 from typing import Optional as _Optional
 
@@ -10,12 +11,21 @@
 from azure.identity import ClientSecretCredential as _CSC
 from azure.storage.blob import ContainerClient as _ContainerClient
 from botocore.exceptions import ClientError as _CE
+from google.cloud.storage import Client as _GCSClient
+from google.api_core.page_iterator import HTTPIterator as _GCSHTTPIter
+from stat import S_ISDIR as _is_dir
 
 
 from .auth import AWSAuth as _AWSAuth
 from .auth import AzureAuth as _AzureAuth
+from .auth import GCPAuth as _GCPAuth
 from .auth import RemoteAuth as _RemoteAuth
-from ._cache import CacheManager as _CacheManager
+from ._cache import DirCache as _DirCache
+from ._exceptions import BucketNotFoundError as _BNFE
+from ._exceptions import ContainerNotFoundError as _CNFE 
+from ._helper import join_paths as _join_paths
+from ._helper import infer_separator as _infer_sep
+from ._helper import relativize_path as _relativize
 from ._iohandlers import _FileReader
 from ._iohandlers import _FileWriter
 from ._iohandlers import LocalFileReader as _LocalFileReader
@@ -26,11 +36,8 @@
 from ._iohandlers import AmazonS3FileWriter as _AmazonS3FileWriter
 from ._iohandlers import AzureBlobReader as _AzureBlobReader
 from ._iohandlers import AzureBlobWriter as _AzureBlobWriter
-from ._exceptions import UnknownKeyTypeError as _UKTE
-from ._exceptions import BucketNotFoundError as _BNFE
-from ._helper import join_paths as _join_paths
-from ._helper import infer_separator as _infer_sep
-from ._helper import relativize_path as _relativize
+from ._iohandlers import GCPFileReader as _GCPFileReader
+from ._iohandlers import GCPFileWriter as _GCPFileWriter
 
 
 class ClientHandler(_ABC):
@@ -38,23 +45,21 @@ class ClientHandler(_ABC):
     An abstract class which serves as the \
     base class for all client-handler-like classes.
 
-    :param bool cache: Indicates whether it is allowed for \
-        any fetched data to be cached for faster subsequent \
-        access. Defaults to ``False``.
+    :param DirCache | None cache: Either a ``DirCache`` \
+        instance used for managing the cache, or ``None`` \
+        if caching is not activated.
     '''
 
-
-    def __init__(self, cache: bool):
+    def __init__(self, cache: _Optional[_DirCache]):
         '''
         An abstract class which serves as the \
         base class for all client-handler-like classes.
 
-        :param bool cache: Indicates whether it is allowed for \
-            any fetched data to be cached for faster subsequent \
-            access. Defaults to ``False``.
+        :param DirCache | None cache: Either a ``DirCache`` \
+            instance used for managing the cache, or ``None`` \
+            if caching is not activated.
         '''
-
-        self.__cache_manager = _CacheManager() if cache else None
+        self.__cache = cache
 
 
     def is_cacheable(self) -> bool:
@@ -63,7 +68,7 @@ def is_cacheable(self) -> bool:
         handler instance has been defined so that \
         it is able to cache data.
         '''
-        return self.__cache_manager is not None
+        return self.__cache is not None
     
 
     def purge(self) -> None:
@@ -72,7 +77,7 @@ def purge(self) -> None:
         else does nothing.
         '''
         if self.is_cacheable():
-            self.__cache_manager.purge()
+            self.__cache.purge()
 
 
     def get_file_size(self, file_path: str) -> int:
@@ -93,11 +98,11 @@ def get_file_size(self, file_path: str) -> int:
             after it has been retrieved.
         '''
         if self.is_cacheable():
-            if (size := self.__cache_manager.get_size(file_path=file_path)) is not None:
+            if (size := self.__cache.get_size(path=file_path)) is not None:
                 return size
             else:
                 size = self._get_file_size_impl(file_path)
-                self.__cache_manager.cache_size(file_path, size)
+                self.__cache.cache_size(file_path, size)
                 return size
         else:
             return self._get_file_size_impl(file_path)
@@ -121,13 +126,13 @@ def get_file_metadata(self, file_path: str) -> dict[str, str]:
             after it has been retrieved.
         '''
         if self.is_cacheable():
-            if (metadata := self.__cache_manager.get_metadata(
-                file_path=file_path)
+            if (metadata := self.__cache.get_metadata(
+                path=file_path)
             ) is not None:
                 return metadata
             else:
                 metadata = self._get_file_metadata_impl(file_path)
-                self.__cache_manager.cache_metadata(file_path, metadata)
+                self.__cache.cache_metadata(file_path, metadata)
                 return metadata
         else:
             return self._get_file_metadata_impl(file_path)
@@ -173,8 +178,8 @@ def relativize_iter(iterator: _Iterator[str]):
 
         if self.is_cacheable():
             # Grab content iterator from cache if it exists.
-            if (iterator := self.__cache_manager.get_content_iterator(
-                dir_path=dir_path,
+            if (iterator := self.__cache.get_content_iterator(
+                path=dir_path,
                 recursively=recursively,
                 include_dirs=include_dirs)
             ) is not None:
@@ -183,23 +188,23 @@ def relativize_iter(iterator: _Iterator[str]):
             # Else...
             else:
                 # Fetch content iterator.
-                # NOTE: Set "show_abs_path" to "True" so that all
-                #       contents are cached via their absolute paths.
+                # NOTE: Set "show_abs_path" to "True".
                 iterator = self._traverse_dir_impl(
                     dir_path=dir_path,
                     recursively=recursively,
                     show_abs_path=True)
                 # Cache all contents.
-                self.__cache_manager.cache_contents(
-                    dir_path=dir_path,
+                self.__cache.cache_contents(
+                    path=dir_path,
                     iterator=iterator,
                     recursively=recursively,
                     is_file=self.is_file)
                 # Reset iterator by grabbing it from cache.
-                iterator = self.__cache_manager.get_content_iterator(
-                    dir_path=dir_path,
+                iterator = self.__cache.get_content_iterator(
+                    path=dir_path,
                     recursively=recursively,
                     include_dirs=include_dirs)
+                # Return (modified) iterator.
                 if show_abs_path:
                     return iterator
                 else:
@@ -298,7 +303,7 @@ def get_writer(
         self,
         file_path: str,
         metadata: _Optional[dict[str, str]],
-        in_chunks: bool
+        chunk_size: _Optional[int]
     ) -> '_FileWriter':
         '''
         Returns an ``_FileWriter`` class instance \
@@ -310,9 +315,10 @@ def get_writer(
             dictionary containing the metadata that \
             are to be assigned to the file in question. \
             If ``None``, then no metadata are assigned.
-        :param bool in_chunks: Indicates whether to \
-            write the file in distinct chunks or \
-            all at once.
+        :param int | None chunk_size: Indicates whether \
+            the size of distinct chunks in which the file \
+            is written. If ``None``, then the file is to be \
+            written as a single chunk of bytes.
         '''
         pass
 
@@ -384,7 +390,7 @@ def __init__(self):
         A class used in handling all local file \
         system operations.
         '''
-        super().__init__(cache=False)
+        super().__init__(cache=None)
 
 
     def is_open(self) -> bool:
@@ -415,9 +421,14 @@ def path_exists(self, path: str) -> bool:
         Returns ``True`` if the provided path exists \
         within the directory, else returns ``False``.
 
-        :param str path: Either an absolute path or a \
-            path relative to the directory.
+        :param str path: An absolute path.
         '''
+        # NOTE: Strip seperator at the end of the
+        # path only if said path is not equal to
+        # the separator itself.
+        sep = _infer_sep(path)
+        if path != sep:
+            path = path.rstrip(sep)
         return _os.path.exists(path=path)
     
 
@@ -429,6 +440,12 @@ def is_file(self, file_path: str) -> bool:
         :param str file_path: The absolute path of the \
             file in question.
         '''
+        # NOTE: Strip seperator at the end of the
+        # path only if said path is not equal to
+        # the separator itself.
+        sep = _infer_sep(file_path)
+        if file_path != sep:
+            file_path = file_path.rstrip(sep)
         return _os.path.isfile(file_path)
     
 
@@ -460,7 +477,7 @@ def get_writer(
         self,
         file_path: str,
         metadata: _Optional[dict[str, str]],
-        in_chunks: bool
+        chunk_size: _Optional[int]
     ) -> _LocalFileWriter:
         '''
         Returns an ``LocalFileWriter`` class instance \
@@ -473,9 +490,10 @@ def get_writer(
             dictionary containing the metadata that \
             are to be assigned to the file in question. \
             If ``None``, then no metadata are assigned.
-        :param bool in_chunks: Indicates whether to \
-            write the file in distinct chunks or \
-            all at once.
+        :param int | None chunk_size: Indicates whether \
+            the size of distinct chunks in which the file \
+            is written. If ``None``, then the file is to be \
+            written as a single chunk of bytes.
         '''
         return _LocalFileWriter(file_path=file_path)
     
@@ -554,21 +572,25 @@ class SSHClientHandler(ClientHandler):
 
     :param RemoteAuth auth: A ``RemoteAuth`` instance used \
         for authenticating with a remote machine.
-    :param bool cache: Indicates whether it is allowed for \
-        any fetched data to be cached for faster subsequent \
-        access. Defaults to ``False``.
+    :param DirCache | None cache: Either a ``DirCache`` \
+        instance used for managing the cache, or ``None`` \
+        if caching is not activated.
     '''        
 
-    def __init__(self, auth: _RemoteAuth, cache: bool):
+    def __init__(
+        self,
+        auth: _RemoteAuth,
+        cache: _Optional[_DirCache]
+    ):
         '''
         A class used in handling the SSH and SFTP \
         connections to a remote server.
 
         :param RemoteAuth auth: A ``RemoteAuth`` instance used \
             for authenticating with a remote machine.
-        :param bool cache: Indicates whether it is allowed for \
-            any fetched data to be cached for faster subsequent \
-            access. Defaults to ``False``.
+        :param DirCache | None cache: Either a ``DirCache`` \
+            instance used for managing the cache, or ``None`` \
+            if caching is not activated.
         '''
         super().__init__(cache=cache)
         self.__auth: _RemoteAuth = auth
@@ -599,41 +621,32 @@ def open_connections(self):
         credentials = self.__auth.get_credentials()
 
         public_key = credentials.pop('public_key')
-        key_type = credentials.pop('key_type')
         verify_host = credentials.pop('verify_host')
 
         print(f"\nEstablishing connection to '{credentials['hostname']}'...")
 
-        # Load all known hosts.
-        ssh.load_system_host_keys()
-
-        # If 'verify_host' has been set to 'False',
-        # then automatically add any host to the list of
-        # known hosts.
-        if not verify_host:
-            ssh.set_missing_host_key_policy(_prmk.AutoAddPolicy)
-        # Else if 'public_key' and 'key_type' have been set,
-        # try verifying the host before adding them to the
-        # list of known hosts.
-        elif public_key is not None and key_type is not None:
-            if key_type == 'ssh-rsa':
+        if public_key is None:
+            # If the host's public key has not been provided.
+            if verify_host:
+                # Either try to verify host from known hosts.
+                ssh.load_system_host_keys()
+            else:
+                # Or ignore host verification all together.
+                ssh.set_missing_host_key_policy(_prmk.AutoAddPolicy)
+        else:
+            if public_key.type == _RemoteAuth.PublicKey._KeyType.SSH_RSA:
                 key_builder = _prmk.RSAKey
-            elif key_type == 'ssh-dss':
+            elif public_key.type == _RemoteAuth.PublicKey._KeyType.SSH_DSS:
                 key_builder = _prmk.DSSKey
-            elif key_type == 'ssh-ed25519':
+            elif public_key.type == _RemoteAuth.PublicKey._KeyType.SSH_ED25519:
                 key_builder = _prmk.Ed25519Key    
-            elif 'ecdsa-sha2' in key_type:
-                key_builder = _prmk.ECDSAKey
             else:
-                raise _UKTE(key_type=key_type)
-            
-            from base64 import decodebytes as _decodebytes
-
+                key_builder = _prmk.ECDSAKey
             ssh.get_host_keys().add(
                 hostname=credentials['hostname'],
-                keytype=str(key_type),
+                keytype=str(public_key.type),
                 key=key_builder(data=_decodebytes(
-                    public_key.encode())))
+                    public_key.key.encode())))
             
         # If key-based authentication has been chosen,
         # then create a ``PKey`` instance.
@@ -670,11 +683,17 @@ def path_exists(self, path: str) -> bool:
         Returns ``True`` if the provided path exists \
         within the directory, else returns ``False``.
 
-        :param str path: Either an absolute path or a \
-            path relative to the directory.
+        :param str path: An absolute path.
         '''
+        # NOTE: Strip seperator at the end of the
+        # path only if said path is not equal to
+        # the separator itself.
+        sep = _infer_sep(path)
+        if path != sep:
+            path = path.rstrip(sep)
+        
         try:
-            self.__sftp.stat(path=path)
+            self.__sftp.lstat(path=path)
         except FileNotFoundError:
             return False
         return True
@@ -688,8 +707,15 @@ def is_file(self, file_path: str) -> bool:
         :param str file_path: The absolute path of the \
             file in question.
         '''
-        from stat import S_ISDIR as _is_dir
-        return not _is_dir(self.__sftp.stat(path=file_path).st_mode)
+        # NOTE: Strip seperator at the end of the
+        # path only if said path is not equal to
+        # the separator itself.
+        sep = _infer_sep(file_path)
+        if file_path != sep:
+            file_path = file_path.rstrip(sep)
+
+        return not _is_dir(self.__sftp.lstat(
+            path=file_path).st_mode)
     
     
     def mkdir(self, path: str) -> None:
@@ -721,7 +747,7 @@ def get_writer(
         self,
         file_path: str,
         metadata: _Optional[dict[str, str]],
-        in_chunks: bool
+        chunk_size: _Optional[int]
     ) -> _RemoteFileWriter:
         '''
         Returns a ``RemoteFileWriter`` class instance \
@@ -734,9 +760,10 @@ def get_writer(
             dictionary containing the metadata that \
             are to be assigned to the file in question. \
             If ``None``, then no metadata are assigned.
-        :param bool in_chunks: Indicates whether to \
-            write the file in distinct chunks or \
-            all at once.
+        :param int | None chunk_size: Indicates whether \
+            the size of distinct chunks in which the file \
+            is written. If ``None``, then the file is to be \
+            written as a single chunk of bytes.
         '''
         return _RemoteFileWriter(
             file_path=file_path, sftp=self.__sftp)
@@ -748,7 +775,7 @@ def _get_file_size_impl(self, file_path) -> int:
 
         :param str file_path: The path of the file in question.
         '''
-        return self.__sftp.stat(path=file_path).st_size
+        return self.__sftp.lstat(path=file_path).st_size
     
 
     def _get_file_metadata_impl(self, file_path: str) -> dict[str, str]:
@@ -788,8 +815,6 @@ def _traverse_dir_impl(
         :note: The resulting iterator may vary depending on the \
             value of parameter ``recursively``.
         '''
-        from stat import S_ISDIR as _is_dir
-
         sep = _infer_sep(dir_path)
 
         if recursively:
@@ -806,17 +831,23 @@ def filter_obj(
 
                 if _is_dir(attr.st_mode):
                     try:
-                        for sub_attr in sftp.listdir_attr(path=abs_path):
+                        for sub_attr in sorted(
+                            sftp.listdir_iter(path=abs_path),
+                            key=lambda at: at.filename
+                        ):
                             yield from filter_obj(
                                 sftp=sftp,
                                 attr=sub_attr,
                                 parent_dir=abs_path)
-                    except:
+                    except Exception:
                         pass
                 else:
                     yield abs_path
 
-            for attr in self.__sftp.listdir_attr(path=dir_path):
+            for attr in sorted(
+                self.__sftp.listdir_iter(path=dir_path),
+                key=lambda at: at.filename
+            ):
                 for file_path in filter_obj(
                     sftp=self.__sftp,
                     attr=attr,
@@ -828,7 +859,10 @@ def filter_obj(
                             child=file_path,
                             sep=sep))
         else:
-            for attr in self.__sftp.listdir_attr(path=dir_path):
+            for attr in sorted(
+                self.__sftp.listdir_iter(path=dir_path),
+                key=lambda at: at.filename
+            ):
                 path = attr.filename
                 if _is_dir(attr.st_mode):
                     path += sep
@@ -845,12 +879,17 @@ class AWSClientHandler(ClientHandler):
         used in authenticating with AWS.
     :param str bucket: The name of the Amazon S3 bucket \
         to which a connection is to be established.
-    :param bool cache: Indicates whether it is allowed for \
-        any fetched data to be cached for faster subsequent \
-        access. Defaults to ``False``.
+    :param DirCache | None cache: Either a ``DirCache`` \
+        instance used for managing the cache, or ``None`` \
+        if caching is not activated.
     '''
 
-    def __init__(self, auth: _AWSAuth, bucket: str, cache: bool):
+    def __init__(
+        self,
+        auth: _AWSAuth,
+        bucket: str,
+        cache: _Optional[_DirCache]
+    ):
         '''
         A class used in handling the HTTP \
         connection to an Amazon S3 bucket.
@@ -859,9 +898,9 @@ def __init__(self, auth: _AWSAuth, bucket: str, cache: bool):
             used in authenticating with AWS.
         :param str bucket: The name of the Amazon S3 bucket \
             to which a connection is to be established.
-        :param bool cache: Indicates whether it is allowed for \
-            any fetched data to be cached for faster subsequent \
-            access. Defaults to ``False``.
+        :param DirCache | None cache: Either a ``DirCache`` \
+            instance used for managing the cache, or ``None`` \
+            if caching is not activated.
         '''
         super().__init__(cache=cache)
         self.__auth = auth
@@ -903,7 +942,7 @@ def open_connections(self) -> None:
         try:
             self.__bucket.meta.client.head_bucket(Bucket=self.__bucket_name)
         except _CE:
-            self.__bucket = None
+            self.close_connections()
             raise _BNFE(bucket=self.__bucket_name)
 
         print("Connection established.")
@@ -923,14 +962,15 @@ def path_exists(self, path: str) -> bool:
         Returns ``True`` if the provided path exists \
         within the directory, else returns ``False``.
 
-        :param str path: Either an absolute path or a \
-            path relative to the directory.
+        :param str path: An absolute path.
         '''
-        try:
-            self.__bucket.Object(path).load()
-        except _CE:
-            return False
-        return True
+        for _ in self._traverse_dir_impl(
+            dir_path=path.rstrip(_infer_sep(path)),
+            recursively=False,
+            show_abs_path=True
+        ):    
+            return True
+        return False
 
 
     def is_file(self, file_path: str) -> bool:
@@ -941,7 +981,12 @@ def is_file(self, file_path: str) -> bool:
         :param str file_path: The absolute path of the \
             file in question.
         '''
-        return not file_path.endswith('/')
+        try:
+            file_path = file_path.rstrip(_infer_sep(file_path))
+            self.__bucket.Object(file_path).load()
+            return not self.dir_exists(file_path)
+        except _CE:
+            return False
     
 
     def dir_exists(self, path: str) -> bool:
@@ -952,9 +997,11 @@ def dir_exists(self, path: str) -> bool:
         :param str path: Either an absolute path or a \
             path relative to the parent directory.
         '''
-        for _ in self.__bucket.objects.filter(Prefix=path):
-            return True
-        return False
+        return 'CommonPrefixes' in self.__bucket.meta.client.list_objects(
+            Bucket=self.get_bucket_name(),
+            Prefix=path.rstrip(_infer_sep(path)),
+            Delimiter='/',
+            MaxKeys=1)
         
     
     def mkdir(self, path: str) -> None:
@@ -988,7 +1035,7 @@ def get_writer(
         self,
         file_path: str,
         metadata: _Optional[dict[str, str]],
-        in_chunks: bool
+        chunk_size: _Optional[int]
     ) -> _AmazonS3FileWriter:
         '''
         Returns an ``AmazonS3FileWriter`` class instance \
@@ -1001,14 +1048,15 @@ def get_writer(
             dictionary containing the metadata that \
             are to be assigned to the file in question. \
             If ``None``, then no metadata are assigned.
-        :param bool in_chunks: Indicates whether to \
-            write the file in distinct chunks or \
-            all at once.
+        :param int | None chunk_size: Indicates whether \
+            the size of distinct chunks in which the file \
+            is written. If ``None``, then the file is to be \
+            written as a single chunk of bytes.
         '''
         return _AmazonS3FileWriter(
             file_path=file_path,
             metadata=metadata,
-            in_chunks=in_chunks,
+            chunk_size=chunk_size,
             bucket=self.__bucket)
     
 
@@ -1120,12 +1168,17 @@ class AzureClientHandler(ClientHandler):
         used in authenticating with Microsoft Azure.
     :param str container: The name of the Azure blob \
         container to which a connection is to be established.
-    :param bool cache: Indicates whether it is allowed for \
-        any fetched data to be cached for faster subsequent \
-        access. Defaults to ``False``.
+    :param DirCache | None cache: Either a ``DirCache`` \
+        instance used for managing the cache, or ``None`` \
+        if caching is not activated.
     '''
 
-    def __init__(self, auth: _AzureAuth, container: str, cache: bool):
+    def __init__(
+        self,
+        auth: _AzureAuth,
+        container: str,
+        cache: _Optional[_DirCache]
+    ):
         '''
         A class used in handling the HTTP \
         connection to an Azure blob container.
@@ -1134,9 +1187,9 @@ def __init__(self, auth: _AzureAuth, container: str, cache: bool):
             used in authenticating with Microsoft Azure.
         :param str container: The name of the Azure blob \
             container to which a connection is to be established.
-        :param bool cache: Indicates whether it is allowed for \
-            any fetched data to be cached for faster subsequent \
-            access. Defaults to ``False``.
+        :param DirCache | None cache: Either a ``DirCache`` \
+            instance used for managing the cache, or ``None`` \
+            if caching is not activated.
         '''
         super().__init__(cache=cache)
         self.__auth = auth
@@ -1188,6 +1241,11 @@ def open_connections(self) -> None:
                 account_url=credentials.pop('account_url'),
                 container_name=self.__container_name,
                 credential=_CSC(**credentials))
+            
+        if not self.container_exists():
+            self.close_connections()
+            raise _CNFE(self.__container_name)
+        
         print("Connection established!")
 
     def close_connections(self):
@@ -1204,10 +1262,11 @@ def path_exists(self, path: str) -> bool:
         Returns ``True`` if the provided path exists \
         within the directory, else returns ``False``.
 
-        :param str path: Either an absolute path or a \
-            path relative to the directory.
+        :param str path: An absolute path.
         '''
-        with self.__container.get_blob_client(blob=path) as blob:
+        with self.__container.get_blob_client(
+            blob=path.rstrip(_infer_sep(path))
+        ) as blob:
             return blob.exists()
         
 
@@ -1253,7 +1312,7 @@ def get_writer(
         self,
         file_path: str,
         metadata: _Optional[dict[str, str]],
-        in_chunks: bool
+        chunk_size: _Optional[int]
     ) -> _AzureBlobWriter:
         '''
         Returns an ``AzureBlobWriter`` class instance \
@@ -1266,14 +1325,15 @@ def get_writer(
             dictionary containing the metadata that \
             are to be assigned to the file in question. \
             If ``None``, then no metadata are assigned.
-        :param bool in_chunks: Indicates whether to \
-            write the file in distinct chunks or \
-            all at once.
+        :param int | None chunk_size: Indicates whether \
+            the size of distinct chunks in which the file \
+            is written. If ``None``, then the file is to be \
+            written as a single chunk of bytes.
         '''
         return _AzureBlobWriter(
             file_path=file_path,
             metadata=metadata,
-            in_chunks=in_chunks,
+            chunk_size=chunk_size,
             container=self.__container)
      
 
@@ -1350,4 +1410,301 @@ def _traverse_dir_impl(
             if show_abs_path:
                 yield properties['name']
             else:
-                yield _relativize(dir_path, properties['name'], sep)
\ No newline at end of file
+                yield _relativize(dir_path, properties['name'], sep)
+
+
+class GCPClientHandler(ClientHandler):
+    '''
+    A class used in handling the HTTP \
+    connection to a Google Cloud Storage bucket.
+
+    :param GCPAuth auth: A ``GCPAuth`` instance \
+        used in authenticating with Google Cloud Platform.
+    :param str bucket: The name of the Google Cloud Storage \
+        bucket to which a connection is to be established.
+    :param DirCache | None cache: Either a ``DirCache`` \
+        instance used for managing the cache, or ``None`` \
+        if caching is not activated.
+    '''
+
+    # NOTE: This is used instead of directly instantiating a client
+    #       due to certain issues while attempting to mock the client
+    #       by patching the class' method ``__new__``.
+    _CLIENT_GENERATOR = lambda _, project_id: _GCSClient(project=project_id)
+
+    def __init__(
+        self,
+        auth: _GCPAuth,
+        bucket: str,
+        cache: _Optional[_DirCache]
+    ):
+        '''
+        A class used in handling the HTTP \
+        connection to a Google Cloud Storage bucket.
+
+        :param GCPAuth auth: A ``GCPAuth`` instance \
+            used in authenticating with Google Cloud Platform.
+        :param str bucket: The name of the Google Cloud Storage \
+            bucket to which a connection is to be established.
+        :param DirCache | None cache: Either a ``DirCache`` \
+            instance used for managing the cache, or ``None`` \
+            if caching is not activated.
+        '''
+        super().__init__(cache=cache)
+        self.__auth = auth
+        self.__bucket_name = bucket
+        self.__bucket = None
+
+
+    def bucket_exists(self) -> bool:
+        '''
+        Returns a value indicating whether the \
+        specified bucket exists or not.
+        '''
+        self.__bucket.exists()
+
+
+    def get_bucket_name(self) -> str:
+        '''
+        Returns the name of the bucket to which \
+        a connection has been established.
+        '''
+        return self.__bucket_name
+    
+
+    def is_open(self) -> bool:
+        '''
+        Returns a value indicating whether \
+        this handler's underlying client connection \
+        is open or not.
+        '''
+        return self.__bucket is not None
+
+
+    def open_connections(self) -> None:
+        '''
+        Opens an HTTP connection to the \
+        Google Cloud Storage bucket.
+        '''
+        if self.__bucket is not None:
+            return
+
+        credentials = self.__auth.get_credentials()
+
+        print(f"\nEstablishing connection to '{self.__bucket_name}' Google Cloud Storage bucket...")
+
+        if _GCPAuth._APPLICATION_DEFAULT_CREDENTIALS in credentials:
+            _os.environ.update({
+                "GOOGLE_APPLICATION_CREDENTIALS":
+                credentials[_GCPAuth._APPLICATION_DEFAULT_CREDENTIALS]
+            })
+            client = self._CLIENT_GENERATOR(
+                project_id=credentials[_GCPAuth._PROJECT_ID])
+        elif _GCPAuth._SERVICE_ACCOUNT_KEY in credentials:           
+            client = _GCSClient.from_service_account_json(
+                json_credentials_path=credentials[_GCPAuth._SERVICE_ACCOUNT_KEY])
+        for bucket in client.list_buckets():
+            if bucket.name == self.__bucket_name:
+                self.__bucket = bucket
+                break
+        else:
+            self.close_connections()
+            raise _BNFE(bucket=self.__bucket_name)
+
+        print("Connection established!")
+
+
+    def close_connections(self):
+        '''
+        Closes the HTTP connection to Google Cloud Storage.
+        '''
+        if self.__bucket is not None:
+            self.__bucket.client.close()
+            self.__bucket = None
+
+
+    def path_exists(self, path: str) -> bool:
+        '''
+        Returns ``True`` if the provided path exists \
+        within the directory, else returns ``False``.
+
+        :param str path: An absolute path.
+        '''
+        for _ in self.__bucket.list_blobs(
+            prefix=path.rstrip('/')
+        ):   
+            return True
+        return False
+        
+
+    def is_file(self, file_path: str) -> bool:
+        '''
+        Returns ``True`` if the provided path points \
+        to a file, else returns ``False``.
+
+        :param str file_path: The absolute path of the \
+            file in question.
+        '''
+        return not file_path.endswith(_infer_sep(file_path))
+    
+
+    def mkdir(self, path: str) -> None:
+        '''
+        Creates a directory into the provided path.
+
+        :param str path: The path of the directory \
+            that is to be created.
+        '''
+        (self.__bucket
+            .blob(blob_name=path)
+            .upload_from_string(
+                '',
+                content_type='application/x-www-form-urlencoded;charset=UTF-8'))
+
+
+    def get_reader(self, file_path: str) -> _GCPFileReader:
+        '''
+        Returns a ``GCPFileReader`` class instance \
+        used for reading from a file which resides within \
+        a Google Cloud Storage bucket.
+
+        :param str file_path: The absolute path of \
+            the file in question.
+        '''
+        return _GCPFileReader(
+            file_path=file_path,
+            file_size=self.get_file_size(file_path),
+            bucket=self.__bucket)
+
+
+    def get_writer(
+        self,
+        file_path: str,
+        metadata: _Optional[dict[str, str]],
+        chunk_size: _Optional[int]
+    ) -> _GCPFileWriter:
+        '''
+        Returns a ``GCPFileWriter`` class instance \
+        used for writing to a file which resides within \
+        a Google Cloud Storage bucket.
+
+        :param str file_path: The absolute path of \
+            the file in question.
+        :param dict[str, str] | None metadata: A \
+            dictionary containing the metadata that \
+            are to be assigned to the file in question. \
+            If ``None``, then no metadata are assigned.
+        :param int | None chunk_size: Indicates whether \
+            the size of distinct chunks in which the file \
+            is written. If ``None``, then the file is to be \
+            written as a single chunk of bytes.
+        '''
+        return _GCPFileWriter(
+            file_path=file_path,
+            metadata=metadata,
+            chunk_size=chunk_size,
+            bucket=self.__bucket)
+     
+
+    def _get_file_size_impl(self, file_path) -> int:
+        '''
+        Fetches and returns the size of a file in bytes.
+
+        :param str file_path: The absolute path of the \
+            file in question.
+        '''
+        return self.__bucket.get_blob(file_path).size
+    
+
+    def _get_file_metadata_impl(self, file_path: str) -> dict[str, str]:
+        '''
+        Fetches and returns a dictionary containing the \
+        metadata of a file.
+
+        :param str file_path: The absolute path of the \
+            file in question.
+        '''
+        return dict() if (
+            metadata := self.__bucket.get_blob(file_path).metadata
+        ) is None else metadata
+        
+
+    def _traverse_dir_impl(
+        self,
+        dir_path: str,
+        recursively: bool,
+        show_abs_path: bool
+    ) -> _Iterator[str]:
+        '''
+        Returns an iterator capable of going through \
+        the directory's contents as their paths.
+
+        :param str dir_path: The absolute path of the directory \
+            whose contents are to be iterated.
+        :param bool recursively: Indicates whether the directory \
+            is to be traversed recursively or not. If set to  ``False``, \
+            then only those files that reside directly within the \
+            directory are to be considered. If set to ``True``, \
+            then all files are considered, no matter whether they \
+            reside directly within the directory or within any of \
+            its subdirectories.
+        :param bool show_abs_path: Indicates whether it \
+            should be displayed the absolute or the relative \
+            path of the contents.
+
+        :note: The resulting iterator may vary depending on the \
+            value of parameter ``recursively``.
+        '''
+        sep = '/'
+
+        if recursively:
+            for blob in self.__bucket.list_blobs(
+                prefix=dir_path
+            ):
+                if blob.name != dir_path and self.is_file(blob.name):
+                    if show_abs_path:
+                        yield blob.name
+                    else:
+                        yield _relativize(dir_path, blob.name, sep)
+        else:
+            dir_iter = _GCSHTTPIter(
+                client=self.__bucket.client,
+                api_request=self.__bucket.client._connection.api_request,
+                path=f"/b/{self.get_bucket_name()}/o",
+                items_key='prefixes',
+                item_to_value=lambda _, item: item,
+                extra_params={
+                    "projection": "noAcl",
+                    "prefix": dir_path,
+                    "delimiter": '/'})
+
+            obj_iter = filter(
+                lambda obj: obj != dir_path,
+                map(
+                    lambda obj: obj.name,
+                    self.__bucket.list_blobs(
+                        prefix=dir_path,
+                        delimiter='/')))
+
+            obj = next(obj_iter, None)
+            dir = next(dir_iter, None)
+        
+            name_fun = lambda name: (
+                name if show_abs_path
+                else _relativize(dir_path, name, sep))
+
+            while True:
+                if obj is None and dir is None:
+                    break
+                elif obj is None and dir is not None:
+                    yield name_fun(dir)
+                    dir = next(dir_iter, None)
+                elif obj is not None and dir is None:
+                    yield name_fun(obj)
+                    obj = next(obj_iter, None)
+                elif obj > dir:
+                    yield name_fun(dir)
+                    dir = next(dir_iter, None)
+                else:
+                    yield name_fun(obj)
+                    obj = next(obj_iter, None)
diff --git a/fluke/_helper.py b/fluke/_helper.py
index e938f55..651925c 100644
--- a/fluke/_helper.py
+++ b/fluke/_helper.py
@@ -34,7 +34,6 @@ def relativize_path(parent: str, child: str, sep: str) -> str:
     :param str sep: The path separator used.
     '''
     return child.removeprefix(parent)
-    #return child.removeprefix(parent).removeprefix(sep)
 
 
 def infer_separator(path: str) -> str:
diff --git a/fluke/_iohandlers.py b/fluke/_iohandlers.py
index 2f6531c..5374ac1 100644
--- a/fluke/_iohandlers.py
+++ b/fluke/_iohandlers.py
@@ -10,6 +10,10 @@
 import boto3 as _boto3
 from azure.storage.blob import ContainerClient as _ContainerClient
 from azure.storage.blob import BlobType as _BlobType
+from google.cloud.storage import Bucket as _GCPBucket
+from google.resumable_media.requests import ResumableUpload as _ResumableUpload
+from google.auth.transport.requests import AuthorizedSession as _AuthSession
+from google.resumable_media.requests import MultipartUpload as _MultipartUpload
 from botocore.exceptions import ClientError as _BotoClientError
 from azure.core.exceptions import HttpResponseError as _AzureResponseError
 
@@ -202,17 +206,14 @@ def read_range(
     @_absmethod
     def _read_impl(
         self,
-        start: _Optional[int],
-        end: _Optional[int]
+        start: int,
+        end: int
     ) -> bytes:
         '''
         Reads and returns the specified byte range.
 
-        :param int | None start: The point to start reading from. \
-            If ``None``, then starts reading from the beginning \
-            of the file.
-        :param int | None end: The point to stop reading from. \
-            If ``None``, then stops reading at the end of the file.
+        :param int start: The point to start reading from.
+        :param int end: The point to stop reading from.
         '''
         pass
 
@@ -313,17 +314,14 @@ def close(self) -> None:
 
     def _read_impl(
         self,
-        start: _Optional[int],
-        end: _Optional[int]
+        start: int,
+        end: int
     ) -> bytes:
         '''
         Reads and returns the specified byte range.
 
-        :param int | None start: The point to start reading from. \
-            If ``None``, then starts reading from the beginning \
-            of the file.
-        :param int | None end: The point to stop reading from. \
-            If ``None``, then stops reading at the end of the file.
+        :param int start: The point to start reading from.
+        :param int end: The point to stop reading from.
         '''
         self.__file.seek(start)
         return self.__file.read(end - start)
@@ -417,17 +415,14 @@ def close(self) -> None:
 
     def _read_impl(
         self,
-        start: _Optional[int],
-        end: _Optional[int]
+        start: int,
+        end: int
     ) -> bytes:
         '''
         Reads and returns the specified byte range.
 
-        :param int | None start: The point to start reading from. \
-            If ``None``, then starts reading from the beginning \
-            of the file.
-        :param int | None end: The point to stop reading from. \
-            If ``None``, then stops reading at the end of the file.
+        :param int start: The point to start reading from.
+        :param int end: The point to stop reading from.
         '''
         self.__file.seek(start)
         return self.__file.read(end - start)
@@ -442,8 +437,6 @@ class RemoteFileWriter(_FileWriter):
         the file in question.
     :param SFTPClient sftp: An ``SFTPClient`` \
         class instance.
-    :param int offset: The byte offset. Defaults \
-        to ``0``.
     '''
 
     def __init__(
@@ -557,18 +550,15 @@ def close(self) -> None:
 
     def _read_impl(
         self,
-        start: _Optional[int],
-        end: _Optional[int]
+        start: int,
+        end: int
     ) -> bytes:
         '''
         Reads and returns the specified byte range.
 
-        :param int | None start: The point to start reading from. \
-            If ``None``, then starts reading from the beginning \
-            of the file.
-        :param int | None end: The point to stop reading from. \
-            If ``None``, then stops reading at the end of the file.
-        '''    
+        :param int start: The point to start reading from.
+        :param int end: The point to stop reading from.
+        ''' 
         range = f"bytes={start}-{end-1}"
         return self.__file.get(Range=range)['Body'].read()
 
@@ -584,18 +574,17 @@ class AmazonS3FileWriter(_FileWriter):
         dictionary containing the metadata that \
         are to be assigned to the file in question. \
         If ``None``, then no metadata are assigned.
-    :param bool in_chunks: Indicates whether to \
-        write the file in distinct chunks or \
-        all at once.
-    :param SFTPClient sftp: An ``SFTPClient`` \
-        class instance.
+    :param int | None chunk_size: Indicates whether \
+        the size of distinct chunks in which the file \
+        is written. If ``None``, then the file is to be \
+        written as a single chunk of bytes.
+    :param Bucket bucket: A ``Bucket`` class instance.
     '''
-
     def __init__(
         self,
         file_path: str,
         metadata: _Optional[dict[str, str]],
-        in_chunks: bool,
+        chunk_size: _Optional[int],
         bucket: '_boto3.resources.factory.s3.Bucket'
     ) -> None:
         '''
@@ -608,9 +597,10 @@ def __init__(
             dictionary containing the metadata that \
             are to be assigned to the file in question. \
             If ``None``, then no metadata are assigned.
-        :param bool in_chunks: Indicates whether to \
-            write the file in distinct chunks or \
-            all at once.
+        :param int | None chunk_size: Indicates whether \
+            the size of distinct chunks in which the file \
+            is written. If ``None``, then the file is to be \
+            written as a single chunk of bytes.
         :param Bucket bucket: A ``Bucket`` class instance.
         '''
         super().__init__(file_path=file_path)
@@ -620,7 +610,7 @@ def __init__(
         # multipart-upload, including any metadata that 
         # may exist. Else, store the metadata dictionary
         # so as to include them during the bulk upload.
-        if in_chunks:
+        if chunk_size is not None:
             if metadata is None:
                 self.__mpu = self.__file.initiate_multipart_upload()
             else:
@@ -712,18 +702,15 @@ def close(self) -> None:
 
     def _read_impl(
         self,
-        start: _Optional[int],
-        end: _Optional[int]
+        start: int,
+        end: int
     ) -> bytes:
         '''
         Reads and returns the specified byte range.
 
-        :param int | None start: The point to start reading from. \
-            If ``None``, then starts reading from the beginning \
-            of the file.
-        :param int | None end: The point to stop reading from. \
-            If ``None``, then stops reading at the end of the file.
-        '''  
+        :param int start: The point to start reading from.
+        :param int end: The point to stop reading from.
+        '''
         return self.__file.download_blob(
             offset=start,
             length=end-start).read()
@@ -740,20 +727,19 @@ class AzureBlobWriter(_FileWriter):
         dictionary containing the metadata that \
         are to be assigned to the file in question. \
         If ``None``, then no metadata are assigned.
-    :param bool in_chunks: Indicates whether to \
-        write the file in distinct chunks or \
-        all at once.
+    :param int | None chunk_size: Indicates whether \
+        the size of distinct chunks in which the file \
+        is written. If ``None``, then the file is to be \
+        written as a single chunk of bytes.
     :param ContainerClient container: A \
         ``ContainerClient`` class instance.
-    :param int offset: The byte offset. Defaults \
-        to ``0``.
     '''
 
     def __init__(
         self,
         file_path: str,
         metadata: _Optional[dict[str, str]],
-        in_chunks: bool,
+        chunk_size: _Optional[int],
         container: _ContainerClient,
     ) -> None:
         '''
@@ -766,18 +752,17 @@ def __init__(
             dictionary containing the metadata that \
             are to be assigned to the file in question. \
             If ``None``, then no metadata are assigned.
-        :param bool in_chunks: Indicates whether to \
-            write the file in distinct chunks or \
-            all at once.
+        :param int | None chunk_size: Indicates whether \
+            the size of distinct chunks in which the file \
+            is written. If ``None``, then the file is to be \
+            written as a single chunk of bytes.
         :param ContainerClient container: A \
             ``ContainerClient`` class instance.
-        :param int offset: The byte offset. Defaults \
-            to ``0``.
         '''
         super().__init__(file_path=file_path)
         self.__file = container.get_blob_client(blob=file_path)
         self.__metadata = metadata
-        self.__in_chunks = in_chunks
+        self.__chunk_size = chunk_size
         # NOTE: If blob already exists, then it has to be deleted only
         #       in the case that its type has to change. Furthermore,
         #       in the case the blob is to be written in chunks, it must
@@ -786,14 +771,14 @@ def __init__(
             blob_type = self.__file.get_blob_properties().blob_type
             if blob_type == _BlobType.PAGEBLOB:
                 self.__file.delete_blob()
-                if in_chunks:
+                if chunk_size is not None:
                     self.__file.create_append_blob()
-            elif in_chunks and blob_type == _BlobType.BLOCKBLOB:
+            elif chunk_size is not None and blob_type == _BlobType.BLOCKBLOB:
                 self.__file.delete_blob()
                 self.__file.create_append_blob()
-            elif not in_chunks and blob_type == _BlobType.APPENDBLOB:
+            elif chunk_size is None and blob_type == _BlobType.APPENDBLOB:
                 self.__file.delete_blob()
-        elif in_chunks:
+        elif chunk_size is not None:
             self.__file.create_append_blob()
 
 
@@ -803,7 +788,7 @@ def close(self) -> None:
         '''
         # If blob was uploaded in chunks,
         # then set file metadatan here.
-        if self.__in_chunks:
+        if self.__chunk_size is not None:
             self.__file.set_blob_metadata(
                 metadata=self.__metadata)
         # Close the file.
@@ -819,15 +804,200 @@ def _write_impl(self, chunk: bytes) -> int:
             is to be written to the file.
         '''
         n = len(chunk)
-        if self.__in_chunks:
-            n = len(chunk)
-            self.__file.append_block(
-                data=chunk,
-                length=n)
-        else:
+        if self.__chunk_size is None:
             self.__file.upload_blob(
                 data=chunk,
                 length=n,
                 metadata=self.__metadata,
                 overwrite=True)
-        return n
\ No newline at end of file
+        else:
+            self.__file.append_block(
+                data=chunk,
+                length=n)
+        return n
+    
+
+class GCPFileReader(_FileReader):
+    '''
+    A class used in reading from files which \
+    reside within a Google Cloud Storage bucket.
+
+    :param str file_path: The absolute path of \
+        the file in question.
+    :param int file_size: The size in bytes of \
+        the file in question.
+    :param Bucket bucket: A ``Bucket`` class instance.
+    '''
+
+    def __init__(
+        self,
+        file_path: str,
+        file_size: int,
+        bucket: _GCPBucket
+    ) -> None:
+        '''
+        A class used in reading from files which \
+        reside within a Google Cloud Storage bucket.
+
+        :param str file_path: The absolute path of \
+            the file in question.
+        :param int file_size: The size in bytes of \
+            the file in question.
+        :param Bucket bucket: A ``Bucket`` class instance.
+        '''
+        super().__init__(file_path=file_path, file_size=file_size)
+        self.__file = bucket.get_blob(blob_name=file_path)
+
+
+    def close(self) -> None:
+        '''
+        Closes the handler's underlying file.
+        '''
+        # NOTE: No need to close file, just the
+        #       underlying HTTPS connection which
+        #       will be closed by the `GCPClientHandler``
+        #       class instance.
+        pass
+
+
+    def _read_impl(
+        self,
+        start: int,
+        end: int
+    ) -> bytes:
+        '''
+        Reads and returns the specified byte range.
+
+        :param int start: The point to start reading from.
+        :param int end: The point to stop reading from.
+        '''
+        return self.__file.download_as_bytes(start=start, end=end-1)
+
+            
+class GCPFileWriter(_FileWriter):
+    '''
+    A class used in writing to files which \
+    reside within a Google Cloud Storage bucket.
+
+    :param str file_path: The absolute path of \
+        the file in question.
+    :param dict[str, str] | None metadata: A \
+        dictionary containing the metadata that \
+        are to be assigned to the file in question. \
+        If ``None``, then no metadata are assigned.
+    :param int | None chunk_size: Indicates whether \
+        the size of distinct chunks in which the file \
+        is written. If ``None``, then the file is to be \
+        written as a single chunk of bytes.
+    :param Bucket bucket: A ``Bucket`` class instance.
+    '''
+    def __init__(
+        self,
+        file_path: str,
+        metadata: _Optional[dict[str, str]],
+        chunk_size: _Optional[int],
+        bucket: _GCPBucket
+    ) -> None:
+        '''
+        A class used in reading from files which \
+        reside within a remote file system.
+
+        :param str file_path: The absolute path of \
+            the file in question.
+        :param dict[str, str] | None metadata: A \
+            dictionary containing the metadata that \
+            are to be assigned to the file in question. \
+            If ``None``, then no metadata are assigned.
+        :param int | None chunk_size: Indicates whether \
+            the size of distinct chunks in which the file \
+            is written. If ``None``, then the file is to be \
+            written as a single chunk of bytes.
+        :param Bucket bucket: A ``Bucket`` class instance.
+        '''
+        super().__init__(file_path=file_path)
+
+        # NOTE: If uploading file in chunks, then
+        #       create a resumable upload session.
+        if chunk_size is None:
+            self.__file = bucket.blob(blob_name=file_path)
+            self.__metadata = metadata
+        else:
+            self.__file = None
+            self.__rus = self._create_resumable_upload_session(
+                bucket=bucket.name,
+                file_path=file_path,
+                chunk_size=chunk_size)
+            self.__transport = _AuthSession(
+                credentials=bucket.client._credentials)
+            self.__stream = _io.BytesIO()
+            self.__rus.initiate(
+                transport=self.__transport,
+                content_type='application/octet-stream',
+                stream=self.__stream,
+                stream_final=False,
+                metadata={'metadata': metadata})
+
+
+    def close(self) -> None:
+        '''
+        Closes the handler's underlying file.
+        '''
+        # NOTE: No need to close file, just the
+        #       underlying HTTPS connection which
+        #       will be closed by the `GCPClientHandler``
+        #       class instance.
+        if self.__file is None:
+            # NOTE: Due to setting ``stream_final`` to ``False``,
+            #       the upload is automatically iferred to be over
+            #       whenever a chunk whose size is less than the
+            #       specified chunk size is uploaded. If a file
+            #       happens to be divided into equally sized chunks,
+            #       then the algorithm cannot infer the final chunk.
+            #       In that case, a zero-byte chunk should be uploaded
+            #       to indicate that the upload has been finalized.
+            if not self.__rus.finished:
+                self.__rus.transmit_next_chunk(
+                    transport=self.__transport)
+            self.__transport.close()
+            self.__stream.close()
+        elif self.__metadata is not None:
+            # NOTE: Call ``patch`` in order to
+            #       upload the file's metadata.
+            self.__file.metadata = self.__metadata
+            self.__file.patch()
+
+
+    def _write_impl(self, chunk: bytes) -> int:
+        '''
+        Writes the provided chunk to the opened file,
+        and returns the number of bytes written.
+
+        :param bytes chunk: The chunk of bytes that \
+            is to be written to the file.
+        '''
+        if self.__file is None:
+            self.__stream.write(chunk)
+            self.__stream.seek(-len(chunk), 1)
+            self.__rus.transmit_next_chunk(
+                transport=self.__transport)
+        else:
+            with _io.BytesIO(chunk) as buffer:
+                self.__file.upload_from_file(
+                    file_obj=buffer,
+                    size=len(chunk))
+        return len(chunk)
+    
+
+    @staticmethod
+    def _create_resumable_upload_session(
+        bucket: str,
+        file_path: str,
+        chunk_size: int
+    ) -> _ResumableUpload:
+        return _ResumableUpload(
+            upload_url=(
+                "https://storage.googleapis.com" +
+                f"/upload/storage/v1/b/{bucket}/" +
+                f"o?uploadType=resumable&name={file_path}"
+            ),
+            chunk_size=chunk_size)
\ No newline at end of file
diff --git a/fluke/auth.py b/fluke/auth.py
index b6facf8..c08c833 100644
--- a/fluke/auth.py
+++ b/fluke/auth.py
@@ -3,7 +3,8 @@
 __all__ = [
     'AWSAuth',
     'AzureAuth',
-    'RemoteAuth'
+    'RemoteAuth',
+    'GCPAuth'
 ]
 
 
@@ -18,17 +19,98 @@ class RemoteAuth():
     remote machine via the SSH protocol.
     '''
 
-    class KeyType(_Enum):
+
+    class PublicKey():
         '''
-        This enum-class represents various types \
-        of SSH keys.
+        This class is used in order to generate a \
+        keys in case you need to explicitly provide \
+        the public of the host to which you wish to \
+        establish a connection.
         '''
-        SSH_RSA = "ssh-rsa"
-        SSH_DSS = "ssh-dss"
-        SSH_ED25519 = "ssh-ed25519"
-        ECDSA_SHA2_NISTP256 = "ecdsa-sha2-nistp256"
-        ECDSA_SHA2_NISTP384 = "ecdsa-sha2-nistp384"
-        ECDA_SHA_NISTP521 = "ecdsa-sha2-nistp521"
+
+        class _KeyType(_Enum):
+            '''
+            This enum-class represents various types \
+            of SSH keys.
+            '''
+            SSH_RSA = "ssh-rsa"
+            SSH_DSS = "ssh-dss"
+            SSH_ED25519 = "ssh-ed25519"
+            ECDSA_SHA2_NISTP256 = "ecdsa-sha2-nistp256"
+            ECDSA_SHA2_NISTP384 = "ecdsa-sha2-nistp384"
+            ECDSA_SHA2_NISTP521 = "ecdsa-sha2-nistp521"
+
+        @classmethod
+        def generate_ssh_rsa_key(cls, key: str) -> 'RemoteAuth.PublicKey':
+            '''
+            Generates an SSH RSA key.
+
+            :param str key: The public key.
+            '''
+            pkey = cls()
+            pkey.key = key
+            pkey.type = cls._KeyType.SSH_RSA
+            return pkey
+        
+        @classmethod
+        def generate_ssh_dss_key(cls, key: str) -> 'RemoteAuth.PublicKey':
+            '''
+            Generates an SSH DSA key.
+
+            :param str key: The public key.
+            '''
+            pkey = cls()
+            pkey.key = key
+            pkey.type = cls._KeyType.SSH_DSS
+            return pkey
+        
+        @classmethod
+        def generate_ssh_ed25519_key(cls, key: str) -> 'RemoteAuth.PublicKey':
+            '''
+            Generates an SSH ED25519 key.
+
+            :param str key: The public key.
+            '''
+            pkey = cls()
+            pkey.key = key
+            pkey.type = cls._KeyType.SSH_ED25519
+            return pkey
+        
+        @classmethod
+        def generate_ecdsa_sha2_nistp256_key(cls, key: str) -> 'RemoteAuth.PublicKey':
+            '''
+            Generates an SHA2 NISTP256 key.
+
+            :param str key: The public key.
+            '''
+            pkey = cls()
+            pkey.key = key
+            pkey.type = cls._KeyType.ECDSA_SHA2_NISTP256
+            return pkey
+        
+        @classmethod
+        def generate_ecdsa_sha2_nistp384_key(cls, key: str) -> 'RemoteAuth.PublicKey':
+            '''
+            Generates an SHA2 NISTP384 key.
+
+            :param str key: The public key.
+            '''
+            pkey = cls()
+            pkey.key = key
+            pkey.type = cls._KeyType.ECDSA_SHA2_NISTP384
+            return pkey
+        
+        @classmethod
+        def generate_ecdsa_sha2_nistp521_key(cls, key: str) -> 'RemoteAuth.PublicKey':
+            '''
+            Generates an SHA2 NISTP521 key.
+
+            :param str key: The public key.
+            '''
+            pkey = cls()
+            pkey.key = key
+            pkey.type = cls._KeyType.ECDSA_SHA2_NISTP521
+            return pkey
 
 
     @classmethod
@@ -38,8 +120,7 @@ def from_password(
         username: str,
         password: str,
         port: int = 22,
-        public_key: _Optional[str] = None,
-        key_type: _Optional[KeyType] = None,
+        public_key: _Optional[PublicKey] = None,
         verify_host: bool = True
     )-> 'RemoteAuth':
         '''
@@ -52,10 +133,8 @@ def from_password(
         :param str password: The user's password.
         :param int port: The port to which you will be connecting. \
             Defaults to ``22``.
-        :param str | None public_key: The host's public SSH key. \
-            Defaults to ``None``.
-        :param KeyType | None key_type: The type of the host's \
-            public SSH key. Defaults to ``None``.
+        :param RemoteAuth.PublicKey | None public_key: The host's public \
+            SSH key. Defaults to ``None``.
         :param bool verify_host: Unless set to ``False``, a connection \
             can only be established if the host is known to the local \
             machine. Defaults to ``True``.
@@ -67,7 +146,6 @@ def from_password(
             'password': password,
             'port': port,
             'public_key': public_key,
-            'key_type': key_type if key_type is None else key_type.value,
             'verify_host': verify_host
         }  
         return auth
@@ -81,8 +159,7 @@ def from_key(
         pkey: str,
         passphrase: _Optional[str] = None,
         port: int = 22,
-        public_key: _Optional[str] = None,
-        key_type: _Optional[KeyType] = None,
+        public_key: _Optional[PublicKey] = None,
         verify_host: bool = True
     )-> 'RemoteAuth':
         '''
@@ -99,10 +176,8 @@ def from_key(
             encrypted. Defaults to ``None``.
         :param int port: The port to which you will be connecting. \
             Defaults to ``22``.
-        :param str | None public_key: The host's public SSH key. \
-            Defaults to ``None``.
-        :param KeyType | None key_type: The type of the host's \
-            public SSH key. Defaults to ``None``.
+        :param RemoteAuth.PublicKey | None public_key: The host's public \
+            SSH key. Defaults to ``None``.
         :param bool verify_host: Unless set to ``False``, a connection \
             can only be established if the host is known to the local \
             machine. Defaults to ``True``.
@@ -115,7 +190,6 @@ def from_key(
             'passphrase': passphrase,
             'port': port,
             'public_key': public_key,
-            'key_type': key_type if key_type is None else key_type.value,
             'verify_host': verify_host
         }  
         return auth
@@ -283,4 +357,64 @@ def _get_storage_account(self) -> str:
         Returns the storage account that is \
         associated with this instance.
         '''
-        return self.__storage_account
\ No newline at end of file
+        return self.__storage_account
+
+
+class GCPAuth():
+    '''
+    This class is used for authenticating with GCP.
+    '''
+    _PROJECT_ID = 'project_id'
+    _APPLICATION_DEFAULT_CREDENTIALS = 'adc'
+    _SERVICE_ACCOUNT_KEY = 'sak'
+
+
+    @classmethod
+    def from_application_default_credentials(
+        cls,
+        project_id: str,
+        credentials: str
+    ) -> 'GCPAuth':
+        '''
+        Returns a ``GCPAuth`` instance used for \
+        authenticating with a GCP project via the \
+        Application Default Credentials (ADC) strategy.
+
+        :param str project_id: The GCP project's identifier.
+        :param str credentials: The path to the JSON file \
+            containing the credentials.
+        '''
+        auth = cls()
+        auth.__credentials = {
+            cls._PROJECT_ID: project_id,
+            cls._APPLICATION_DEFAULT_CREDENTIALS: credentials
+        }
+        return auth
+    
+
+    @classmethod
+    def from_service_account_key(
+        cls,
+        credentials: str
+    ) -> 'GCPAuth':
+        '''
+        Returns a ``GCPAuth`` instance used for \
+        authenticating with a GCP project via a \
+        service account key file.
+
+        :param str credentials: The path to the \
+            service account key JSON file.
+        '''
+        auth = cls()
+        auth.__credentials = {
+            cls._SERVICE_ACCOUNT_KEY: credentials
+        }
+        return auth
+
+
+    def get_credentials(self) -> dict[str, str]:
+        '''
+        Returns the provided credentials stored \
+        within a dictionary.
+        '''
+        return dict(self.__credentials)
\ No newline at end of file
diff --git a/fluke/queues.py b/fluke/queues.py
index cccba57..b43aacc 100644
--- a/fluke/queues.py
+++ b/fluke/queues.py
@@ -668,7 +668,7 @@ def poll(
                             try:
                                 self.__queue.delete_message(msg.id, msg.pop_receipt)
                                 messages.append(msg.content)
-                            except:
+                            except Exception:
                                 if not suppress_output:
                                     print(f'Failed to delete message "{msg}".')
                         # Then deliver messages.
@@ -683,7 +683,7 @@ def poll(
                         for msg in messages:
                             try:
                                 self.__queue.delete_message(msg.id, msg.pop_receipt)
-                            except:
+                            except Exception:
                                 if not suppress_output:
                                     print(f'Failed to delete message "{msg}".')
                     # Indicate there are still messages left.
diff --git a/fluke/storage.py b/fluke/storage.py
index 83f9a35..9801d1c 100644
--- a/fluke/storage.py
+++ b/fluke/storage.py
@@ -5,6 +5,8 @@
     'AmazonS3File',
     'AzureBlobDir',
     'AzureBlobFile',
+    'GCPStorageDir',
+    'GCPStorageFile',
     'LocalFile',
     'LocalDir',
     'RemoteFile',
@@ -22,23 +24,26 @@
 from tqdm import tqdm as _tqdm
 
 
-from ._helper import join_paths as _join_paths
-from ._helper import infer_separator as _infer_sep
 from .auth import AWSAuth as _AWSAuth
 from .auth import AzureAuth as _AzureAuth
+from .auth import GCPAuth as _GCPAuth
 from .auth import RemoteAuth as _RemoteAuth
+from ._cache import DirCache as _DirCache
 from ._handlers import ClientHandler as _ClientHandler
 from ._handlers import FileSystemHandler as _FileSystemHandler
 from ._handlers import SSHClientHandler as _SSHClientHandler
 from ._handlers import AWSClientHandler as _AWSClientHandler
 from ._handlers import AzureClientHandler as _AzureClientHandler
+from ._handlers import GCPClientHandler as _GCPClientHandler
+from ._helper import join_paths as _join_paths
+from ._helper import infer_separator as _infer_sep
 from ._exceptions import InvalidPathError as _IPE
 from ._exceptions import InvalidFileError as _IFE
 from ._exceptions import InvalidDirectoryError as _IDE
 from ._exceptions import OverwriteError as _OverwriteError
 from ._exceptions import NonStringMetadataKeyError as _NSMKE
 from ._exceptions import NonStringMetadataValueError as _NSMVE
-from ._exceptions import ContainerNotFoundError as _CNFE 
+from ._exceptions import InvalidChunkSizeError as _ICSE
 
 
 class _File(_ABC):
@@ -193,7 +198,7 @@ def read_text(
         :param str encoding: The encoding with which to decode \
             the bytes. Defaults to ``utf-8``.
 
-        :note: See
+        :note: See \
             `Standard Encodings <https://docs.python.org/3/library/codecs.html#standard-encodings>`_ \
             for all available encodings.
         '''
@@ -235,6 +240,20 @@ def read_lines(
             yield text_temp
 
 
+    def cat(self, encoding: str = 'utf-8') -> None:
+        '''
+        Prints the contents of a file as text.
+
+        :param str encoding: The encoding with which to decode \
+            the bytes. Defaults to ``utf-8``.
+
+        :note: See \
+            `Standard Encodings <https://docs.python.org/3/library/codecs.html#standard-encodings>`_ \
+            for all available encodings.
+        '''
+        print(self.read_text(encoding=encoding))
+
+
     def transfer_to(
         self,
         dst: '_Directory',
@@ -260,7 +279,13 @@ def transfer_to(
             value. Defaults to ``None``.
         :param bool suppress_output: If set to ``True``, then \
             suppresses all output. Defaults to ``False``.
+
+        :raises InvalidChunkSizeError: Transferring files in chunks of \
+            the given size is not supported by the specified destination.
         '''
+        if chunk_size is not None:
+            dst._validate_chunk_size(chunk_size)
+
         source = self.get_uri() \
             if isinstance(self, _NonLocalFile) \
             else self.get_path()
@@ -305,7 +330,7 @@ def transfer_to(
                 dst._get_handler().get_writer(
                     file_path=dst_fp,
                     metadata=metadata,
-                    in_chunks=chunk_size is not None
+                    chunk_size=chunk_size
                 ) as writer
             ):
                 if chunk_size is None:
@@ -403,8 +428,7 @@ def __init__(self, path: str):
         if not _os.path.exists(path):
             raise _IPE(path)
         if not _os.path.isfile(path):
-            raise _IFE(path)
-        
+            raise _IFE(path) 
         sep = _infer_sep(path=path)
         super().__init__(
             path=_os.path.abspath(path).replace(_os.sep, sep),
@@ -454,6 +478,11 @@ class _NonLocalFile(_File, _ABC):
     :param str path: A path pointing to the file.
     :param ClientHandler handler: A ``ClientHandler`` class \
         instance used for interacting with the underlying handler.
+
+    :raises InvalidPathError: The provided path \
+        does not exist.
+    :raises InvalidFileError: The provided path \
+        points to a directory.
     '''
 
     def __init__(
@@ -469,12 +498,25 @@ def __init__(
         :param str path: A path pointing to the file.
         :param ClientHandler handler: A ``ClientHandler`` class \
             instance used for interacting with the underlying handler.
+
+        :raises InvalidPathError: The provided path \
+            does not exist.
+        :raises InvalidFileError: The provided path \
+            points to a directory.
         '''
         super().__init__(
             path=path,
             metadata=dict(),
             handler=handler)
+        # Open connection.
         self.open()
+        # Check if path is valid.
+        if not handler.path_exists(path=path):
+            self.close()
+            raise _IPE(path)
+        if not handler.is_file(file_path=path):
+            self.close()
+            raise _IFE(path)
 
 
     def is_cacheable(self) -> bool:
@@ -552,7 +594,7 @@ class RemoteFile(_NonLocalFile):
     :param str path: A path pointing to the file.
     :param bool cache: Indicates whether it is allowed for \
         any fetched data to be cached for faster subsequent \
-        access. Defaults to ``True``.
+        access. Defaults to ``False``.
 
     :raises InvalidPathError: The provided path \
         does not exist.
@@ -583,23 +625,13 @@ def __init__(
         :raises InvalidFileError: The provided path \
             points to a directory.
         '''
-        # Instantiate a connection handler.
-        ssh_handler = _SSHClientHandler(auth=auth, cache=cache)
-
         # Set up hostname.
         self.__host = auth.get_credentials()['hostname']
-
         super().__init__(
             path=path,
-            handler=ssh_handler)
-
-        if not ssh_handler.path_exists(path=path):
-            self.close()
-            raise _IPE(path)
-        
-        if not ssh_handler.is_file(file_path=path):
-            self.close()
-            raise _IFE(path)
+            handler=_SSHClientHandler(
+                auth=auth,
+                cache=_DirCache(path) if cache else None))
 
 
     def get_hostname(self) -> str:
@@ -658,7 +690,41 @@ class _CloudFile(_NonLocalFile, _ABC):
     '''
     An abstract class which serves as the base class for \
     all file-like classes that represent files in the cloud.
+
+    :param str path: A path pointing to the file.
+    :param bool load_metadata: Indicates whether any \
+        any existing metadata should be loaded during the \
+        the file's instantiation.
+    :param ClientHandler handler: A ``ClientHandler`` class \
+        instance used for interacting with the underlying handler.
     '''
+    def __init__(
+        self,
+        path: str,
+        load_metadata: bool,
+        handler: _ClientHandler
+    ) -> None:
+        '''
+        An abstract class which serves as the base class for \
+        all file-like classes that represent files in the cloud.
+
+        :param str path: A path pointing to the file.
+        :param bool load_metadata: Indicates whether any \
+            any existing metadata should be loaded during the \
+            the file's instantiation.
+        :param ClientHandler handler: A ``ClientHandler`` class \
+            instance used for interacting with the underlying handler.
+        '''
+        # Validate path.
+        sep = _infer_sep(path=path)
+        if path.startswith(sep):
+            raise _IPE(path=path)
+        # Invoke constuctor, thereby opening a connection.
+        super().__init__(path, handler)
+        # Load metadata if instructed to do so.
+        if load_metadata:
+            self.load_metadata()
+
 
     def load_metadata(self) -> None:
         '''
@@ -667,12 +733,11 @@ def load_metadata(self) -> None:
         ``get_metadata`` method.
 
         :note: Any metadata set via the ``set_metadata`` \
-            method will be overriden after invoking this \
+            method will be overridden after invoking this \
             method.
         '''
-        metadata = self._get_handler().get_file_metadata(
-            self.get_path())
-        self.set_metadata(metadata=metadata)
+        self.set_metadata(metadata=self._get_handler()
+            .get_file_metadata(file_path=self.get_path()))
 
 
 class AmazonS3File(_CloudFile):
@@ -685,9 +750,12 @@ class AmazonS3File(_CloudFile):
     :param str bucket: The name of the bucket in which \
         the file resides.
     :param str path: The path pointing to the file.
+    :param bool load_metadata: Indicates whether any \
+        existing metadata should be loaded during the \
+        the file's instantiation. Defaults to ``False``.
     :param bool cache: Indicates whether it is allowed for \
         any fetched data to be cached for faster subsequent \
-        access.
+        access. Defaults to ``False``.
 
     :raises BucketNotFoundError: The specified \
         bucket does not exist.
@@ -708,6 +776,7 @@ def __init__(
         auth: _AWSAuth,
         bucket: str,
         path: str,
+        load_metadata: bool = False,
         cache: bool = False
     ):
         '''
@@ -719,6 +788,9 @@ def __init__(
         :param str bucket: The name of the bucket in which \
             the file resides.
         :param str path: The path pointing to the file.
+        :param bool load_metadata: Indicates whether any \
+            existing metadata should be loaded during the \
+            the file's instantiation. Defaults to ``False``.
         :param bool cache: Indicates whether it is allowed for \
             any fetched data to be cached for faster subsequent \
             access. Defaults to ``False``.
@@ -736,30 +808,13 @@ def __init__(
                 * Wrong: ``/path/to/file.txt``
                 * Right: ``path/to/file.txt``
         '''
-        # Validate path.
-        sep = _infer_sep(path=path)
-        if path.startswith(sep):
-            raise _IPE(path=path)
-        
-        # Instantiate a connection handler.
-        aws_handler = _AWSClientHandler(
-            auth=auth,
-            bucket=bucket,
-            cache=cache)
-
         super().__init__(
             path=path,
-            handler=aws_handler)
-
-        if not aws_handler.path_exists(path=path):
-            if aws_handler.dir_exists(path=path):
-                self.close()
-                raise _IFE(path)
-            self.close()
-            raise _IPE(path)     
-        if not aws_handler.is_file(file_path=path):
-            self.close()
-            raise _IFE(path)
+            load_metadata=load_metadata,
+            handler=_AWSClientHandler(
+                auth=auth,
+                bucket=bucket,
+                cache=_DirCache(path) if cache else None))
         
 
     def get_bucket_name(self) -> str:
@@ -820,9 +875,12 @@ class AzureBlobFile(_CloudFile):
     :param str container: The name of the container in \
         which the blob resides.
     :param str path: The path pointing to the file.
+    :param bool load_metadata: Indicates whether any \
+        existing metadata should be loaded during the \
+        the file's instantiation. Defaults to ``False``.
     :param bool cache: Indicates whether it is allowed for \
         any fetched data to be cached for faster subsequent \
-        access.
+        access. Defaults to ``False``.
 
     :raises ContainerNotFoundError: The \
         specified container does not exist.
@@ -843,6 +901,7 @@ def __init__(
         auth: _AzureAuth,
         container: str,
         path: str,
+        load_metadata: bool = False,
         cache: bool = False,
     ):
         '''
@@ -854,6 +913,9 @@ def __init__(
         :param str container: The name of the container in \
             which the blob resides.
         :param str path: The path pointing to the file.
+        :param bool load_metadata: Indicates whether any \
+            existing metadata should be loaded during the \
+            the file's instantiation. Defaults to ``False``.
         :param bool cache: Indicates whether it is allowed for \
             any fetched data to be cached for faster subsequent \
             access. Defaults to ``False``.
@@ -871,36 +933,16 @@ def __init__(
                 * Wrong: ``/path/to/file.txt``
                 * Right: ``path/to/file.txt``
         '''
-        # Validate path.
-        sep = _infer_sep(path=path)
-        if path.startswith(sep):
-            raise _IPE(path=path)
-
-        # Instantiate a connection handler.
-        azr_handler = _AzureClientHandler(
-            auth=auth,
-            container=container,
-            cache=cache)
-        
         # Infer storage account.
         self.__storage_account = auth._get_storage_account()
-        
         super().__init__(
             path=path,
-            handler=azr_handler)
-        
-        # Throw an exception if container does not exist.
-        if not azr_handler.container_exists():
-            self.close()
-            raise _CNFE(container)
-        
-        if not azr_handler.path_exists(path=path):
-            self.close()
-            raise _IPE(path)
-        if not azr_handler.is_file(file_path=path):
-            self.close()
-            raise _IFE(path)
-            
+            load_metadata=load_metadata,
+            handler=_AzureClientHandler(
+                auth=auth,
+                container=container,
+                cache=_DirCache(path) if cache else None))
+  
 
     def get_container_name(self) -> str:
         '''
@@ -954,6 +996,130 @@ def __enter__(self) -> 'AzureBlobFile':
         Enter the runtime context related to this instance.
         '''
         return self
+    
+
+class GCPStorageFile(_CloudFile):
+    '''
+    This class represents an object which resides \
+    within a Google Cloud Storage bucket.
+
+    :param GCPAuth auth: A ``GCPAuth`` instance used \
+        for authenticating with GCP.
+    :param str bucket: The name of the bucket in which \
+        the file resides.
+    :param str path: The path pointing to the file.
+    :param bool load_metadata: Indicates whether any \
+        existing metadata should be loaded during the \
+        the file's instantiation. Defaults to ``False``.
+    :param bool cache: Indicates whether it is allowed for \
+        any fetched data to be cached for faster subsequent \
+        access. Defaults to ``False``.
+
+    :raises BucketNotFoundError: The specified \
+        bucket does not exist.
+    :raises InvalidPathError: The provided path \
+        does not exist.
+    :raises InvalidFileError: The provided path \
+        points to a directory.
+
+    :note: The provided path must not begin with \
+        a separator.
+
+            * Wrong: ``/path/to/file.txt``
+            * Right: ``path/to/file.txt``
+    '''
+    def __init__(
+        self,
+        auth: _GCPAuth,
+        bucket: str,
+        path: str,
+        load_metadata: bool = False,
+        cache: bool = False
+    ):
+        '''
+        This class represents an object which resides \
+        within a Google Cloud Storage bucket.
+
+        :param GCPAuth auth: A ``GCPAuth`` instance used \
+            for authenticating with GCP.
+        :param str bucket: The name of the bucket in which \
+            the file resides.
+        :param str path: The path pointing to the file.
+        :param bool load_metadata: Indicates whether any \
+            existing metadata should be loaded during the \
+            the file's instantiation. Defaults to ``False``.
+        :param bool cache: Indicates whether it is allowed for \
+            any fetched data to be cached for faster subsequent \
+            access. Defaults to ``False``.
+
+        :raises BucketNotFoundError: The specified \
+            bucket does not exist.
+        :raises InvalidPathError: The provided path \
+            does not exist.
+        :raises InvalidFileError: The provided path \
+            points to a directory.
+
+        :note: The provided path must not begin with \
+            a separator.
+
+                * Wrong: ``/path/to/file.txt``
+                * Right: ``path/to/file.txt``
+        '''
+        super().__init__(
+            path=path,
+            load_metadata=load_metadata,
+            handler=_GCPClientHandler(
+                auth=auth,
+                bucket=bucket,
+                cache=_DirCache(path) if cache else None))
+        
+
+    def get_bucket_name(self) -> str:
+        '''
+        Returns the name of the bucket in which \
+        the directory resides.
+        '''
+        return self._get_handler().get_bucket_name()
+
+
+    def get_uri(self) -> str:
+        '''
+        Returns the object's URI.
+        '''
+        return f"gs://{self.get_bucket_name()}{self._get_separator()}{self.get_path()}"
+    
+
+    @classmethod
+    def _create_file(
+        cls,
+        path: str,
+        handler: _GCPClientHandler,
+        metadata: dict[str, str]
+    ) -> 'GCPStorageFile':
+        '''
+        Creates and returns a ``GCPStorageFile`` instance.
+
+        :param str path: The path pointing to the file.
+        :param GCPClientHandler handler: A ``GCPClientHandler`` \
+            class instance.
+        :param dict[str, str] metadata: A dictionary containing \
+            any metadata associated with the file.
+        '''
+        instance = cls.__new__(cls)
+        _File.__init__(
+            instance,
+            path=path,
+            metadata=metadata,
+            handler=handler,
+            close_after_use=False)
+        return instance
+    
+
+    def __enter__(self) -> 'GCPStorageFile':
+        '''
+        Enter the runtime context related to this instance.
+        '''
+        return self
 
 
 class _Directory(_ABC):
@@ -1150,7 +1316,11 @@ def get_contents(
             show_abs_path=show_abs_path))
     
 
-    def ls(self, recursively: bool = False, show_abs_path: bool = False) -> None:
+    def ls(
+        self,
+        recursively: bool = False,
+        show_abs_path: bool = False
+    ) -> None:
         '''
         Lists the contents of the directory.
 
@@ -1165,14 +1335,59 @@ def ls(self, recursively: bool = False, show_abs_path: bool = False) -> None:
             contents' absolute path or their path relative to this directory. \
             Defaults to ``False``.
 
-        :note: The resulting output may vary depending on the value \
-            of parameter ``recursively``.
+        :note: Empty directories will not be considered if parameter \
+            ``recursively`` is set to ``True``.
         '''
-        for entity in self.traverse(
+        iterator = self.traverse(
             recursively=recursively,
-            show_abs_path=show_abs_path
-        ):
-            print(entity)
+            show_abs_path=show_abs_path)
+        
+        if recursively:
+
+            fs = dict()
+            fs_path = self.get_path()
+            sep = self._get_separator()
+
+            for path in map(
+                lambda p: p.removeprefix(fs_path),
+                iterator
+            ):
+                parent = fs
+                for i in range(num_entities := len(
+                    entities := path.split(sep=sep)
+                )):
+                    # If final entity of path.
+                    if i == num_entities - 1:
+                        # Add it only if it is a file,
+                        # in which case it will be other than ''.
+                        if entities[i] != '':
+                            parent.update({entities[i]: None})
+                        break
+                    # Add sep back to dir entity name.
+                    entities[i] = entities[i] + sep
+                    # Add dir entity if it has not been added.
+                    if entities[i] not in parent:
+                        parent.update({entities[i]: dict()})
+                    # Reference current dir entity via parent.
+                    parent = parent[entities[i]]
+
+            def print_entities(d: dict, level: int) -> None:
+                for (key, val) in d.items():
+                    print(f"{3 * (level - 1) * ' '}{'|__' if level > 0 else ''}{key}")
+                    if val is not None:
+                        print_entities(d=val, level=level+1)
+            print()
+            print_entities(d={
+                (sep if (name := self.get_name()) is None
+                else (
+                    self.get_path() if show_abs_path
+                    else name + sep)
+                ): fs
+            }, level=0)
+        else:
+            print()
+            for entity in iterator:
+                print(entity)
 
 
     def count(self, recursively: bool = False) -> int:
@@ -1274,7 +1489,12 @@ def transfer_to(
             transfer (``True``) or not (``False``). Defaults to ``None``.
         :param bool suppress_output: If set to ``True``, then \
             suppresses all output. Defaults to ``False``.
+
+        :raises InvalidChunkSizeError: Transferring files in chunks of \
+            the given size is not supported by the specified destination.
         '''
+        if chunk_size is not None:
+            dst._validate_chunk_size(chunk_size)
         if filter is None:
             filter = lambda _: True
 
@@ -1534,6 +1754,20 @@ def _get_subdir_impl(self, dir_path: str) -> '_Directory':
         pass
 
 
+    @_absmethod
+    def _validate_chunk_size(self, chunk_size: int) -> None:
+        '''
+        This method goes on to throw an ``InvalidChunkSizeError``, \
+        displaying an appropriate message, if file uploading to \
+        the directory is not supported for the provided chunk size. \
+        Else, this method does nothing.
+
+        :raises InvalidChunkSizeError: File uploading in chunks is not \
+            supported for the provided chunk size.
+        '''
+        pass
+
+
 class LocalDir(_Directory):
     '''
     This class represents a directory which resides \
@@ -1690,6 +1924,21 @@ def _get_subdir_impl(self, dir_path: str) -> 'LocalDir':
             path=dir_path,
             handler=self._get_handler(),
             metadata=self._get_metadata_ref())
+    
+
+    def _validate_chunk_size(self, chunk_size: int) -> None:
+        '''
+        This method goes on to throw an ``InvalidChunkSizeError``, \
+        displaying an appropriate message, if file uploading to \
+        the directory is not supported for the provided chunk size. \
+        Else, this method does nothing.
+
+        :raises InvalidChunkSizeError: File uploading in chunks is not \
+            supported for the provided chunk size.
+
+        :note: Does nothing as all chunk sizes are supported.
+        '''
+        pass
 
 
 class _NonLocalDir(_Directory, _ABC):
@@ -1699,12 +1948,22 @@ class _NonLocalDir(_Directory, _ABC):
     directories or directories in the cloud.
 
     :param str path: The path pointing to the directory.
+    :param bool create_if_missing: If set to ``True``, then the directory \
+        to which the provided path points will be automatically created \
+        in case it does not already exist, instead of an exception being \
+        thrown.
     :param ClientHandler handler: A ``ClientHandler`` class \
         instance used for interacting with the underlying handler.
+
+    :raises InvalidPathError: The provided path \
+        does not exist.
+    :raises InvalidDirectoryError: The provided path \
+        does not point to a directory.
     '''
     def __init__(
         self,
         path: str,
+        create_if_missing: bool,
         handler: _ClientHandler
     ):
         '''
@@ -1713,11 +1972,35 @@ def __init__(
         directories or directories in the cloud.
 
         :param str path: The path pointing to the directory.
+        :param bool create_if_missing: If set to ``True``, then the directory \
+            to which the provided path points will be automatically created \
+            in case it does not already exist, instead of an exception being \
+            thrown.
         :param ClientHandler handler: A ``ClientHandler`` class \
             instance used for interacting with the underlying handler.
+
+        :raises InvalidPathError: The provided path \
+            does not exist.
+        :raises InvalidDirectoryError: The provided path \
+            does not point to a directory.
         '''
         super().__init__(path=path, metadata=dict(), handler=handler)
         self.open()
+        # Refresh path after any modifications.
+        path = self.get_path()
+        # NOTE: Use ``path != ''`` as when it comes to cloud directories
+        #       one can use the empty path in order to reference the
+        #       bucket's/container's "top-level" virtual directory.
+        if path != '':
+            if not handler.path_exists(path=path):
+                if create_if_missing:
+                    handler.mkdir(path=path)
+                else:
+                    self.close()
+                    raise _IPE(path)
+            if handler.is_file(file_path=path):
+                self.close()
+                raise _IDE(path)
 
 
     def is_cacheable(self) -> bool:
@@ -1834,27 +2117,17 @@ def __init__(
         :raises InvalidDirectoryError: The provided path \
             does not point to a directory.
         '''        
-        ssh_handler = _SSHClientHandler(auth=auth, cache=cache)
-        self.__host = auth.get_credentials()['hostname']
-
-        super().__init__(
-            path=path,
-            handler=ssh_handler)
-        
         # Validate path.
         if path == '':
-            self.close()
             raise _IPE(path=path)
-
-        if not ssh_handler.path_exists(path=path):
-            if create_if_missing:
-                ssh_handler.mkdir(path=path)
-            else:
-                self.close()
-                raise _IPE(path)
-        if ssh_handler.is_file(file_path=path):
-            self.close()
-            raise _IDE(path)
+        # Set hostname.
+        self.__host = auth.get_credentials()['hostname']
+        super().__init__(
+            path=path,
+            create_if_missing=create_if_missing,
+            handler=_SSHClientHandler(
+                auth=auth,
+                cache=_DirCache(path) if cache else None))
 
 
     def get_hostname(self) -> str:
@@ -1973,11 +2246,26 @@ def _get_subdir_impl(self, dir_path: str) -> 'RemoteDir':
             metadata=self._get_metadata_ref())
     
 
-    def __enter__(self) -> 'RemoteDir':
-        '''
-        Enter the runtime context related to this instance.
+    def _validate_chunk_size(self, chunk_size: int) -> None:
         '''
-        return super().__enter__()
+        This method goes on to throw an ``InvalidChunkSizeError``, \
+        displaying an appropriate message, if file uploading to \
+        the directory is not supported for the provided chunk size. \
+        Else, this method does nothing.
+
+        :raises InvalidChunkSizeError: File uploading in chunks is not \
+            supported for the provided chunk size.
+
+        :note: Does nothing as all chunk sizes are supported.
+        '''
+        pass
+    
+
+    def __enter__(self) -> 'RemoteDir':
+        '''
+        Enter the runtime context related to this instance.
+        '''
+        return super().__enter__()
 
 
 class _CloudDir(_NonLocalDir, _ABC):
@@ -1985,7 +2273,43 @@ class _CloudDir(_NonLocalDir, _ABC):
     An abstract class which serves as the base class for \
     all directory-like classes that represent directories \
     in the cloud.
+
+    :param str path: The path pointing to the directory.
+    :param bool create_if_missing: If set to ``True``, then the directory \
+        to which the provided path points will be automatically created \
+        in case it does not already exist, instead of an exception being \
+        thrown.
+    :param ClientHandler handler: A ``ClientHandler`` class \
+        instance used for interacting with the underlying handler.
     '''
+    def __init__(
+        self,
+        path: str,
+        create_if_missing: bool,
+        handler: _ClientHandler
+    ) -> None:
+        '''
+        An abstract class which serves as the base class for \
+        all directory-like classes that represent directories \
+        in the cloud.
+
+        :param str path: The path pointing to the directory.
+        :param bool create_if_missing: If set to ``True``, then the directory \
+            to which the provided path points will be automatically created \
+            in case it does not already exist, instead of an exception being \
+            thrown.
+        :param ClientHandler handler: A ``ClientHandler`` class \
+            instance used for interacting with the underlying handler.
+        '''
+        # Validate path.
+        if path is None:
+            path = ''
+        else:
+            sep = _infer_sep(path=path)
+            if path.startswith(sep):
+                raise _IPE(path=path)
+        super().__init__(path, create_if_missing, handler)
+
 
     def load_metadata(self, recursively: bool = False) -> None:
         '''
@@ -2005,7 +2329,7 @@ def load_metadata(self, recursively: bool = False) -> None:
             - The number of the loaded metadata may vary depending \
               on the value of parameter ``recursively``.
             - Any metadata set via the ``set_metadata`` \
-              method will be overriden after invoking this \
+              method will be overridden after invoking this \
               method.
         '''
         handler = self._get_handler()
@@ -2044,6 +2368,8 @@ class AmazonS3Dir(_CloudDir):
         bucket does not exist.
     :raises InvalidPathError: The provided path \
         does not exist.
+    :raises InvalidDirectoryError: The provided path \
+        does not point to a directory.
 
     :note: The provided path must not begin with \
         a separator.
@@ -2082,6 +2408,8 @@ def __init__(
             bucket does not exist.
         :raises InvalidPathError: The provided path \
             does not exist.
+        :raises InvalidDirectoryError: The provided path \
+            does not point to a directory.
 
         :note: The provided path must not begin with \
             a separator.
@@ -2089,33 +2417,13 @@ def __init__(
                 * Wrong: ``/path/to/dir``
                 * Right: ``path/to/dir``
         '''
-        # Validate path.
-        if path is None:
-            path = ''
-        else:
-            sep = _infer_sep(path=path)
-            if path.startswith(sep):
-                raise _IPE(path=path)
-            
-        # Instantiate a connection handler.
-        aws_handler = _AWSClientHandler(
-            auth=auth,
-            bucket=bucket,
-            cache=cache)
-
         super().__init__(
             path=path,
-            handler=aws_handler)
-
-        # Create directory or throw an exception
-        # depending on the value of "create_if_missing".
-        if path != '':
-            if not aws_handler.dir_exists(path=path):
-                if create_if_missing:
-                    aws_handler.mkdir(path=path)
-                else:
-                    self.close()
-                    raise _IPE(path)
+            create_if_missing=create_if_missing,
+            handler=_AWSClientHandler(
+                auth=auth,
+                bucket=bucket,
+                cache=_DirCache(path) if cache else None))
 
 
     def get_bucket_name(self) -> str:
@@ -2242,6 +2550,22 @@ def _get_subdir_impl(self, dir_path: str) -> 'AmazonS3Dir':
             metadata=self._get_metadata_ref())
     
 
+    def _validate_chunk_size(self, chunk_size: int) -> None:
+        '''
+        This method goes on to throw an ``InvalidChunkSizeError``, \
+        displaying an appropriate message, if file uploading to \
+        the directory is not supported for the provided chunk size. \
+        Else, this method does nothing.
+
+        :raises InvalidChunkSizeError: File uploading in chunks is not \
+            supported for the provided chunk size.
+        '''
+        if chunk_size < 5000000:
+            msg = "Uploading a file to Amazon S3 in chunks"
+            msg += " requires a chunk size of greater than 5 MB."
+            raise _ICSE(msg)
+    
+
     def __enter__(self) -> 'AmazonS3Dir':
         '''
         Enter the runtime context related to this instance.
@@ -2273,6 +2597,8 @@ class AzureBlobDir(_CloudDir):
         specified container does not exist.
     :raises InvalidPathError: The provided path \
         does not exist.
+    :raises InvalidDirectoryError: The provided path \
+        does not point to a directory.
 
     :note: The provided path must not begin with \
         a separator.
@@ -2317,42 +2643,16 @@ def __init__(
                 
                 * Wrong: ``/path/to/dir``
                 * Right: ``path/to/dir``
-        '''        
-        # Validate path.
-        if path is None:
-            path = ''
-        else:
-            sep = _infer_sep(path=path)
-            if path.startswith(sep):
-                raise _IPE(path=path)
-            
-        # Instantiate a connection handler.
-        azr_handler = _AzureClientHandler(
-            auth=auth,
-            cache=cache,
-            container=container)
-        
+        '''                    
         # Infer storage account.
         self.__storage_account = auth._get_storage_account()
-
         super().__init__(
             path=path,
-            handler=azr_handler)
-
-        # Throw an exception if container does not exist.
-        if not azr_handler.container_exists():
-            self.close()
-            raise _CNFE(container)
-
-        # Create directory or throw an exception
-        # depending on the value of "create_if_missing".
-        if path != '':
-            if not azr_handler.path_exists(path=path):
-                if create_if_missing:
-                    azr_handler.mkdir(path=path)
-                else:
-                    self.close()
-                    raise _IPE(path)
+            create_if_missing=create_if_missing,
+            handler=_AzureClientHandler(
+                auth=auth,
+                container=container,
+                cache=_DirCache(path) if cache else None))
 
 
     def get_container_name(self) -> str:
@@ -2483,6 +2783,22 @@ def _get_subdir_impl(self, dir_path: str) -> 'AzureBlobDir':
             storage_account=self.__storage_account,
             handler=self._get_handler(),
             metadata=self._get_metadata_ref())
+    
+
+    def _validate_chunk_size(self, chunk_size: int) -> None:
+        '''
+        This method goes on to throw an ``InvalidChunkSizeError``, \
+        displaying an appropriate message, if file uploading to \
+        the directory is not supported for the provided chunk size. \
+        Else, this method does nothing.
+
+        :raises InvalidChunkSizeError: File uploading in chunks is not \
+            supported for the provided chunk size.
+        '''
+        if chunk_size > 4194304:
+            msg = "Uploading a file to Azure Blob Storage in chunks"
+            msg += " requires a chunk size of less than 4 MiB."
+            raise _ICSE(msg)
 
     
     def __enter__(self) -> 'AzureBlobDir':
@@ -2490,3 +2806,229 @@ def __enter__(self) -> 'AzureBlobDir':
         Enter the runtime context related to this instance.
         '''
         return super().__enter__()
+    
+
+class GCPStorageDir(_CloudDir):
+    '''
+    This class represents a virtual directory which resides \
+    within a Google Cloud Storage bucket.
+
+    :param GCPAuth auth: A ``GCPAuth`` instance used \
+        for authenticating with GCP.
+    :param str bucket: The name of the bucket in which \
+        the directory resides.
+    :param str | None path: The path pointing to the directory. \
+        If ``None``, then the whole bucket is considered. \
+        Defaults to ``None``.
+    :param bool cache: Indicates whether it is allowed for \
+        any fetched data to be cached for faster subsequent \
+        access. Defaults to ``False``.
+    :param bool create_if_missing: If set to ``True``, then the directory \
+        to which the provided path points will be automatically created \
+        in case it does not already exist, instead of an exception being \
+        thrown. Defaults to ``False``.
+
+    :raises BucketNotFoundError: The specified \
+        bucket does not exist.
+    :raises InvalidPathError: The provided path \
+        does not exist.
+    :raises InvalidDirectoryError: The provided path \
+        does not point to a directory.
+
+    :note: The provided path must not begin with \
+        a separator.
+            
+            * Wrong: ``/path/to/dir``
+            * Right: ``path/to/dir``
+    '''
+    def __init__(
+        self,
+        auth: _GCPAuth,
+        bucket: str,
+        path: _typ.Optional[str] = None,
+        cache: bool = False,
+        create_if_missing: bool = False
+    ):
+        '''
+        This class represents a virtual directory which resides \
+        within a Google Cloud Storage bucket.
+
+        :param GCPAuth auth: A ``GCPAuth`` instance used \
+            for authenticating with GCP.
+        :param str bucket: The name of the bucket in which \
+            the directory resides.
+        :param str | None path: The path pointing to the directory. \
+            If ``None``, then the whole bucket is considered. \
+            Defaults to ``None``.
+        :param bool cache: Indicates whether it is allowed for \
+            any fetched data to be cached for faster subsequent \
+            access. Defaults to ``True``.
+        :param bool create_if_missing: If set to ``True``, then the directory \
+            to which the provided path points will be automatically created \
+            in case it does not already exist, instead of an exception being \
+            thrown. Defaults to ``False``.
+
+        :raises BucketNotFoundError: The specified \
+            bucket does not exist.
+        :raises InvalidPathError: The provided path \
+            does not exist.
+        :raises InvalidDirectoryError: The provided path \
+            does not point to a directory.
+
+        :note: The provided path must not begin with \
+            a separator.
+                
+                * Wrong: ``/path/to/dir``
+                * Right: ``path/to/dir``
+        '''
+        super().__init__(
+            path=path,
+            create_if_missing=create_if_missing,
+            handler=_GCPClientHandler(
+                auth=auth,
+                bucket=bucket,
+                cache=_DirCache(path) if cache else None))
+
+
+    def get_bucket_name(self) -> str:
+        '''
+        Returns the name of the bucket in which \
+        the directory resides.
+        '''
+        return self._get_handler().get_bucket_name()
+
+
+    def get_uri(self) -> str:
+        '''
+        Returns the directory's URI.
+        '''
+        return f"gs://{self.get_bucket_name()}/{self.get_path()}"
+    
+
+    def get_file(self, path: str) -> GCPStorageFile:
+        '''
+        Returns the file residing in the specified \
+        path as a ``GCPStorageFile`` instance.
+
+        :param str path: Either the absolute path or the \
+            path relative to the directory of the file in \
+            question.
+
+        :raises InvalidPathError: The provided path \
+            does not exist.
+        :raises InvalidFileError: The provided path does \
+            not point to a file within the directory.
+
+        :note: The provided path, if absolute, must not \
+            begin with a separator.
+
+                * Wrong: ``/path/to/file.txt``
+                * Right: ``path/to/file.txt``
+        '''
+        if not self.path_exists(path):
+            raise _IPE(path=path)
+        if not self.is_file(path):
+            raise _IFE(path=path)
+        
+        path = self._to_absolute(path, replace_sep=False)
+        return GCPStorageFile._create_file(
+            path=path,
+            handler=self._get_handler(),
+            metadata=self._get_file_metadata_ref(path))
+    
+
+    def get_subdir(self, path: str) -> 'GCPStorageDir':
+        '''
+        Returns the directory residing in the specified \
+        path as an ``GCPStorageDir`` instance.
+
+        :param str path: Either the absolute path or the \
+            path relative to the directory of the subdirectory \
+            in question.
+
+        :raises InvalidPathError: The provided path \
+            does not exist.
+
+        :note: The provided path, if absolute, must not \
+            begin with a separator.
+
+                * Wrong: ``/path/to/dir``
+                * Right: ``path/to/dir``
+        '''
+        sep = '/'
+        path = f"{path.removesuffix(sep)}{sep}"
+
+        if not self.path_exists(path):
+            raise _IPE(path=path)
+        
+        return self._get_subdir_impl(path)
+
+
+    @classmethod
+    def _create_dir(
+        cls,
+        path: str,
+        handler: _GCPClientHandler,
+        metadata: dict[str, str]
+    ) -> 'GCPStorageDir':
+        '''
+        Creates and returns an ``GCPStorageDir`` instance.
+
+        :param str path: The path pointing to the directory.
+        :param GCPClientHandler handler: A ``GCPClientHandler`` \
+            class instance.
+        :param dict[str, str] metadata: A dictionary containing \
+            file metadata.
+        '''
+        instance = cls.__new__(cls)
+        _Directory.__init__(
+            instance,
+            path=path,
+            metadata=metadata,
+            handler=handler,
+            close_after_use=False)
+        return instance
+    
+
+    def _get_subdir_impl(self, dir_path: str) -> 'GCPStorageDir':
+        '''
+        Returns the directory residing in the specified \
+        path as an ``GCPStorageDir`` instance.
+
+        :param str dir_path: Either the absolute path \
+            or the path relative to the directory of the \
+            subdirectory in question.
+        '''
+        sep = self._get_separator()
+        dir_path = f"{dir_path.removesuffix(sep)}{sep}"
+        abs_dir_path = self._to_absolute(
+            path=dir_path, replace_sep=False)
+        
+        return __class__._create_dir(
+            path=abs_dir_path,
+            handler=self._get_handler(),
+            metadata=self._get_metadata_ref())
+    
+
+    def _validate_chunk_size(self, chunk_size: int) -> None:
+        '''
+        This method goes on to throw an ``InvalidChunkSizeError``, \
+        displaying an appropriate message, if file uploading to \
+        the directory is not supported for the provided chunk size. \
+        Else, this method does nothing.
+
+        :raises InvalidChunkSizeError: File uploading in chunks is not \
+            supported for the provided chunk size.
+        '''
+        if chunk_size % 262144:
+            msg = "Uploading a file to Google Cloud Storage"
+            msg += " requires a chunk size that is a multiple"
+            msg += " of 256 KiB, that is, 256 x 1024 bytes."
+            raise _ICSE(msg)
+    
+
+    def __enter__(self) -> 'GCPStorageDir':
+        '''
+        Enter the runtime context related to this instance.
+        '''
+        return super().__enter__()
diff --git a/pyproject.toml b/pyproject.toml
index 7fa1e7d..9354961 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,12 +1,12 @@
 [project]
 name = "fluke-api"
-version = "0.4.1"
+version = "0.5.0"
 authors = [
   {email = "manosstoumpos@gmail.com"},
   {name = "Manos Stoumpos"}
 ]
-description = "Move your data around"
-keywords = ["data", "transfer", "file", "filesystem", "cloud"]
+description = "Cloud-agnostic Python API"
+keywords = ["cloud", "storage", "message-queues", "aws", "azure", "gcp"]
 readme = "README.md"
 license = {text = "MIT"}
 requires-python = ">=3.9"
@@ -20,6 +20,7 @@ dependencies = [
   "azure-storage-blob == 12.17.0",
   "azure-storage-queue == 12.6.0",
   "boto3 == 1.28.0",
+  "google-cloud-storage == 2.10.0",
   "paramiko == 3.2.0",
   "tqdm == 4.65.0"
 ]


Test patch:
diff --git a/tests/test_auth.py b/tests/test_auth.py
index 16808aa..d1aea77 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -1,6 +1,6 @@
 import unittest
 
-from fluke.auth import RemoteAuth, AWSAuth, AzureAuth
+from fluke.auth import RemoteAuth, AWSAuth, AzureAuth, GCPAuth
 
 
 class TestRemoteAuth(unittest.TestCase):
@@ -12,7 +12,6 @@ class TestRemoteAuth(unittest.TestCase):
     PASSPHRASE = "PASSPHRASE"
     PORT = 22
     PUBLIC_KEY = "PUBLIC_KEY"
-    KEY_TYPE = RemoteAuth.KeyType.SSH_RSA
     VERIFY_HOST = False
 
     def test_get_credentials_from_key(self):
@@ -22,12 +21,10 @@ def test_get_credentials_from_key(self):
             'pkey': self.PKEY,
             'passphrase': self.PASSPHRASE,
             'port': self.PORT,
-            'public_key': self.PUBLIC_KEY,
-            'key_type': self.KEY_TYPE,
+            'public_key': None,
             'verify_host': self.VERIFY_HOST 
         }
         auth = RemoteAuth.from_key(**credentials)
-        credentials.update({'key_type': credentials['key_type'].value})
         self.assertEqual(
             auth.get_credentials(),
             credentials)
@@ -38,29 +35,128 @@ def test_get_credentials_from_password(self):
             'username': self.USERNAME,
             'password': self.PASSWORD,
             'port': self.PORT,
-            'public_key': self.PUBLIC_KEY,
-            'key_type': self.KEY_TYPE,
+            'public_key': None,
             'verify_host': self.VERIFY_HOST 
         }
         auth = RemoteAuth.from_password(**credentials)
-        credentials.update({'key_type': credentials['key_type'].value})
         self.assertEqual(
             auth.get_credentials(),
             credentials)
         
+    def test_get_credentials_on_public_key_value(self):
+        credentials = {
+            'hostname': self.HOST,
+            'username': self.USERNAME,
+            'pkey': self.PKEY,
+            'passphrase': self.PASSPHRASE,
+            'port': self.PORT,
+            'public_key': RemoteAuth.PublicKey.generate_ssh_rsa_key(self.PUBLIC_KEY),
+            'verify_host': self.VERIFY_HOST 
+        }
+        auth = RemoteAuth.from_key(**credentials)
+        self.assertEqual(auth.get_credentials()['public_key'].key, self.PUBLIC_KEY)
+
+    def test_get_credentials_on_public_key_ssh_rsa_type(self):
+        credentials = {
+            'hostname': self.HOST,
+            'username': self.USERNAME,
+            'pkey': self.PKEY,
+            'passphrase': self.PASSPHRASE,
+            'port': self.PORT,
+            'public_key': RemoteAuth.PublicKey.generate_ssh_rsa_key(self.PUBLIC_KEY),
+            'verify_host': self.VERIFY_HOST 
+        }
+        auth = RemoteAuth.from_key(**credentials)
+        self.assertEqual(
+            auth.get_credentials()['public_key'].type,
+            RemoteAuth.PublicKey._KeyType.SSH_RSA)
+        
+    def test_get_credentials_on_public_key_ssh_dss_type(self):
+        credentials = {
+            'hostname': self.HOST,
+            'username': self.USERNAME,
+            'pkey': self.PKEY,
+            'passphrase': self.PASSPHRASE,
+            'port': self.PORT,
+            'public_key': RemoteAuth.PublicKey.generate_ssh_dss_key(self.PUBLIC_KEY),
+            'verify_host': self.VERIFY_HOST 
+        }
+        auth = RemoteAuth.from_key(**credentials)
+        self.assertEqual(
+            auth.get_credentials()['public_key'].type,
+            RemoteAuth.PublicKey._KeyType.SSH_DSS)
+        
+    def test_get_credentials_on_public_key_ssh_ed25519_type(self):
+        credentials = {
+            'hostname': self.HOST,
+            'username': self.USERNAME,
+            'pkey': self.PKEY,
+            'passphrase': self.PASSPHRASE,
+            'port': self.PORT,
+            'public_key': RemoteAuth.PublicKey.generate_ssh_ed25519_key(self.PUBLIC_KEY),
+            'verify_host': self.VERIFY_HOST 
+        }
+        auth = RemoteAuth.from_key(**credentials)
+        self.assertEqual(
+            auth.get_credentials()['public_key'].type,
+            RemoteAuth.PublicKey._KeyType.SSH_ED25519)
+        
+    def test_get_credentials_on_public_key_ecdsa_sha2_nistp256_type(self):
+        credentials = {
+            'hostname': self.HOST,
+            'username': self.USERNAME,
+            'pkey': self.PKEY,
+            'passphrase': self.PASSPHRASE,
+            'port': self.PORT,
+            'public_key': RemoteAuth.PublicKey.generate_ecdsa_sha2_nistp256_key(self.PUBLIC_KEY),
+            'verify_host': self.VERIFY_HOST 
+        }
+        auth = RemoteAuth.from_key(**credentials)
+        self.assertEqual(
+            auth.get_credentials()['public_key'].type,
+            RemoteAuth.PublicKey._KeyType.ECDSA_SHA2_NISTP256)
+        
+    def test_get_credentials_on_public_key_ecdsa_sha2_nistp384_type(self):
+        credentials = {
+            'hostname': self.HOST,
+            'username': self.USERNAME,
+            'pkey': self.PKEY,
+            'passphrase': self.PASSPHRASE,
+            'port': self.PORT,
+            'public_key': RemoteAuth.PublicKey.generate_ecdsa_sha2_nistp384_key(self.PUBLIC_KEY),
+            'verify_host': self.VERIFY_HOST 
+        }
+        auth = RemoteAuth.from_key(**credentials)
+        self.assertEqual(
+            auth.get_credentials()['public_key'].type,
+            RemoteAuth.PublicKey._KeyType.ECDSA_SHA2_NISTP384)
+        
+    def test_get_credentials_on_public_key_ecdsa_sha2_nistp521_type(self):
+        credentials = {
+            'hostname': self.HOST,
+            'username': self.USERNAME,
+            'pkey': self.PKEY,
+            'passphrase': self.PASSPHRASE,
+            'port': self.PORT,
+            'public_key': RemoteAuth.PublicKey.generate_ecdsa_sha2_nistp521_key(self.PUBLIC_KEY),
+            'verify_host': self.VERIFY_HOST 
+        }
+        auth = RemoteAuth.from_key(**credentials)
+        self.assertEqual(
+            auth.get_credentials()['public_key'].type,
+            RemoteAuth.PublicKey._KeyType.ECDSA_SHA2_NISTP521)
+        
     def test_get_credentials_on_default_port(self):
         credentials = {
             'hostname': self.HOST,
             'username': self.USERNAME,
             'password': self.PASSWORD,
-            'public_key': self.PUBLIC_KEY,
-            'key_type': self.KEY_TYPE,
+            'public_key': None,
             'verify_host': self.VERIFY_HOST
         }
         auth = RemoteAuth.from_password(**credentials)
         credentials.update({
-            'port': 22,
-            'key_type': credentials['key_type'].value
+            'port': 22
         })
         self.assertEqual(
             auth.get_credentials(),
@@ -114,5 +210,31 @@ def test_get_credentials_from_connection_string(self):
             credentials)
         
 
+class TestGCPAuth(unittest.TestCase):
+
+    PROJECT_ID = "PROJECT_ID"
+    CREDENTIALS = "PATH/TO/CREDENTIALS/FILE"
+        
+    def test_get_credentials_from_application_default_credentials(self):
+        self.assertEqual(
+            GCPAuth.from_application_default_credentials(**{
+                'project_id': self.PROJECT_ID,
+                'credentials': self.CREDENTIALS,
+            }).get_credentials(),
+            {
+                GCPAuth._PROJECT_ID: self.PROJECT_ID,
+                GCPAuth._APPLICATION_DEFAULT_CREDENTIALS: self.CREDENTIALS
+            })
+        
+    def test_get_credentials_from_service_account_key(self):
+        self.assertEqual(
+            GCPAuth.from_service_account_key(**{
+                'credentials': self.CREDENTIALS,
+            }).get_credentials(),
+            {
+                GCPAuth._SERVICE_ACCOUNT_KEY: self.CREDENTIALS
+            })
+        
+
 if __name__=="__main__": 
      unittest.main() 
\ No newline at end of file
diff --git a/tests/test_files/dir/subdir/file3.txt b/tests/test_files/dir/subdir/file3.txt
index b39f7c6..3430429 100644
--- a/tests/test_files/dir/subdir/file3.txt
+++ b/tests/test_files/dir/subdir/file3.txt
@@ -1,1 +1,1 @@
-ΤΕΧΤ
\ No newline at end of file
+TEXT
\ No newline at end of file
diff --git a/tests/test_queues.py b/tests/test_queues.py
index 87fc06b..6fe6976 100644
--- a/tests/test_queues.py
+++ b/tests/test_queues.py
@@ -1,11 +1,7 @@
-import os
-import io
-import sys
 import time
-import shutil
 import unittest
-from typing import Optional, Iterator, Callable
 from unittest.mock import Mock, patch
+from typing import Iterator, Callable
 
 
 import boto3
@@ -216,16 +212,18 @@ def send_messages(self, messages: list[str]) -> None:
     def purge(self) -> None:
         self.QUEUE.purge()
 
-    def setUp(self):
-        self.MOCK_SQS.start()
-        self.QUEUE = boto3.resource(
+    @classmethod
+    def setUpClass(cls):
+        cls.MOCK_SQS.start()
+        cls.QUEUE = boto3.resource(
             'sqs',
             endpoint_url="https://sqs.eu-west-1.amazonaws.com",
             region_name="eu-west-1"
         ).create_queue(QueueName=QUEUE)
 
-    def tearDown(self):
-        self.MOCK_SQS.stop()
+    @classmethod
+    def tearDownClass(cls):
+        cls.MOCK_SQS.stop()
 
     def test_is_open_on_True(self):
         with self.build_queue() as queue:
@@ -349,7 +347,7 @@ def fetch_messages(self) -> list[str]:
                 try:
                     self.queue.delete_message(msg.id, msg.pop_recipt)
                     messages.append(msg.content)
-                except:
+                except Exception:
                     continue
             
         return messages
@@ -364,11 +362,13 @@ def send_messages(self, messages: list[str]) -> None:
     def purge(self) -> None:
         self.queue.clear_messages()
 
-    def setUp(self):
+    @classmethod
+    def setUpClass(cls):
         for k, v in MockQueueClient.get_mock_methods().items():
             patch(k, v).start()
 
-    def tearDown(self):
+    @classmethod
+    def tearDownClass(cls):
         patch.stopall()
 
     @staticmethod
diff --git a/tests/test_storage.py b/tests/test_storage.py
index 9943ee2..d9ac0a2 100644
--- a/tests/test_storage.py
+++ b/tests/test_storage.py
@@ -4,19 +4,24 @@
 import time
 import shutil
 import unittest
-from typing import Optional, Iterator, Callable
+from uuid import uuid4
 from unittest.mock import Mock, patch
+from typing import Optional, Iterator, Callable
 
 
 import boto3
-import paramiko
 from moto import mock_s3
+from requests import Session
+from google.cloud.storage import Client
+from google.api_core.client_options import ClientOptions
+from google.auth.credentials import AnonymousCredentials
+from google.resumable_media.requests import ResumableUpload
 
 
-from fluke.auth import RemoteAuth, AWSAuth, AzureAuth
+from fluke.auth import RemoteAuth, AWSAuth, AzureAuth, GCPAuth
 from fluke.storage import LocalFile, LocalDir, \
     RemoteFile, RemoteDir, AmazonS3File, AmazonS3Dir, \
-    AzureBlobFile, AzureBlobDir
+    AzureBlobFile, AzureBlobDir, GCPStorageFile, GCPStorageDir
 from fluke._exceptions import OverwriteError
 from fluke._exceptions import InvalidPathError
 from fluke._exceptions import InvalidFileError
@@ -25,6 +30,7 @@
 from fluke._exceptions import NonStringMetadataValueError
 from fluke._exceptions import BucketNotFoundError
 from fluke._exceptions import ContainerNotFoundError
+from fluke._exceptions import InvalidChunkSizeError
 
 
 '''
@@ -46,16 +52,58 @@ def to_abs(path: str) -> str:
     return join_paths(os.getcwd().replace(os.sep, SEPARATOR), path)
 
 
-def get_remote_auth_instance(hostname: str) -> RemoteAuth:
+def get_tmp_dir_name() -> str:
+    '''
+    Returns a random UUID to be used as a \
+    temporary directory name.
+    '''
+    return uuid4().hex
+
+
+def create_tmp_dir(func: Callable):
+    '''
+    A decorator function used for creating \
+    and deleting temporary directories.
+    '''
+    def wrapper(*args, **kwargs):
+        tmp_dir_path = to_abs(REL_DIR_PATH.replace(DIR_NAME, get_tmp_dir_name()))
+        kwargs.update({'tmp_dir_path': tmp_dir_path})
+        os.mkdir(tmp_dir_path)
+        try:
+            func(*args, **kwargs)
+        except Exception as e:
+            raise e
+        finally:
+            shutil.rmtree(tmp_dir_path)
+    return wrapper
+
+
+def clone_tmp_dir(func: Callable):
+    '''
+    A decorator function used for creating \
+    and deleting clone directories.
     '''
-    Returns a dummy ``RemoteAuth`` instance.
+    def wrapper(*args, **kwargs):
+        tmp_dir_path = to_abs(REL_DIR_PATH.replace(DIR_NAME, get_tmp_dir_name()))
+        kwargs.update({'tmp_dir_path': tmp_dir_path})
+        shutil.copytree(src=ABS_DIR_PATH, dst=tmp_dir_path)
+        func(*args, **kwargs)
+        shutil.rmtree(tmp_dir_path)
+    return wrapper
 
-    :param str hostname: The instance's hostname.
+
+def get_remote_auth_instance() -> RemoteAuth:
+    '''
+    Returns a ``RemoteAuth`` instance user \
+    in order to establish a connection to \
+    the fake SSH server.
     '''
     return RemoteAuth.from_password(
-        hostname=hostname,
-        username='',
-        password='')
+        hostname=HOST,
+        username='test',
+        password='test',
+        port=2222,
+        verify_host=False)
 
 
 def get_aws_auth_instance() -> AWSAuth:
@@ -82,8 +130,16 @@ def get_azure_auth_instance(from_conn_string: bool) -> AzureAuth:
             account_url=STORAGE_ACCOUNT_URL,
             tenant_id='',
             client_id='',
-            client_secret='')
-    )
+            client_secret=''))
+
+
+def get_gcp_auth_instance() -> GCPAuth:
+    '''
+    Returns a dummy ``GCPAuth`` instance.
+    '''
+    return GCPAuth.from_application_default_credentials(
+        project_id='',
+        credentials='')
 
 
 def create_aws_s3_bucket(mock_s3, bucket_name: str, metadata: dict[str, str]):
@@ -103,7 +159,7 @@ def create_aws_s3_bucket(mock_s3, bucket_name: str, metadata: dict[str, str]):
     s3 = boto3.resource("s3")
     s3.create_bucket(Bucket=bucket_name)
     bucket = s3.Bucket(bucket_name)
-    # Structure it like "test_files".
+    # Structure it like "test_files/bucket".
     for dp, dn, fn in os.walk(TEST_FILES_DIR):
         dn.sort()
         for file in sorted(fn):
@@ -113,11 +169,6 @@ def create_aws_s3_bucket(mock_s3, bucket_name: str, metadata: dict[str, str]):
                     Key=file_path.replace(os.sep, SEPARATOR),
                     Fileobj=file,
                     ExtraArgs={ "Metadata": metadata })
-    # Finally, create a TMP dir.
-    with io.BytesIO() as empty_buffer:
-        bucket.upload_fileobj(
-            Key=join_paths(TEST_FILES_DIR, TMP_DIR_NAME, 'DUMMY'),
-            Fileobj=empty_buffer)
 
         
 def get_aws_s3_object(bucket_name: str, path: str):
@@ -130,6 +181,55 @@ def get_aws_s3_object(bucket_name: str, path: str):
     return boto3.resource("s3").Bucket(bucket_name).Object(path)
 
 
+def get_gcs_client():
+    '''
+    Returns a ``google.cloud.storage.Client`` instance
+    used in establishing a connection to the fake-gcs-server.
+    '''
+    session = Session()
+    session.verify = False
+
+    return Client(
+        credentials=AnonymousCredentials(),
+        project="test",
+        _http=session,
+        client_options=ClientOptions(api_endpoint="https://127.0.0.1:4443"))
+
+
+def set_up_gcs_bucket():
+    '''
+    Sets up the dummy GCS bucket and returns \
+    the client used in order to interact with it.
+    '''
+    client = get_gcs_client()
+
+    # Assign metadata to each blob.
+    for obj in client.bucket(BUCKET).list_blobs():
+        obj.metadata = METADATA
+        obj.patch()
+
+    # Patch methods.
+    def get_resumable_upload_session(*args, **kwargs):
+        return ResumableUpload(
+            upload_url=(
+                "https://127.0.0.1:4443" +
+                f"/upload/storage/v1/b/{kwargs['bucket']}/" +
+                f"o?uploadType=resumable&name={kwargs['file_path']}"
+            ),
+            chunk_size=kwargs['chunk_size'])
+
+    for k, v in {
+        'fluke._handlers.GCPClientHandler._CLIENT_GENERATOR': Mock(return_value=client),
+        'google.auth.transport.requests.AuthorizedSession.__init__': Mock(return_value=None),
+        'google.auth.transport.requests.AuthorizedSession.__new__': Mock(return_value=client._http),
+        'fluke._iohandlers.GCPFileWriter._create_resumable_upload_session': Mock(
+            wraps=get_resumable_upload_session)
+    }.items():
+        patch(k, v).start()
+
+    return client
+
+
 def simulate_latency(func: Callable):
     '''
     This function is to be used as a decorator \
@@ -177,7 +277,12 @@ def wrapper(*args, **kwargs):
     DIR_FILE_NAME,
     f'{DIR_SUBDIR_NAME}{DIR_SUBDIR_FILE_NAME}',
     f'{DIR_SUBDIR_NAME}file4.txt']
-TMP_DIR_NAME = 'TMP_DIR'
+LS_RECURSIVE_OUTPUT = """
+|__file2.txt
+|__subdir/
+   |__file3.txt
+   |__file4.txt
+"""
 
 
 def get_abs_contents(recursively: bool):
@@ -190,10 +295,10 @@ def get_abs_contents(recursively: bool):
 '''
 REMOTE-RELATED CONSTANTS
 '''
-HOST = "host"
+HOST = "localhost"
 
 '''
-AWS-RELATED CONSTANTS
+AWS/GPC-RELATED CONSTANTS
 '''
 BUCKET = "bucket"
 
@@ -209,78 +314,6 @@ def get_abs_contents(recursively: bool):
 '''
 Helper Mock Classes
 '''
-class MockSFTPClient():
-
-    class MockSFTPFile():
-
-        def __init__(self, filename: str, mode: str) -> None:
-            self.__file = open(file=filename, mode=mode)
-
-        def seek(self, offset: int) -> None:
-            self.__file.seek(offset)
-
-        def tell(self) -> int:
-            return self.__file.tell()
-
-        def read(self, size: Optional[int] = None) -> None:
-            if size is None:
-                return self.__file.read()
-            return self.__file.read(size)
-        
-        def write(self, chunk: bytes) -> None:
-            self.__file.write(chunk)
-        
-        def flush(self) -> None:
-            self.__file.flush()
-
-        def close(self):
-            self.__file.close()
-            
-
-    @staticmethod
-    def get_mock_methods() -> dict[str, Mock]:
-        '''
-        Returns a dictionary containing all the \
-        methods that are to be mocked.
-        '''
-        return {
-            'paramiko.SSHClient.load_host_keys': Mock(),
-            'paramiko.SSHClient.get_host_keys': Mock(),
-            'paramiko.SSHClient.connect': Mock(),
-            'paramiko.SSHClient.close': Mock(),
-            'paramiko.SSHClient.open_sftp': Mock(return_value=(MockSFTPClient())),
-        }
-
-    @simulate_latency
-    def stat(self, path: str) -> paramiko.SFTPAttributes:
-
-        if not os.path.exists(path):
-            raise FileNotFoundError()
-        
-        os_stats = os.stat(path=path)
-        stats = paramiko.SFTPAttributes()
-        stats.filename = path.split(SEPARATOR)[-1]
-        stats.st_mode = os_stats.st_mode
-        stats.st_size = os_stats.st_size
-        return stats
-
-    @simulate_latency
-    def mkdir(self, path: str):
-        os.makedirs(name=path)
-
-    @simulate_latency
-    def listdir_attr(self, path: str) -> list[paramiko.SFTPAttributes]:
-        return [self.stat(join_paths(path, f)) for f in sorted(os.listdir(path=path))]
-    
-    @simulate_latency
-    def open(self, filename: str, mode: str) -> MockSFTPFile:
-        return MockSFTPClient.MockSFTPFile(filename, mode)
-
-    @simulate_latency
-    def close(self):
-        pass
-
-
 class MockContainerClient():
 
     class MockBlobProperties():
@@ -607,6 +640,18 @@ def test_read_lines_on_chunk_size(self):
         for line in file.read_lines(chunk_size=1):
             self.assertEqual(data, line)
 
+    def test_cat(self):
+        file = self.build_file()
+        data = "TEXT"
+        with io.StringIO() as stdo:
+            sys.stdout = stdo
+
+            file.cat()
+
+            sys.stdout = sys.__stdout__
+
+            self.assertEqual(stdo.getvalue().strip('\n'), data)
+
     def test_transfer_to(self):
         file = self.build_file()
         dir = TestLocalDir.build_dir(path=ABS_DIR_PATH)
@@ -662,30 +707,58 @@ def test_transfer_to_on_overwrite_error(self):
 
 class TestRemoteFile(unittest.TestCase):
 
+    @classmethod
+    def setUpClass(cls):
+        super(TestRemoteFile, cls).setUpClass()
+
+        from fluke._handlers import SSHClientHandler
+
+        m1 = patch.object(SSHClientHandler, '_get_file_size_impl', autospec=True)
+        m2 = patch.object(SSHClientHandler, '_get_file_metadata_impl', autospec=True)
+        m3 = patch.object(SSHClientHandler, '_traverse_dir_impl', autospec=True)
+
+        def simulate_latency_1(*args, **kwargs):
+            time.sleep(0.2)
+            return m1.temp_original(*args, **kwargs)
+        
+        def simulate_latency_2(*args, **kwargs):
+            time.sleep(0.2)
+            return m2.temp_original(*args, **kwargs)
+        
+        def simulate_latency_3(*args, **kwargs):
+            time.sleep(0.2)
+            return m3.temp_original(*args, **kwargs)
+
+        m1.start().side_effect = simulate_latency_1
+        m2.start().side_effect = simulate_latency_2
+        m3.start().side_effect = simulate_latency_3
+
+
+    @classmethod
+    def tearDownClass(cls):
+        super(TestRemoteFile, cls).tearDownClass()
+        patch.stopall()
+
     @staticmethod
-    def build_file(path: str = ABS_FILE_PATH, cache: bool = False) -> RemoteFile:
+    def build_file(
+        path: str = f"/{REL_FILE_PATH}",
+        cache: bool = False
+    ) -> RemoteFile:
         return RemoteFile(**{
-            'auth': get_remote_auth_instance(hostname=HOST),
+            'auth': get_remote_auth_instance(),
             'path': path,
             'cache': cache
         })
 
-    def setUp(self):        
-        for k, v in MockSFTPClient.get_mock_methods().items():
-            patch(k, v).start()
-
-    def tearDown(self):
-        patch.stopall()
-
     def test_constructor(self):
         with self.build_file() as file:
-            self.assertEqual(file.get_path(), ABS_FILE_PATH)
+            self.assertEqual(file.get_path(), f"/{REL_FILE_PATH}")
 
     def test_constructor_on_invalid_path_error(self):
         self.assertRaises(InvalidPathError, self.build_file, path="NON_EXISTING_PATH")
 
     def test_constructor_on_invalid_file_error(self):
-        self.assertRaises(InvalidFileError, self.build_file, path=to_abs(REL_DIR_PATH))
+        self.assertRaises(InvalidFileError, self.build_file, path=f"/{REL_DIR_PATH}")
 
     def test_get_name(self):
         with self.build_file() as file:
@@ -697,7 +770,7 @@ def test_get_hostname(self):
 
     def test_get_uri(self):
         with self.build_file() as file:
-            self.assertEqual(file.get_uri(), f"sftp://{HOST}/{ABS_FILE_PATH.removeprefix(os.sep)}")
+            self.assertEqual(file.get_uri(), f"sftp://{HOST}/{REL_FILE_PATH.removeprefix(os.sep)}")
 
     def test_get_metadata(self):
         with self.build_file() as file:
@@ -773,6 +846,20 @@ def test_read_lines_on_chunk_size(self):
             for line in file.read_lines(chunk_size=1):
                 self.assertEqual(data, line)
 
+    def test_cat(self):
+        data = "TEXT"
+        with (
+            io.StringIO() as stdo,
+            self.build_file() as file
+        ):
+            sys.stdout = stdo
+
+            file.cat()
+
+            sys.stdout = sys.__stdout__
+
+            self.assertEqual(stdo.getvalue().strip('\n'), data)
+
     def test_transfer_to(self):
         with self.build_file() as file:
             # Copy file into dir.
@@ -901,15 +988,15 @@ def test_purge(self):
             # Fetch size via HTTP.
             _ = file.get_size()
             # Fetch size from cache and time it.
-            t = time.time()
+            t = time.perf_counter()
             _ = file.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Purge cache.
             file.purge()
             # Re-fetch size via HTTP and time it.
-            t = time.time()
+            t = time.perf_counter()
             _ = file.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             self.assertGreater(normal_time, cache_time)
 
     def test_get_size_from_cache_on_value(self):
@@ -921,13 +1008,13 @@ def test_get_size_from_cache_on_value(self):
     def test_get_size_from_cache_on_time(self):
         with self.build_file(cache=True) as file:
             # Fetch object size via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = file.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object size from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = file.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -936,8 +1023,10 @@ class TestAmazonS3File(unittest.TestCase):
 
     MOCK_S3 = mock_s3()
 
-    def setUp(self):
-        create_aws_s3_bucket(self.MOCK_S3, BUCKET, METADATA)
+    @classmethod
+    def setUpClass(cls):
+        super(TestAmazonS3File, cls).setUpClass()  
+        create_aws_s3_bucket(cls.MOCK_S3, BUCKET, METADATA)
 
         from fluke._handlers import AWSClientHandler
 
@@ -962,20 +1051,24 @@ def simulate_latency_3(*args, **kwargs):
         m3.start().side_effect = simulate_latency_3
 
 
-    def tearDown(self):
-        self.MOCK_S3.stop()
+    @classmethod
+    def tearDownClass(cls):
+        super(TestAmazonS3File, cls).tearDownClass()  
+        cls.MOCK_S3.stop()
         patch.stopall()
     
     @staticmethod
     def build_file(
         path: str = REL_FILE_PATH,
         bucket: str = BUCKET,
+        load_metadata: bool = False,
         cache: bool = False
     ) -> AmazonS3File:
         return AmazonS3File(**{
             'auth': get_aws_auth_instance(),
             'bucket': bucket,
             'path': path,
+            'load_metadata': load_metadata,
             'cache': cache
         })
 
@@ -983,6 +1076,14 @@ def test_constructor(self):
         with self.build_file() as file:
             self.assertEqual(file.get_path(), REL_FILE_PATH)
 
+    def test_constructor_on_load_metadata_set_to_false(self):
+        with self.build_file(load_metadata=False) as file:
+            self.assertEqual(file.get_metadata(), {})
+
+    def test_constructor_on_load_metadata_set_to_true(self):
+        with self.build_file(load_metadata=True) as file:
+            self.assertEqual(file.get_metadata(), METADATA)
+
     def test_constructor_on_invalid_path_error(self):
         self.assertRaises(InvalidPathError, self.build_file, path="NON_EXISTING_PATH")
 
@@ -1096,6 +1197,20 @@ def test_read_lines_on_chunk_size(self):
             for line in file.read_lines(chunk_size=1):
                 self.assertEqual(data, line)
 
+    def test_cat(self):
+        data = "TEXT"
+        with (
+            io.StringIO() as stdo,
+            self.build_file() as file
+        ):
+            sys.stdout = stdo
+
+            file.cat()
+
+            sys.stdout = sys.__stdout__
+
+            self.assertEqual(stdo.getvalue().strip('\n'), data)
+
     def test_transfer_to(self):
         with self.build_file() as file:
             # Copy file into dir.
@@ -1207,15 +1322,15 @@ def test_purge(self):
             # Load metadata via HTTP.
             file.load_metadata()
             # Load metadata via cache and time it.
-            t = time.time()
+            t = time.perf_counter()
             file.load_metadata()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Purge cache.
             file.purge()
             # Load metadata via HTTP and time it.
-            t = time.time()
+            t = time.perf_counter()
             file.load_metadata()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             self.assertGreater(normal_time, cache_time)
 
     def test_load_metadata_from_cache_on_value(self):
@@ -1229,13 +1344,13 @@ def test_load_metadata_from_cache_on_value(self):
     def test_load_metadata_from_cache_on_time(self):
         with self.build_file(cache=True) as file:
             # Fetch object metadata via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = file.load_metadata()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object metadata from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = file.load_metadata()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -1247,13 +1362,13 @@ def test_get_size_from_cache_on_value(self):
     def test_get_size_from_cache_on_time(self):
         with self.build_file(cache=True) as file:
             # Fetch object size via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = file.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object size from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = file.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -1265,20 +1380,26 @@ def build_file(
         path: str = REL_FILE_PATH,
         container: str = CONTAINER,
         cache: bool = False,
+        load_metadata: bool = False,
         from_conn_string: bool = False
     ) -> AzureBlobFile:
         return AzureBlobFile(**{
             'auth': get_azure_auth_instance(from_conn_string),
             'container': container,
             'path': path,
+            'load_metadata': load_metadata,
             'cache': cache
         })
 
-    def setUp(self):
+    @classmethod
+    def setUpClass(cls):
+        super(TestAzureBlobFile, cls).setUpClass()  
         for k, v in MockContainerClient.get_mock_methods().items():
             patch(k, v).start()
 
-    def tearDown(self):
+    @classmethod
+    def tearDownClass(cls):
+        super(TestAzureBlobFile, cls).tearDownClass()  
         patch.stopall()
 
     def test_constructor(self):
@@ -1289,6 +1410,14 @@ def test_constructor_from_conn_string(self):
         with self.build_file(from_conn_string=True) as file:
             self.assertEqual(file.get_path(), REL_FILE_PATH)
 
+    def test_constructor_on_load_metadata_set_to_false(self):
+        with self.build_file(load_metadata=False) as file:
+            self.assertEqual(file.get_metadata(), {})
+
+    def test_constructor_on_load_metadata_set_to_true(self):
+        with self.build_file(load_metadata=True) as file:
+            self.assertEqual(file.get_metadata(), METADATA)
+
     def test_constructor_on_invalid_path_error(self):
         self.assertRaises(InvalidPathError, self.build_file, path="NON_EXISTING_PATH")
 
@@ -1404,6 +1533,20 @@ def test_read_lines_on_chunk_size(self):
             for line in file.read_lines(chunk_size=1):
                 self.assertEqual(data, line)
 
+    def test_cat(self):
+        data = "TEXT"
+        with (
+            io.StringIO() as stdo,
+            self.build_file() as file
+        ):
+            sys.stdout = stdo
+
+            file.cat()
+
+            sys.stdout = sys.__stdout__
+
+            self.assertEqual(stdo.getvalue().strip('\n'), data)
+
     def test_transfer_to(self):
         with self.build_file() as file:
             # Copy file into dir.
@@ -1532,15 +1675,15 @@ def test_purge(self):
             # Load metadata via HTTP.
             file.load_metadata()
             # Load metadata via cache and time it.
-            t = time.time()
+            t = time.perf_counter()
             file.load_metadata()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Purge cache.
             file.purge()
             # Load metadata via HTTP and time it.
-            t = time.time()
+            t = time.perf_counter()
             file.load_metadata()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             self.assertGreater(normal_time, cache_time)
 
     def test_load_metadata_from_cache_on_value(self):
@@ -1554,13 +1697,13 @@ def test_load_metadata_from_cache_on_value(self):
     def test_load_metadata_from_cache_on_time(self):
         with self.build_file(cache=True) as file:
             # Fetch object metadata via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = file.load_metadata()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object metadata from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = file.load_metadata()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -1572,500 +1715,1623 @@ def test_get_size_from_cache_on_value(self):
     def test_get_size_from_cache_on_time(self):
         with self.build_file(cache=True) as file:
             # Fetch object size via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = file.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object size from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = file.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
 
-class TestLocalDir(unittest.TestCase):
+class TestGCPStorageFile(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestGCPStorageFile, cls).setUpClass()
+
+        from fluke._handlers import GCPClientHandler
+
+        m1 = patch.object(GCPClientHandler, '_get_file_size_impl', autospec=True)
+        m2 = patch.object(GCPClientHandler, '_get_file_metadata_impl', autospec=True)
+        m3 = patch.object(GCPClientHandler, '_traverse_dir_impl', autospec=True)
+
+        def simulate_latency_1(*args, **kwargs):
+            time.sleep(0.2)
+            return m1.temp_original(*args, **kwargs)
+        
+        def simulate_latency_2(*args, **kwargs):
+            time.sleep(0.2)
+            return m2.temp_original(*args, **kwargs)
+        
+        def simulate_latency_3(*args, **kwargs):
+            time.sleep(0.2)
+            return m3.temp_original(*args, **kwargs)
+
+        m1.start().side_effect = simulate_latency_1
+        m2.start().side_effect = simulate_latency_2
+        m3.start().side_effect = simulate_latency_3
+        
+        # Set up the GCS bucket.
+        cls.__client = set_up_gcs_bucket()
+
+    @classmethod
+    def tearDownClass(cls):
+        super(TestGCPStorageFile, cls).tearDownClass()
+        cls.__client.close()
+        patch.stopall()
     
     @staticmethod
-    def build_dir(
-        path: str = ABS_DIR_PATH,
-        create_if_missing: bool = False
-    ) -> LocalDir:
-        return LocalDir(
-            path=path,
-            create_if_missing=create_if_missing)
+    def build_file(
+        path: str = REL_FILE_PATH,
+        bucket: str = BUCKET,
+        load_metadata: bool = False,
+        cache: bool = False
+    ) -> GCPStorageFile:
+        return GCPStorageFile(**{
+            'auth': get_gcp_auth_instance(),
+            'bucket': bucket,
+            'path': path,
+            'load_metadata': load_metadata,
+            'cache': cache
+        })
 
-    
-    def test_constructor_on_create_if_missing(self):
-        path = to_abs("NON_EXISTING_DIR")
-        _ = self.build_dir(path, create_if_missing=True)
-        self.assertTrue(os.path.isdir(path))
-        os.rmdir(path)
+    def test_constructor(self):
+        with self.build_file() as file:
+            self.assertEqual(file.get_path(), REL_FILE_PATH)
 
-    def test_constructor_on_invalid_path_error(self):
-        self.assertRaises(InvalidPathError, self.build_dir, path="NON_EXISTING_PATH")
+    def test_constructor_on_load_metadata_set_to_false(self):
+        with self.build_file(load_metadata=False) as file:
+            self.assertEqual(file.get_metadata(), {})
 
-    def test_constructor_on_invalid_directory_error(self):
-        self.assertRaises(InvalidDirectoryError, self.build_dir, path=ABS_DIR_FILE_PATH)
+    def test_constructor_on_load_metadata_set_to_true(self):
+        with self.build_file(load_metadata=True) as file:
+            self.assertEqual(file.get_metadata(), METADATA)
 
-    def test_get_path_on_relative_path(self):
-        dir = self.build_dir(path=REL_DIR_PATH)
-        self.assertEqual(dir.get_path(), ABS_DIR_PATH)
+    def test_constructor_on_invalid_path_error(self):
+        self.assertRaises(InvalidPathError, self.build_file, path="NON_EXISTING_PATH")
 
-    def test_get_path_on_absolute_path(self):
-        dir = self.build_dir(path=ABS_DIR_PATH)
-        self.assertEqual(dir.get_path(), ABS_DIR_PATH)
+    def test_constructor_on_invalid_file_error(self):
+        self.assertRaises(InvalidFileError, self.build_file, path=REL_DIR_PATH)
 
-    def test__get_separator(self):
-        self.assertEqual(self.build_dir(path=os.getcwd())._get_separator(), os.sep)
+    def test_constructor_on_bucket_not_found_error(self):
+        self.assertRaises(BucketNotFoundError, self.build_file, bucket='UNKNOWN')
 
     def test_get_name(self):
-        dir = self.build_dir(path=ABS_DIR_PATH)
-        self.assertEqual(dir.get_name(), DIR_NAME.removesuffix(SEPARATOR))
+        with self.build_file() as file:
+            self.assertEqual(file.get_name(), FILE_NAME)
 
     def test_get_uri(self):
-        dir = self.build_dir(path=ABS_DIR_PATH)
-        self.assertEqual(dir.get_uri(), f"file:///{ABS_DIR_PATH.removeprefix(SEPARATOR)}")
+        with self.build_file() as file:
+            self.assertEqual(file.get_uri(), f"gs://{BUCKET}/{REL_FILE_PATH}")
 
-    def test_get_metadata_on_invalid_file_error(self):
-        dir = self.build_dir()
-        self.assertRaises(InvalidFileError, dir.get_metadata, file_path="NON_EXISTING_PATH")
+    def test_get_bucket_name(self):
+        with self.build_file() as file:
+            self.assertEqual(file.get_bucket_name(), BUCKET)
 
     def test_get_metadata(self):
-        dir = self.build_dir(path=ABS_DIR_PATH)
-        self.assertEqual(dir.get_metadata(file_path=DIR_FILE_NAME), {})
-
-    def test_set_and_get_metadata_on_relative_path(self):
-        dir = self.build_dir(path=ABS_DIR_PATH)
-        file_path, metadata = DIR_FILE_NAME, {'a': '1'}
-        dir.set_metadata(file_path=file_path, metadata=metadata)
-        self.assertEqual(dir.get_metadata(file_path=file_path), metadata)
+        with self.build_file() as file:
+            self.assertEqual(file.get_metadata(), {})
 
-    def test_set_and_get_metadata_on_absolute_path(self):
-        dir = self.build_dir(path=ABS_DIR_PATH)
-        file_path = ABS_DIR_FILE_PATH
-        metadata = {'a': '1'}
-        dir.set_metadata(file_path=file_path, metadata=metadata)
-        self.assertEqual(dir.get_metadata(file_path=file_path), metadata)
+    def test_set_and_get_metadata(self):
+        with self.build_file() as file:
+            metadata = {'a': '1'}
+            file.set_metadata(metadata=metadata)
+            self.assertEqual(file.get_metadata(), metadata)
 
     def test_set_metadata_on_non_string_metadata_key_error(self):
-        dir = self.build_dir(path=ABS_DIR_PATH)
-        args = {
-            'file_path': DIR_FILE_NAME,
-            'metadata': {1: '1'}
-        }
-        self.assertRaises(NonStringMetadataKeyError, dir.set_metadata, **args)
+        with self.build_file() as file:
+            metadata = {1: '1'}
+            self.assertRaises(NonStringMetadataKeyError, file.set_metadata, metadata=metadata)
 
     def test_set_metadata_on_non_string_metadata_value_error(self):
-        dir = self.build_dir(path=ABS_DIR_PATH)
-        args = {
-            'file_path': DIR_FILE_NAME,
-            'metadata': {'1': 1}
-        }
-        self.assertRaises(NonStringMetadataValueError, dir.set_metadata, **args)
-
-    def test_set_metadata_on_invalid_file_error(self):
-        dir = self.build_dir(path=ABS_DIR_PATH)
-        args = {
-            'file_path': 'NON_EXISTING_FILE',
-            'metadata': {'1': '1'}
-        }
-        self.assertRaises(InvalidFileError, dir.set_metadata, **args)
-
-    def test_path_exists_on_abs_path(self):
-        self.assertEqual(self.build_dir(
-            path=ABS_DIR_PATH).path_exists(ABS_DIR_FILE_PATH), True)
-
-    def test_path_exists_on_relative_path(self):
-        self.assertEqual(self.build_dir(
-            path=ABS_DIR_PATH).path_exists(DIR_FILE_NAME), True)
-
-    def test_path_not_exists_on_abs_path(self):
-        file_path = join_paths(ABS_DIR_PATH, 'NON_EXISTING_FILE')
-        self.assertEqual(self.build_dir(
-            path=ABS_DIR_PATH).path_exists(file_path), False)
+        with self.build_file() as file:
+            metadata = {'1': 1}
+            self.assertRaises(NonStringMetadataValueError, file.set_metadata, metadata=metadata)
 
-    def test_path_not_exists_on_relative_path(self):
-        file_path = 'NON_EXISTING_FILE'
-        self.assertEqual(self.build_dir(
-            path=ABS_DIR_PATH).path_exists(file_path), False)
+    def test_load_metadata(self):
+        with self.build_file() as file:
+            file.load_metadata()
+            self.assertEqual(file.get_metadata(), METADATA)
 
-    def test_get_contents(self):
-        self.assertEqual(self.build_dir(
-            path=ABS_DIR_PATH).get_contents(), CONTENTS)
+    def test_load_metadata_after_set_metadata(self):
+        with self.build_file() as file:
+            file.set_metadata({'a': 'a'})
+            file.load_metadata()
+            self.assertEqual(file.get_metadata(), METADATA)
 
-    def test_get_contents_on_show_abs_path(self):
-        self.assertEqual(
-            self.build_dir(path=ABS_DIR_PATH).get_contents(show_abs_path=True),
-            get_abs_contents(recursively=False))
-        
-    def test_get_contents_on_recursively(self):
-        self.assertEqual(
-            self.build_dir(path=ABS_DIR_PATH).get_contents(recursively=True),
-            RECURSIVE_CONTENTS)
-        
-    def test_get_contents_on_show_abs_path_and_recursively(self):
-        self.assertEqual(
-            self.build_dir(path=ABS_DIR_PATH).get_contents(
-                show_abs_path=True, recursively=True),
-            get_abs_contents(recursively=True))
-        
-    def test_traverse(self):
-        self.assertEqual(list(self.build_dir(
-            path=ABS_DIR_PATH).traverse()), CONTENTS)
+    def test_set_metadata_after_load_metadata(self):
+        with self.build_file() as file:
+            file.load_metadata()
+            new_metadata = {'a': 'a'}
+            file.set_metadata(new_metadata)
+            self.assertEqual(file.get_metadata(), new_metadata)
 
-    def test_traverse_on_show_abs_path(self):
-        self.assertEqual(
-            list(self.build_dir(
-                path=ABS_DIR_PATH).traverse(show_abs_path=True)),
-            get_abs_contents(recursively=False))
-        
-    def test_traverse_on_recursively(self):
-        self.assertEqual(
-            list(self.build_dir(
-                path=ABS_DIR_PATH).traverse(recursively=True)),
-            RECURSIVE_CONTENTS)
-        
-    def test_traverse_on_show_abs_path_and_recursively(self):
-        self.assertEqual(
-            list(self.build_dir(
-                path=ABS_DIR_PATH).traverse(show_abs_path=True, recursively=True)),
-            get_abs_contents(recursively=True))
+    def test_get_size(self):
+        with self.build_file() as file:
+            self.assertEqual(file.get_size(), 4)
 
-    def test_ls(self):
-        with io.StringIO() as stdo:
-            sys.stdout = stdo
+    def test_read(self):
+        with self.build_file() as file:
+            self.assertEqual(file.read(), b"TEXT")
 
-            self.build_dir(path=ABS_DIR_PATH).ls()
+    def test_read_chunks(self):
+        data, chunk_size = b"TEXT", 1
+        with (
+            self.build_file() as file,
+            io.BytesIO(data) as buffer
+        ):
+            for chunk in file.read_chunks(chunk_size):
+                self.assertEqual(chunk, buffer.read(chunk_size))
 
-            sys.stdout = sys.__stdout__
+    def test_read_range(self):
+        data, start, end = b"EX", 1, 3
+        with self.build_file() as file:
+            self.assertEqual(data, file.read_range(start, end))
 
-            ls_expected_output = '\n'.join(CONTENTS) + '\n'
-            self.assertEqual(stdo.getvalue(), ls_expected_output)
+    def test_read_range_on_start_is_none(self):
+        with self.build_file() as file:
+            data, start, end = b"TE", None, 2
+            self.assertEqual(data, file.read_range(start, end))
 
-    def test_ls_on_show_abs_path(self):
-        with io.StringIO() as stdo:
-            sys.stdout = stdo
+    def test_read_range_on_end_is_none(self):
+        with self.build_file() as file:
+            data, start, end = b"XT", 2, None
+            self.assertEqual(data, file.read_range(start, end))
 
-            self.build_dir(path=ABS_DIR_PATH).ls(show_abs_path=True)
+    def test_read_range_on_start_greater_than_end(self):
+        with self.build_file() as file:
+            data, start, end = b"", 100, 1
+            self.assertEqual(data, file.read_range(start, end))
 
-            sys.stdout = sys.__stdout__
+    def test_read_text(self):
+        with self.build_file() as file:
+            data = "TEXT"
+            self.assertEqual(data, file.read_text())
 
-            ls_expected_output = '\n'.join(get_abs_contents(recursively=False)) + '\n'
+    def test_read_lines(self):
+        with self.build_file() as file:
+            data = "TEXT"
+            for line in file.read_lines():
+                self.assertEqual(data, line)
 
-            self.assertEqual(stdo.getvalue(), ls_expected_output)
+    def test_read_lines_on_chunk_size(self):
+        with self.build_file() as file:
+            data = "TEXT"
+            for line in file.read_lines(chunk_size=1):
+                self.assertEqual(data, line)
 
-    def test_ls_on_recursively(self):
-        with io.StringIO() as stdo:
+    def test_cat(self):
+        data = "TEXT"
+        with (
+            io.StringIO() as stdo,
+            self.build_file() as file
+        ):
             sys.stdout = stdo
 
-            self.build_dir(path=ABS_DIR_PATH).ls(recursively=True)
-
-            sys.stdout = sys.__stdout__
-
-            ls_expected_output = '\n'.join(RECURSIVE_CONTENTS) + '\n'
-            self.assertEqual(stdo.getvalue(), ls_expected_output)
-
-    def test_ls_on_show_abs_path_and_recursively(self):
-        with io.StringIO() as stdo:
-            sys.stdout = stdo
+            file.cat()
 
-            self.build_dir(path=ABS_DIR_PATH).ls(
-                show_abs_path=True,
-                recursively=True)
-            
             sys.stdout = sys.__stdout__
 
-            ls_expected_output = '\n'.join(get_abs_contents(recursively=True)) + '\n'
-
-            self.assertEqual(stdo.getvalue(), ls_expected_output)
-
-    def test_count(self):
-        self.assertEqual(self.build_dir(path=ABS_DIR_PATH).count(), 2)
-
-    def test_count_on_recursively(self):
-        self.assertEqual(self.build_dir(path=ABS_DIR_PATH).count(recursively=True), 3)
-
-    def test_get_size(self):
-        self.assertEqual(self.build_dir(path=ABS_DIR_PATH).get_size(), 4)
-
-    def test_get_size_on_recursively(self):
-        self.assertEqual(self.build_dir(path=ABS_DIR_PATH).get_size(recursively=True), 16)
+            self.assertEqual(stdo.getvalue().strip('\n'), data)
 
     def test_transfer_to(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace('dir', TMP_DIR_NAME))
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
-        self.build_dir(path=ABS_DIR_PATH).transfer_to(
-            dst=self.build_dir(path=tmp_dir_path))
-        # Assert that the two directories contains the same contents.
-        original = [s for s in sorted(os.listdir(ABS_DIR_PATH)) if s.endswith('.txt')]
-        copies = [s for s in sorted(os.listdir(tmp_dir_path))]
-        # 1. Assert number of copied files are the same.
-        self.assertEqual(len(original), len(copies))
-        # 2. Iterate over all files.
-        for ofp, cfp in zip(original, copies):
-            # Assert their contents are the same.
-            with (
-                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
-                open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
-            ):
-                self.assertEqual(of.read(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
-
-    def test_transfer_to_on_recursively(self):
-        # Create a temporary dictionary.
-        tmp_dir_name = TMP_DIR_NAME
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace('dir', tmp_dir_name))
-        os.mkdir(tmp_dir_path)
-        # Recursively copy the directory's contents
-        # into this tmp directory.
-        self.build_dir(path=ABS_DIR_PATH).transfer_to(
-            dst=self.build_dir(path=tmp_dir_path),
-            recursively=True)
-        # Assert that the two directories contains the same contents.
-        original = [join_paths(dp, f) for dp, _, fn in 
-                    os.walk(ABS_DIR_PATH) for f in fn]
-        copies = [join_paths(dp, f) for dp, _, fn in 
-                os.walk(tmp_dir_path) for f in fn]
-        # 1. Assert number of copied files are the same.
-        self.assertEqual(len(original), len(copies))
-        # 2. Iterate over all files.
-        for ofp, cfp in zip(original, copies):
-            # Assert their contents are the same.
+        with self.build_file() as file:
+            # Copy file into dir.
+            file.transfer_to(dst=TestLocalDir.build_dir(path=ABS_DIR_PATH))
+            # Confirm that file was indeed copied.
+            copy_path = join_paths(ABS_DIR_PATH, FILE_NAME)
             with (
-                open(file=ofp, mode='rb') as of,
-                open(file=cfp, mode='rb') as cp
+                open(ABS_FILE_PATH, mode='rb') as file,
+                open(copy_path, mode='rb') as copy
             ):
-                self.assertEqual(of.read(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+                self.assertEqual(file.read(), copy.read())
+            # Remove copy of the file.
+            os.remove(copy_path)
 
     def test_transfer_to_on_chunk_size(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace('dir', TMP_DIR_NAME))
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
-        self.build_dir(path=ABS_DIR_PATH).transfer_to(
-            dst=self.build_dir(path=tmp_dir_path),
-            chunk_size=1)
-        # Assert that the two directories contains the same contents.
-        original = [s for s in sorted(os.listdir(ABS_DIR_PATH)) if s.endswith('.txt')]
-        copies = [s for s in sorted(os.listdir(tmp_dir_path))]
-        # 1. Assert number of copied files are the same.
-        self.assertEqual(len(original), len(copies))
-        # 2. Iterate over all files.
-        for ofp, cfp in zip(original, copies):
-            # Assert their contents are the same.
+        with self.build_file() as file:
+            # Copy file into dir.
+            file.transfer_to(
+                dst=TestLocalDir.build_dir(path=ABS_DIR_PATH),
+                chunk_size=1)
+            # Confirm that file was indeed copied.
+            copy_path = join_paths(ABS_DIR_PATH, FILE_NAME)
             with (
-                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
-                open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
+                open(ABS_FILE_PATH, mode='rb') as file,
+                open(copy_path, mode='rb') as copy
             ):
-                self.assertEqual(of.read(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+                self.assertEqual(file.read(), copy.read())
+            # Remove copy of the file.
+            os.remove(copy_path)
 
-    def test_transfer_to_on_filter(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace('dir', TMP_DIR_NAME))
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
-        self.build_dir(path=ABS_DIR_PATH).transfer_to(
-            dst=self.build_dir(path=tmp_dir_path),
-            filter=lambda x: not x.endswith('.txt'))
-        # Assert that the two directories contain the same files.
-        original = [s for s in sorted(os.listdir(ABS_DIR_PATH))
-                    if os.path.isfile(s) and not s.endswith('.txt')]
-        copies = [s for s in sorted(os.listdir(tmp_dir_path))]
-        # 1. Assert number of copied files are the same.
-        self.assertEqual(len(original), len(copies))
-        # 2. Iterate over all files.
-        for ofp, cfp in zip(original, copies):
-            # Assert their contents are the same.
-            with (
-                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
-                open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
+    def test_transfer_to_on_overwrite_error(self):
+        with self.build_file() as file:
+            dir = TestLocalDir.build_dir(path=ABS_DIR_PATH)
+            # Copy file into dir.
+            file.transfer_to(dst=dir)
+            # Ensure OverwriteError is raised when trying
+            # to copy file a second time.
+            self.assertFalse(file.transfer_to(dst=dir))
+            # Remove copy of the file.
+            copy_path = join_paths(ABS_DIR_PATH, FILE_NAME)
+            os.remove(copy_path)
+
+    def test_transfer_to_on_include_metadata_set_to_false(self):
+        with (
+            mock_s3() as mocks3,
+            self.build_file() as file
+        ):
+            # Create AWS bucket and contents.
+            create_aws_s3_bucket(
+                mocks3,
+                BUCKET,
+                metadata=METADATA)
+           # Copy file into dir, including its metadata.
+            file.set_metadata(metadata=METADATA)
+            dir_path = REL_DIR_PATH
+            with (AmazonS3Dir(
+                    auth=get_aws_auth_instance(),
+                    bucket=BUCKET,
+                    path=dir_path) as s3_dir
             ):
-                self.assertEqual(of.read(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+                file.transfer_to(dst=s3_dir, include_metadata=False)
+            # Gain access to the uploaded object.
+            file_path = join_paths(dir_path, FILE_NAME)
+            obj = get_aws_s3_object(BUCKET, file_path)
+            # Assert that metadata were copied to the object.
+            self.assertEqual(obj.metadata, {})
+            # Delete object.
+            obj.delete()
 
-    def test_transfer_to_on_overwrite_set_to_false(self):
-        # Create a copy of the directory.
-        tmp_dir_name = TMP_DIR_NAME
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace('dir', tmp_dir_name))
-        shutil.copytree(src=ABS_DIR_PATH, dst=tmp_dir_path)
-        # While capturing stdout...
-        with io.StringIO() as stdo:
-            sys.stdout = stdo
-            # Copy directory with "overwrite" set to "False".
-            self.build_dir(path=ABS_DIR_PATH).transfer_to(
-                dst=LocalDir(path=tmp_dir_path),
-                overwrite=False)
+    def test_transfer_to_on_include_metadata_set_to_true(self):
+        with (
+            mock_s3() as mocks3,
+            self.build_file() as file
+        ):
+            # Create AWS bucket and contents.
+            create_aws_s3_bucket(
+                mocks3,
+                BUCKET,
+                metadata=METADATA)
+            # Copy file into dir, including its metadata.
+            new_metadata = {'2': '2'}
+            file.set_metadata(metadata=new_metadata)
+            dir_path = REL_DIR_PATH
+            with (AmazonS3Dir(
+                    auth=get_aws_auth_instance(),
+                    bucket=BUCKET,
+                    path=dir_path) as s3_dir
+            ):
+                file.transfer_to(dst=s3_dir, include_metadata=True)
+            # Gain access to the uploaded object.
+            file_path = join_paths(dir_path, FILE_NAME)
+            obj = get_aws_s3_object(BUCKET, file_path)
+            # Assert that metadata were copied to the object.
+            self.assertEqual(obj.metadata, new_metadata)
+            # Delete object.
+            obj.delete()
 
-            sys.stdout = sys.__stdout__
+    '''
+    Test connection methods.
+    '''
+    def test_open(self):
+        file = self.build_file()
+        file.close()
+        file.open()
+        self.assertTrue(file._get_handler().is_open())
+        file.close()
 
-            self.assertTrue("Operation unsuccessful" in stdo.getvalue())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+    def test_close(self):
+        file = self.build_file()
+        file.close()
+        self.assertFalse(file._get_handler().is_open())
 
-    def test_transfer_to_on_overwrite_set_to_true(self):
-        # Create a copy of the directory.
-        tmp_dir_name = TMP_DIR_NAME
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace('dir', tmp_dir_name))
-        shutil.copytree(src=ABS_DIR_PATH, dst=tmp_dir_path)
-        # While capturing stdout...
-        with io.StringIO() as stdo:
-            sys.stdout = stdo
-            # Copy directory with "overwrite" set to "True".
-            self.build_dir(path=ABS_DIR_PATH).transfer_to(
-                dst=self.build_dir(path=tmp_dir_path),
-                overwrite=True)
+    '''
+    Test cache methods.
+    '''
+    def test_is_cachable_on_false(self):
+        with self.build_file() as file:
+            self.assertEqual(file.is_cacheable(), False)
 
-            sys.stdout = sys.__stdout__
+    def test_is_cachable_on_true(self):
+        with self.build_file(cache=True) as file:
+            self.assertEqual(file.is_cacheable(), True)
 
-            self.assertTrue("Operation successful" in stdo.getvalue())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+    def test_purge(self):
+        with self.build_file(cache=True) as file:
+            # Load metadata via HTTP.
+            file.load_metadata()
+            # Load metadata via cache and time it.
+            t = time.perf_counter()
+            file.load_metadata()
+            cache_time = time.perf_counter() - t
+            # Purge cache.
+            file.purge()
+            # Load metadata via HTTP and time it.
+            t = time.perf_counter()
+            file.load_metadata()
+            normal_time = time.perf_counter() - t
+            self.assertGreater(normal_time, cache_time)
 
-    def test_transfer_to_on_include_metadata_set_to_false(self):
-        # Set metadata for a directory's file.
-        filename, metadata = DIR_FILE_NAME, {'1': '1'}
+    def test_load_metadata_from_cache_on_value(self):
+        with self.build_file(cache=True) as file:
+            # Load metadata via HTTP.
+            file.load_metadata()
+            # Load metadata from cache.
+            file.load_metadata()
+            self.assertEqual(file.get_metadata(), METADATA)
+
+    def test_load_metadata_from_cache_on_time(self):
+        with self.build_file(cache=True) as file:
+            # Fetch object metadata via HTTP.
+            t = time.perf_counter()
+            _ = file.load_metadata()
+            normal_time = time.perf_counter() - t
+            # Fetch object metadata from cache.
+            t = time.perf_counter()
+            _ = file.load_metadata()
+            cache_time = time.perf_counter() - t
+            # Compare fetch times.
+            self.assertGreater(normal_time, cache_time)
+
+    def test_get_size_from_cache_on_value(self):
+        with self.build_file(cache=True) as file:
+            _ = file.get_size()
+            self.assertEqual(file.get_size(), 4)
+
+    def test_get_size_from_cache_on_time(self):
+        with self.build_file(cache=True) as file:
+            # Fetch object size via HTTP.
+            t = time.perf_counter()
+            _ = file.get_size()
+            normal_time = time.perf_counter() - t
+            # Fetch object size from cache.
+            t = time.perf_counter()
+            _ = file.get_size()
+            cache_time = time.perf_counter() - t
+            # Compare fetch times.
+            self.assertGreater(normal_time, cache_time)
+
+
+class TestLocalDir(unittest.TestCase):
+    
+    @staticmethod
+    def build_dir(
+        path: str = ABS_DIR_PATH,
+        create_if_missing: bool = False
+    ) -> LocalDir:
+        return LocalDir(
+            path=path,
+            create_if_missing=create_if_missing)
+
+    
+    def test_constructor_on_create_if_missing(self):
+        path = to_abs("NON_EXISTING_DIR")
+        _ = self.build_dir(path, create_if_missing=True)
+        self.assertTrue(os.path.isdir(path))
+        os.rmdir(path)
+
+    def test_constructor_on_invalid_path_error(self):
+        self.assertRaises(InvalidPathError, self.build_dir, path="NON_EXISTING_PATH")
+
+    def test_constructor_on_invalid_directory_error(self):
+        self.assertRaises(InvalidDirectoryError, self.build_dir, path=ABS_DIR_FILE_PATH)
+
+    def test_get_path_on_relative_path(self):
+        dir = self.build_dir(path=REL_DIR_PATH)
+        self.assertEqual(dir.get_path(), ABS_DIR_PATH)
+
+    def test_get_path_on_absolute_path(self):
         dir = self.build_dir(path=ABS_DIR_PATH)
-        dir.set_metadata(file_path=filename, metadata=metadata)
-        # Create a temporary dictionary.
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace('dir', TMP_DIR_NAME))
-        os.mkdir(tmp_dir_path)
-        tmp_dir = self.build_dir(path=tmp_dir_path)
-        # Copy the directory's contents into this
-        # tmp directory without including metadata.
-        dir.transfer_to(dst=tmp_dir, include_metadata=False)
-        # Assert that no metadata were transfered.
-        self.assertEqual(tmp_dir.get_metadata(file_path=filename), {})
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+        self.assertEqual(dir.get_path(), ABS_DIR_PATH)
 
-    def test_transfer_to_on_include_metadata_set_to_true(self):
-        # Set metadata for a directory's file.
-        filename, metadata = DIR_FILE_NAME, {'1': '1'}
+    def test__get_separator(self):
+        self.assertEqual(self.build_dir(path=os.getcwd())._get_separator(), os.sep)
+
+    def test_get_name(self):
         dir = self.build_dir(path=ABS_DIR_PATH)
-        dir.set_metadata(file_path=filename, metadata=metadata)
-        # Create a temporary dictionary.
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace('dir', TMP_DIR_NAME))
-        os.mkdir(tmp_dir_path)
-        tmp_dir = self.build_dir(path=tmp_dir_path)
-        # Copy the directory's contents into this
-        # tmp directory while including metadata.
-        dir.transfer_to(dst=tmp_dir, include_metadata=True)
-        # Assert that the two directories contains the same contents.
-        self.assertEqual(
-            tmp_dir.get_metadata(file_path=filename),
-            metadata)
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+        self.assertEqual(dir.get_name(), DIR_NAME.removesuffix(SEPARATOR))
 
-    def test_transfer_to_on_aws_cloud_dir_include_metadata_set_to_true(self):
-        # Set metadata for a directory's file.
-        with mock_s3() as mocks3:
-            # Create AWSS3 bucket.
-            create_aws_s3_bucket(mocks3, BUCKET, METADATA)
-            # Copy directory contents including metadata
-            filename, metadata = DIR_FILE_NAME, {'2': '2'}
-            dir = self.build_dir(path=ABS_DIR_PATH)
-            with (TestAmazonS3Dir.build_dir() as s3_dir):
-                dir.set_metadata(file_path=filename, metadata=metadata)
-                dir.transfer_to(dst=s3_dir, overwrite=True, include_metadata=True)
-                # Assert that the object's metadata have been modified.
-                self.assertEqual(
-                    get_aws_s3_object(
-                        s3_dir.get_bucket_name(),
-                        REL_DIR_FILE_PATH).metadata,
-                    metadata)
+    def test_get_uri(self):
+        dir = self.build_dir(path=ABS_DIR_PATH)
+        self.assertEqual(dir.get_uri(), f"file:///{ABS_DIR_PATH.removeprefix(SEPARATOR)}")
+
+    def test_get_metadata_on_invalid_file_error(self):
+        dir = self.build_dir()
+        self.assertRaises(InvalidFileError, dir.get_metadata, file_path="NON_EXISTING_PATH")
+
+    def test_get_metadata(self):
+        dir = self.build_dir(path=ABS_DIR_PATH)
+        self.assertEqual(dir.get_metadata(file_path=DIR_FILE_NAME), {})
+
+    def test_set_and_get_metadata_on_relative_path(self):
+        dir = self.build_dir(path=ABS_DIR_PATH)
+        file_path, metadata = DIR_FILE_NAME, {'a': '1'}
+        dir.set_metadata(file_path=file_path, metadata=metadata)
+        self.assertEqual(dir.get_metadata(file_path=file_path), metadata)
+
+    def test_set_and_get_metadata_on_absolute_path(self):
+        dir = self.build_dir(path=ABS_DIR_PATH)
+        file_path = ABS_DIR_FILE_PATH
+        metadata = {'a': '1'}
+        dir.set_metadata(file_path=file_path, metadata=metadata)
+        self.assertEqual(dir.get_metadata(file_path=file_path), metadata)
+
+    def test_set_metadata_on_non_string_metadata_key_error(self):
+        dir = self.build_dir(path=ABS_DIR_PATH)
+        args = {
+            'file_path': DIR_FILE_NAME,
+            'metadata': {1: '1'}
+        }
+        self.assertRaises(NonStringMetadataKeyError, dir.set_metadata, **args)
+
+    def test_set_metadata_on_non_string_metadata_value_error(self):
+        dir = self.build_dir(path=ABS_DIR_PATH)
+        args = {
+            'file_path': DIR_FILE_NAME,
+            'metadata': {'1': 1}
+        }
+        self.assertRaises(NonStringMetadataValueError, dir.set_metadata, **args)
+
+    def test_set_metadata_on_invalid_file_error(self):
+        dir = self.build_dir(path=ABS_DIR_PATH)
+        args = {
+            'file_path': 'NON_EXISTING_FILE',
+            'metadata': {'1': '1'}
+        }
+        self.assertRaises(InvalidFileError, dir.set_metadata, **args)
+
+    def test_path_exists_on_abs_path(self):
+        self.assertEqual(self.build_dir(
+            path=ABS_DIR_PATH).path_exists(ABS_DIR_FILE_PATH), True)
+
+    def test_path_exists_on_relative_path(self):
+        self.assertEqual(self.build_dir(
+            path=ABS_DIR_PATH).path_exists(DIR_FILE_NAME), True)
+
+    def test_path_not_exists_on_abs_path(self):
+        file_path = join_paths(ABS_DIR_PATH, 'NON_EXISTING_FILE')
+        self.assertEqual(self.build_dir(
+            path=ABS_DIR_PATH).path_exists(file_path), False)
+
+    def test_path_not_exists_on_relative_path(self):
+        file_path = 'NON_EXISTING_FILE'
+        self.assertEqual(self.build_dir(
+            path=ABS_DIR_PATH).path_exists(file_path), False)
+
+    def test_get_contents(self):
+        self.assertEqual(self.build_dir(
+            path=ABS_DIR_PATH).get_contents(), CONTENTS)
+
+    def test_get_contents_on_show_abs_path(self):
+        self.assertEqual(
+            self.build_dir(path=ABS_DIR_PATH).get_contents(show_abs_path=True),
+            get_abs_contents(recursively=False))
+        
+    def test_get_contents_on_recursively(self):
+        self.assertEqual(
+            self.build_dir(path=ABS_DIR_PATH).get_contents(recursively=True),
+            RECURSIVE_CONTENTS)
+        
+    def test_get_contents_on_show_abs_path_and_recursively(self):
+        self.assertEqual(
+            self.build_dir(path=ABS_DIR_PATH).get_contents(
+                show_abs_path=True, recursively=True),
+            get_abs_contents(recursively=True))
+        
+    def test_traverse(self):
+        self.assertEqual(list(self.build_dir(
+            path=ABS_DIR_PATH).traverse()), CONTENTS)
+
+    def test_traverse_on_show_abs_path(self):
+        self.assertEqual(
+            list(self.build_dir(
+                path=ABS_DIR_PATH).traverse(show_abs_path=True)),
+            get_abs_contents(recursively=False))
+        
+    def test_traverse_on_recursively(self):
+        self.assertEqual(
+            list(self.build_dir(
+                path=ABS_DIR_PATH).traverse(recursively=True)),
+            RECURSIVE_CONTENTS)
+        
+    def test_traverse_on_show_abs_path_and_recursively(self):
+        self.assertEqual(
+            list(self.build_dir(
+                path=ABS_DIR_PATH).traverse(show_abs_path=True, recursively=True)),
+            get_abs_contents(recursively=True))
+
+    def test_ls(self):
+        with io.StringIO() as stdo:
+            sys.stdout = stdo
+
+            self.build_dir(path=ABS_DIR_PATH).ls()
+
+            sys.stdout = sys.__stdout__
+
+            ls_expected_output = '\n' + '\n'.join(CONTENTS) + '\n'
+            self.assertEqual(stdo.getvalue(), ls_expected_output)
+
+    def test_ls_on_show_abs_path(self):
+        with io.StringIO() as stdo:
+            sys.stdout = stdo
+
+            self.build_dir(path=ABS_DIR_PATH).ls(show_abs_path=True)
+
+            sys.stdout = sys.__stdout__
+
+            ls_expected_output = '\n' + '\n'.join(get_abs_contents(recursively=False)) + '\n'
+
+            self.assertEqual(stdo.getvalue(), ls_expected_output)
+
+    def test_ls_on_recursively(self):
+        with io.StringIO() as stdo:
+            sys.stdout = stdo
+
+            self.build_dir(path=ABS_DIR_PATH).ls(recursively=True)
+
+            sys.stdout = sys.__stdout__
+
+            ls_expected_output = '\n' + DIR_NAME + SEPARATOR + LS_RECURSIVE_OUTPUT
+
+            self.assertEqual(stdo.getvalue(), ls_expected_output)
+
+    def test_ls_on_show_abs_path_and_recursively(self):
+        with io.StringIO() as stdo:
+            sys.stdout = stdo
+
+            self.build_dir(path=ABS_DIR_PATH).ls(
+                show_abs_path=True,
+                recursively=True)
+            
+            sys.stdout = sys.__stdout__
+
+            ls_expected_output = '\n' + ABS_DIR_PATH + LS_RECURSIVE_OUTPUT
+
+            self.assertEqual(stdo.getvalue(), ls_expected_output)
+
+    def test_count(self):
+        self.assertEqual(self.build_dir(path=ABS_DIR_PATH).count(), 2)
+
+    def test_count_on_recursively(self):
+        self.assertEqual(self.build_dir(path=ABS_DIR_PATH).count(recursively=True), 3)
+
+    def test_get_size(self):
+        self.assertEqual(self.build_dir(path=ABS_DIR_PATH).get_size(), 4)
+
+    def test_get_size_on_recursively(self):
+        self.assertEqual(self.build_dir(path=ABS_DIR_PATH).get_size(recursively=True), 12)
+
+    @create_tmp_dir
+    def test_transfer_to(self, tmp_dir_path):
+        # Copy the directory's contents into this tmp directory.
+        self.build_dir(path=ABS_DIR_PATH).transfer_to(
+            dst=self.build_dir(path=tmp_dir_path))
+        # Assert that the two directories contains the same contents.
+        original = [s for s in sorted(os.listdir(ABS_DIR_PATH)) if s.endswith('.txt')]
+        copies = [s for s in sorted(os.listdir(tmp_dir_path))]
+        # 1. Assert number of copied files are the same.
+        self.assertEqual(len(original), len(copies))
+        # 2. Iterate over all files.
+        for ofp, cfp in zip(original, copies):
+            # Assert their contents are the same.
+            with (
+                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
+                open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
+            ):
+                self.assertEqual(of.read(), cp.read())
+
+    @create_tmp_dir
+    def test_transfer_to_on_recursively(self, tmp_dir_path):
+        # Recursively copy the directory's contents
+        # into this tmp directory.
+        self.build_dir(path=ABS_DIR_PATH).transfer_to(
+            dst=self.build_dir(path=tmp_dir_path),
+            recursively=True)
+        # Assert that the two directories contains the same contents.
+        original = [join_paths(dp, f) for dp, _, fn in 
+                    os.walk(ABS_DIR_PATH) for f in fn]
+        copies = [join_paths(dp, f) for dp, _, fn in 
+                os.walk(tmp_dir_path) for f in fn]
+        # 1. Assert number of copied files are the same.
+        self.assertEqual(len(original), len(copies))
+        # 2. Iterate over all files.
+        for ofp, cfp in zip(original, copies):
+            # Assert their contents are the same.
+            with (
+                open(file=ofp, mode='rb') as of,
+                open(file=cfp, mode='rb') as cp
+            ):
+                self.assertEqual(of.read(), cp.read())
+
+    @create_tmp_dir
+    def test_transfer_to_on_chunk_size(self, tmp_dir_path):
+        # Copy the directory's contents into this tmp directory.
+        self.build_dir(path=ABS_DIR_PATH).transfer_to(
+            dst=self.build_dir(path=tmp_dir_path),
+            chunk_size=1)
+        # Assert that the two directories contains the same contents.
+        original = [s for s in sorted(os.listdir(ABS_DIR_PATH)) if s.endswith('.txt')]
+        copies = [s for s in sorted(os.listdir(tmp_dir_path))]
+        # 1. Assert number of copied files are the same.
+        self.assertEqual(len(original), len(copies))
+        # 2. Iterate over all files.
+        for ofp, cfp in zip(original, copies):
+            # Assert their contents are the same.
+            with (
+                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
+                open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
+            ):
+                self.assertEqual(of.read(), cp.read())
+
+    @create_tmp_dir
+    def test_transfer_to_on_filter(self, tmp_dir_path):
+        # Copy the directory's contents into this tmp directory.
+        self.build_dir(path=ABS_DIR_PATH).transfer_to(
+            dst=self.build_dir(path=tmp_dir_path),
+            filter=lambda x: not x.endswith('.txt'))
+        # Assert that the two directories contain the same files.
+        original = [s for s in sorted(os.listdir(ABS_DIR_PATH))
+                    if os.path.isfile(s) and not s.endswith('.txt')]
+        copies = [s for s in sorted(os.listdir(tmp_dir_path))]
+        # 1. Assert number of copied files are the same.
+        self.assertEqual(len(original), len(copies))
+        # 2. Iterate over all files.
+        for ofp, cfp in zip(original, copies):
+            # Assert their contents are the same.
+            with (
+                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
+                open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
+            ):
+                self.assertEqual(of.read(), cp.read())
+
+    @clone_tmp_dir
+    def test_transfer_to_on_overwrite_set_to_false(self, tmp_dir_path):
+        # While capturing stdout...
+        with io.StringIO() as stdo:
+            sys.stdout = stdo
+            # Copy directory with "overwrite" set to "False".
+            self.build_dir(path=ABS_DIR_PATH).transfer_to(
+                dst=LocalDir(path=tmp_dir_path),
+                overwrite=False)
+
+            sys.stdout = sys.__stdout__
+
+            self.assertTrue("Operation unsuccessful" in stdo.getvalue())
+
+    @clone_tmp_dir
+    def test_transfer_to_on_overwrite_set_to_true(self, tmp_dir_path):
+        # While capturing stdout...
+        with io.StringIO() as stdo:
+            sys.stdout = stdo
+            # Copy directory with "overwrite" set to "True".
+            self.build_dir(path=ABS_DIR_PATH).transfer_to(
+                dst=self.build_dir(path=tmp_dir_path),
+                overwrite=True)
+
+            sys.stdout = sys.__stdout__
+
+            self.assertTrue("Operation successful" in stdo.getvalue())
+
+    @create_tmp_dir
+    def test_transfer_to_on_include_metadata_set_to_false(self, tmp_dir_path):
+        # Set metadata for a directory's file.
+        filename, metadata = DIR_FILE_NAME, {'1': '1'}
+        dir = self.build_dir(path=ABS_DIR_PATH)
+        dir.set_metadata(file_path=filename, metadata=metadata)
+        # Create a temporary dictionary.
+        tmp_dir = self.build_dir(path=tmp_dir_path)
+        # Copy the directory's contents into this
+        # tmp directory without including metadata.
+        dir.transfer_to(dst=tmp_dir, include_metadata=False)
+        # Assert that no metadata were transfered.
+        self.assertEqual(tmp_dir.get_metadata(file_path=filename), {})
+
+    @create_tmp_dir
+    def test_transfer_to_on_include_metadata_set_to_true(self, tmp_dir_path):
+        # Set metadata for a directory's file.
+        filename, metadata = DIR_FILE_NAME, {'1': '1'}
+        dir = self.build_dir(path=ABS_DIR_PATH)
+        dir.set_metadata(file_path=filename, metadata=metadata)
+        # Create a temporary dictionary.
+        tmp_dir = self.build_dir(path=tmp_dir_path)
+        # Copy the directory's contents into this
+        # tmp directory while including metadata.
+        dir.transfer_to(dst=tmp_dir, include_metadata=True)
+        # Assert that the two directories contains the same contents.
+        self.assertEqual(
+            tmp_dir.get_metadata(file_path=filename),
+            metadata)
+
+    def test_transfer_to_on_aws_cloud_dir_include_metadata_set_to_true(self):
+        # Set metadata for a directory's file.
+        with mock_s3() as mocks3:
+            # Create AWSS3 bucket.
+            create_aws_s3_bucket(mocks3, BUCKET, METADATA)
+            # Copy directory contents including metadata
+            filename, metadata = DIR_FILE_NAME, {'2': '2'}
+            dir = self.build_dir(path=ABS_DIR_PATH)
+            with (TestAmazonS3Dir.build_dir() as s3_dir):
+                dir.set_metadata(file_path=filename, metadata=metadata)
+                dir.transfer_to(dst=s3_dir, overwrite=True, include_metadata=True)
+                # Assert that the object's metadata have been modified.
+                self.assertEqual(
+                    get_aws_s3_object(
+                        s3_dir.get_bucket_name(),
+                        REL_DIR_FILE_PATH).metadata,
+                    metadata)
+        
+    def test_get_file(self):
+        dir = self.build_dir()
+        file = dir.get_file(DIR_FILE_NAME)
+        self.assertEqual(file.get_path(), ABS_DIR_FILE_PATH)
+
+    def test_get_file_on_invalid_path_error(self):
+        dir = self.build_dir()
+        self.assertRaises(InvalidPathError, dir.get_file, "NON_EXISTING_PATH")
+
+    def test_get_file_on_invalid_file_error(self):
+        dir = self.build_dir()
+        self.assertRaises(InvalidFileError, dir.get_file, DIR_SUBDIR_NAME)
+
+    def test_get_subdir(self):
+        dir = self.build_dir()
+        subdir = dir.get_subdir(DIR_SUBDIR_NAME)
+        self.assertEqual(subdir.get_path(), ABS_DIR_SUBDIR_PATH)
+
+    def test_get_subdir_on_invalid_path_error(self):
+        dir = self.build_dir()
+        self.assertRaises(InvalidPathError, dir.get_subdir, "NON_EXISTING_PATH")
+
+    def test_get_subdir_on_invalid_directory_error(self):
+        dir = self.build_dir()
+        self.assertRaises(InvalidDirectoryError, dir.get_subdir, DIR_FILE_NAME)
+
+    def test_file_shared_metadata_on_modify_from_dir(self):
+        # Create dir and get file.
+        dir = self.build_dir()
+        file = dir.get_file(DIR_FILE_NAME)
+        # Change metadata via "Dir" API.
+        dir.set_metadata(DIR_FILE_NAME, METADATA)
+        # Assert file metadata have been changed.
+        self.assertEqual(file.get_metadata(), METADATA)
+
+    def test_file_shared_metadata_on_modify_from_file(self):
+        # Create dir and get file.
+        dir = self.build_dir()
+        file = dir.get_file(DIR_FILE_NAME)
+        # Change metadata via "File" API.
+        file.set_metadata(METADATA)
+        # Assert file metadata have been changed.
+        self.assertEqual(dir.get_metadata(DIR_FILE_NAME), METADATA)
+
+    def test_subdir_shared_metadata_on_modify_from_dir(self):
+        # Create dir and get file.
+        dir = self.build_dir()
+        subdir = dir.get_subdir(DIR_SUBDIR_NAME)
+        # Change metadata via "Dir" API.
+        dir.set_metadata(
+            DIR_SUBDIR_NAME + DIR_SUBDIR_FILE_NAME, METADATA)
+        # Assert file metadata have been changed.
+        self.assertEqual(
+            subdir.get_metadata(DIR_SUBDIR_FILE_NAME), METADATA)
+
+    def test_subdir_shared_metadata_on_modify_from_subdir(self):
+        # Create dir and get file.
+        dir = self.build_dir()
+        subdir = dir.get_subdir(DIR_SUBDIR_NAME)
+        # Change metadata via "File" API.
+        subdir.set_metadata(DIR_SUBDIR_FILE_NAME, METADATA)
+        # Assert file metadata have been changed.
+        self.assertEqual(
+            dir.get_metadata(DIR_SUBDIR_NAME + DIR_SUBDIR_FILE_NAME), METADATA)
+        
+
+class TestRemoteDir(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestRemoteDir, cls).setUpClass()
+
+        from fluke._handlers import SSHClientHandler
+
+        m1 = patch.object(SSHClientHandler, '_get_file_size_impl', autospec=True)
+        m2 = patch.object(SSHClientHandler, '_get_file_metadata_impl', autospec=True)
+        m3 = patch.object(SSHClientHandler, '_traverse_dir_impl', autospec=True)
+
+        def simulate_latency_1(*args, **kwargs):
+            time.sleep(0.2)
+            return m1.temp_original(*args, **kwargs)
+        
+        def simulate_latency_2(*args, **kwargs):
+            time.sleep(0.2)
+            return m2.temp_original(*args, **kwargs)
+        
+        def simulate_latency_3(*args, **kwargs):
+            time.sleep(0.2)
+            return m3.temp_original(*args, **kwargs)
+
+        m1.start().side_effect = simulate_latency_1
+        m2.start().side_effect = simulate_latency_2
+        m3.start().side_effect = simulate_latency_3
+
+    @classmethod
+    def tearDownClass(cls):
+        super(TestRemoteDir, cls).tearDownClass()
+        patch.stopall()
+
+    @staticmethod
+    def get_abs_contents(recursively: bool):
+        return [
+            join_paths('/', REL_DIR_PATH, p) for p in (
+                RECURSIVE_CONTENTS if recursively else CONTENTS
+            )
+        ]
+    
+    @staticmethod
+    def build_dir(
+        path: str = f"/{REL_DIR_PATH}",
+        cache: bool = False,
+        create_if_missing: bool = False
+    ) -> RemoteDir:
+        return RemoteDir(**{
+            'auth': get_remote_auth_instance(),
+            'path': path,
+            'cache': cache,
+            'create_if_missing': create_if_missing
+        })
+    
+    def test_constructor(self):
+        with self.build_dir() as dir:
+            self.assertEqual(dir.get_path(), f"/{REL_DIR_PATH}")
+
+    def test_constructor_on_root_path(self):
+        root_path = '/'
+        with self.build_dir(path=root_path) as dir:
+            self.assertEqual(dir.get_path(), root_path)
+
+    def test_constructor_on_create_if_missing(self):
+        dir_path = f"{TEST_FILES_DIR}{SEPARATOR}NON_EXISTING_DIR/"
+        with self.build_dir(path=f"/{dir_path}", create_if_missing=True) as _:
+            self.assertTrue(os.path.isdir(dir_path))
+        os.rmdir(dir_path)
+
+    def test_constructor_on_empty_path_error(self):
+        self.assertRaises(InvalidPathError, self.build_dir, path="")
+
+    def test_constructor_on_invalid_path_error(self):
+        self.assertRaises(InvalidPathError, self.build_dir, path="NON_EXISTING_PATH")
+
+    def test_constructor_on_invalid_directory_error(self):
+        self.assertRaises(InvalidDirectoryError, self.build_dir, path=f"/{REL_DIR_FILE_PATH}")
+
+    def test_get_hostname(self):
+        with self.build_dir() as dir:
+            self.assertEqual(dir.get_hostname(), HOST)
+
+    def test_get_name(self):
+        with self.build_dir() as dir:
+            self.assertEqual(dir.get_name(), DIR_NAME)    
+
+    def test_get_uri(self):
+        with self.build_dir() as dir:
+            self.assertEqual(dir.get_uri(), f"sftp://{HOST}/{REL_DIR_PATH.removeprefix(os.sep)}")
+            
+    def test_set_and_get_metadata(self):
+        with self.build_dir() as dir:
+            file_path = f'/{REL_DIR_FILE_PATH}'
+            metadata = {'a': '1'}
+            dir.set_metadata(file_path=file_path, metadata=metadata)
+            self.assertEqual(dir.get_metadata(file_path=file_path), metadata)
+
+    def test_get_metadata(self):
+        with self.build_dir() as dir:
+            self.assertEqual(dir.get_metadata(file_path=f'/{REL_DIR_FILE_PATH}'), {})
+
+    def test_get_metadata_on_invalid_file_error(self):
+        with self.build_dir() as dir:
+            self.assertRaises(InvalidFileError, dir.get_metadata, file_path="NON_EXISTING_PATH")
+
+    def test_set_metadata_on_non_string_metadata_key_error(self):
+        with self.build_dir() as dir:
+            args = {
+                'file_path': ABS_DIR_FILE_PATH,
+                'metadata': {1: '1'}
+            }
+            self.assertRaises(NonStringMetadataKeyError, dir.set_metadata, **args)
+
+    def test_set_metadata_on_non_string_metadata_value_error(self):
+        with self.build_dir() as dir:
+            args = {
+                'file_path': ABS_DIR_FILE_PATH,
+                'metadata': {'1': 1}
+            }
+            self.assertRaises(NonStringMetadataValueError, dir.set_metadata, **args)
+
+    def test_set_metadata_on_invalid_file_error(self):
+        with self.build_dir() as dir:
+            args = {
+                'file_path': 'NON_EXISTING_FILE',
+                'metadata': {'1': '1'}
+            }
+            self.assertRaises(InvalidFileError, dir.set_metadata, **args)
+
+    def test_path_exists(self):
+        with self.build_dir() as dir:
+            self.assertEqual(dir.path_exists(f"/{REL_DIR_FILE_PATH}"), True)
+
+    def test_path_not_exists(self):
+        with self.build_dir() as dir:
+            self.assertEqual(dir.path_exists("NON_EXISTING_FILE"), False)
+
+    def test_get_contents(self):
+        with self.build_dir() as dir:
+            self.assertEqual(dir.get_contents(), CONTENTS)
+
+    def test_get_contents_on_show_abs_path(self):
+        with self.build_dir() as dir:
+            self.assertEqual(
+                dir.get_contents(show_abs_path=True),
+                self.get_abs_contents(recursively=False))
+        
+    def test_get_contents_on_recursively(self):
+        with self.build_dir() as dir:
+            self.assertEqual(
+                dir.get_contents(recursively=True),
+                RECURSIVE_CONTENTS)
+        
+    def test_get_contents_on_show_abs_path_and_recursively(self):
+        with self.build_dir() as dir:
+            self.assertEqual(
+                dir.get_contents(show_abs_path=True, recursively=True),
+                self.get_abs_contents(recursively=True))
+            
+    def test_traverse(self):
+        with self.build_dir() as dir:
+            self.assertEqual(list(dir.traverse()), CONTENTS)
+
+    def test_traverse_on_show_abs_path(self):
+        with self.build_dir() as dir:
+            self.assertEqual(
+                list(dir.traverse(show_abs_path=True)),
+                self.get_abs_contents(recursively=False))
+        
+    def test_traverse_on_recursively(self):
+        with self.build_dir() as dir:
+            self.assertEqual(
+                list(dir.traverse(recursively=True)),
+                RECURSIVE_CONTENTS)
+        
+    def test_traverse_on_show_abs_path_and_recursively(self):
+        with self.build_dir() as dir:
+            self.assertEqual(
+                list(dir.traverse(show_abs_path=True, recursively=True)),
+                self.get_abs_contents(recursively=True))
+            
+    def test_ls(self):
+        with (
+            io.StringIO() as stdo,
+            self.build_dir() as dir
+        ):
+            sys.stdout = stdo
+
+            dir.ls()
+
+            sys.stdout = sys.__stdout__
+
+            ls_expected_output = '\n' + '\n'.join(CONTENTS) + '\n'
+            self.assertEqual(stdo.getvalue(), ls_expected_output)
+
+
+    def test_ls_on_show_abs_path(self):
+        with (
+            io.StringIO() as stdo,
+            self.build_dir() as dir
+        ):
+            sys.stdout = stdo
+
+            dir.ls(show_abs_path=True)
+
+            sys.stdout = sys.__stdout__
+
+            ls_expected_output = '\n' + '\n'.join(self.get_abs_contents(recursively=False)) + '\n'
+            self.assertEqual(stdo.getvalue(), ls_expected_output)
+
+    def test_ls_on_recursively(self):
+        with (
+            io.StringIO() as stdo,
+            self.build_dir() as dir
+        ):
+            sys.stdout = stdo
+
+            dir.ls(recursively=True)
+
+            sys.stdout = sys.__stdout__
+
+            ls_expected_output = '\n' + DIR_NAME + SEPARATOR + LS_RECURSIVE_OUTPUT
+            self.assertEqual(stdo.getvalue(), ls_expected_output)
+
+    def test_ls_on_show_abs_path_and_recursively(self):
+        with (
+            io.StringIO() as stdo,
+            self.build_dir() as dir
+        ):
+            sys.stdout = stdo
+
+            dir.ls(show_abs_path=True, recursively=True)
+            
+            sys.stdout = sys.__stdout__
+
+            ls_expected_output = '\n/' + REL_DIR_PATH + LS_RECURSIVE_OUTPUT
+            self.assertEqual(stdo.getvalue(), ls_expected_output)
+
+    def test_count(self):
+        with self.build_dir() as dir:
+            self.assertEqual(dir.count(), 2)
+
+    def test_count_on_recursively(self):
+        with self.build_dir() as dir:
+            self.assertEqual(dir.count(recursively=True), 3)
+
+    def test_get_size(self):
+        with self.build_dir() as dir:
+            self.assertEqual(dir.get_size(), 4)
+
+    def test_get_size_on_recursively(self):
+        with self.build_dir() as dir:
+            self.assertEqual(dir.get_size(recursively=True), 12)
+
+    @create_tmp_dir
+    def test_transfer_to(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
+        with self.build_dir() as dir:
+            dir.transfer_to(dst=LocalDir(path=tmp_dir_path))
+        # Assert that the two directories contains the same contents.
+        original = [s for s in sorted(os.listdir(ABS_DIR_PATH)) if s.endswith('.txt')]
+        copies = [s for s in sorted(os.listdir(tmp_dir_path))]
+        # 1. Assert number of copied files are the same.
+        self.assertEqual(len(original), len(copies))
+        # 2. Iterate over all files.
+        for ofp, cfp in zip(original, copies):
+            # Assert their contents are the same.
+            with (
+                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
+                open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
+            ):
+                self.assertEqual(of.read(), cp.read())
+
+    @create_tmp_dir
+    def test_transfer_to_on_recursively(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
+        with self.build_dir() as dir:
+            dir.transfer_to(
+                dst=LocalDir(path=tmp_dir_path),
+                recursively=True)
+        # Assert that the two directories contains the same contents.
+        original = [join_paths(dp, f) for dp, _, fn in 
+                    os.walk(ABS_DIR_PATH) for f in fn]
+        copies = [join_paths(dp, f) for dp, _, fn in 
+                os.walk(tmp_dir_path) for f in fn]
+        # 1. Assert number of copied files are the same.
+        self.assertEqual(len(original), len(copies))
+        # 2. Iterate over all files.
+        for ofp, cfp in zip(original, copies):
+            # Assert their contents are the same.
+            with (
+                open(file=ofp, mode='rb') as of,
+                open(file=cfp, mode='rb') as cp
+            ):
+                self.assertEqual(of.read(), cp.read())
+
+    @create_tmp_dir
+    def test_transfer_to_on_chunk_size(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
+        with self.build_dir() as dir:
+            dir.transfer_to(
+                dst=TestLocalDir.build_dir(path=tmp_dir_path),
+                chunk_size=1)
+        # Assert that the two directories contains the same contents.
+        original = [s for s in sorted(os.listdir(ABS_DIR_PATH)) if s.endswith('.txt')]
+        copies = [s for s in sorted(os.listdir(tmp_dir_path))]
+        # 1. Assert number of copied files are the same.
+        self.assertEqual(len(original), len(copies))
+        # 2. Iterate over all files.
+        for ofp, cfp in zip(original, copies):
+            # Assert their contents are the same.
+            with (
+                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
+                open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
+            ):
+                self.assertEqual(of.read(), cp.read())
+
+    @create_tmp_dir
+    def test_transfer_to_on_filter(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
+        with self.build_dir() as dir:
+            dir.transfer_to(
+                dst=LocalDir(path=tmp_dir_path),
+                filter=lambda x: not x.endswith('.txt'))
+        # Assert that the two directories contain the same files.
+        original = [s for s in sorted(os.listdir(ABS_DIR_PATH))
+                    if os.path.isfile(s) and not s.endswith('.txt')]
+        copies = [s for s in sorted(os.listdir(tmp_dir_path))]
+        # 1. Assert number of copied files are the same.
+        self.assertEqual(len(original), len(copies))
+        # 2. Iterate over all files.
+        for ofp, cfp in zip(original, copies):
+            # Assert their contents are the same.
+            with (
+                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
+                open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
+            ):
+                self.assertEqual(of.read(), cp.read())
+
+    @clone_tmp_dir
+    def test_transfer_to_on_overwrite_set_to_false(self, tmp_dir_path):
+        # While capturing stdout...
+        with (
+            io.StringIO() as stdo,
+            self.build_dir() as dir
+        ):
+            sys.stdout = stdo
+            # Copy directory with "overwrite" set to "False".
+            dir.transfer_to(
+                dst=LocalDir(path=tmp_dir_path),
+                overwrite=False)
+
+            sys.stdout = sys.__stdout__
+
+            self.assertTrue("Operation unsuccessful" in stdo.getvalue())
+
+    @clone_tmp_dir
+    def test_transfer_to_on_overwrite_set_to_true(self, tmp_dir_path):
+        # While capturing stdout...
+        with (
+            io.StringIO() as stdo,
+            self.build_dir() as dir
+        ):
+            sys.stdout = stdo
+            # Copy directory with "overwrite" set to "True".
+            dir.transfer_to(
+                dst=LocalDir(path=tmp_dir_path),
+                overwrite=True)
+
+            sys.stdout = sys.__stdout__
+
+            self.assertTrue("Operation successful" in stdo.getvalue())
+
+    @create_tmp_dir
+    def test_transfer_to_on_include_metadata_set_to_false(self, tmp_dir_path):
+        # Set metadata for a directory's file.
+        filename, metadata = f'/{REL_DIR_FILE_PATH}', {'1': '1'}
+        with self.build_dir() as dir:
+            dir.set_metadata(file_path=filename, metadata=metadata)
+            # Create a temporary dictionary.
+            tmp_dir = LocalDir(path=tmp_dir_path)
+            # Copy the directory's contents into this
+            # tmp directory without including metadata.
+            dir.transfer_to(dst=tmp_dir, include_metadata=False)
+        # Assert that no metadata have been transfered.
+        self.assertEqual(tmp_dir.get_metadata(
+            file_path=filename.replace(f'/{REL_DIR_PATH}', '')), {})
+
+    @create_tmp_dir
+    def test_transfer_to_on_include_metadata_set_to_true(self, tmp_dir_path):
+        # Set metadata for a directory's file.
+        filename, metadata = f'/{REL_DIR_FILE_PATH}', {'1': '1'}
+        with self.build_dir() as dir:
+            dir.set_metadata(file_path=filename, metadata=metadata)
+            # Create a temporary dictionary.
+            tmp_dir = LocalDir(path=tmp_dir_path)
+            # Copy the directory's contents into this
+            # tmp directory without including metadata.
+            dir.transfer_to(dst=tmp_dir, include_metadata=True)
+        # Assert the file's metadata are the same.
+        self.assertEqual(
+            tmp_dir.get_metadata(
+                file_path=filename.replace(f'/{REL_DIR_PATH}', '')),
+            metadata)
+                
+    @create_tmp_dir
+    def test_transfer_to_as_dst(self, tmp_dir_path):
+        # Get source file.
+        src_file = TestLocalFile.build_file()
+        # Create a temporary "remote" dictionary.
+        with self.build_dir(
+            path=tmp_dir_path.replace(f"{ABS_DIR_PATH.rstrip('dir/')}/", f"/{REL_DIR_PATH.rstrip('dir/')}/")
+        ) as remote_dir:
+            # Copy file into dir.
+            src_file.transfer_to(dst=remote_dir)
+            # Confirm that file was indeed copied.
+            copy_path = join_paths(tmp_dir_path, FILE_NAME)
+            with (
+                open(ABS_FILE_PATH, mode='rb') as file,
+                open(copy_path, mode='rb') as copy
+            ):
+                self.assertEqual(file.read(), copy.read())
+
+    @create_tmp_dir
+    def test_transfer_to_as_dst_on_chunk_size(self, tmp_dir_path):
+        # Get source file.
+        src_file = TestLocalFile.build_file()
+        # Create a temporary "remote" dictionary.
+        with self.build_dir(
+            path=tmp_dir_path.replace(f"{ABS_DIR_PATH.rstrip('dir/')}/", f"/{REL_DIR_PATH.rstrip('dir/')}/")
+        ) as remote_dir:
+            # Copy file into dir.
+            src_file.transfer_to(dst=remote_dir, chunk_size=1)
+            # Confirm that file was indeed copied.
+            copy_path = join_paths(tmp_dir_path, FILE_NAME)
+            with (
+                open(ABS_FILE_PATH, mode='rb') as file,
+                open(copy_path, mode='rb') as copy
+            ):
+                self.assertEqual(file.read(), copy.read())
         
     def test_get_file(self):
+        with self.build_dir() as dir:
+            file = dir.get_file(DIR_FILE_NAME)
+            self.assertEqual(file.get_path(), f'/{REL_DIR_FILE_PATH}')
+
+    def test_get_file_on_invalid_path_error(self):
+        with self.build_dir() as dir:
+            self.assertRaises(InvalidPathError, dir.get_file, "NON_EXISTING_PATH")
+
+    def test_get_file_on_invalid_file_error(self):
+        with self.build_dir() as dir:
+            self.assertRaises(InvalidFileError, dir.get_file, DIR_SUBDIR_NAME)
+
+    def test_get_subdir(self):
+        with self.build_dir() as dir:
+            subdir = dir.get_subdir(DIR_SUBDIR_NAME)
+            self.assertEqual(subdir.get_path(), f'/{REL_DIR_SUBDIR_PATH}')
+
+    def test_get_subdir_on_invalid_path_error(self):
+        with self.build_dir() as dir:
+            self.assertRaises(InvalidPathError, dir.get_subdir, "NON_EXISTING_PATH")
+
+    def test_get_subdir_on_invalid_directory_error(self):
+        with self.build_dir() as dir:
+            self.assertRaises(InvalidDirectoryError, dir.get_subdir, DIR_FILE_NAME)
+
+    def test_file_shared_metadata_on_modify_from_dir(self):
+        with self.build_dir() as dir:
+            # Access file via dir.
+            file = dir.get_file(DIR_FILE_NAME)
+            # Change metadata via "Dir" API.
+            dir.set_metadata(DIR_FILE_NAME, METADATA)
+            # Assert file metadata have been changed.
+            self.assertEqual(file.get_metadata(), METADATA)
+
+    def test_file_shared_metadata_on_modify_from_file(self):
+        with self.build_dir() as dir:
+            # Access file via dir.
+            file = dir.get_file(DIR_FILE_NAME)
+            # Change metadata via "File" API.
+            file.set_metadata(METADATA)
+            # Assert file metadata have been changed.
+            self.assertEqual(dir.get_metadata(DIR_FILE_NAME), METADATA)
+
+    def test_subdir_shared_metadata_on_modify_from_dir(self):
+        # Create dir and get file.
+        with self.build_dir() as dir:
+            subdir = dir.get_subdir(DIR_SUBDIR_NAME)
+            # Change metadata via "Dir" API.
+            dir.set_metadata(
+                DIR_SUBDIR_NAME + DIR_SUBDIR_FILE_NAME, METADATA)
+            # Assert file metadata have been changed.
+            self.assertEqual(
+                subdir.get_metadata(DIR_SUBDIR_FILE_NAME), METADATA)
+
+    def test_subdir_shared_metadata_on_modify_from_subdir(self):
+        # Create dir and get file.
+        with self.build_dir() as dir:
+            subdir = dir.get_subdir(DIR_SUBDIR_NAME)
+            # Change metadata via "File" API.
+            subdir.set_metadata(DIR_SUBDIR_FILE_NAME, METADATA)
+            # Assert file metadata have been changed.
+            self.assertEqual(
+                dir.get_metadata(DIR_SUBDIR_NAME + DIR_SUBDIR_FILE_NAME), METADATA)
+        
+    '''
+    Test connection methods.
+    '''
+    def test_open(self):
         dir = self.build_dir()
-        file = dir.get_file(DIR_FILE_NAME)
-        self.assertEqual(file.get_path(), ABS_DIR_FILE_PATH)
+        dir.close()
+        dir.open()
+        self.assertTrue(dir._get_handler().is_open())
+        dir.close()
+
+    def test_close(self):
+        dir = self.build_dir()
+        dir.close()
+        self.assertFalse(dir._get_handler().is_open())
+
+    '''
+    Test cache methods.
+    '''
+    def test_is_cachable_on_false(self):
+        with self.build_dir() as dir:
+            self.assertEqual(dir.is_cacheable(), False)
+
+    def test_is_cachable_on_true(self):
+        with self.build_dir(cache=True) as dir:
+            self.assertEqual(dir.is_cacheable(), True)
+
+    def test_purge(self):
+        with self.build_dir(cache=True) as dir:
+            # Fetch size via HTTP.
+            dir.get_size()
+            # Fetch size from cache and time it.
+            t = time.perf_counter()
+            dir.get_size()
+            cache_time = time.perf_counter() - t
+            # Purge cache.
+            dir.purge()
+            # Re-fetch size via HTTP and time it.
+            t = time.perf_counter()
+            dir.get_size()
+            normal_time = time.perf_counter() - t
+            self.assertGreater(normal_time, cache_time)
+
+    def test_traverse_from_cache_on_value(self):
+        with self.build_dir(cache=True) as dir:
+            _ = (_ for _ in dir.traverse())
+            self.assertEqual(
+                ''.join([p for p in dir.traverse()]),
+                ''.join(CONTENTS))
+            
+    def test_traverse_recursively_from_cache_on_value(self):
+        with self.build_dir(cache=True) as dir:
+            _ = (_ for _ in dir.traverse(recursively=True))
+            expected_results = []
+            for dp, dn, fn in os.walk(REL_DIR_PATH):
+                dn.sort()
+                for f in sorted(fn):
+                    expected_results.append(
+                        join_paths(dp, f).removeprefix(REL_DIR_PATH))
+            self.assertEqual(
+                ''.join([p for p in dir.traverse(recursively=True)]),
+                ''.join(expected_results))
+
+    def test_traverse_from_cache_on_time(self):
+        with self.build_dir(cache=True) as dir:
+            # Fetch contents via HTTP.
+            t = time.perf_counter()
+            _ = (_ for _ in dir.traverse())
+            normal_time = time.perf_counter() - t
+            # Fetch contents from cache.
+            t = time.perf_counter()
+            _ = (_ for _ in dir.traverse())
+            cache_time = time.perf_counter() - t
+            # Compare fetch times.
+            self.assertGreater(normal_time, cache_time)
+
+    def test_traverse_recursively_from_cache_on_time(self):
+        with self.build_dir(cache=True) as dir:
+            # Fetch contents via HTTP.
+            t = time.perf_counter()
+            _ = (_ for _ in dir.traverse())
+            normal_time = time.perf_counter() - t
+            # Fetch contents from cache.
+            t = time.perf_counter()
+            _ = (_ for _ in dir.traverse())
+            cache_time = time.perf_counter() - t
+            # Compare fetch times.
+            self.assertGreater(normal_time, cache_time)
+
+    def test_get_size_from_cache_on_value(self):
+        with self.build_dir(cache=True) as dir:
+            _ = dir.get_size()
+            self.assertEqual(dir.get_size(), 4)
+
+    def test_get_size_recursively_from_cache_on_value(self):
+        with self.build_dir(cache=True) as dir:
+            _ = dir.get_size(recursively=True)
+            self.assertEqual(
+                dir.get_size(recursively=True), 12)
+
+    def test_get_size_from_cache_on_time(self):
+        with self.build_dir(cache=True) as dir:
+            # Fetch object size via HTTP.
+            t = time.perf_counter()
+            _ = dir.get_size()
+            normal_time = time.perf_counter() - t
+            # Fetch object size from cache.
+            t = time.perf_counter()
+            _ = dir.get_size()
+            cache_time = time.perf_counter() - t
+            # Compare fetch times.
+            self.assertGreater(normal_time, cache_time)
+
+    def test_get_size_recursively_from_cache_on_time(self):
+        with self.build_dir(cache=True) as dir:
+            # Fetch object size via HTTP.
+            t = time.perf_counter()
+            _ = dir.get_size(recursively=True)
+            normal_time = time.perf_counter() - t
+            # Fetch object size from cache.
+            t = time.perf_counter()
+            _ = dir.get_size(recursively=True)
+            cache_time = time.perf_counter() - t
+            # Compare fetch times.
+            self.assertGreater(normal_time, cache_time)
+
+    def test_file_shared_cache_on_cache_via_dir(self):
+        with (
+            self.build_dir(cache=False) as no_cache_dir,
+            self.build_dir(cache=True) as cache_dir
+        ):
+            # Access file via both dirs.
+            no_cache_file = no_cache_dir.get_file(DIR_FILE_NAME)
+            cache_file = cache_dir.get_file(DIR_FILE_NAME)
+            # Count total size for both dirs.
+            _ = no_cache_dir.get_size()
+            _ = cache_dir.get_size()
+            # Time no-cache-file's "get_size"
+            t = time.perf_counter()
+            _ = no_cache_file.get_size()
+            normal_time = time.perf_counter() - t
+            # Time cache-file's "get_size"
+            t = time.perf_counter()
+            _ = cache_file.get_size()
+            cache_time = time.perf_counter() - t
+            # Compare fetch times.
+            self.assertGreater(normal_time, cache_time)
+
+    def test_file_shared_cache_on_cache_via_file(self):
+        with (
+            self.build_dir(cache=False) as no_cache_dir,
+            self.build_dir(cache=True) as cache_dir
+        ):
+            # Count size of files via both dirs using the "File" API.
+            for path in no_cache_dir.traverse():
+                try:
+                    _ = no_cache_dir.get_file(path).get_size()
+                    _ = cache_dir.get_file(path).get_size()
+                except Exception:
+                    continue
+            # Time no-cache-dir's "get_size"
+            t = time.perf_counter()
+            _ = no_cache_dir.get_size()
+            normal_time = time.perf_counter() - t
+            # Time cache-dir's "get_size"
+            t = time.perf_counter()
+            _ = cache_dir.get_size()
+            cache_time = time.perf_counter() - t
+            # Compare fetch times.
+            self.assertGreater(normal_time, cache_time)
+
+    def test_subdir_shared_cache_on_cache_via_dir(self):
+        with (
+            self.build_dir(cache=False) as no_cache_dir,
+            self.build_dir(cache=True) as cache_dir
+        ):
+            # Access subdir via both dirs.
+            no_cache_subdir = no_cache_dir.get_subdir(DIR_SUBDIR_NAME)
+            cache_subdir = cache_dir.get_subdir(DIR_SUBDIR_NAME)
+            # Count total size for both dirs.
+            _ = no_cache_dir.get_size(recursively=True)
+            _ = cache_dir.get_size(recursively=True)
+            # Time no-cache-subdir's "get_size"
+            t = time.perf_counter()
+            _ = no_cache_subdir.get_size()
+            normal_time = time.perf_counter() - t
+            # Time cache-subdir's "get_size"
+            t = time.perf_counter()
+            _ = cache_subdir.get_size()
+            cache_time = time.perf_counter() - t
+            # Compare fetch times.
+            self.assertGreater(normal_time, cache_time)
+
+    def test_subdir_shared_cache_on_cache_via_subdir(self):
+        with (
+            self.build_dir(cache=False) as no_cache_dir,
+            self.build_dir(cache=True) as cache_dir
+        ):
+            # Count size of both subdirs.
+            _ = no_cache_dir.get_subdir(DIR_SUBDIR_NAME).get_size()
+            _ = cache_dir.get_subdir(DIR_SUBDIR_NAME).get_size()
+            # Time no-cache-dir's "get_size"
+            t = time.perf_counter()
+            _ = no_cache_dir.get_size(recursively=True)
+            normal_time = time.perf_counter() - t
+            # Time cache-dir's "get_size"
+            t = time.perf_counter()
+            _ = cache_dir.get_size(recursively=True)
+            cache_time = time.perf_counter() - t
+            # Compare fetch times.
+            self.assertGreater(normal_time, cache_time)
 
-    def test_get_file_on_invalid_path_error(self):
-        dir = self.build_dir()
-        self.assertRaises(InvalidPathError, dir.get_file, "NON_EXISTING_PATH")
 
-    def test_get_file_on_invalid_file_error(self):
-        dir = self.build_dir()
-        self.assertRaises(InvalidFileError, dir.get_file, DIR_SUBDIR_NAME)
+class TestAmazonS3Dir(unittest.TestCase):
 
-    def test_get_subdir(self):
-        dir = self.build_dir()
-        subdir = dir.get_subdir(DIR_SUBDIR_NAME)
-        self.assertEqual(subdir.get_path(), ABS_DIR_SUBDIR_PATH)
+    MOCK_S3 = mock_s3()
 
-    def test_get_subdir_on_invalid_path_error(self):
-        dir = self.build_dir()
-        self.assertRaises(InvalidPathError, dir.get_subdir, "NON_EXISTING_PATH")
+    def create_tmp_s3_dir(func: Callable):
+        '''
+        A decorator function used for creating \
+        and deleting temporary directories.
+        '''
+        def wrapper(*args, **kwargs):
+            tmp_dir_name = get_tmp_dir_name()
+            tmp_dir_path = REL_DIR_PATH.replace(DIR_NAME, tmp_dir_name)
+            kwargs.update({'tmp_dir_path': tmp_dir_path})
+            # Finally, create a TMP dir.
+            with io.BytesIO() as empty_buffer:
+                bucket = boto3.resource("s3").Bucket(BUCKET)
+                bucket.upload_fileobj(
+                    Key=join_paths(TEST_FILES_DIR, tmp_dir_name, 'DUMMY'),
+                    Fileobj=empty_buffer)
+            func(*args, **kwargs)
+        return wrapper
 
-    def test_get_subdir_on_invalid_directory_error(self):
-        dir = self.build_dir()
-        self.assertRaises(InvalidDirectoryError, dir.get_subdir, DIR_FILE_NAME)
+    def get_abs_contents(self, recursively: bool):
+        return [join_paths(REL_DIR_PATH, p) for p in (
+            RECURSIVE_CONTENTS if recursively else CONTENTS
+        )]
+    
+    def iterate_aws_s3_dir_objects(
+        self,
+        path: str = REL_DIR_PATH,
+        recursively: bool = False,
+        show_abs_path: bool = False
+    ):
+        delimiter = '' if recursively else SEPARATOR
+        for obj in boto3.resource('s3').Bucket(BUCKET).objects.filter(
+            Prefix=path,
+            Delimiter=delimiter
+        ):
+            yield obj.key if show_abs_path else obj.key.removeprefix(path)
 
-    def test_file_shared_metadata_on_modify_from_dir(self):
-        # Create dir and get file.
-        dir = self.build_dir()
-        file = dir.get_file(DIR_FILE_NAME)
-        # Change metadata via "Dir" API.
-        dir.set_metadata(DIR_FILE_NAME, METADATA)
-        # Assert file metadata have been changed.
-        self.assertEqual(file.get_metadata(), METADATA)
+    @classmethod
+    def setUpClass(cls):
+        super(TestAmazonS3Dir, cls).setUpClass()
 
-    def test_file_shared_metadata_on_modify_from_file(self):
-        # Create dir and get file.
-        dir = self.build_dir()
-        file = dir.get_file(DIR_FILE_NAME)
-        # Change metadata via "File" API.
-        file.set_metadata(METADATA)
-        # Assert file metadata have been changed.
-        self.assertEqual(dir.get_metadata(DIR_FILE_NAME), METADATA)
+        create_aws_s3_bucket(cls.MOCK_S3, BUCKET, METADATA)
 
-    def test_subdir_shared_metadata_on_modify_from_dir(self):
-        # Create dir and get file.
-        dir = self.build_dir()
-        subdir = dir.get_subdir(DIR_SUBDIR_NAME)
-        # Change metadata via "Dir" API.
-        dir.set_metadata(
-            DIR_SUBDIR_NAME + DIR_SUBDIR_FILE_NAME, METADATA)
-        # Assert file metadata have been changed.
-        self.assertEqual(
-            subdir.get_metadata(DIR_SUBDIR_FILE_NAME), METADATA)
+        from fluke._handlers import AWSClientHandler
 
-    def test_subdir_shared_metadata_on_modify_from_subdir(self):
-        # Create dir and get file.
-        dir = self.build_dir()
-        subdir = dir.get_subdir(DIR_SUBDIR_NAME)
-        # Change metadata via "File" API.
-        subdir.set_metadata(DIR_SUBDIR_FILE_NAME, METADATA)
-        # Assert file metadata have been changed.
-        self.assertEqual(
-            dir.get_metadata(DIR_SUBDIR_NAME + DIR_SUBDIR_FILE_NAME), METADATA)
-        
+        m1 = patch.object(AWSClientHandler, '_get_file_size_impl', autospec=True)
+        m2 = patch.object(AWSClientHandler, '_get_file_metadata_impl', autospec=True)
+        m3 = patch.object(AWSClientHandler, '_traverse_dir_impl', autospec=True)
 
-class TestRemoteDir(unittest.TestCase):
+        def simulate_latency_1(*args, **kwargs):
+            time.sleep(0.2)
+            return m1.temp_original(*args, **kwargs)
+        
+        def simulate_latency_2(*args, **kwargs):
+            time.sleep(0.2)
+            return m2.temp_original(*args, **kwargs)
+        
+        def simulate_latency_3(*args, **kwargs):
+            time.sleep(0.2)
+            return m3.temp_original(*args, **kwargs)
 
-    def setUp(self):
-        for k, v in MockSFTPClient.get_mock_methods().items():
-            patch(k, v).start()
+        m1.start().side_effect = simulate_latency_1
+        m2.start().side_effect = simulate_latency_2
+        m3.start().side_effect = simulate_latency_3
 
-    def tearDown(self):
+    @classmethod
+    def tearDownClass(cls):
+        super(TestAmazonS3Dir, cls).tearDownClass()
+        cls.MOCK_S3.stop()
         patch.stopall()
     
     @staticmethod
     def build_dir(
-        path: str = ABS_DIR_PATH,
+        path: str = REL_DIR_PATH,
+        bucket: str = BUCKET,
         cache: bool = False,
         create_if_missing: bool = False
-    ) -> RemoteDir:
-        return RemoteDir(**{
-            'auth': get_remote_auth_instance(hostname=HOST),
+    ) -> AmazonS3Dir:
+        return AmazonS3Dir(**{
+            'auth': get_aws_auth_instance(),
+            'bucket': bucket,
             'path': path,
             'cache': cache,
             'create_if_missing': create_if_missing
@@ -2073,54 +3339,69 @@ def build_dir(
     
     def test_constructor(self):
         with self.build_dir() as dir:
-            self.assertEqual(dir.get_path(), ABS_DIR_PATH)
+            self.assertEqual(dir.get_path(), REL_DIR_PATH)
 
     def test_constructor_on_create_if_missing(self):
-        dir_path = to_abs(f"{TEST_FILES_DIR}{SEPARATOR}NON_EXISTING_DIR/")
+        dir_path = "NON_EXISTING_DIR/"
         with self.build_dir(path=dir_path, create_if_missing=True) as _:
-            self.assertTrue(os.path.isdir(dir_path))
-        os.rmdir(dir_path)
-
-    def test_constructor_on_empty_path_error(self):
-        self.assertRaises(InvalidPathError, self.build_dir, path="")
+            obj = boto3.resource("s3").Bucket(BUCKET).Object(dir_path)
+            try:
+                obj.load()
+                obj.delete()
+            except Exception:
+                self.fail(f"Directory {dir_path} was not created!")
 
     def test_constructor_on_invalid_path_error(self):
         self.assertRaises(InvalidPathError, self.build_dir, path="NON_EXISTING_PATH")
 
-    def test_constructor_on_invalid_directory_error(self):
-        self.assertRaises(InvalidDirectoryError, self.build_dir, path=ABS_DIR_FILE_PATH)
+    def test_constructor_on_bucket_not_found_error(self):
+        self.assertRaises(BucketNotFoundError, self.build_dir, bucket='UNKNOWN')
 
-    def test_get_hostname(self):
-        with self.build_dir() as dir:
-            self.assertEqual(dir.get_hostname(), HOST)
+    def test_get_path_on_none_path(self):
+        with self.build_dir(path=None) as dir:
+            self.assertEqual(dir.get_path(), '')  
 
     def test_get_name(self):
         with self.build_dir() as dir:
-            self.assertEqual(dir.get_name(), DIR_NAME)    
+            self.assertEqual(dir.get_name(), DIR_NAME)
+
+    def test_get_name_on_none_path(self):
+        with self.build_dir(path=None) as dir:
+            self.assertIsNone(dir.get_name())      
 
     def test_get_uri(self):
         with self.build_dir() as dir:
-            self.assertEqual(dir.get_uri(), f"sftp://{HOST}/{ABS_DIR_PATH.removeprefix(os.sep)}")
-            
-    def test_set_and_get_metadata(self):
+            self.assertEqual(dir.get_uri(), f"s3://{BUCKET}/{REL_DIR_PATH}")
+
+    def test_get_bucket_name(self):
         with self.build_dir() as dir:
-            file_path = ABS_DIR_FILE_PATH
-            metadata = {'a': '1'}
-            dir.set_metadata(file_path=file_path, metadata=metadata)
-            self.assertEqual(dir.get_metadata(file_path=file_path), metadata)
+            self.assertEqual(dir.get_bucket_name(), BUCKET)
 
     def test_get_metadata(self):
         with self.build_dir() as dir:
-            self.assertEqual(dir.get_metadata(file_path=ABS_DIR_FILE_PATH), {})
+            self.assertEqual(dir.get_metadata(file_path='file2.txt'), {})
 
     def test_get_metadata_on_invalid_file_error(self):
         with self.build_dir() as dir:
             self.assertRaises(InvalidFileError, dir.get_metadata, file_path="NON_EXISTING_PATH")
 
+    def test_set_and_get_metadata_on_relative_path(self):
+        with self.build_dir() as dir:
+            file_path, metadata = 'file2.txt', {'a': '1'}
+            dir.set_metadata(file_path=file_path, metadata=metadata)
+            self.assertEqual(dir.get_metadata(file_path=file_path), metadata)
+
+    def test_set_and_get_metadata_on_absolute_path(self):
+        with self.build_dir() as dir:
+            file_path = join_paths(dir.get_path(), 'file2.txt')
+            metadata = {'a': '1'}
+            dir.set_metadata(file_path=file_path, metadata=metadata)
+            self.assertEqual(dir.get_metadata(file_path=file_path), metadata)
+
     def test_set_metadata_on_non_string_metadata_key_error(self):
         with self.build_dir() as dir:
             args = {
-                'file_path': ABS_DIR_FILE_PATH,
+                'file_path': 'file2.txt',
                 'metadata': {1: '1'}
             }
             self.assertRaises(NonStringMetadataKeyError, dir.set_metadata, **args)
@@ -2128,7 +3409,7 @@ def test_set_metadata_on_non_string_metadata_key_error(self):
     def test_set_metadata_on_non_string_metadata_value_error(self):
         with self.build_dir() as dir:
             args = {
-                'file_path': ABS_DIR_FILE_PATH,
+                'file_path': 'file2.txt',
                 'metadata': {'1': 1}
             }
             self.assertRaises(NonStringMetadataValueError, dir.set_metadata, **args)
@@ -2141,13 +3422,43 @@ def test_set_metadata_on_invalid_file_error(self):
             }
             self.assertRaises(InvalidFileError, dir.set_metadata, **args)
 
-    def test_path_exists(self):
+    def test_set_metadata_after_load_metadata(self):
         with self.build_dir() as dir:
-            self.assertEqual(dir.path_exists(ABS_DIR_FILE_PATH), True)
+            dir.load_metadata()
+            new_metadata = {'a': 'a'}
+            dir.set_metadata(REL_DIR_FILE_PATH, new_metadata)
+            self.assertEqual(dir.get_metadata(REL_DIR_FILE_PATH), new_metadata)
 
-    def test_path_not_exists(self):
+    def test_load_metadata(self):
         with self.build_dir() as dir:
-            self.assertEqual(dir.path_exists("NON_EXISTING_FILE"), False)
+            dir.load_metadata()
+            self.assertEqual(dir.get_metadata(REL_DIR_FILE_PATH), METADATA)
+
+    def test_load_metadata_after_set_metadata(self):
+        with self.build_dir() as dir:
+            dir.set_metadata(REL_DIR_FILE_PATH, {'a': 'a'})
+            dir.load_metadata()
+            self.assertEqual(dir.get_metadata(REL_DIR_FILE_PATH), METADATA)
+
+    def test_path_exists_on_abs_path(self):
+        with self.build_dir() as dir:
+            file_path = join_paths(REL_DIR_PATH, 'file2.txt')
+            self.assertEqual(dir.path_exists(file_path), True)
+
+    def test_path_exists_on_relative_path(self):
+        with self.build_dir() as dir:
+            file_path = 'file2.txt'
+            self.assertEqual(dir.path_exists(file_path), True)
+
+    def test_path_not_exists_on_abs_path(self):
+        with self.build_dir() as dir:
+            file_path = join_paths(REL_DIR_PATH, 'NON_EXISTING_FILE')
+            self.assertEqual(dir.path_exists(file_path), False)
+
+    def test_path_not_exists_on_relative_path(self):
+        with self.build_dir() as dir:
+            file_path = 'NON_EXISTING_FILE'
+            self.assertEqual(dir.path_exists(file_path), False)
 
     def test_get_contents(self):
         with self.build_dir() as dir:
@@ -2157,7 +3468,7 @@ def test_get_contents_on_show_abs_path(self):
         with self.build_dir() as dir:
             self.assertEqual(
                 dir.get_contents(show_abs_path=True),
-                get_abs_contents(recursively=False))
+                self.get_abs_contents(recursively=False))
         
     def test_get_contents_on_recursively(self):
         with self.build_dir() as dir:
@@ -2169,7 +3480,7 @@ def test_get_contents_on_show_abs_path_and_recursively(self):
         with self.build_dir() as dir:
             self.assertEqual(
                 dir.get_contents(show_abs_path=True, recursively=True),
-                get_abs_contents(recursively=True))
+                self.get_abs_contents(recursively=True))
             
     def test_traverse(self):
         with self.build_dir() as dir:
@@ -2179,7 +3490,7 @@ def test_traverse_on_show_abs_path(self):
         with self.build_dir() as dir:
             self.assertEqual(
                 list(dir.traverse(show_abs_path=True)),
-                get_abs_contents(recursively=False))
+                self.get_abs_contents(recursively=False))
         
     def test_traverse_on_recursively(self):
         with self.build_dir() as dir:
@@ -2191,7 +3502,7 @@ def test_traverse_on_show_abs_path_and_recursively(self):
         with self.build_dir() as dir:
             self.assertEqual(
                 list(dir.traverse(show_abs_path=True, recursively=True)),
-                get_abs_contents(recursively=True))
+                self.get_abs_contents(recursively=True))
             
     def test_ls(self):
         with (
@@ -2204,7 +3515,7 @@ def test_ls(self):
 
             sys.stdout = sys.__stdout__
 
-            ls_expected_output = '\n'.join(CONTENTS) + '\n'
+            ls_expected_output = '\n' + '\n'.join(CONTENTS) + '\n'
             self.assertEqual(stdo.getvalue(), ls_expected_output)
 
 
@@ -2219,7 +3530,8 @@ def test_ls_on_show_abs_path(self):
 
             sys.stdout = sys.__stdout__
 
-            ls_expected_output = '\n'.join(get_abs_contents(recursively=False)) + '\n'
+            ls_expected_output = '\n' + '\n'.join(
+                self.get_abs_contents(recursively=False)) + '\n'
             self.assertEqual(stdo.getvalue(), ls_expected_output)
 
     def test_ls_on_recursively(self):
@@ -2233,7 +3545,7 @@ def test_ls_on_recursively(self):
 
             sys.stdout = sys.__stdout__
 
-            ls_expected_output = '\n'.join(RECURSIVE_CONTENTS) + '\n'
+            ls_expected_output = '\n' + DIR_NAME + SEPARATOR + LS_RECURSIVE_OUTPUT
             self.assertEqual(stdo.getvalue(), ls_expected_output)
 
     def test_ls_on_show_abs_path_and_recursively(self):
@@ -2247,7 +3559,7 @@ def test_ls_on_show_abs_path_and_recursively(self):
             
             sys.stdout = sys.__stdout__
 
-            ls_expected_output = '\n'.join(get_abs_contents(recursively=True)) + '\n'
+            ls_expected_output = '\n' + REL_DIR_PATH + LS_RECURSIVE_OUTPUT
             self.assertEqual(stdo.getvalue(), ls_expected_output)
 
     def test_count(self):
@@ -2264,17 +3576,15 @@ def test_get_size(self):
 
     def test_get_size_on_recursively(self):
         with self.build_dir() as dir:
-            self.assertEqual(dir.get_size(recursively=True), 16)
+            self.assertEqual(dir.get_size(recursively=True), 12)
 
-    def test_transfer_to(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = ABS_DIR_PATH.replace(DIR_NAME, TMP_DIR_NAME)
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
+    @create_tmp_dir
+    def test_transfer_to(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
         with self.build_dir() as dir:
-            dir.transfer_to(dst=LocalDir(path=tmp_dir_path))
+            dir.transfer_to(dst=TestLocalDir.build_dir(path=tmp_dir_path))
         # Assert that the two directories contains the same contents.
-        original = [s for s in sorted(os.listdir(ABS_DIR_PATH)) if s.endswith('.txt')]
+        original = [s for s in sorted(self.iterate_aws_s3_dir_objects(show_abs_path=True))]
         copies = [s for s in sorted(os.listdir(tmp_dir_path))]
         # 1. Assert number of copied files are the same.
         self.assertEqual(len(original), len(copies))
@@ -2282,25 +3592,22 @@ def test_transfer_to(self):
         for ofp, cfp in zip(original, copies):
             # Assert their contents are the same.
             with (
-                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
+                io.BytesIO() as buffer,
                 open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
             ):
-                self.assertEqual(of.read(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+                get_aws_s3_object(BUCKET, ofp).download_fileobj(buffer)
+                self.assertEqual(buffer.getvalue(), cp.read())
 
-    def test_transfer_to_on_recursively(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = ABS_DIR_PATH.replace(DIR_NAME, TMP_DIR_NAME)
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
+    @create_tmp_dir
+    def test_transfer_to_on_recursively(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
         with self.build_dir() as dir:
             dir.transfer_to(
-                dst=LocalDir(path=tmp_dir_path),
+                dst=TestLocalDir.build_dir(path=tmp_dir_path),
                 recursively=True)
         # Assert that the two directories contains the same contents.
-        original = [join_paths(dp, f) for dp, _, fn in 
-                    os.walk(ABS_DIR_PATH) for f in fn]
+        original = [s for s in sorted(self.iterate_aws_s3_dir_objects(
+            recursively=True, show_abs_path=True))]
         copies = [join_paths(dp, f) for dp, _, fn in 
                 os.walk(tmp_dir_path) for f in fn]
         # 1. Assert number of copied files are the same.
@@ -2309,24 +3616,21 @@ def test_transfer_to_on_recursively(self):
         for ofp, cfp in zip(original, copies):
             # Assert their contents are the same.
             with (
-                open(file=ofp, mode='rb') as of,
-                open(file=cfp, mode='rb') as cp
+                io.BytesIO() as buffer,
+                open(cfp, mode='rb') as cp
             ):
-                self.assertEqual(of.read(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+                get_aws_s3_object(BUCKET, ofp).download_fileobj(buffer)
+                self.assertEqual(buffer.getvalue(), cp.read())
 
-    def test_transfer_to_on_chunk_size(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = ABS_DIR_PATH.replace(DIR_NAME, TMP_DIR_NAME)
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
+    @create_tmp_dir
+    def test_transfer_to_on_chunk_size(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
         with self.build_dir() as dir:
             dir.transfer_to(
-                dst=LocalDir(path=tmp_dir_path),
+                dst=TestLocalDir.build_dir(path=tmp_dir_path),
                 chunk_size=1)
         # Assert that the two directories contains the same contents.
-        original = [s for s in sorted(os.listdir(ABS_DIR_PATH)) if s.endswith('.txt')]
+        original = [s for s in sorted(self.iterate_aws_s3_dir_objects(show_abs_path=True))]
         copies = [s for s in sorted(os.listdir(tmp_dir_path))]
         # 1. Assert number of copied files are the same.
         self.assertEqual(len(original), len(copies))
@@ -2334,18 +3638,15 @@ def test_transfer_to_on_chunk_size(self):
         for ofp, cfp in zip(original, copies):
             # Assert their contents are the same.
             with (
-                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
+                io.BytesIO() as buffer,
                 open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
             ):
-                self.assertEqual(of.read(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+                get_aws_s3_object(BUCKET, ofp).download_fileobj(buffer)
+                self.assertEqual(buffer.getvalue(), cp.read())
 
-    def test_transfer_to_on_filter(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace('dir', TMP_DIR_NAME))
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
+    @create_tmp_dir
+    def test_transfer_to_on_filter(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
         with self.build_dir() as dir:
             dir.transfer_to(
                 dst=LocalDir(path=tmp_dir_path),
@@ -2360,18 +3661,14 @@ def test_transfer_to_on_filter(self):
         for ofp, cfp in zip(original, copies):
             # Assert their contents are the same.
             with (
-                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
+                io.BytesIO() as buffer,
                 open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
             ):
-                self.assertEqual(of.read(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+                get_aws_s3_object(BUCKET, ofp).download_fileobj(buffer)
+                self.assertEqual(buffer.getvalue(), cp.read())
 
-    def test_transfer_to_on_overwrite_set_to_false(self):
-        # Create a copy of the directory.
-        tmp_dir_name = TMP_DIR_NAME
-        tmp_dir_path = ABS_DIR_PATH.replace(DIR_NAME, tmp_dir_name)
-        shutil.copytree(src=ABS_DIR_PATH, dst=tmp_dir_path)
+    @clone_tmp_dir
+    def test_transfer_to_on_overwrite_set_to_false(self, tmp_dir_path):
         # While capturing stdout...
         with (
             io.StringIO() as stdo,
@@ -2380,20 +3677,15 @@ def test_transfer_to_on_overwrite_set_to_false(self):
             sys.stdout = stdo
             # Copy directory with "overwrite" set to "False".
             dir.transfer_to(
-                dst=LocalDir(path=tmp_dir_path),
+                dst=TestLocalDir.build_dir(path=tmp_dir_path),
                 overwrite=False)
 
             sys.stdout = sys.__stdout__
 
             self.assertTrue("Operation unsuccessful" in stdo.getvalue())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
 
-    def test_transfer_to_on_overwrite_set_to_true(self):
-        # Create a copy of the directory.
-        tmp_dir_name = TMP_DIR_NAME
-        tmp_dir_path = ABS_DIR_PATH.replace(DIR_NAME, tmp_dir_name)
-        shutil.copytree(src=ABS_DIR_PATH, dst=tmp_dir_path)
+    @clone_tmp_dir
+    def test_transfer_to_on_overwrite_set_to_true(self, tmp_dir_path):
         # While capturing stdout...
         with (
             io.StringIO() as stdo,
@@ -2402,94 +3694,154 @@ def test_transfer_to_on_overwrite_set_to_true(self):
             sys.stdout = stdo
             # Copy directory with "overwrite" set to "True".
             dir.transfer_to(
-                dst=LocalDir(path=tmp_dir_path),
+                dst=TestLocalDir.build_dir(path=tmp_dir_path),
                 overwrite=True)
 
             sys.stdout = sys.__stdout__
 
             self.assertTrue("Operation successful" in stdo.getvalue())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
 
-    def test_transfer_to_on_include_metadata_set_to_false(self):
+    @create_tmp_dir
+    def test_transfer_to_on_include_metadata_set_to_false(self, tmp_dir_path):
         # Set metadata for a directory's file.
-        filename, metadata = ABS_DIR_FILE_PATH, {'1': '1'}
+        filename, metadata = DIR_FILE_NAME, {'1': '1'}
         with self.build_dir() as dir:
             dir.set_metadata(file_path=filename, metadata=metadata)
             # Create a temporary dictionary.
-            tmp_dir_path = ABS_DIR_PATH.replace(DIR_NAME, TMP_DIR_NAME)
-            os.makedirs(tmp_dir_path)
-            tmp_dir = LocalDir(path=tmp_dir_path)
+            tmp_dir = TestLocalDir.build_dir(path=tmp_dir_path)
             # Copy the directory's contents into this
             # tmp directory without including metadata.
             dir.transfer_to(dst=tmp_dir, include_metadata=False)
         # Assert that no metadata have been transfered.
-        self.assertEqual(tmp_dir.get_metadata(
-            file_path=filename.replace(ABS_DIR_PATH, '')), {})
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+        self.assertEqual(tmp_dir.get_metadata(file_path=filename), {})
 
-    def test_transfer_to_on_include_metadata_set_to_true(self):
+    @create_tmp_dir
+    def test_transfer_to_on_include_metadata_with_set_metadata(self, tmp_dir_path):
         # Set metadata for a directory's file.
-        filename, metadata = ABS_DIR_FILE_PATH, {'1': '1'}
+        filename, metadata = DIR_FILE_NAME, {'1': '1'}
         with self.build_dir() as dir:
             dir.set_metadata(file_path=filename, metadata=metadata)
             # Create a temporary dictionary.
-            tmp_dir_path = ABS_DIR_PATH.replace(DIR_NAME, TMP_DIR_NAME)
-            os.mkdir(tmp_dir_path)
-            tmp_dir = LocalDir(path=tmp_dir_path)
+            tmp_dir = TestLocalDir.build_dir(path=tmp_dir_path)
             # Copy the directory's contents into this
             # tmp directory without including metadata.
             dir.transfer_to(dst=tmp_dir, include_metadata=True)
         # Assert the file's metadata are the same.
         self.assertEqual(
-            tmp_dir.get_metadata(
-                file_path=filename.replace(ABS_DIR_PATH, '')),
+            tmp_dir.get_metadata(file_path=filename),
             metadata)
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
-                
-    def test_transfer_to_as_dst(self):
+
+    @create_tmp_dir
+    def test_transfer_to_on_include_metadata_without_set_metadata(self, tmp_dir_path):
+        # Set metadata for a directory's file.
+        filename = DIR_FILE_NAME
+        with self.build_dir() as dir:
+            # Create a temporary dictionary.
+            tmp_dir = TestLocalDir.build_dir(path=tmp_dir_path)
+            # Copy the directory's contents into this
+            # tmp directory without including metadata.
+            dir.transfer_to(dst=tmp_dir, include_metadata=True)
+        # Assert the file's metadata are the same.
+        self.assertEqual(
+            tmp_dir.get_metadata(file_path=filename),
+            METADATA)
+        
+    @create_tmp_s3_dir
+    def test_transfer_to_as_dst(self, tmp_dir_path):
         # Get source file.
         src_file = TestLocalFile.build_file()
-        # Create a temporary "remote" dictionary.
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace('dir', TMP_DIR_NAME))
-        os.mkdir(tmp_dir_path)
-        with self.build_dir(path=tmp_dir_path) as remote_dir:
+        with self.build_dir(path=tmp_dir_path) as s3_dir:
             # Copy file into dir.
-            src_file.transfer_to(dst=remote_dir)
-            # Confirm that file was indeed copied.
+            src_file.transfer_to(dst=s3_dir)
+            # Fetch transferred object.
             copy_path = join_paths(tmp_dir_path, FILE_NAME)
+            obj = get_aws_s3_object(BUCKET, copy_path)
             with (
                 open(ABS_FILE_PATH, mode='rb') as file,
-                open(copy_path, mode='rb') as copy
+                io.BytesIO() as buffer
             ):
-                self.assertEqual(file.read(), copy.read())
-        shutil.rmtree(tmp_dir_path)
+                # Confirm that file was indeed copied.
+                obj.download_fileobj(buffer)
+                self.assertEqual(
+                    file.read(),
+                    buffer.getvalue())
+            # Delete object.
+            obj.delete()
 
-    def test_transfer_to_as_dst_on_chunk_size(self):
+    @create_tmp_s3_dir
+    def test_transfer_to_as_dst_on_chunk_size(self, tmp_dir_path):
         # Get source file.
         src_file = TestLocalFile.build_file()
-        # Create a temporary "remote" dictionary.
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace('dir', TMP_DIR_NAME))
-        os.mkdir(tmp_dir_path)
-        with self.build_dir(path=tmp_dir_path) as remote_dir:
+        with self.build_dir(path=tmp_dir_path) as s3_dir:
             # Copy file into dir.
-            src_file.transfer_to(dst=remote_dir, chunk_size=1)
-            # Confirm that file was indeed copied.
+            src_file.transfer_to(dst=s3_dir, chunk_size=5000000)
+            # Fetch transferred object.
             copy_path = join_paths(tmp_dir_path, FILE_NAME)
+            obj = get_aws_s3_object(BUCKET, copy_path)
             with (
                 open(ABS_FILE_PATH, mode='rb') as file,
-                open(copy_path, mode='rb') as copy
+                io.BytesIO() as buffer
             ):
-                self.assertEqual(file.read(), copy.read())
-        shutil.rmtree(tmp_dir_path)
+                # Confirm that file was indeed copied.
+                obj.download_fileobj(buffer)
+                self.assertEqual(
+                    file.read(),
+                    buffer.getvalue())
+            # Delete object.
+            obj.delete()
+
+    @create_tmp_s3_dir
+    def test_transfer_to_as_dst_on_include_metadata(self, tmp_dir_path):
+        # Get source file and metadata.
+        src_file = TestLocalFile.build_file()
+        metadata = {'2': '2'}
+        src_file.set_metadata(metadata=metadata)
+        with self.build_dir(path=tmp_dir_path) as s3_dir:
+            # Copy file into dir.
+            src_file.transfer_to(dst=s3_dir, include_metadata=True)
+            # Fetch transferred object.
+            copy_path = join_paths(tmp_dir_path, FILE_NAME)
+            obj = get_aws_s3_object(BUCKET, copy_path)
+            # Confirm that metadata was indeed assigned.
+            self.assertEqual(obj.metadata, metadata)
+            # Delete object.
+            obj.delete()
+        
+    @create_tmp_s3_dir
+    def test_transfer_to_as_dst_on_chunk_size_and_include_metadata(self, tmp_dir_path):
+        # Get source file and metadata.
+        src_file = TestLocalFile.build_file()
+        metadata = {'2': '2'}
+        src_file.set_metadata(metadata=metadata)
+        with self.build_dir(path=tmp_dir_path) as s3_dir:
+            # Copy file into dir.
+            src_file.transfer_to(
+                dst=s3_dir,
+                include_metadata=True,
+                chunk_size=5000000)
+            # Fetch transferred object.
+            copy_path = join_paths(tmp_dir_path, FILE_NAME)
+            obj = get_aws_s3_object(BUCKET, copy_path)
+            # Confirm that metadata was indeed assigned.
+            self.assertEqual(obj.metadata, metadata)
+            # Delete object.
+            obj.delete()
 
+    @create_tmp_s3_dir
+    def test_transfer_to_as_dst_on_invalid_chunk_size_error(self, tmp_dir_path):
+        # Get source file.
+        src_file = TestLocalFile.build_file()
+        with self.build_dir(path=tmp_dir_path) as s3_dir:
+            self.assertRaises(
+                InvalidChunkSizeError,
+                src_file.transfer_to,
+                dst=s3_dir,
+                chunk_size=1000)
         
     def test_get_file(self):
         with self.build_dir() as dir:
             file = dir.get_file(DIR_FILE_NAME)
-            self.assertEqual(file.get_path(), ABS_DIR_FILE_PATH)
+            self.assertEqual(file.get_path(), REL_DIR_FILE_PATH)
 
     def test_get_file_on_invalid_path_error(self):
         with self.build_dir() as dir:
@@ -2502,16 +3854,12 @@ def test_get_file_on_invalid_file_error(self):
     def test_get_subdir(self):
         with self.build_dir() as dir:
             subdir = dir.get_subdir(DIR_SUBDIR_NAME)
-            self.assertEqual(subdir.get_path(), ABS_DIR_SUBDIR_PATH)
+            self.assertEqual(subdir.get_path(), REL_DIR_SUBDIR_PATH)
 
     def test_get_subdir_on_invalid_path_error(self):
         with self.build_dir() as dir:
             self.assertRaises(InvalidPathError, dir.get_subdir, "NON_EXISTING_PATH")
 
-    def test_get_subdir_on_invalid_directory_error(self):
-        with self.build_dir() as dir:
-            self.assertRaises(InvalidDirectoryError, dir.get_subdir, DIR_FILE_NAME)
-
     def test_file_shared_metadata_on_modify_from_dir(self):
         with self.build_dir() as dir:
             # Access file via dir.
@@ -2580,25 +3928,23 @@ def test_is_cachable_on_true(self):
     def test_purge(self):
         with self.build_dir(cache=True) as dir:
             # Fetch size via HTTP.
-            dir.get_size()
+            dir.load_metadata()
             # Fetch size from cache and time it.
-            t = time.time()
-            dir.get_size()
-            cache_time = time.time() - t
+            t = time.perf_counter()
+            dir.load_metadata()
+            cache_time = time.perf_counter() - t
             # Purge cache.
             dir.purge()
             # Re-fetch size via HTTP and time it.
-            t = time.time()
-            dir.get_size()
-            normal_time = time.time() - t
+            t = time.perf_counter()
+            dir.load_metadata()
+            normal_time = time.perf_counter() - t
             self.assertGreater(normal_time, cache_time)
 
     def test_traverse_from_cache_on_value(self):
         with self.build_dir(cache=True) as dir:
             _ = (_ for _ in dir.traverse())
-            self.assertEqual(
-                ''.join([p for p in dir.traverse()]),
-                ''.join(CONTENTS))
+            self.assertEqual(list(dir.traverse()), CONTENTS)
             
     def test_traverse_recursively_from_cache_on_value(self):
         with self.build_dir(cache=True) as dir:
@@ -2612,67 +3958,107 @@ def test_traverse_recursively_from_cache_on_value(self):
             self.assertEqual(
                 ''.join([p for p in dir.traverse(recursively=True)]),
                 ''.join(expected_results))
-
+            
     def test_traverse_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch contents via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = (_ for _ in dir.traverse())
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch contents from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = (_ for _ in dir.traverse())
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
     def test_traverse_recursively_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch contents via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = (_ for _ in dir.traverse())
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch contents from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = (_ for _ in dir.traverse())
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
+
+    def test_load_metadata_from_cache_on_value(self):
+        with self.build_dir(cache=True) as dir:
+            dir.load_metadata()
+            self.assertEqual(
+                dir.get_metadata(REL_DIR_FILE_PATH),
+                METADATA)
+
     def test_get_size_from_cache_on_value(self):
         with self.build_dir(cache=True) as dir:
             _ = dir.get_size()
             self.assertEqual(dir.get_size(), 4)
+            
+    def test_load_metadata_recursively_from_cache_on_value(self):
+        with self.build_dir(cache=True) as dir:
+            dir.load_metadata(recursively=True)
+            self.assertEqual(
+                dir.get_metadata(f"{REL_DIR_PATH}subdir/file4.txt"),
+                METADATA)
 
     def test_get_size_recursively_from_cache_on_value(self):
         with self.build_dir(cache=True) as dir:
             _ = dir.get_size(recursively=True)
-            self.assertEqual(
-                dir.get_size(recursively=True), 16)
+            self.assertEqual(dir.get_size(recursively=True), 12)
+
+    def test_load_metadata_from_cache_on_time(self):
+        with self.build_dir(cache=True) as dir:
+            # Fetch object metadata via HTTP.
+            t = time.perf_counter()
+            _ = dir.load_metadata()
+            normal_time = time.perf_counter() - t
+            # Fetch object metadata from cache.
+            t = time.perf_counter()
+            _ = dir.load_metadata()
+            cache_time = time.perf_counter() - t
+            # Compare fetch times.
+            self.assertGreater(normal_time, cache_time)
 
     def test_get_size_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch object size via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object size from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
+            # Compare fetch times.
+            self.assertGreater(normal_time, cache_time)
+
+    def test_load_metadata_recursively_from_cache_on_time(self):
+        with self.build_dir(cache=True) as dir:
+            # Fetch object metadata via HTTP.
+            t = time.perf_counter()
+            _ = dir.load_metadata(recursively=True)
+            normal_time = time.perf_counter() - t
+            # Fetch object metadata from cache.
+            t = time.perf_counter()
+            _ = dir.load_metadata(recursively=True)
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
     def test_get_size_recursively_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch object size via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.get_size(recursively=True)
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object size from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.get_size(recursively=True)
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -2688,13 +4074,13 @@ def test_file_shared_cache_on_cache_via_dir(self):
             _ = no_cache_dir.get_size()
             _ = cache_dir.get_size()
             # Time no-cache-file's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = no_cache_file.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Time cache-file's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = cache_file.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -2708,16 +4094,16 @@ def test_file_shared_cache_on_cache_via_file(self):
                 try:
                     _ = no_cache_dir.get_file(path).get_size()
                     _ = cache_dir.get_file(path).get_size()
-                except:
+                except Exception:
                     continue
             # Time no-cache-dir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = no_cache_dir.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Time cache-dir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = cache_dir.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -2733,13 +4119,13 @@ def test_subdir_shared_cache_on_cache_via_dir(self):
             _ = no_cache_dir.get_size(recursively=True)
             _ = cache_dir.get_size(recursively=True)
             # Time no-cache-subdir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = no_cache_subdir.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Time cache-subdir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = cache_subdir.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -2752,78 +4138,63 @@ def test_subdir_shared_cache_on_cache_via_subdir(self):
             _ = no_cache_dir.get_subdir(DIR_SUBDIR_NAME).get_size()
             _ = cache_dir.get_subdir(DIR_SUBDIR_NAME).get_size()
             # Time no-cache-dir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = no_cache_dir.get_size(recursively=True)
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Time cache-dir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = cache_dir.get_size(recursively=True)
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
 
-class TestAmazonS3Dir(unittest.TestCase):
-
-    MOCK_S3 = mock_s3()
+class TestAzureBlobDir(unittest.TestCase):
 
     def get_abs_contents(self, recursively: bool):
         return [join_paths(REL_DIR_PATH, p) for p in (
             RECURSIVE_CONTENTS if recursively else CONTENTS
         )]
-    
-    def iterate_aws_s3_dir_objects(
-        self,
-        path: str = REL_DIR_PATH,
-        recursively: bool = False,
-        show_abs_path: bool = False
-    ):
-        delimiter = '' if recursively else SEPARATOR
-        for obj in boto3.resource('s3').Bucket(BUCKET).objects.filter(
-            Prefix=path,
-            Delimiter=delimiter
-        ):
-            yield obj.key if show_abs_path else obj.key.removeprefix(path)
-
-    def setUp(self):
-        create_aws_s3_bucket(self.MOCK_S3, BUCKET, METADATA)
-
-        from fluke._handlers import AWSClientHandler
-
-        m1 = patch.object(AWSClientHandler, '_get_file_size_impl', autospec=True)
-        m2 = patch.object(AWSClientHandler, '_get_file_metadata_impl', autospec=True)
-        m3 = patch.object(AWSClientHandler, '_traverse_dir_impl', autospec=True)
-
-        def simulate_latency_1(*args, **kwargs):
-            time.sleep(0.2)
-            return m1.temp_original(*args, **kwargs)
-        
-        def simulate_latency_2(*args, **kwargs):
-            time.sleep(0.2)
-            return m2.temp_original(*args, **kwargs)
-        
-        def simulate_latency_3(*args, **kwargs):
-            time.sleep(0.2)
-            return m3.temp_original(*args, **kwargs)
 
-        m1.start().side_effect = simulate_latency_1
-        m2.start().side_effect = simulate_latency_2
-        m3.start().side_effect = simulate_latency_3
+    def create_tmp_azure_dir(func: Callable):
+        '''
+        A decorator function used for creating \
+        and deleting temporary directories.
+        '''
+        def wrapper(*args, **kwargs):
+            tmp_dir_path = REL_DIR_PATH.replace(DIR_NAME, get_tmp_dir_name())
+            kwargs.update({'tmp_dir_path': tmp_dir_path})
+            os.mkdir(tmp_dir_path)
+            try:
+                func(*args, **kwargs)
+            except Exception as e:
+                raise e
+            finally:
+                shutil.rmtree(tmp_dir_path)
+        return wrapper
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestAzureBlobDir, cls).setUpClass()
+        for k, v in MockContainerClient.get_mock_methods().items():
+            patch(k, v).start()
 
-    def tearDown(self):
-        self.MOCK_S3.stop()
+    @classmethod
+    def tearDownClass(cls):
+        super(TestAzureBlobDir, cls).tearDownClass()
         patch.stopall()
     
     @staticmethod
     def build_dir(
         path: str = REL_DIR_PATH,
-        bucket: str = BUCKET,
+        container: str = CONTAINER,
         cache: bool = False,
-        create_if_missing: bool = False
-    ) -> AmazonS3Dir:
-        return AmazonS3Dir(**{
-            'auth': get_aws_auth_instance(),
-            'bucket': bucket,
+        create_if_missing: bool = False,
+        from_conn_string: bool = False
+    ) -> AzureBlobDir:
+        return AzureBlobDir(**{
+            'auth': get_azure_auth_instance(from_conn_string),
+            'container': container,
             'path': path,
             'cache': cache,
             'create_if_missing': create_if_missing
@@ -2833,21 +4204,21 @@ def test_constructor(self):
         with self.build_dir() as dir:
             self.assertEqual(dir.get_path(), REL_DIR_PATH)
 
+    def test_constructor_from_conn_string(self):
+        with self.build_dir(from_conn_string=True) as dir:
+            self.assertEqual(dir.get_path(), REL_DIR_PATH)
+
     def test_constructor_on_create_if_missing(self):
-        dir_path = "NON_EXISTING_DIR/"
+        dir_path = f"{TEST_FILES_DIR}{SEPARATOR}NON_EXISTING_DIR{SEPARATOR}"
         with self.build_dir(path=dir_path, create_if_missing=True) as _:
-            obj = boto3.resource("s3").Bucket(BUCKET).Object(dir_path)
-            try:
-                obj.load()
-                obj.delete()
-            except:
-                self.fail(f"Directory {dir_path} was not created!")
+            self.assertTrue(os.path.exists(dir_path))
+            os.rmdir(dir_path)
 
     def test_constructor_on_invalid_path_error(self):
         self.assertRaises(InvalidPathError, self.build_dir, path="NON_EXISTING_PATH")
 
-    def test_constructor_on_bucket_not_found_error(self):
-        self.assertRaises(BucketNotFoundError, self.build_dir, bucket='UNKNOWN')
+    def test_constructor_on_container_not_found_error(self):
+        self.assertRaises(ContainerNotFoundError, self.build_dir, container='UNKNOWN')
 
     def test_get_path_on_none_path(self):
         with self.build_dir(path=None) as dir:
@@ -2859,15 +4230,17 @@ def test_get_name(self):
 
     def test_get_name_on_none_path(self):
         with self.build_dir(path=None) as dir:
-            self.assertIsNone(dir.get_name())      
+            self.assertIsNone(dir.get_name())
 
-    def test_get_uri(self):
+    def test_get_container_name(self):
         with self.build_dir() as dir:
-            self.assertEqual(dir.get_uri(), f"s3://{BUCKET}/{REL_DIR_PATH}")
+            self.assertEqual(dir.get_container_name(), CONTAINER)   
 
-    def test_get_bucket_name(self):
+    def test_get_uri(self):
         with self.build_dir() as dir:
-            self.assertEqual(dir.get_bucket_name(), BUCKET)
+            uri = f"abfss://{CONTAINER}@{STORAGE_ACCOUNT}"
+            uri += f".dfs.core.windows.net/{REL_DIR_PATH}"
+            self.assertEqual(dir.get_uri(), uri)
 
     def test_get_metadata(self):
         with self.build_dir() as dir:
@@ -2879,13 +4252,13 @@ def test_get_metadata_on_invalid_file_error(self):
 
     def test_set_and_get_metadata_on_relative_path(self):
         with self.build_dir() as dir:
-            file_path, metadata = 'file2.txt', {'a': '1'}
+            file_path, metadata = DIR_FILE_NAME, {'a': '1'}
             dir.set_metadata(file_path=file_path, metadata=metadata)
             self.assertEqual(dir.get_metadata(file_path=file_path), metadata)
 
     def test_set_and_get_metadata_on_absolute_path(self):
         with self.build_dir() as dir:
-            file_path = join_paths(dir.get_path(), 'file2.txt')
+            file_path = join_paths(dir.get_path(), DIR_FILE_NAME)
             metadata = {'a': '1'}
             dir.set_metadata(file_path=file_path, metadata=metadata)
             self.assertEqual(dir.get_metadata(file_path=file_path), metadata)
@@ -2893,7 +4266,7 @@ def test_set_and_get_metadata_on_absolute_path(self):
     def test_set_metadata_on_non_string_metadata_key_error(self):
         with self.build_dir() as dir:
             args = {
-                'file_path': 'file2.txt',
+                'file_path': DIR_FILE_NAME,
                 'metadata': {1: '1'}
             }
             self.assertRaises(NonStringMetadataKeyError, dir.set_metadata, **args)
@@ -2901,7 +4274,7 @@ def test_set_metadata_on_non_string_metadata_key_error(self):
     def test_set_metadata_on_non_string_metadata_value_error(self):
         with self.build_dir() as dir:
             args = {
-                'file_path': 'file2.txt',
+                'file_path': DIR_FILE_NAME,
                 'metadata': {'1': 1}
             }
             self.assertRaises(NonStringMetadataValueError, dir.set_metadata, **args)
@@ -2934,13 +4307,11 @@ def test_load_metadata_after_set_metadata(self):
 
     def test_path_exists_on_abs_path(self):
         with self.build_dir() as dir:
-            file_path = join_paths(REL_DIR_PATH, 'file2.txt')
-            self.assertEqual(dir.path_exists(file_path), True)
+            self.assertEqual(dir.path_exists(REL_DIR_FILE_PATH), True)
 
     def test_path_exists_on_relative_path(self):
         with self.build_dir() as dir:
-            file_path = 'file2.txt'
-            self.assertEqual(dir.path_exists(file_path), True)
+            self.assertEqual(dir.path_exists(DIR_FILE_NAME), True)
 
     def test_path_not_exists_on_abs_path(self):
         with self.build_dir() as dir:
@@ -3007,10 +4378,9 @@ def test_ls(self):
 
             sys.stdout = sys.__stdout__
 
-            ls_expected_output = '\n'.join(CONTENTS) + '\n'
+            ls_expected_output = '\n' + '\n'.join(CONTENTS) + '\n'
             self.assertEqual(stdo.getvalue(), ls_expected_output)
 
-
     def test_ls_on_show_abs_path(self):
         with (
             io.StringIO() as stdo,
@@ -3022,7 +4392,7 @@ def test_ls_on_show_abs_path(self):
 
             sys.stdout = sys.__stdout__
 
-            ls_expected_output = '\n'.join(
+            ls_expected_output = '\n' + '\n'.join(
                 self.get_abs_contents(recursively=False)) + '\n'
             self.assertEqual(stdo.getvalue(), ls_expected_output)
 
@@ -3037,7 +4407,7 @@ def test_ls_on_recursively(self):
 
             sys.stdout = sys.__stdout__
 
-            ls_expected_output = '\n'.join(RECURSIVE_CONTENTS) + '\n'
+            ls_expected_output = '\n' + DIR_NAME + SEPARATOR + LS_RECURSIVE_OUTPUT
             self.assertEqual(stdo.getvalue(), ls_expected_output)
 
     def test_ls_on_show_abs_path_and_recursively(self):
@@ -3051,8 +4421,7 @@ def test_ls_on_show_abs_path_and_recursively(self):
             
             sys.stdout = sys.__stdout__
 
-            ls_expected_output = '\n'.join(
-                self.get_abs_contents(recursively=True)) + '\n'
+            ls_expected_output = '\n' + REL_DIR_PATH + LS_RECURSIVE_OUTPUT
             self.assertEqual(stdo.getvalue(), ls_expected_output)
 
     def test_count(self):
@@ -3069,17 +4438,15 @@ def test_get_size(self):
 
     def test_get_size_on_recursively(self):
         with self.build_dir() as dir:
-            self.assertEqual(dir.get_size(recursively=True), 16)
+            self.assertEqual(dir.get_size(recursively=True), 12)
 
-    def test_transfer_to(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace(DIR_NAME, TMP_DIR_NAME))
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
+    @create_tmp_dir
+    def test_transfer_to(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
         with self.build_dir() as dir:
-            dir.transfer_to(dst=TestLocalDir.build_dir(path=tmp_dir_path))
+            dir.transfer_to(dst=LocalDir(path=tmp_dir_path))
         # Assert that the two directories contains the same contents.
-        original = [s for s in sorted(self.iterate_aws_s3_dir_objects(show_abs_path=True))]
+        original = [s for s in sorted(os.listdir(ABS_DIR_PATH)) if s.endswith('.txt')]
         copies = [s for s in sorted(os.listdir(tmp_dir_path))]
         # 1. Assert number of copied files are the same.
         self.assertEqual(len(original), len(copies))
@@ -3087,26 +4454,21 @@ def test_transfer_to(self):
         for ofp, cfp in zip(original, copies):
             # Assert their contents are the same.
             with (
-                io.BytesIO() as buffer,
+                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
                 open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
             ):
-                get_aws_s3_object(BUCKET, ofp).download_fileobj(buffer)
-                self.assertEqual(buffer.getvalue(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+                self.assertEqual(of.read(), cp.read())
 
-    def test_transfer_to_on_recursively(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace(DIR_NAME, TMP_DIR_NAME))
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
+    @create_tmp_dir
+    def test_transfer_to_on_recursively(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
         with self.build_dir() as dir:
             dir.transfer_to(
-                dst=TestLocalDir.build_dir(path=tmp_dir_path),
+                dst=LocalDir(path=tmp_dir_path),
                 recursively=True)
         # Assert that the two directories contains the same contents.
-        original = [s for s in sorted(self.iterate_aws_s3_dir_objects(
-            recursively=True, show_abs_path=True))]
+        original = [join_paths(dp, f) for dp, _, fn in 
+                    os.walk(ABS_DIR_PATH) for f in fn]
         copies = [join_paths(dp, f) for dp, _, fn in 
                 os.walk(tmp_dir_path) for f in fn]
         # 1. Assert number of copied files are the same.
@@ -3115,25 +4477,20 @@ def test_transfer_to_on_recursively(self):
         for ofp, cfp in zip(original, copies):
             # Assert their contents are the same.
             with (
-                io.BytesIO() as buffer,
-                open(cfp, mode='rb') as cp
+                open(file=ofp, mode='rb') as of,
+                open(file=cfp, mode='rb') as cp
             ):
-                get_aws_s3_object(BUCKET, ofp).download_fileobj(buffer)
-                self.assertEqual(buffer.getvalue(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+                self.assertEqual(of.read(), cp.read())
 
-    def test_transfer_to_on_chunk_size(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace(DIR_NAME, TMP_DIR_NAME))
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
+    @create_tmp_dir
+    def test_transfer_to_on_chunk_size(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
         with self.build_dir() as dir:
             dir.transfer_to(
                 dst=TestLocalDir.build_dir(path=tmp_dir_path),
                 chunk_size=1)
         # Assert that the two directories contains the same contents.
-        original = [s for s in sorted(self.iterate_aws_s3_dir_objects(show_abs_path=True))]
+        original = [s for s in sorted(os.listdir(ABS_DIR_PATH)) if s.endswith('.txt')]
         copies = [s for s in sorted(os.listdir(tmp_dir_path))]
         # 1. Assert number of copied files are the same.
         self.assertEqual(len(original), len(copies))
@@ -3141,19 +4498,14 @@ def test_transfer_to_on_chunk_size(self):
         for ofp, cfp in zip(original, copies):
             # Assert their contents are the same.
             with (
-                io.BytesIO() as buffer,
+                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
                 open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
             ):
-                get_aws_s3_object(BUCKET, ofp).download_fileobj(buffer)
-                self.assertEqual(buffer.getvalue(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+                self.assertEqual(of.read(), cp.read())
 
-    def test_transfer_to_on_filter(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace('dir', TMP_DIR_NAME))
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
+    @create_tmp_dir
+    def test_transfer_to_on_filter(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
         with self.build_dir() as dir:
             dir.transfer_to(
                 dst=LocalDir(path=tmp_dir_path),
@@ -3168,19 +4520,13 @@ def test_transfer_to_on_filter(self):
         for ofp, cfp in zip(original, copies):
             # Assert their contents are the same.
             with (
-                io.BytesIO() as buffer,
+                open(file=join_paths(ABS_DIR_PATH, ofp), mode='rb') as of,
                 open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
             ):
-                get_aws_s3_object(BUCKET, ofp).download_fileobj(buffer)
-                self.assertEqual(buffer.getvalue(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
+                self.assertEqual(of.read(), cp.read())
 
-    def test_transfer_to_on_overwrite_set_to_false(self):
-        # Create a copy of the directory.
-        tmp_dir_name = TMP_DIR_NAME
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace(DIR_NAME, tmp_dir_name))
-        shutil.copytree(src=ABS_DIR_PATH, dst=tmp_dir_path)
+    @clone_tmp_dir
+    def test_transfer_to_on_overwrite_set_to_false(self, tmp_dir_path):
         # While capturing stdout...
         with (
             io.StringIO() as stdo,
@@ -3195,14 +4541,9 @@ def test_transfer_to_on_overwrite_set_to_false(self):
             sys.stdout = sys.__stdout__
 
             self.assertTrue("Operation unsuccessful" in stdo.getvalue())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
 
-    def test_transfer_to_on_overwrite_set_to_true(self):
-        # Create a copy of the directory.
-        tmp_dir_name = TMP_DIR_NAME
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace(DIR_NAME, tmp_dir_name))
-        shutil.copytree(src=ABS_DIR_PATH, dst=tmp_dir_path)
+    @clone_tmp_dir
+    def test_transfer_to_on_overwrite_set_to_true(self, tmp_dir_path):
         # While capturing stdout...
         with (
             io.StringIO() as stdo,
@@ -3217,34 +4558,28 @@ def test_transfer_to_on_overwrite_set_to_true(self):
             sys.stdout = sys.__stdout__
 
             self.assertTrue("Operation successful" in stdo.getvalue())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
 
-    def test_transfer_to_on_include_metadata_set_to_false(self):
+    @create_tmp_dir
+    def test_transfer_to_on_include_metadata_set_to_false(self, tmp_dir_path):
         # Set metadata for a directory's file.
-        filename, metadata = DIR_FILE_NAME, {'1': '1'}
+        filename, metadata = 'file2.txt', {'1': '1'}
         with self.build_dir() as dir:
             dir.set_metadata(file_path=filename, metadata=metadata)
             # Create a temporary dictionary.
-            tmp_dir_path = to_abs(REL_DIR_PATH.replace(DIR_NAME, "TMP_DIR"))
-            os.mkdir(tmp_dir_path)
             tmp_dir = TestLocalDir.build_dir(path=tmp_dir_path)
             # Copy the directory's contents into this
             # tmp directory without including metadata.
             dir.transfer_to(dst=tmp_dir, include_metadata=False)
         # Assert that no metadata have been transfered.
         self.assertEqual(tmp_dir.get_metadata(file_path=filename), {})
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
 
-    def test_transfer_to_on_include_metadata_with_set_metadata(self):
+    @create_tmp_dir
+    def test_transfer_to_on_include_metadata_with_set_metadata(self, tmp_dir_path):
         # Set metadata for a directory's file.
         filename, metadata = DIR_FILE_NAME, {'1': '1'}
         with self.build_dir() as dir:
             dir.set_metadata(file_path=filename, metadata=metadata)
             # Create a temporary dictionary.
-            tmp_dir_path = to_abs(REL_DIR_PATH.replace(DIR_NAME, "TMP_DIR"))
-            os.mkdir(tmp_dir_path)
             tmp_dir = TestLocalDir.build_dir(path=tmp_dir_path)
             # Copy the directory's contents into this
             # tmp directory without including metadata.
@@ -3253,16 +4588,13 @@ def test_transfer_to_on_include_metadata_with_set_metadata(self):
         self.assertEqual(
             tmp_dir.get_metadata(file_path=filename),
             metadata)
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
 
-    def test_transfer_to_on_include_metadata_without_set_metadata(self):
+    @create_tmp_dir
+    def test_transfer_to_on_include_metadata_without_set_metadata(self, tmp_dir_path):
         # Set metadata for a directory's file.
         filename = DIR_FILE_NAME
         with self.build_dir() as dir:
             # Create a temporary dictionary.
-            tmp_dir_path = to_abs(REL_DIR_PATH.replace(DIR_NAME, "TMP_DIR"))
-            os.mkdir(tmp_dir_path)
             tmp_dir = TestLocalDir.build_dir(path=tmp_dir_path)
             # Copy the directory's contents into this
             # tmp directory without including metadata.
@@ -3271,85 +4603,85 @@ def test_transfer_to_on_include_metadata_without_set_metadata(self):
         self.assertEqual(
             tmp_dir.get_metadata(file_path=filename),
             METADATA)
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
-        
-    def test_transfer_to_as_dst(self):
+
+    @create_tmp_azure_dir
+    def test_transfer_to_as_dst(self, tmp_dir_path):
         # Get source file.
         src_file = TestLocalFile.build_file()
-        # Get tmp dir path (already created in bucket).
-        tmp_dir_path = REL_DIR_PATH.replace('dir', TMP_DIR_NAME)
-        with self.build_dir(path=tmp_dir_path) as s3_dir:
+        # Create a temporary "blob" directory.
+        with self.build_dir(path=tmp_dir_path) as azr_dir:
             # Copy file into dir.
-            src_file.transfer_to(dst=s3_dir)
+            src_file.transfer_to(dst=azr_dir)
             # Confirm that file was indeed copied.
             copy_path = join_paths(tmp_dir_path, FILE_NAME)
             with (
                 open(ABS_FILE_PATH, mode='rb') as file,
-                io.BytesIO() as buffer
+                open(copy_path, mode='rb') as copy
             ):
-                get_aws_s3_object(
-                    bucket_name=BUCKET, path=copy_path
-                ).download_fileobj(buffer)
-                self.assertEqual(
-                    file.read(),
-                    buffer.getvalue())
+                self.assertEqual(file.read(), copy.read())
 
-    def test_transfer_to_as_dst_on_chunk_size(self):
+    @create_tmp_azure_dir
+    def test_transfer_to_as_dst_on_chunk_size(self, tmp_dir_path):
         # Get source file.
         src_file = TestLocalFile.build_file()
-        # Get tmp dir path (already created in bucket).
-        tmp_dir_path = REL_DIR_PATH.replace('dir', TMP_DIR_NAME)
-        with self.build_dir(path=tmp_dir_path) as s3_dir:
+        # Create a temporary "blob" directory.
+        with self.build_dir(path=tmp_dir_path) as azr_dir:
             # Copy file into dir.
-            src_file.transfer_to(dst=s3_dir, chunk_size=1000)
+            src_file.transfer_to(dst=azr_dir, chunk_size=1000)
             # Confirm that file was indeed copied.
             copy_path = join_paths(tmp_dir_path, FILE_NAME)
             with (
                 open(ABS_FILE_PATH, mode='rb') as file,
-                io.BytesIO() as buffer
+                open(copy_path, mode='rb') as copy
             ):
-                get_aws_s3_object(
-                    bucket_name=BUCKET, path=copy_path
-                ).download_fileobj(buffer)
-                self.assertEqual(
-                    file.read(),
-                    buffer.getvalue())
+                self.assertEqual(file.read(), copy.read())
 
-    def test_transfer_to_as_dst_on_include_metadata(self):
+    @create_tmp_azure_dir
+    def test_transfer_to_as_dst_on_include_metadata(self, tmp_dir_path):
         # Get source file and metadata.
         src_file = TestLocalFile.build_file()
         metadata = {'2': '2'}
         src_file.set_metadata(metadata=metadata)
-        # Get tmp dir path (already created in bucket).
-        tmp_dir_path = REL_DIR_PATH.replace('dir', TMP_DIR_NAME)
-        with self.build_dir(path=tmp_dir_path) as s3_dir:
+        # Create a temporary "blob" directory.
+        with self.build_dir(path=tmp_dir_path) as azr_dir:
             # Copy file into dir.
-            src_file.transfer_to(dst=s3_dir, include_metadata=True)
+            src_file.transfer_to(dst=azr_dir, include_metadata=True)
             # Confirm that metadata was indeed assigned.
             copy_path = join_paths(tmp_dir_path, FILE_NAME)
             self.assertEqual(
-                get_aws_s3_object(BUCKET, copy_path).metadata,
+                azr_dir.get_metadata(file_path=copy_path),
                 metadata)
-        
-    def test_transfer_to_as_dst_on_chunk_size_and_include_metadata(self):
+
+    @create_tmp_azure_dir
+    def test_transfer_to_as_dst_on_chunk_size_and_include_metadata(self, tmp_dir_path):
         # Get source file and metadata.
         src_file = TestLocalFile.build_file()
         metadata = {'2': '2'}
         src_file.set_metadata(metadata=metadata)
-        # Get tmp dir path (already created in bucket).
-        tmp_dir_path = REL_DIR_PATH.replace('dir', TMP_DIR_NAME)
-        with self.build_dir(path=tmp_dir_path) as s3_dir:
+        # Create a temporary "blob" directory.
+        with self.build_dir(path=tmp_dir_path) as azr_dir:
             # Copy file into dir.
             src_file.transfer_to(
-                dst=s3_dir,
+                dst=azr_dir,
                 include_metadata=True,
                 chunk_size=1000)
             # Confirm that metadata was indeed assigned.
             copy_path = join_paths(tmp_dir_path, FILE_NAME)
             self.assertEqual(
-                get_aws_s3_object(BUCKET, copy_path).metadata,
+                azr_dir.get_metadata(file_path=copy_path),
                 metadata)
+
+    @create_tmp_azure_dir
+    def test_transfer_to_as_dst_on_invalid_chunk_size_error(self, tmp_dir_path):
+        # Get source file.
+        src_file = TestLocalFile.build_file()
+        # Create a temporary "blob" directory.
+        with self.build_dir(path=tmp_dir_path) as azr_dir:
+            self.assertRaises(
+                InvalidChunkSizeError,
+                src_file.transfer_to,
+                dst=azr_dir,
+                chunk_size=4*1024*1024 + 1)
         
     def test_get_file(self):
         with self.build_dir() as dir:
@@ -3372,7 +4704,7 @@ def test_get_subdir(self):
     def test_get_subdir_on_invalid_path_error(self):
         with self.build_dir() as dir:
             self.assertRaises(InvalidPathError, dir.get_subdir, "NON_EXISTING_PATH")
-
+            
     def test_file_shared_metadata_on_modify_from_dir(self):
         with self.build_dir() as dir:
             # Access file via dir.
@@ -3443,21 +4775,23 @@ def test_purge(self):
             # Fetch size via HTTP.
             dir.load_metadata()
             # Fetch size from cache and time it.
-            t = time.time()
+            t = time.perf_counter()
             dir.load_metadata()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Purge cache.
             dir.purge()
             # Re-fetch size via HTTP and time it.
-            t = time.time()
+            t = time.perf_counter()
             dir.load_metadata()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             self.assertGreater(normal_time, cache_time)
 
     def test_traverse_from_cache_on_value(self):
         with self.build_dir(cache=True) as dir:
             _ = (_ for _ in dir.traverse())
-            self.assertEqual(list(dir.traverse()), CONTENTS)
+            self.assertEqual(
+                ''.join([p for p in dir.traverse()]),
+                ''.join(CONTENTS))
             
     def test_traverse_recursively_from_cache_on_value(self):
         with self.build_dir(cache=True) as dir:
@@ -3475,30 +4809,29 @@ def test_traverse_recursively_from_cache_on_value(self):
     def test_traverse_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch contents via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = (_ for _ in dir.traverse())
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch contents from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = (_ for _ in dir.traverse())
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
     def test_traverse_recursively_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch contents via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = (_ for _ in dir.traverse())
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch contents from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = (_ for _ in dir.traverse())
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
-
     def test_load_metadata_from_cache_on_value(self):
         with self.build_dir(cache=True) as dir:
             dir.load_metadata()
@@ -3521,57 +4854,57 @@ def test_load_metadata_recursively_from_cache_on_value(self):
     def test_get_size_recursively_from_cache_on_value(self):
         with self.build_dir(cache=True) as dir:
             _ = dir.get_size(recursively=True)
-            self.assertEqual(dir.get_size(recursively=True), 16)
+            self.assertEqual(dir.get_size(recursively=True), 12)
 
     def test_load_metadata_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch object metadata via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.load_metadata()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object metadata from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.load_metadata()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
     def test_get_size_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch object size via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object size from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
     def test_load_metadata_recursively_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch object metadata via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.load_metadata(recursively=True)
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object metadata from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.load_metadata(recursively=True)
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
     def test_get_size_recursively_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch object size via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.get_size(recursively=True)
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object size from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.get_size(recursively=True)
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -3587,13 +4920,13 @@ def test_file_shared_cache_on_cache_via_dir(self):
             _ = no_cache_dir.get_size()
             _ = cache_dir.get_size()
             # Time no-cache-file's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = no_cache_file.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Time cache-file's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = cache_file.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -3607,16 +4940,16 @@ def test_file_shared_cache_on_cache_via_file(self):
                 try:
                     _ = no_cache_dir.get_file(path).get_size()
                     _ = cache_dir.get_file(path).get_size()
-                except:
+                except Exception:
                     continue
             # Time no-cache-dir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = no_cache_dir.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Time cache-dir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = cache_dir.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -3632,13 +4965,13 @@ def test_subdir_shared_cache_on_cache_via_dir(self):
             _ = no_cache_dir.get_size(recursively=True)
             _ = cache_dir.get_size(recursively=True)
             # Time no-cache-subdir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = no_cache_subdir.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Time cache-subdir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = cache_subdir.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -3651,42 +4984,83 @@ def test_subdir_shared_cache_on_cache_via_subdir(self):
             _ = no_cache_dir.get_subdir(DIR_SUBDIR_NAME).get_size()
             _ = cache_dir.get_subdir(DIR_SUBDIR_NAME).get_size()
             # Time no-cache-dir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = no_cache_dir.get_size(recursively=True)
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Time cache-dir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = cache_dir.get_size(recursively=True)
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
 
-class TestAzureBlobDir(unittest.TestCase):
+class TestGCPStorageDir(unittest.TestCase):
 
     def get_abs_contents(self, recursively: bool):
         return [join_paths(REL_DIR_PATH, p) for p in (
             RECURSIVE_CONTENTS if recursively else CONTENTS
         )]
 
-    def setUp(self):
-        for k, v in MockContainerClient.get_mock_methods().items():
-            patch(k, v).start()
+    def create_tmp_gcs_dir(func: Callable):
+        '''
+        A decorator function used for creating \
+        and deleting temporary directories.
+        '''
+        def wrapper(*args, **kwargs):
+            tmp_dir_path = join_paths(TEST_FILES_DIR, get_tmp_dir_name())
+            kwargs.update({'tmp_dir_path': tmp_dir_path})
+            get_gcs_client().bucket(BUCKET).blob(
+                blob_name=join_paths(tmp_dir_path, 'DUMMY')
+            ).upload_from_string(data=b'')
+            func(*args, **kwargs)
+        return wrapper
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestGCPStorageDir, cls).setUpClass()
+
+        from fluke._handlers import GCPClientHandler
+
+        m1 = patch.object(GCPClientHandler, '_get_file_size_impl', autospec=True)
+        m2 = patch.object(GCPClientHandler, '_get_file_metadata_impl', autospec=True)
+        m3 = patch.object(GCPClientHandler, '_traverse_dir_impl', autospec=True)
+
+        def simulate_latency_1(*args, **kwargs):
+            time.sleep(0.2)
+            return m1.temp_original(*args, **kwargs)
+        
+        def simulate_latency_2(*args, **kwargs):
+            time.sleep(0.2)
+            return m2.temp_original(*args, **kwargs)
+        
+        def simulate_latency_3(*args, **kwargs):
+            time.sleep(0.2)
+            return m3.temp_original(*args, **kwargs)
+
+        m1.start().side_effect = simulate_latency_1
+        m2.start().side_effect = simulate_latency_2
+        m3.start().side_effect = simulate_latency_3
+        
+        # Set up the GCS bucket.
+        cls.__client = set_up_gcs_bucket()
 
-    def tearDown(self):
+    @classmethod
+    def tearDownClass(cls):
+        super(TestGCPStorageDir, cls).tearDownClass()
+        cls.__client.close()
         patch.stopall()
     
     @staticmethod
     def build_dir(
         path: str = REL_DIR_PATH,
-        container: str = CONTAINER,
+        bucket: str = BUCKET,
         cache: bool = False,
         create_if_missing: bool = False,
-        from_conn_string: bool = False
-    ) -> AzureBlobDir:
-        return AzureBlobDir(**{
-            'auth': get_azure_auth_instance(from_conn_string),
-            'container': container,
+    ) -> GCPStorageDir:
+        return GCPStorageDir(**{
+            'auth': get_gcp_auth_instance(),
+            'bucket': bucket,
             'path': path,
             'cache': cache,
             'create_if_missing': create_if_missing
@@ -3696,21 +5070,26 @@ def test_constructor(self):
         with self.build_dir() as dir:
             self.assertEqual(dir.get_path(), REL_DIR_PATH)
 
-    def test_constructor_from_conn_string(self):
-        with self.build_dir(from_conn_string=True) as dir:
-            self.assertEqual(dir.get_path(), REL_DIR_PATH)
-
     def test_constructor_on_create_if_missing(self):
-        dir_path = f"{TEST_FILES_DIR}{SEPARATOR}NON_EXISTING_DIR{SEPARATOR}"
+        # NOTE: Skip due to incompatibilities with
+        #       fake-gcs-server.
+        '''
+        dir_path = "NON_EXISTING_DIR/"
         with self.build_dir(path=dir_path, create_if_missing=True) as _:
-            self.assertTrue(os.path.exists(dir_path))
-            os.rmdir(dir_path)
+            for _ in self.__client.bucket(BUCKET).list_blobs(
+                prefix=dir_path.rstrip('/')
+            ):
+                break
+            else:
+                self.fail(f"Directory {dir_path} was not created!")
+        '''
+        pass
 
     def test_constructor_on_invalid_path_error(self):
         self.assertRaises(InvalidPathError, self.build_dir, path="NON_EXISTING_PATH")
 
-    def test_constructor_on_container_not_found_error(self):
-        self.assertRaises(ContainerNotFoundError, self.build_dir, container='UNKNOWN')
+    def test_constructor_on_bucket_not_found_error(self):
+        self.assertRaises(BucketNotFoundError, self.build_dir, bucket='UNKNOWN')
 
     def test_get_path_on_none_path(self):
         with self.build_dir(path=None) as dir:
@@ -3724,15 +5103,13 @@ def test_get_name_on_none_path(self):
         with self.build_dir(path=None) as dir:
             self.assertIsNone(dir.get_name())
 
-    def test_get_container_name(self):
+    def test_get_bucket_name(self):
         with self.build_dir() as dir:
-            self.assertEqual(dir.get_container_name(), CONTAINER)   
+            self.assertEqual(dir.get_bucket_name(), BUCKET)
 
     def test_get_uri(self):
         with self.build_dir() as dir:
-            uri = f"abfss://{CONTAINER}@{STORAGE_ACCOUNT}"
-            uri += f".dfs.core.windows.net/{REL_DIR_PATH}"
-            self.assertEqual(dir.get_uri(), uri)
+            self.assertEqual(dir.get_uri(), f"gs://{BUCKET}/{REL_DIR_PATH}")
 
     def test_get_metadata(self):
         with self.build_dir() as dir:
@@ -3870,7 +5247,7 @@ def test_ls(self):
 
             sys.stdout = sys.__stdout__
 
-            ls_expected_output = '\n'.join(CONTENTS) + '\n'
+            ls_expected_output = '\n' + '\n'.join(CONTENTS) + '\n'
             self.assertEqual(stdo.getvalue(), ls_expected_output)
 
     def test_ls_on_show_abs_path(self):
@@ -3884,7 +5261,7 @@ def test_ls_on_show_abs_path(self):
 
             sys.stdout = sys.__stdout__
 
-            ls_expected_output = '\n'.join(
+            ls_expected_output = '\n' + '\n'.join(
                 self.get_abs_contents(recursively=False)) + '\n'
             self.assertEqual(stdo.getvalue(), ls_expected_output)
 
@@ -3899,7 +5276,7 @@ def test_ls_on_recursively(self):
 
             sys.stdout = sys.__stdout__
 
-            ls_expected_output = '\n'.join(RECURSIVE_CONTENTS) + '\n'
+            ls_expected_output = '\n' + DIR_NAME + SEPARATOR + LS_RECURSIVE_OUTPUT
             self.assertEqual(stdo.getvalue(), ls_expected_output)
 
     def test_ls_on_show_abs_path_and_recursively(self):
@@ -3913,8 +5290,7 @@ def test_ls_on_show_abs_path_and_recursively(self):
             
             sys.stdout = sys.__stdout__
 
-            ls_expected_output = '\n'.join(
-                self.get_abs_contents(recursively=True)) + '\n'
+            ls_expected_output = '\n' + REL_DIR_PATH + LS_RECURSIVE_OUTPUT
             self.assertEqual(stdo.getvalue(), ls_expected_output)
 
     def test_count(self):
@@ -3931,13 +5307,11 @@ def test_get_size(self):
 
     def test_get_size_on_recursively(self):
         with self.build_dir() as dir:
-            self.assertEqual(dir.get_size(recursively=True), 16)
+            self.assertEqual(dir.get_size(recursively=True), 12)
 
-    def test_transfer_to(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = REL_DIR_PATH.replace(DIR_NAME, TMP_DIR_NAME)
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
+    @create_tmp_dir
+    def test_transfer_to(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
         with self.build_dir() as dir:
             dir.transfer_to(dst=LocalDir(path=tmp_dir_path))
         # Assert that the two directories contains the same contents.
@@ -3953,14 +5327,10 @@ def test_transfer_to(self):
                 open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
             ):
                 self.assertEqual(of.read(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
 
-    def test_transfer_to_on_recursively(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = REL_DIR_PATH.replace(DIR_NAME, TMP_DIR_NAME)
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
+    @create_tmp_dir
+    def test_transfer_to_on_recursively(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
         with self.build_dir() as dir:
             dir.transfer_to(
                 dst=LocalDir(path=tmp_dir_path),
@@ -3980,17 +5350,13 @@ def test_transfer_to_on_recursively(self):
                 open(file=cfp, mode='rb') as cp
             ):
                 self.assertEqual(of.read(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
 
-    def test_transfer_to_on_chunk_size(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = REL_DIR_PATH.replace(DIR_NAME, TMP_DIR_NAME)
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
+    @create_tmp_dir
+    def test_transfer_to_on_chunk_size(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
         with self.build_dir() as dir:
             dir.transfer_to(
-                dst=LocalDir(path=tmp_dir_path),
+                dst=TestLocalDir.build_dir(path=tmp_dir_path),
                 chunk_size=1)
         # Assert that the two directories contains the same contents.
         original = [s for s in sorted(os.listdir(ABS_DIR_PATH)) if s.endswith('.txt')]
@@ -4005,15 +5371,10 @@ def test_transfer_to_on_chunk_size(self):
                 open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
             ):
                 self.assertEqual(of.read(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
-
 
-    def test_transfer_to_on_filter(self):
-        # Create a temporary dictionary.
-        tmp_dir_path = to_abs(REL_DIR_PATH.replace('dir', TMP_DIR_NAME))
-        os.mkdir(tmp_dir_path)
-        # Copy the directory's contents into this tmp directory.
+    @create_tmp_dir
+    def test_transfer_to_on_filter(self, tmp_dir_path):
+        # Copy the directory's contents into a tmp directory.
         with self.build_dir() as dir:
             dir.transfer_to(
                 dst=LocalDir(path=tmp_dir_path),
@@ -4032,14 +5393,9 @@ def test_transfer_to_on_filter(self):
                 open(file=join_paths(tmp_dir_path, cfp), mode='rb') as cp
             ):
                 self.assertEqual(of.read(), cp.read())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
 
-    def test_transfer_to_on_overwrite_set_to_false(self):
-        # Create a copy of the directory.
-        tmp_dir_name = TMP_DIR_NAME
-        tmp_dir_path = REL_DIR_PATH.replace(DIR_NAME, tmp_dir_name)
-        shutil.copytree(src=ABS_DIR_PATH, dst=tmp_dir_path)
+    @clone_tmp_dir
+    def test_transfer_to_on_overwrite_set_to_false(self, tmp_dir_path):
         # While capturing stdout...
         with (
             io.StringIO() as stdo,
@@ -4054,14 +5410,9 @@ def test_transfer_to_on_overwrite_set_to_false(self):
             sys.stdout = sys.__stdout__
 
             self.assertTrue("Operation unsuccessful" in stdo.getvalue())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
 
-    def test_transfer_to_on_overwrite_set_to_true(self):
-        # Create a copy of the directory.
-        tmp_dir_name = TMP_DIR_NAME
-        tmp_dir_path = REL_DIR_PATH.replace(DIR_NAME, tmp_dir_name)
-        shutil.copytree(src=ABS_DIR_PATH, dst=tmp_dir_path)
+    @clone_tmp_dir
+    def test_transfer_to_on_overwrite_set_to_true(self, tmp_dir_path):
         # While capturing stdout...
         with (
             io.StringIO() as stdo,
@@ -4076,34 +5427,28 @@ def test_transfer_to_on_overwrite_set_to_true(self):
             sys.stdout = sys.__stdout__
 
             self.assertTrue("Operation successful" in stdo.getvalue())
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
 
-    def test_transfer_to_on_include_metadata_set_to_false(self):
+    @create_tmp_dir
+    def test_transfer_to_on_include_metadata_set_to_false(self, tmp_dir_path):
         # Set metadata for a directory's file.
         filename, metadata = 'file2.txt', {'1': '1'}
         with self.build_dir() as dir:
             dir.set_metadata(file_path=filename, metadata=metadata)
-            # Create a temporary dictionary.
-            tmp_dir_path = REL_DIR_PATH.replace(DIR_NAME, TMP_DIR_NAME)
-            os.mkdir(tmp_dir_path)
+            # Create a temporary directory.
             tmp_dir = TestLocalDir.build_dir(path=tmp_dir_path)
             # Copy the directory's contents into this
             # tmp directory without including metadata.
             dir.transfer_to(dst=tmp_dir, include_metadata=False)
         # Assert that no metadata have been transfered.
         self.assertEqual(tmp_dir.get_metadata(file_path=filename), {})
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
 
-    def test_transfer_to_on_include_metadata_with_set_metadata(self):
+    @create_tmp_dir
+    def test_transfer_to_on_include_metadata_with_set_metadata(self, tmp_dir_path):
         # Set metadata for a directory's file.
         filename, metadata = DIR_FILE_NAME, {'1': '1'}
         with self.build_dir() as dir:
             dir.set_metadata(file_path=filename, metadata=metadata)
-            # Create a temporary dictionary.
-            tmp_dir_path = REL_DIR_PATH.replace(DIR_NAME, TMP_DIR_NAME)
-            os.mkdir(tmp_dir_path)
+            # Create a temporary directory.
             tmp_dir = TestLocalDir.build_dir(path=tmp_dir_path)
             # Copy the directory's contents into this
             # tmp directory without including metadata.
@@ -4112,16 +5457,13 @@ def test_transfer_to_on_include_metadata_with_set_metadata(self):
         self.assertEqual(
             tmp_dir.get_metadata(file_path=filename),
             metadata)
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
 
-    def test_transfer_to_on_include_metadata_without_set_metadata(self):
+    @create_tmp_dir
+    def test_transfer_to_on_include_metadata_without_set_metadata(self, tmp_dir_path):
         # Set metadata for a directory's file.
         filename = DIR_FILE_NAME
         with self.build_dir() as dir:
-            # Create a temporary dictionary.
-            tmp_dir_path = REL_DIR_PATH.replace(DIR_NAME, TMP_DIR_NAME)
-            os.mkdir(tmp_dir_path)
+            # Create a temporary directory.
             tmp_dir = TestLocalDir.build_dir(path=tmp_dir_path)
             # Copy the directory's contents into this
             # tmp directory without including metadata.
@@ -4130,83 +5472,103 @@ def test_transfer_to_on_include_metadata_without_set_metadata(self):
         self.assertEqual(
             tmp_dir.get_metadata(file_path=filename),
             METADATA)
-        # Remove temporary directory.
-        shutil.rmtree(tmp_dir_path)
 
-    def test_transfer_to_as_dst(self):
+    @create_tmp_gcs_dir
+    def test_transfer_to_as_dst(self, tmp_dir_path):
         # Get source file.
         src_file = TestLocalFile.build_file()
-        # Create a temporary "blob" dictionary.
-        tmp_dir_path = REL_DIR_PATH.replace('dir', TMP_DIR_NAME)
-        os.mkdir(tmp_dir_path)
-        with self.build_dir(path=tmp_dir_path) as azr_dir:
+        # Create a temporary GCS directory.
+        with self.build_dir(path=tmp_dir_path) as gcp_dir:
             # Copy file into dir.
-            src_file.transfer_to(dst=azr_dir)
-            # Confirm that file was indeed copied.
+            src_file.transfer_to(dst=gcp_dir)
+            # Fetch transferred object.
             copy_path = join_paths(tmp_dir_path, FILE_NAME)
+            obj = self.__client.bucket(BUCKET).get_blob(copy_path)
             with (
                 open(ABS_FILE_PATH, mode='rb') as file,
-                open(copy_path, mode='rb') as copy
+                io.BytesIO() as buffer
             ):
-                self.assertEqual(file.read(), copy.read())
-        shutil.rmtree(tmp_dir_path)
+                # Confirm that file was indeed copied.
+                obj.download_to_file(buffer)
+                self.assertEqual(
+                    file.read(),
+                    buffer.getvalue())
+            # Delete object.
+            obj.delete()
 
-    def test_transfer_to_as_dst_on_chunk_size(self):
+    @create_tmp_gcs_dir
+    def test_transfer_to_as_dst_on_chunk_size(self, tmp_dir_path):
         # Get source file.
         src_file = TestLocalFile.build_file()
-        # Create a temporary "blob" dictionary.
-        tmp_dir_path = REL_DIR_PATH.replace('dir', TMP_DIR_NAME)
-        os.mkdir(tmp_dir_path)
-        with self.build_dir(path=tmp_dir_path) as azr_dir:
+        # Create a temporary GCS directory.
+        with self.build_dir(path=tmp_dir_path) as gcp_dir:
             # Copy file into dir.
-            src_file.transfer_to(dst=azr_dir, chunk_size=1)
-            # Confirm that file was indeed copied.
+            src_file.transfer_to(dst=gcp_dir, chunk_size=256*1024)
+            # Fetch transferred object.
             copy_path = join_paths(tmp_dir_path, FILE_NAME)
+            obj = self.__client.bucket(BUCKET).get_blob(copy_path)
             with (
                 open(ABS_FILE_PATH, mode='rb') as file,
-                open(copy_path, mode='rb') as copy
+                io.BytesIO() as buffer
             ):
-                self.assertEqual(file.read(), copy.read())
-        shutil.rmtree(tmp_dir_path)
+                # Confirm that file was indeed copied.
+                obj.download_to_file(buffer)
+                self.assertEqual(
+                    file.read(),
+                    buffer.getvalue())
+            # Delete object.
+            obj.delete()
 
-    def test_transfer_to_as_dst_on_include_metadata(self):
+    @create_tmp_gcs_dir
+    def test_transfer_to_as_dst_on_include_metadata(self, tmp_dir_path):
         # Get source file and metadata.
         src_file = TestLocalFile.build_file()
         metadata = {'2': '2'}
         src_file.set_metadata(metadata=metadata)
-        # Create a temporary "blob" dictionary.
-        tmp_dir_path = REL_DIR_PATH.replace('dir', TMP_DIR_NAME)
-        os.mkdir(tmp_dir_path)
-        with self.build_dir(path=tmp_dir_path) as azr_dir:
+        # Create a temporary GCS directory.
+        with self.build_dir(path=tmp_dir_path) as gcp_dir:
             # Copy file into dir.
-            src_file.transfer_to(dst=azr_dir, include_metadata=True)
-            # Confirm that metadata was indeed assigned.
+            src_file.transfer_to(dst=gcp_dir, include_metadata=True)
+            # Fetch transferred object.
             copy_path = join_paths(tmp_dir_path, FILE_NAME)
-            self.assertEqual(
-                azr_dir.get_metadata(file_path=copy_path),
-                metadata)
-        shutil.rmtree(tmp_dir_path)
-
-    def test_transfer_to_as_dst_on_chunk_size_and_include_metadata(self):
+            obj = self.__client.bucket(BUCKET).get_blob(copy_path)
+            # Confirm that metadata was indeed assigned.
+            self.assertEqual(obj.metadata, metadata)
+            # Delete object.
+            obj.delete()
+    
+    @create_tmp_gcs_dir
+    def test_transfer_to_as_dst_on_chunk_size_and_include_metadata(self, tmp_dir_path):
         # Get source file and metadata.
         src_file = TestLocalFile.build_file()
         metadata = {'2': '2'}
         src_file.set_metadata(metadata=metadata)
-        # Create a temporary "blob" dictionary.
-        tmp_dir_path = REL_DIR_PATH.replace('dir', TMP_DIR_NAME)
-        os.mkdir(tmp_dir_path)
-        with self.build_dir(path=tmp_dir_path) as azr_dir:
+        # Create a temporary GCS directory.
+        with self.build_dir(path=tmp_dir_path) as gcp_dir:
             # Copy file into dir.
             src_file.transfer_to(
-                dst=azr_dir,
+                dst=gcp_dir,
                 include_metadata=True,
-                chunk_size=1)
-            # Confirm that metadata was indeed assigned.
+                chunk_size=256*1024)
+            # Fetch transferred object.
             copy_path = join_paths(tmp_dir_path, FILE_NAME)
-            self.assertEqual(
-                azr_dir.get_metadata(file_path=copy_path),
-                metadata)
-        shutil.rmtree(tmp_dir_path)
+            obj = self.__client.bucket(BUCKET).get_blob(copy_path)
+            # Confirm that metadata was indeed assigned.
+            self.assertEqual(obj.metadata, metadata)
+            # Delete object.
+            obj.delete()
+
+    @create_tmp_gcs_dir
+    def test_transfer_to_as_dst_on_invalid_chunk_size_error(self, tmp_dir_path):
+        # Get source file.
+        src_file = TestLocalFile.build_file()
+        # Create a temporary GCS directory.
+        with self.build_dir(path=tmp_dir_path) as gcp_dir:
+            self.assertRaises(
+                InvalidChunkSizeError,
+                src_file.transfer_to,
+                dst=gcp_dir,
+                chunk_size=1000)
         
     def test_get_file(self):
         with self.build_dir() as dir:
@@ -4300,15 +5662,15 @@ def test_purge(self):
             # Fetch size via HTTP.
             dir.load_metadata()
             # Fetch size from cache and time it.
-            t = time.time()
+            t = time.perf_counter()
             dir.load_metadata()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Purge cache.
             dir.purge()
             # Re-fetch size via HTTP and time it.
-            t = time.time()
+            t = time.perf_counter()
             dir.load_metadata()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             self.assertGreater(normal_time, cache_time)
 
     def test_traverse_from_cache_on_value(self):
@@ -4334,26 +5696,26 @@ def test_traverse_recursively_from_cache_on_value(self):
     def test_traverse_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch contents via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = (_ for _ in dir.traverse())
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch contents from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = (_ for _ in dir.traverse())
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
     def test_traverse_recursively_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch contents via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = (_ for _ in dir.traverse())
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch contents from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = (_ for _ in dir.traverse())
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -4379,57 +5741,57 @@ def test_load_metadata_recursively_from_cache_on_value(self):
     def test_get_size_recursively_from_cache_on_value(self):
         with self.build_dir(cache=True) as dir:
             _ = dir.get_size(recursively=True)
-            self.assertEqual(dir.get_size(recursively=True), 16)
+            self.assertEqual(dir.get_size(recursively=True), 12)
 
     def test_load_metadata_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch object metadata via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.load_metadata()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object metadata from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.load_metadata()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
     def test_get_size_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch object size via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object size from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
     def test_load_metadata_recursively_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch object metadata via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.load_metadata(recursively=True)
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object metadata from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.load_metadata(recursively=True)
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
     def test_get_size_recursively_from_cache_on_time(self):
         with self.build_dir(cache=True) as dir:
             # Fetch object size via HTTP.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.get_size(recursively=True)
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Fetch object size from cache.
-            t = time.time()
+            t = time.perf_counter()
             _ = dir.get_size(recursively=True)
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -4445,13 +5807,13 @@ def test_file_shared_cache_on_cache_via_dir(self):
             _ = no_cache_dir.get_size()
             _ = cache_dir.get_size()
             # Time no-cache-file's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = no_cache_file.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Time cache-file's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = cache_file.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -4465,16 +5827,16 @@ def test_file_shared_cache_on_cache_via_file(self):
                 try:
                     _ = no_cache_dir.get_file(path).get_size()
                     _ = cache_dir.get_file(path).get_size()
-                except:
+                except Exception:
                     continue
             # Time no-cache-dir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = no_cache_dir.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Time cache-dir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = cache_dir.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -4490,13 +5852,13 @@ def test_subdir_shared_cache_on_cache_via_dir(self):
             _ = no_cache_dir.get_size(recursively=True)
             _ = cache_dir.get_size(recursively=True)
             # Time no-cache-subdir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = no_cache_subdir.get_size()
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Time cache-subdir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = cache_subdir.get_size()
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
@@ -4509,16 +5871,16 @@ def test_subdir_shared_cache_on_cache_via_subdir(self):
             _ = no_cache_dir.get_subdir(DIR_SUBDIR_NAME).get_size()
             _ = cache_dir.get_subdir(DIR_SUBDIR_NAME).get_size()
             # Time no-cache-dir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = no_cache_dir.get_size(recursively=True)
-            normal_time = time.time() - t
+            normal_time = time.perf_counter() - t
             # Time cache-dir's "get_size"
-            t = time.time()
+            t = time.perf_counter()
             _ = cache_dir.get_size(recursively=True)
-            cache_time = time.time() - t
+            cache_time = time.perf_counter() - t
             # Compare fetch times.
             self.assertGreater(normal_time, cache_time)
 
 
 if __name__=="__main__":
-    unittest.main()
+    unittest.main()
\ No newline at end of file