PHP
Article
By Wern Ancheta

Build Your Own Dropbox Client with the Dropbox API

By Wern Ancheta

There are lots of file hosting solutions out there, but few things compare to Dropbox because of its simplicity, auto-sync feature, cross-platform support and other cool features.

Dropbox logo

As a PHP developer you can even take advantage of their API in order to create apps that use its full capabilities. In this article, you’ll learn how to build one such app to perform different operations in a user’s Dropbox account. You will be using the Dropbox API version 2 in this tutorial. If you want to follow along, you can clone the project from Github.

Creating a Dropbox App

The first thing that you need to do is go to the Dropbox’s developer site and create a new app.

create dropbox app

Dropbox offers two APIs: the Dropbox API which is the API for the general public and the Business API for teams. These two APIs are pretty much the same, the only difference being that the business API is specifically used for business accounts, so team features such as access to team information, team member file access and team member management are baked into it by default. We’ll use the former.

Once you’ve created the app, you’ll see the app settings page:

app settings

From here you can set the following:

  • Development users – this allows you to add Dropbox users for testing your app. By default the status of the app is development. This means that only you can test its functionality. If you let any other user access your app, they won’t be able to use it.
  • Permission type – this is the setting that you have selected earlier when you created the app. There are only two permission types: folder and full dropbox. Folder means that your app only has access to the folder that you specify. Full Dropbox means that your app has access to all the user’s files.
  • App key and secret this is the unique key that’s used by Dropbox to identify your app. We’ll need this later.
  • OAuth2 redirect urls – this is where you can set URLs to which your app can redirect right after the user has approved the necessary permissions. Leave this blank for now, you will be adding a value to it later. For now, take note that only the URLs that you have specified here can be used for redirection.
  • Allow implicit grant – whether or not to automatically generate an access token once the user has granted the necessary permissions to your app. If you’re making use of Dropbox in the client-side, this should be set to allow so that you can get an access token through JavaScript. For the purpose of this project, you should set it to disallow.
  • Generated access token – you can use this to generate an access token for your account. The access token can be used when making requests to the API.
  • Chooser/saver domains – if you’re using drop-ins such as the chooser and saver, this is where you need to specify the domains in which you’re embedding those.
  • Webhooks – you can use webhooks if you want your server to perform specific actions whenever a file in the user’s dropbox account changes. You won’t be going through webhooks in this tutorial so I recommend you go check out the webhooks documentation if you need the functionality in your app.
--ADVERTISEMENT--

Building the App

Now you’re ready to build the app. You’ll be using Laravel.

Installing Dependencies

composer create-project --prefer-dist laravel/laravel pinch

Once installed, you also need to install Guzzle, Purl and Carbon.

composer require nesbot/carbon jwage/purl guzzlehttp/guzzle

You’ll be using Guzzle to make HTTP requests to the Dropbox API, Purl for constructing the Dropbox login URL, and Carbon to express the file dates in the user’s timezone.

Configuration

Once Laravel is installed, open the .env file in the root of your project and add the dropbox configuration:

DROPBOX_APP_KEY="YOUR DROPBOX APP KEY"
DROPBOX_APP_SECRET="YOUR DROPBOX APP SECRET"
DROPBOX_REDIRECT_URI="YOUR DROPBOX LOGIN REDIRECT URL"

Use the app key and app secret that you got earlier from the Dropbox developer website as the value for DROPBOX_APP_KEY and DROPBOX_APP_SECRET. For the DROPBOX_REDIRECT_URI you have to specify an http URL, so if you’re using a virtual host, you will need to use something like Ngrok to serve the app. Then, in your virtual host configuration, add the URL provided by Ngrok as the ServerAlias.

<VirtualHost *:80>
        ServerName pinch.dev
        ServerAlias xxxxxxx.ngrok.io
        ServerAdmin wern@localhost
        DocumentRoot /home/wern/www/pinch/public
</VirtualHost>

Routes

The different pages in the app are defined in the app/Http/routes.php file:

Route::get('/', 'HomeController@index');
Route::post('/', 'HomeController@postIndex');

Route::get('/login', 'HomeController@login');

Route::group(
    ['middleware' => ['admin']], 
    function($app){ 
        Route::get('/dashboard', 'AdminController@dashboard');

        Route::get('/user', 'AdminController@user');

        Route::get('/search', 'AdminController@search');

        Route::get('/upload', 'AdminController@upload');
        Route::post('/upload', 'AdminController@doUpload');

        Route::get('/revisions', 'AdminController@revisions');
        Route::get('/restore', 'AdminController@restoreRevision');

        Route::get('/download', 'AdminController@download');
});

Breaking down the code above, first you have the routes which deal with logging into Dropbox:

//displays the view for logging in to dropbox
Route::get('/', 'HomeController@index');

//generates the dropbox login URL
Route::post('/', 'HomeController@postIndex');

//generates the access token based on the token provided by Dropbox
Route::get('/login', 'HomeController@login');

The admin pages are wrapped in a route group so that you could use a middleware to check whether the user accessing it is logged in or not.

Route::group(
    ['middleware' => ['admin']], 
    function($app){ 
        ...
});

Inside the admin route group, you have the route for serving the dashboard page. This page contains the links for each of the different operations that you can perform in the app.

Route::get('/dashboard', 'AdminController@dashboard');

The user details page:

Route::get('/user', 'AdminController@user');

The page for searching files:

Route::get('/search', 'AdminController@search');

The page for uploading files:

Route::get('/upload', 'AdminController@upload');
Route::post('/upload', 'AdminController@doUpload');

The page for viewing the different versions of a specific file and the page for restoring a specific version:

Route::get('/revisions', 'AdminController@revisions');
Route::get('/restore', 'AdminController@restoreRevision');

And lastly, the route for handling file downloads:

Route::get('/download', 'AdminController@download');

Admin Middleware

Next is the admin middleware (app/Http/Middleware/AdminMiddleware.php):

<?php 
namespace App\Http\Middleware;

use Closure;

class AdminMiddleware 
{
    public function handle($request, Closure $next)
    {
        if ($request->session()->has('access_token')) {
            return $next($request);
        }

        return redirect('/')
            ->with('message', ['type' => 'danger', 'text' => 'You need to login']);
    }
}

What the above code does is check whether the access_token has been set in the session. If it hasn’t been set, simply redirect to the home page, otherwise proceed with processing the request.

Dropbox Class

The Dropbox class (app/Dropbox.php) is used for initializing the Guzzle client used for making requests to Dropbox. It has two methods: api and content. api is used for making requests to the API and content is used for dealing with content such as when you upload or download files.

<?php
namespace App;

use GuzzleHttp\Client;

class Dropbox 
{
    public function api()
    {
        $client = new Client([
            'base_uri' => 'https://api.dropboxapi.com',
        ]);
        return $client;
    }

    public function content()
    {
        $client = new Client([
            'base_uri' => 'https://content.dropboxapi.com'
        ]);

        return $client;
    }

}

HomeController

The HomeController contains the logic for the home pages. This is where you add the code for logging into Dropbox and acquiring the access token which can be used by the app to make requests to the API.

Open the app/Http/Controllers/HomeController.php file:

<?php
namespace App\Http\Controllers;

use Purl\Url;
use App\Dropbox;
use Illuminate\Http\Request;

class HomeController extends Controller
{
    private $api_client;

    public function __construct(Dropbox $dropbox)
    {
        $this->api_client = $dropbox->api();
    }

    public function index()
    {
        return view('index');
    }

    public function postIndex()
    {
        $url = new Url('https://www.dropbox.com/1/oauth2/authorize');

        $url->query->setData([
            'response_type' => 'code',
            'client_id' => env('DROPBOX_APP_KEY'),
            'redirect_uri' => env('DROPBOX_REDIRECT_URI')
        ]);

        return redirect($url->getUrl());
    }

    public function login(Request $request)
    {
        if ($request->has('code')) {

            $data = [
                'code' => $request->input('code'),
                'grant_type' => 'authorization_code',
                'client_id' => env('DROPBOX_APP_KEY'),
                'client_secret' => env('DROPBOX_APP_SECRET'),
                'redirect_uri' => env('DROPBOX_REDIRECT_URI')
            ];

            $response = $this->api_client->request(
                'POST',
                '/1/oauth2/token',
                ['form_params' => $data]
            );

            $response_body = json_decode($response->getBody(), true);
            $access_token = $response_body['access_token'];

            session(['access_token' => $access_token]);

            return redirect('dashboard');
        }

        return redirect('/');
    }
}

Breaking down the code above, define a private variable called $api_client. This stores the reference to the Guzzle client that’s returned by calling the api method in the Dropbox class.

private $api_client;

public function __construct(Dropbox $dropbox)
{
    $this->api_client = $dropbox->api();
}

The index method returns the index view:

public function index()
{
    return view('index');
}

The index view (resources/views/index.blade.php) contains the form that allows the user to log into Dropbox. It also has a hidden field which is used for the CSRF token (used for preventing Cross-Site Request Forgery attacks).

@extends('layouts.default')

@section('content')
<form method="POST">
    <input type="hidden" name="_token" value="{{{ csrf_token() }}}" />
    <button class="button">Login with Dropbox</button>
</form>
@stop

The index view inherits from the default template (resources/views/layouts/default.blade.php). Inside the #wrapper div is where the login form is rendered.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ env('APP_TITLE') }}</title>
    <link rel="stylesheet" href="{{ url('assets/lib/picnic/picnic.min.css') }}">
    <link rel="stylesheet" href="{{ url('assets/css/style.css') }}">
</head>
<body>
    <div id="wrapper">
        <h1>{{ env('APP_TITLE') }}</h1>
        @yield('content')
    </div>
</body>
</html>

This template uses picnic css to make things look nicer. The main stylesheet (public/assets/css/style.css) contains some basic styling for the main wrapper and alert boxes:

#wrapper {
    width: 900px;
    margin: 0 auto;
}

.alert {
    padding: 20px;
}

.alert-danger {
    background-color: #F55;
}

.alert-success {
    background-color: #60B152;
}

Going back to the HomeController: you have the postIndex method which is responsible for generating the Dropbox login URL. This is where you use the Purl library to construct the login URL.

public function postIndex()
{
    $url = new Url('https://www.dropbox.com/1/oauth2/authorize');

    $url->query->setData([
        'response_type' => 'code',
        'client_id' => env('DROPBOX_APP_KEY'),
        'redirect_uri' => env('DROPBOX_REDIRECT_URI')
    ]);

    return redirect($url->getUrl());
}

The following are the required query parameters for the login URL:

  • response_type – what Dropbox will append to the redirect URL that you specify. This can have a value of either token or code. Using token means that the access token will be passed as a hash in the redirect URL. This is useful for client side apps but useless in this case since you’re mainly working in the server side, so use code instead. This passes a unique code to the redirect URL as a query parameter. This code can then be used to exchange for an access token. Note that you cannot have a response_type of token if you have disallowed implicit grant in your app settings.
  • client_id – your Dropbox app key.
  • redirect_uri – this can be one of the redirect URLs that you’ve specified in your app settings.

Once that’s done, redirect to the final URL that’s generated by Purl.

Next is the login method. This is responsible for processing the request in the redirect URL that you’ve specified. This is where the authorization code is being passed. You can get the code by using the Request class so you’re injecting it to the login method.

public function login(Request $request)
{
    ...
}

Inside the method, check if the code is passed into the URL. If it’s not, redirect back to the home page.

if ($request->has('code')) {
    ...
}

return redirect('/');

Next, add all the parameters required by the API endpoint for acquiring an access token. This includes the code which is the authorization code that’s passed in the URL. The grant_type is always authorization_code: this is the code that’s passed in the URL that you can use to exchange for an access token. The client_id and client_secret are the Dropbox app key and secret. The redirect_uri is the redirect URL.

$data = [
    'code' => $request->input('code'),
    'grant_type' => 'authorization_code',
    'client_id' => env('DROPBOX_APP_KEY'),
    'client_secret' => env('DROPBOX_APP_SECRET'),
    'redirect_uri' => env('DROPBOX_REDIRECT_URI')
];

Make a POST request to the /1/oauth2/token endpoint and pass the $data as form parameters:

$response = $this->api_client->request(
    'POST', 
    '/1/oauth2/token', 
    [
        'form_params' => $data
        ...
    ]
));

Extract the access token from the response body, set it in the session then redirect to the admin dashboard page:

$response_body = json_decode($response->getBody(), true);
$access_token = $response_body['access_token'];

session(['access_token' => $access_token]);

return redirect('dashboard');

AdminController

Once the user is logged in, all the requests are handled by the AdminController (app/Http/Controllers/AdminController.php) which contains the following code:

<?php
namespace App\Http\Controllers;

use App\Dropbox;
use Illuminate\Http\Request;

class AdminController extends Controller
{
    private $api_client;
    private $content_client;
    private $access_token;

    public function __construct(Dropbox $dropbox)
    {
        $this->api_client = $dropbox->api();
        $this->content_client = $dropbox->content();
        $this->access_token = session('access_token');
    }

    public function dashboard()
    {
        return view('admin.dashboard');
    }

    public function user()
    {
        $response = $this->api_client->request('POST', '/2/users/get_current_account', [
            'headers' => [
                'Authorization' => 'Bearer ' . $this->access_token
            ]
        ]);

        $user = json_decode($response->getBody(), true);

        $page_data = [
            'user' => $user
        ];

        return view('admin.user', $page_data);
    }

    public function search(Request $request)
    {
        $page_data = [
            'path' => '',
            'query' => '',
            'matches' => []
        ];

        if ($request->has('path') && $request->has('query')) {
            $path = $request->input('path');
            $query = $request->input('query');

            $data = json_encode(
                [
                    'path' => $path,
                    'mode' => 'filename',
                    'query' => $query
                ]
            );

            $response = $this->api_client->request(
                'POST', '/2/files/search',
                [
                    'headers' => [
                        'Authorization' => 'Bearer ' . $this->access_token,
                        'Content-Type' => 'application/json'
                    ],
                    'body' => $data
            ]);

            $search_results = json_decode($response->getBody(), true);
            $matches = $search_results['matches'];

            $page_data = [
                'path' => $path,
                'query' => $query,
                'matches' => $matches
            ];
        }

        return view('admin.search', $page_data);
    }

    public function revisions(Request $request)
    {
        if ($request->has('path')) {
            $path = $request->input('path');
            $data = json_encode([
                'path' => $path
            ]);

            $response = $this->api_client->request(
                'POST', '/2/files/list_revisions',
                [
                    'headers' => [
                        'Authorization' => 'Bearer ' . $this->access_token,
                        'Content-Type' => 'application/json'
                    ],
                'body' => $data
            ]);

            $revisions = json_decode($response->getBody(), true);

            $page_data = [
                'revisions' => $revisions['entries'],
                'path' => $path
            ];

            return view('admin.revisions', $page_data);

        } else {
            return redirect('search');
        }
    }

    public function restoreRevision(Request $request)
    {
        if ($request->has('path') && $request->has('rev')) {

            $path = $request->input('path');
            $rev = $request->input('rev');

            $data = json_encode([
                'path' => $path,
                'rev' => $rev
            ]);

            $response = $this->api_client->request(
                'POST', '/2/files/restore',
                [
                    'headers' => [
                        'Authorization' => 'Bearer ' . $this->access_token,
                        'Content-Type' => 'application/json'
                    ],
                    'body' => $data
            ]);

            $response_data = json_decode($response->getBody(), true);

            if (!empty($response_data)) {
                return redirect("revisions?path={$path}")
                    ->with('message', [
                        'type' => 'success',
                        'text' => "File has been restored to the following revision: {$response_data['rev']}"
                    ]);
            } else {
                return redirect("revisions?path={$path}")
                    ->with('message', [
                        'type' => 'danger',
                        'text' => 'The revision request failed. Please try again'
                    ]);
            }

        } else {
            return redirect('search');
        }
    }

    public function download(Request $request)
    {
        if ($request->has('path')) {
            $path = $request->input('path');
            $data = json_encode([
                'path' => $path
            ]);

            $response = $this->content_client->request(
                'POST',
                '/2/files/download',
                [
                    'headers' => [
                        'Authorization' => 'Bearer ' . $this->access_token,
                        'Dropbox-API-Arg' => $data
                    ]
            ]);

            $result = $response->getHeader('dropbox-api-result');
            $file_info = json_decode($result[0], true);

            $content = $response->getBody();

            $filename = $file_info['name'];
            $file_extension = substr($filename, strrpos($filename, '.'));
            $file = uniqid() . $file_extension;

            $file_size = $file_info['size'];

            return response($content)
                ->header('Content-Description', 'File Transfer')
                ->header('Content-Disposition', "attachment; filename={$file}")
                ->header('Content-Transfer-Encoding', 'binary')
                ->header('Connection', 'Keep-Alive')
                ->header('Content-Length', $file_size);
        } else {
            return redirect('search');
        }
    }

    public function upload()
    {
        return view('admin.upload');
    }

    public function doUpload(Request $request)
    {
        if ($request->hasFile('file') && $request->has('path')) {

            $valid_mimetypes = [
                'image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/bmp'
            ];

            $valid_size = 5000000; //5Mb

            $mime_type = $request->file('file')->getMimeType();
            $size = $request->file('file')->getSize();
            $filename = $request->file('file')->getClientOriginalName();
            $path = $request->input('path') . '/' . $filename;

            if (in_array($mime_type, $valid_mimetypes) && $size <= $valid_size) {

                $data = json_encode([
                    'path' => $path,
                    'mode' => 'add',
                    'autorename' => true,
                    'mute' => false
                ]);

                $response = $this->content_client->request(
                    'POST', '/2/files/upload',
                    [
                        'headers' => [
                            'Authorization' => 'Bearer ' . $this->access_token,
                            'Content-Type' => 'application/octet-stream',
                            'Dropbox-API-Arg' => $data
                        ],
                        'body' => fopen($request->file('file'), 'r')
                ]);

                $response_data = json_decode($response->getBody(), true);

                if (!empty($response_data['name'])) {

                    $name = $response_data['name'];
                    return redirect('upload')
                        ->with('message', [
                            'type' => 'success',
                            'text' => "File with the name {$name} was uploaded!"
                        ]);
                }

            }

        }

        return redirect('upload')
                    ->with('message', [
                        'type' => 'danger',
                        'text' => 'There was a problem uploading the file'
                    ]);
    }
}

Breaking down the code above, inside the class you declare three private variables and initialize them in the constructor. These are the API client, content client, and the access token.

private $api_client;
private $content_client;
private $access_token;

public function __construct(Dropbox $dropbox)
{
    $this->api_client = $dropbox->api();
    $this->content_client = $dropbox->content();
    $this->access_token = session('access_token');
}

Next is the method for serving the dashboard view:

public function dashboard()
{
    return view('admin.dashboard');
}

The dashboard view (resources/views/admin/dashboard.blade.php) contains the following code. It links to all the pages which allows the users to perform different operations in the API:

@extends('layouts.admin')
@section('content')
<h3>What do you like to do?</h3>
<ul>
    <li><a href="/user">View User Info</a></li>
    <li><a href="/search">Search Files</a></li>
    <li><a href="/upload">Upload Files</a></li>
</ul>
@stop

All views served by the AdminController inherit from the admin template (resources/views/layouts/admin.blade.php). Just like the default template from earlier, this uses picnic css and some base styling. The only difference is that you’re including the alert partial in the page.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ env('APP_TITLE') }}</title>
    <link rel="stylesheet" href="{{ url('assets/lib/picnic/picnic.min.css') }}">
    <link rel="stylesheet" href="{{ url('assets/css/style.css') }}">
</head>
<body>
    <div id="wrapper">
        <h1>{{ env('APP_TITLE') }}</h1>
        @include('partials.alert')
        @yield('content')
    </div>
</body>
</html>

The alert partial (resources/views/partials/alert.blade.php) is used for outputting any data that’s flashed in the session (temporary data that’s immediately deleted once the request is completed). The alert partial contains the following code:

@if(session('message'))
    <div class="alert alert-{{ session('message.type') }}">
        {{ session('message.text') }}
    </div>
@endif

Getting the User Data

Going back to the AdminController, you have the user method. What this does is make a POST request to the API endpoint which returns the current user’s data. This data is then passed to the user view (resources/views/admin/user.blade.php).

public function user()
{
    $response = $this->api_client->request('POST', '/2/users/get_current_account', [
        'headers' => [
            'Authorization' => 'Bearer ' . $this->access_token
        ]
    ]);

    $user = json_decode($response->getBody(), true);

    $page_data = [
        'user' => $user
    ];

    return view('admin.user', $page_data);
}

The user view shows user data such as the account ID, name and email:

@extends('layouts.admin')
@section('content')
<h3>User Info</h3>
<ul>
    <li>Account ID: {{ $user['account_id'] }}</li>
    <li>Name: {{ $user['name']['display_name'] }}</li>
    <li>Email: {{ $user['email'] }}</li>
    <li>Referral Link: <a href="{{ $user['referral_link'] }}">{{ $user['referral_link'] }}</a></li>
    <li>Account Type: {{ $user['account_type']['.tag'] }}</li>
</ul>
@stop

Searching Files

Next is the method for rendering the view for searching files in the user’s Dropbox account:

public function search(Request $request)
{
    ...
}

Inside the method, initialize the default page data:

$page_data = [
    'path' => '',
    'query' => '',
    'matches' => []
];

Check if the path and the query have been passed as a query parameters. The path is the path where the search will be performed. This needs to be an existing path in the user’s Dropbox (e.g /Files, /Documents, /Public). The forward slash is necessary to indicate that you’re starting from the root directory. The query is the name of the file that the user wants to look for. This doesn’t have to be the exact file name.

if ($request->has('path') && $request->has('query')) {
    ...
}

If the path and query are present, assign them to their own variables and add them to the JSON encoded data required by the search request. The mode that you’re passing along is the type of search that you want to perform. This can be filename, filename_and_content, or deleted_filename.

$path = $request->input('path');
$query = $request->input('query');

$data = json_encode(
    [
        'path' => $path,
        'mode' => 'filename',
        'query' => $query
    ]
);

Send the request:

$response = $this->api_client->request(
    'POST', '/2/files/search', 
    [
        'headers' => [
            'Authorization' => 'Bearer ' . $this->access_token,
            'Content-Type' => 'application/json'
        ],
        'body' => $data
]);

Once you get the response back, extract the response body, convert it to an array then extract the matches that were found. The path and query are also passed in so the user can see what the current query is.

$search_results = json_decode($response->getBody(), true);
$matches = $search_results['matches'];

$page_data = [
    'path' => $path,
    'query' => $query,
    'matches' => $matches
];

Render the search view:

return view('admin.search', $page_data);

The search view (resources/views/admin/search.blade.php) contains the following:

@extends('layouts.admin')
@section('content')
<h3>Search</h3>
<form method="GET">
    <p>
        <label for="path">Path</label>
        <input type="text" name="path" id="path" value="{{ $path }}">
    </p>
    <p>
        <label for="query">Query</label>
        <input type="search" name="query" id="query" value="{{ $query }}">
    </p>
    <button>Search</button>
</form>

@if(count($matches) > 0)
<h5>Search Results</h5>
<table>
    <thead>
        <tr>
            <th>Filename</th>
            <th>Revisions</th>
            <th>Download</th>
        </tr>
    </thead>
    <tbody>
        @foreach($matches as $match)
        <tr>
            <td>{{ $match['metadata']['name'] }}</td>
            <td><a href="/revisions?path={{ urlencode($match['metadata']['path_lower']) }}">view</a></td>
            <td><a href="/download?path={{ urlencode($match['metadata']['path_lower']) }}">download</a></td>
        </tr>
        @endforeach
    </tbody>
</table>
@endif
@stop

From the above code, you can see that you have a form which submits the data via the GET method. If the number of $matches is greater than 0, render the search results table. The table displays the filename, a link for showing the revisions in the file, and a download link for downloading the file. In those two links, you’re passing in the lowercase version of the file path since it’s the required parameter for the revisions and download requests.

File Revisions

The revisions method is responsible for listing the different revisions of a file. This checks if the path is present in the request and just redirects back to the search page if it’s not.

public function revisions(Request $request)
{
    if ($request->has('path')) {
        ...
    } else {
        return redirect('search');
    }
}

Otherwise, it uses the path as a parameter for the request:

$path = $request->input('path');

$data = json_encode([
    'path' => $path
]);

Then, make the request, extract the data from the response body, convert it to an array and pass it as the data for the revisions view.

$response = $this->api_client->request(
    'POST', '/2/files/list_revisions', 
    [
        'headers' => [
            'Authorization' => 'Bearer ' . $this->access_token,
            'Content-Type' => 'application/json'
        ],
    'body' => $data
]);

$revisions = json_decode($response->getBody(), true);

$page_data = [
    'revisions' => $revisions['entries'],
    'path' => $path
];

return view('admin.revisions', $page_data);

The revisions view (resources/views/admin/revisions.blade.php) outputs the file path and displays a table containing the revision details such as the revision ID (a unique ID assigned by Dropbox every time a file is revised), the modification timestamp, the size of the file in bytes, and a link for restoring a specific revision.

@extends('layouts.admin')
@section('content')
<h3>Revisions</h3>
<strong>File: </strong> {{ $path }}
<table>
    <thead>
        <tr>
            <th>Revision ID</th>
            <th>Modified</th>
            <th>Size (Bytes)</th>
            <th>Restore</th>
        </tr>
    </thead>
    <tbody>
        @foreach($revisions as $rev)
        <tr>
            <td>{{ $rev['rev'] }}</td>
            <td>{{ Carbon\Carbon::parse($rev['server_modified'])->setTimezone(env('APP_TIMEZONE'))->toDayDateTimeString() }}</td>
            <td>{{ $rev['size'] }}</td>
            <td><a href="/restore?path={{ urlencode($rev['path_lower']) }}&rev={{ $rev['rev'] }}">restore</a></td>
        </tr>
        @endforeach
    </tbody>
</table>
@stop

Note that this is where you use the Carbon library to express the timestamp into the timezone that was added in the .env file. Also note that the lowercase file path and the revision ID are passed in as query parameters to the route for restoring a specific file revision.

Restoring Revisions

The restoreRevision method lists the different revisions of a file. The API endpoint /2/files/restore requires both the file path and the revision ID, so you check if they were passed in the request. If not, then simply redirect back to the search page.

public function restoreRevision(Request $request)
{
    if ($request->has('path') && $request->has('rev')) {
        ...
    } else {
        return redirect('search');
    }
}

If the path and revision ID were passed, add those to the request parameters and then make the request:

$path = $request->input('path');
$rev = $request->input('rev');

$data = json_encode([
    'path' => $path,
    'rev' => $rev
]);

$response = $this->api_client->request(
    'POST', '/2/files/restore', 
    [
        'headers' => [
            'Authorization' => 'Bearer ' . $this->access_token,
            'Content-Type' => 'application/json'
        ],
        'body' => $data
]);

Once you get the response data back, check if it’s not empty, which means that the revision request was completed. Redirect back with a success message if this happens. If not, redirect back with an error message. To test this, you can open the Dropbox app in your machine. It should immediately sync the restored version if it’s completed.

$response_data = json_decode($response->getBody(), true);

if (!empty($response_data)) {

    return redirect("revisions?path={$path}")
        ->with('message', [
            'type' => 'success',
            'text' => "File has been restored to the following revision: {$response_data['rev']}"
        ]);
} else {

    return redirect("revisions?path={$path}")
        ->with('message', [
            'type' => 'danger',
            'text' => 'The revision request failed. Please try again'
        ]);
}

Downloading Files

Next is the method which handles download requests. Just like the other methods that you’ve used so far, this method also requires the file path.

public function download(Request $request)
{
    if ($request->has('path')) {
        ...
    } else {
        return redirect('search');
    }
}

If the path is present, add it to the request parameters:

$path = $request->input('path');

$data = json_encode([
    'path' => $path
]);

Then, perform the request. Note that this time you’re not passing anything to the body of the request. Instead, you’re passing it in the Dropbox-API-Arg request header.

$response = $this->content_client->request(
    'POST', 
    '/2/files/download', 
    [
        'headers' => [
            'Authorization' => 'Bearer ' . $this->access_token,
            'Dropbox-API-Arg' => $data
        ]
]);

The response body that you get from this request is the binary data of the file itself. The file info is passed in the response header so use the getHeader method provided by Guzzle to extract the header data in the form of an array. The file info is stored in the first item as a JSON string, so use json_decode to convert it to an array.

$result = $response->getHeader('dropbox-api-result');
$file_info = json_decode($result[0], true);

Extract the file contents, generate the file name to be used for the download, and get the file size:

$content = $response->getBody();

$filename = $file_info['name'];
$file_extension = substr($filename, strrpos($filename, '.'));
$file = uniqid() . $file_extension;

$file_size = $file_info['size'];

Return the response data and add the necessary headers so that the browser will treat it as a download request. Note that the uniqid function is used to generate the filename. This is because you’re passing the filename in the header and any illegal character might cause the download request to fail so it serves as a safety measure.

return response($content)
    ->header('Content-Description', 'File Transfer')
    ->header('Content-Disposition', "attachment; filename={$file}")
    ->header('Content-Transfer-Encoding', 'binary')
    ->header('Connection', 'Keep-Alive')
    ->header('Content-Length', $file_size);

Uploading Files

Now proceed with the final operation that you can perform in this app: file uploads. First, render the upload view:

public function upload()
{
    return view('admin.upload');
}

The upload view (resources/views/admin/upload.blade.php) has a form which asks for the path to which the file will be uploaded and the file itself.

@extends('layouts.admin')
@section('content')
<h3>Upload</h3>
<form method="POST" enctype="multipart/form-data">
    <input type="hidden" name="_token" value="{{{ csrf_token() }}}" />
    <p>
        <label for="path">Path</label>
        <input type="text" name="path" id="path">
    </p>
    <p>
        <label for="file">File</label>
        <input type="file" name="file" id="file">
    </p>
    <button>upload</button>
</form>
@stop

Once the form is submitted, it is processed by the doUpload method. This checks if a file has been uploaded and if a path has been specified. If it hasn’t been set, redirect back to the upload page with an error message.

public function doUpload(Request $request)
{
    if ($request->hasFile('file') && $request->has('path')) {
        ...
    }

    return redirect('upload')
                ->with('message', [
                    'type' => 'danger',
                    'text' => 'There was a problem uploading the file'
                ]);
}

If the path and file are present, specify the valid mime types and the valid file size:

$valid_mimetypes = [
    'image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/bmp'
];

$valid_size = 5000000; //5Mb

Get the file info:

$mime_type = $request->file('file')->getMimeType();
$size = $request->file('file')->getSize();

$filename = $request->file('file')->getClientOriginalName();

$path = $request->input('path') . '/' . $filename;

Check if the file has a valid mime type and size. If so, add the request parameters. This includes the path to where the file will be saved in the user’s Dropbox. mode is the action to be performed if the file already exists. This can have a value of either add, overwrite or update. add means that the file will be added but Dropbox will append a number to the filename (file (2).txt, file (3).txt, etc.). Next is autorename, which should always be set to true if the mode is set to add. That way, the new file will be automatically renamed if a file with the same name already exists. The last option is mute which simply tells Dropbox whether to notify the user or not. The default for this option is false which means that the user will be notified by the Dropbox app when a file is uploaded. If this is set to true, the notification is not sent.

if (in_array($mime_type, $valid_mimetypes) && $size <= $valid_size) {

    $data = json_encode([
        'path' => $path,
        'mode' => 'add',
        'autorename' => true,
        'mute' => false
    ]);

}

Make the file upload request. Note that this uses the content client as opposed to the API client since you’re dealing with files and not data. The upload options are passed in the Dropbox-API-Arg header as a JSON string, while the binary data for the file itself is passed in the request body. Also specify the Content-Type to be application/octet-stream as you’re passing in binary data to the request body.

$response = $this->content_client->request(
    'POST', '/2/files/upload', 
    [
        'headers' => [
            'Authorization' => 'Bearer ' . $this->access_token,
            'Content-Type' => 'application/octet-stream',
            'Dropbox-API-Arg' => $data
        ],
        'body' => fopen($request->file('file'), 'r')
]);

Once you get a response back, convert it to an array and check for the filename. If it’s present, redirect back to the upload page and inform the user that the file was uploaded.

$response_data = json_decode($response->getBody(), true);

if (!empty($response_data['name'])) {

    $name = $response_data['name'];

    return redirect('upload')
        ->with('message', [
            'type' => 'success',
            'text' => "File with the name {$name} was uploaded!"
        ]);
}

Conclusion

In this article, you’ve learned how to work with the Dropbox API in PHP. Specifically, you’ve learned how to log into Dropbox, get the current user’s info, search for files, list the file revisions, restore a revision, download a file, and finally upload a file.

You’ve only taken a look at few of the things that you can do with the Dropbox API, so if you want to learn more I recommend you to go check out the Documentation.

Questions? Comments? Leave them below the like button!

  • Tenjou Tenge

    Thx ima try this later!

  • Syarif Maula

    hello sir. can we config it with multiple account ? thanks before

Recommended
Sponsors
Get the latest in PHP, once a week, for free.