Google Drive API and FlySystem – Obtaining the IDs of newly created folders

TL;DR -> Extend the \League\Flysystem\Filesystem class and implement your own version of createDir() that doesn’t cast the adapter’s response to a bool before returning, or make a new method called something like createDirReturnId() that is a clone of createDir() without the bool casting, if like me you are paranoid about breaking some other abstraction that expects the original implementation of createDir() to exist. You’ll also need to instantiate your new child class inside of GoogleDriveServiceProvider.php, replacing \League\Flysystem\Filesystem.

Along with Laravel, I’ve been using the amazing Flysystem – Filesystem Abstraction for PHP with Naoki Sawada’s great adapter for Google Drive.

Unlike many of the filesystems that Flysystem allows you to interact with, Google Drive is not a traditional file system, and is based on file and folder IDs that are generated for you. With Google Drive, the name or the file or folder is different from the ID, and it can be changed, whereas the ID stays the same for the life of that file or folder. And I believe that all files and folders are essentially the same thing, i.e. a folder is just a file with a MIME type of application/vnd.google-apps.folder.

When I create a new Google Drive folder for an entity in my program, I want to get that folder’s ID back so that I can store it in the database for later use. However, when I use Flysystem’s createDir() method to make new folders in Drive, the response is limited to a boolean (true or false):

$response = \Storage::drive('google')
->createDir(\Config::get('constants.drive_paths.people') . '/New Folder');
// Here I'm creating a new folder called 'New Folder' inside my 'people' directory.
// $response will be true if the folder was successfully created, or false if there was a problem.

It seems like Flysystem is built to be pretty streamlined out of the box, and perhaps is expected to be used with more traditional filesystems where the name of the folder is the only identifier you would need. So if you’ve already specified the folder name when you asked for it to be created, then why would you need any extra info back, other than a simple success or failure? You already know where it is and how to address it. Makes sense.

For Google Drive this becomes a problem (at least for the way I was trying to go about it).

At first I thought about making a Temp folder manually, through Drive’s web interface, and then having my program create any new folders in the Temp folder first, then scanning the temp folder to see all the entities inside, which would only be one, hopefully, getting the ID of that one entity (the folder we just created), storing the ID in the DB, and then moving that entity to its final destination. But that seems a bit roundabout, and I was worried that it would break if the program was handling a huge amount of simultaneous calls to create folders (if the scan of the temp folder showed more than one folder inside it). Probably mathematically unlikely, but I don’t really know.

After searching through Naoki’s code on GitHub, I found that his adapter does return more than just a boolean, and after searching through the definition of createDir() in Flysystem, I found that it is casting the return from the adapter to a boolean:

// \League\Flysystem\Filesystem.php
public function createDir($dirname, array $config = [])
{
    $dirname = Util::normalizePath($dirname);
    $config = $this->prepareConfig($config);
    return (bool) $this->getAdapter()->createDir($dirname, $config);
}

So I created a new class that extends theirs, with a new method that doesn’t cast to a boolean:

// \App\Gfilesystem.php

namespace App;

use League\Flysystem\Util;

class Gfilesystem extends \League\Flysystem\Filesystem
{

    public function createDirReturnId($dirname, array $config = [])
    {
        $dirname = Util::normalizePath($dirname);
        $config = $this->prepareConfig($config);

        return $this->getAdapter()->createDir($dirname, $config);
    }

}

And I edited this file:

// App\Providers\GoogleDriveServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class GoogleDriveServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        \Storage::extend('google', function($app, $config) {
            $client = new \Google_Client();
            $client->setClientId($config['clientId']);
            $client->setClientSecret($config['clientSecret']);
            $client->refreshToken($config['refreshToken']);
            $service = new \Google_Service_Drive($client);
            $adapter = new \Hypweb\Flysystem\GoogleDrive\GoogleDriveAdapter($service, $config['folderId']);
            return new \League\Flysystem\Filesystem($adapter);
        });
    }
    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

To instantiate my new child class instead:

public function boot()
{
    \Storage::extend('google', function($app, $config) {
        $client = new \Google_Client();
        $client->setClientId($config['clientId']);
        $client->setClientSecret($config['clientSecret']);
        $client->refreshToken($config['refreshToken']);
        $service = new \Google_Service_Drive($client);
        $adapter = new \Hypweb\Flysystem\GoogleDrive\GoogleDriveAdapter($service, $config['folderId']);
        return new \App\GFilesystem($adapter);
    });
}

The new response is an array that includes the ID of the new folder and the ID of the parent folder, separated by a slash:

$drive_folder = explode("/", $response['path'])[1];

And we’re good! So far this has been working out for my purposes.