Rotate Dropbox Backups with PHP

MySQL Dropbox DB Integration

Today we are going to learn how to rotate Dropbox backups using PHP. Previously we demonstrated how to backup MySQL databases by using PHP and CRON jobs to upload the backups to Dropbox. (https://www.inboundhorizons.com/automatically-backup-mysql-mariadb-databases-to-dropbox-using-php/). We have been successfully backing up our database to Dropbox for years using this script. But over the years the number of backups in Dropbox grew until it started to cause us some problems. Because of the sheer number of files stored in our Dropbox folder it became difficult to access the most recent backups at the bottom of the list.

Next, we found a second issue was with the Dropbox storage space itself. Although our compressed backups were fairly small we had generated thousands after years of backups which filled gigabytes of space in Dropbox. Realistically we did not need to keep years of backups and only needed to keep backups going back a few weeks. For this reason we wrote a second PHP script that removes any backups more than 90 days old in order to keep the number of Dropbox files manageable and storage costs down.

Prerequisites

It is assumed that you have already created a Dropbox app and have been uploading files to Dropbox regularly. You will need the access token of Dropbox app as well as the directory you uploaded your files to. You can head over to https://www.dropbox.com/developers/apps to retrieve the access token if you do not remember where you stored it.

Declare script variables

Before writing our code we need to setup this script by declaring the primary code at the top of the file. This code will be our main variables and the 3 functions we will call to remove the outdated files. Afterwards we will actually define the functions.

 
// Global access key for DropBox
$ACCESS_TOKEN = 'YOUR_DROPBOX_ACCESS_TOKEN';
 
// The path in Dropbox where the files are stored
$DROPBOX_FILE_PATH = "/DROPBOX_DIRECTORY/";
 
// The filenames are all prefixed with this
$PREFIX = "backup_db_";
 
// How many days to keep backups before deleting them
$BACKUP_DAYS = 90;
 
 
// 1.) Send a request to get the files in the backup folder
// 2.) Find the old files that need to be deleted
// 3.) Send a request to delete the old files
$all_files = GetBackupFiles($ACCESS_TOKEN, $DROPBOX_FILE_PATH);
$files_to_delete = GetOldFiles($BACKUP_DAYS, $PREFIX, $all_files);
$async_job_ID = DeleteFiles($ACCESS_TOKEN, $files_to_delete); 

Step 1: Retrieve a list of all files

The first function in our script merely retrieves a list of all the files in Dropbox. By retrieving this list of files we can examine the files’ metadata and determine which files to keep and which to remove. This first step actually consists of two functions; the primary function and a helper function which helps retrieve files if Dropbox resorts to paginating the results.

 

function GetBackupFiles($access_token, $dropbox_file_path) {
    
    $files = array(); // The files that we will return
    $web_response = array(); // The responses that we get from the DropBox API
    
    
    $dropbox_endpoint_url = 
        "https://api.dropboxapi.com/2/files/list_folder";
    
    $parameters = array(                    
        "path" => $dropbox_file_path,
        "recursive" => false,
        "include_media_info" => false,
        "include_deleted" => false,
        "include_has_explicit_shared_members" => false,
        "include_mounted_folders" => true
    );
    
    $ch = curl_init();

    curl_setopt($ch, CURLOPT_URL, $dropbox_endpoint_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($parameters));


    $headers = array();
    $headers = array(
        "Authorization: Bearer " . $access_token,
        'Content-Type: application/json'
    );
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

    $response = curl_exec($ch);
    if (curl_errno($ch)) {
        echo 'Error: ' . curl_error($ch);
    }
    curl_close ($ch);
    
    
    try {
        // Decode the JSON response from DropBox
        $web_response = json_decode($response, true);
    }
    catch (Exception $e) {
        echo "Error getting bDropBox files: ";
        echo $e->getMessage() . PHP_EOL;
        echo print_r($web_response, true) . PHP_EOL;
    }
    
    
    
    try {                   
        // Get only the files from the JSON response
        $files = array_merge($files, $web_response['entries']); 
        
        // While there are more files to get...
        while ((count($web_response) > 0) && $web_response['has_more']) { 
            // Get the extra files
            $cursor = $web_response['cursor'];
            $web_response = GetMoreFiles($access_token, $cursor);   
            // Add the files to the file array
            $files = array_merge($files, $web_response['entries']); 
        }                       
    }
    catch (Exception $e) {
        echo "Error parsing backup files from web response: ";
        echo $e->getMessage() . PHP_EOL;
        echo print_r($web_response, true) . PHP_EOL;
    }
    
    
    
    return ($files);
}

function GetMoreFiles($access_token, $cursor) {
    $web_response = array();
    
    $dropbox_endpoint_url = 
        "https://api.dropboxapi.com/2/files/list_folder/continue";
    
    $parameters = array(                    
        "cursor" => $cursor,
    );
    
    $ch = curl_init();

    curl_setopt($ch, CURLOPT_URL, $dropbox_endpoint_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($parameters));


    $headers = array();
    $headers = array(
        "Authorization: Bearer " . $access_token,
        'Content-Type: application/json'
    );
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

    $response = curl_exec($ch);
    if (curl_errno($ch)) {
        echo 'Error: ' . curl_error($ch);
    }
    curl_close ($ch);
    
    
    try {
        $web_response = json_decode($response, true);
    }
    catch (Exception $e) {
        echo "Error getting extra backup files: ";
        echo $e->getMessage() . PHP_EOL;
        echo print_r($web_response, true) . PHP_EOL;
    }

    return ($web_response);
}

Step 2: Find the old backups that should be rotated

We will take the list of all Dropbox files we retrieved in the previous step and examine their metadata. If a file’s last modified date was more than 90 days ago then it should be deleted.

 
function GetOldFiles($backup_days, $prefix, $files) {
    $old_file_paths = array(); // The filepaths of the files we will delete
    
    // Get only the date; not the datetime
    $cutoff_datetime = date("Y-m-d", strtotime('-'.$backup_days.' days'));    
    
    // Convert the date into Unix seconds
    $cutoff_date = strtotime($cutoff_datetime);    
    
    foreach ($files as $file) {
        // Datetime from Dropbox's "server_modified" metadata
        $file_modified_datetime = strtotime($file['server_modified']);    
        
        // Does the file need to be removed?
        $is_old_file = ($file_modified_datetime < $cutoff_date);    
        
        if ($is_old_file) {    // If the file is old...
            // Generate an entry for the file
            $entry = array("path" => $file['path_display']);    
            // Add the file entry to the array
            array_push($old_file_paths, $entry);    
        }
        
    }
    
    return ($old_file_paths);    // Return the file path entries
}
    

Step 3: Send an HTTP request to Dropbox to delete the old files

The final step is to remove the files from Dropbox. We have a list of old files generated from the previous step and now we need to send that list to Dropbox.

 

function DeleteFiles($access_token, $file_path_entries) {
    $async_job_ID = -1;
    $web_response = array();
    
    $dropbox_endpoint_url = 
        "https://api.dropboxapi.com/2/files/delete_batch";
    
    $parameters = array(                    
        "entries" => $file_path_entries,
    );
    
    $ch = curl_init();

    curl_setopt($ch, CURLOPT_URL, $dropbox_endpoint_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($parameters));


    $headers = array();
    $headers = array(
        "Authorization: Bearer " . $access_token,
        'Content-Type: application/json'
    );
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

    $response = curl_exec($ch);
    if (curl_errno($ch)) {
        echo 'Error: ' . curl_error($ch);
    }
    curl_close ($ch);
    
    
    try {
        $web_response = json_decode($response, true);
        $async_job_ID = $web_response['async_job_id'];
    }
    catch (Exception $e) {
        echo "Error getting response about deleted files: ";
        echo $e-&gt;getMessage() . PHP_EOL;
        echo print_r($web_response, true) . PHP_EOL;
    }

    return ($async_job_ID);
}
 

All Together

Save all of the code created thus far in a PHP file on your server. For example, ROTATE_DROPBOX_BACKUPS.php would be an excellent filename. Be sure to remember where you store it on your server. Then we will schedule the script to run and rotate the backup files every 24 hours on the server. To add a new CRON job enter the following command.

crontab -e

Next, add the following line at the bottom of the CRON entry to run the script every day at 1am and save the changes.

 0 1 * * * php /path/to/ROTATE_DROPBOX_BACKUPS.php 

Conclusion – Rotate Dropbox Backups

Hopefully you found this script as useful as we did to delete or rotate Dropbox backups. When you need a backup during a crisis the last thing you want to do is wait while Dropbox tries loading all your files. Likewise, you don’t want to be paying for storage space in Dropbox that is unnecessary. This script solves both those problems at once. Here is the final script all together.

<?php
// Global access key for DropBox
$ACCESS_TOKEN = 'YOUR_DROPBOX_ACCESS_TOKEN';
// The path in Dropbox where the files are stored
$DROPBOX_FILE_PATH = "/DROPBOX_DIRECTORY/";
// The filenames are all prefixed with this
$PREFIX = "backup_db_";
// How many days to keep backups before deleting them
$BACKUP_DAYS = 90;
// 1.) Send a request to get the files in the backup folder
// 2.) Find the old files that need to be deleted
// 3.) Send a request to delete the old files
$all_files = GetBackupFiles($ACCESS_TOKEN, $DROPBOX_FILE_PATH);
$files_to_delete = GetOldFiles($BACKUP_DAYS, $PREFIX, $all_files);
$async_job_ID = DeleteFiles($ACCESS_TOKEN, $files_to_delete);
function GetBackupFiles($access_token, $dropbox_file_path) {
    
    $files = array(); // The files that we will return
    $web_response = array(); // The responses that we get from the DropBox API
    
    
    $dropbox_endpoint_url = 
        "https://api.dropboxapi.com/2/files/list_folder";
    
    $parameters = array(                    
        "path" => $dropbox_file_path,
        "recursive" => false,
        "include_media_info" => false,
        "include_deleted" => false,
        "include_has_explicit_shared_members" => false,
        "include_mounted_folders" => true
    );
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $dropbox_endpoint_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($parameters));
    $headers = array();
    $headers = array(
        "Authorization: Bearer " . $access_token,
        'Content-Type: application/json'
    );
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    $response = curl_exec($ch);
    if (curl_errno($ch)) {
        echo 'Error: ' . curl_error($ch);
    }
    curl_close ($ch);
    
    
    try {
        // Decode the JSON response from DropBox
        $web_response = json_decode($response, true);
    }
    catch (Exception $e) {
        echo "Error getting bDropBox files: ";
        echo $e->getMessage() . PHP_EOL;
        echo print_r($web_response, true) . PHP_EOL;
    }
    
    
    
    try {                   
        // Get only the files from the JSON response
        $files = array_merge($files, $web_response['entries']); 
        
        // While there are more files to get...
        while ((count($web_response) > 0) && $web_response['has_more']) { 
            // Get the extra files
            $cursor = $web_response['cursor'];
            $web_response = GetMoreFiles($access_token, $cursor);   
            // Add the files to the file array
            $files = array_merge($files, $web_response['entries']); 
        }                       
    }
    catch (Exception $e) {
        echo "Error parsing backup files from web response: ";
        echo $e->getMessage() . PHP_EOL;
        echo print_r($web_response, true) . PHP_EOL;
    }
    
    
    
    return ($files);
}
function GetMoreFiles($access_token, $cursor) {
    $web_response = array();
    
    $dropbox_endpoint_url = 
        "https://api.dropboxapi.com/2/files/list_folder/continue";
    
    $parameters = array(                    
        "cursor" => $cursor,
    );
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $dropbox_endpoint_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($parameters));
    $headers = array();
    $headers = array(
        "Authorization: Bearer " . $access_token,
        'Content-Type: application/json'
    );
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    $response = curl_exec($ch);
    if (curl_errno($ch)) {
        echo 'Error: ' . curl_error($ch);
    }
    curl_close ($ch);
    
    
    try {
        $web_response = json_decode($response, true);
    }
    catch (Exception $e) {
        echo "Error getting extra backup files: ";
        echo $e->getMessage() . PHP_EOL;
        echo print_r($web_response, true) . PHP_EOL;
    }
    return ($web_response);
}
function GetOldFiles($backup_days, $prefix, $files) {
    $old_file_paths = array(); // The filepaths of the files we will delete
    
    // Get only the date; not the datetime
    $cutoff_datetime = date("Y-m-d", strtotime('-'.$backup_days.' days'));    
    
    // Convert the date into Unix seconds
    $cutoff_date = strtotime($cutoff_datetime);    
    
    foreach ($files as $file) {
        // Datetime from Dropbox's "server_modified" metadata
        $file_modified_datetime = strtotime($file['server_modified']);    
        
        // Does the file need to be removed?
        $is_old_file = ($file_modified_datetime < $cutoff_date);    
        
        if ($is_old_file) {    // If the file is old...
            // Generate an entry for the file
            $entry = array("path" => $file['path_display']);    
            // Add the file entry to the array
            array_push($old_file_paths, $entry);    
        }
        
    }
    
    return ($old_file_paths);    // Return the file path entries
}
    
function DeleteFiles($access_token, $file_path_entries) {
    $async_job_ID = -1;
    $web_response = array();
    
    $dropbox_endpoint_url = 
        "https://api.dropboxapi.com/2/files/delete_batch";
    
    $parameters = array(                    
        "entries" => $file_path_entries,
    );
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $dropbox_endpoint_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($parameters));
    $headers = array();
    $headers = array(
        "Authorization: Bearer " . $access_token,
        'Content-Type: application/json'
    );
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    $response = curl_exec($ch);
    if (curl_errno($ch)) {
        echo 'Error: ' . curl_error($ch);
    }
    curl_close ($ch);
    
    
    try {
        $web_response = json_decode($response, true);
        $async_job_ID = $web_response['async_job_id'];
    }
    catch (Exception $e) {
        echo "Error getting response about deleted files: ";
        echo $e->getMessage() . PHP_EOL;
        echo print_r($web_response, true) . PHP_EOL;
    }
    return ($async_job_ID);
}

Sign up to get our latest articles

Don’t worry. We won’t sell your email. We are also really busy managing our clients, so we won’t be filling your inbox with articles every day. We only write them when we have a compelling reason to do so, and some spare time too!