nao-pon / flysystem-google-drive

Flysystem adapter for Google Drive
MIT License
352 stars 128 forks source link

How can I refresh the access token? #115

Open johnRivs opened 2 years ago

johnRivs commented 2 years ago

I'm using this adapter inside a queue. Once it's registered via Storage::extend(), it'll be reused as long as the queue is running. Unfortunately, the access token expires in 1 hour and I can't seem to refresh it.

What I was trying to do was to make a singleton out of the Google client used by the adapter then set up a scheduler task every 30 minutes where I do this app('googleClient')->refreshToken(config('filesystems.disks.google.refreshToken')); then cache the credentials and finally app('googleClient')->setAccessToken(cache('the access token')) on every job I put in the queue. Didn't work. I've also tried to refresh the token on every job and it doesn't work either.

sr2ds commented 2 years ago

Hello @johnRivs ,

I'm with the same issue, maybe we can think together.

I tried cleanup from tinker with:

Storage::disk('google')->getDriver()->getAdapter()->getService()->getClient()->getCache()->clear()

And don't works, but let's think in some points.

I saw that GoogleAPI package did use the PSR CacheItemPoolInterface to deal with the cache. I don't sure if, when the process queue:worker is running, it use the same cache of my tinker, I guess not.

I want mean that, I think is not about default cache defined of application with CACHE_DRIVER and the process running has your own cache.

I dont try yet, but if the JOB run the cleanup command with ->getCache()->clear(), this will cleanup the process cache of driver? What do you think?

One bad way to fix it fast, is try run queue:restart inside our job. Because, when the worker is restarted, works. Because, I guess, the service provider is registered from zero.

johnRivs commented 2 years ago

This is what I ended up doing.

I have a class responsible for getting a new access token:

<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;

class Google
{
    static function refreshToken()
    {
        $credentials = app('googleClient')->refreshToken(config('filesystems.disks.google.refreshToken'));

        Cache::put('google-drive-credentials', $credentials);
    }
}

It's scheduled to run every 30 minutes:

<?php

namespace App\Console;

use App\Services\Google;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    function schedule(Schedule $schedule)
    {
        $schedule->call(function () {
            Google::refreshToken();
        })->everyThirtyMinutes();
    }
}

In EventServiceProvider.php, when a CompleteOrder job is put into the queue, I register the driver. When it's done processing, I remove the driver:

<?php

namespace App\Providers;

use App\Jobs\CompleteOrder;
use League\Flysystem\Filesystem;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Storage;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobProcessing;
use Hypweb\Flysystem\GoogleDrive\GoogleDriveAdapter;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    function register()
    {
        $this->app->singleton('googleClient', function () {
            $client = new \Google_Client();

            $client->setClientId(config('filesystems.disks.google.clientId'));
            $client->setClientSecret(config('filesystems.disks.google.clientSecret'));

            return $client;
        });
    }

    function boot()
    {
        $this->prepareGoogleStorageForQueue();
        $this->registerGoogleStorage();
    }

    function prepareGoogleStorageForQueue()
    {
        Event::listen(JobProcessing::class, function($event) {
            if ($event->job->resolveName() === CompleteOrder::class) $this->registerGoogleStorage();
        });

        Event::listen(JobProcessed::class, function($event) {
            if ($event->job->resolveName() === CompleteOrder::class) Storage::forgetDisk('google');
        });
    }

    function registerGoogleStorage()
    {
        Storage::extend('google', function() {
            app('googleClient')->setAccessToken(cache('google-drive-credentials')['access_token']);

            $service = new \Google_Service_Drive(app('googleClient'));
            $adapter = new GoogleDriveAdapter($service);

            return new Filesystem($adapter);
        });
    }
}
sr2ds commented 2 years ago

Hello my friend, Thanks for share your way.

I merged my way and your way and looks good to run without scheduled task to update token. I'm checking to ensure yet, but looks good.

I create one service to take about this:

<?php

namespace App\Services\Google;

use App\Providers\GoogleDriveAdapter;
use Storage;

class GoogelDriveStorageAuth
{
    public static  function reloadGoogleStorage()
    {
        $accessTokenExpired = Storage::disk('google')->getDriver()
            ?->getAdapter()
            ?->getService()
            ?->getClient()
            ->isAccessTokenExpired();

        if ($accessTokenExpired) {
            \Log::info('GoogelDriveStorageAuth: access token expired - Storage Reloaded');
            Storage::forgetDisk('google');
            self::registerStorage();
        }
    }

    public static function registerStorage()
    {
        \Storage::extend('google', function ($app, $config) {
            $app; // to fix grumphp settings -> variable not used
            $client = new \Google_Client();
            $client->setClientId($config['clientId']);
            $client->setClientSecret($config['clientSecret']);

            /**
             * Cache access token to reduce requests to Google
             */
            if (\Cache::has('google access token')) {
                $token = \Cache::get('google access token');
                $client->setAccessToken($token);
            }

            if ($client->isAccessTokenExpired()) {
                $refreshToken = $config['refreshToken'];
                if ($refreshToken) {
                    $token = $client->fetchAccessTokenWithRefreshToken($refreshToken);
                    $client->setAccessToken($token);
                    \Cache::add('google access token', $token);
                } else {
                    /**
                     * Invalid refresh token and or credentials
                     *
                     * Check cloud wiki on how to create an app and get credentials and refresh token
                     */
                    throw new \Exception("Invalid refresh token, check cloud wiki");
                }
            }

            $service = new \Google_Service_Drive($client);
            $options = [];
            $adapter = new GoogleDriveAdapter($service, $config['folderId'], $options);
            return new \League\Flysystem\Filesystem($adapter);
        });
    }
}

And, inside my Jobs, I put this line to, if necessary, reset the storage and re-auth:

GoogelDriveStorageAuth::reloadGoogleStorage();

Thanks

johnRivs commented 2 years ago

Yeah you can go that route as well. I chose to set up a scheduled task so I wouldn't have to add that line to every job or make some of my jobs extend another base job where I do that in the constructor. It felt more comfortable to write code anywhere assuming there's always a valid token available.

sr2ds commented 2 years ago

Oh yes! Great! Good point! Thanks

parallels999 commented 2 years ago

https://github.com/ivanvermeyen/laravel-google-drive-demo/issues/107 https://github.com/masbug/flysystem-google-drive-ext/pull/34/files