You are here

backup_migrate_dropbox.dropbox_api.inc in Backup and Migrate Dropbox 7.2

backup_migrate_dropbox.api.inc

Dropbox api.

File

backup_migrate_dropbox.dropbox_api.inc
View source
<?php

/**
 * @file backup_migrate_dropbox.api.inc
 *
 * Dropbox api.
 */
class BackupMigrateDropboxAPI {
  private $token;
  private $upload_session = array();
  public function setToken($token) {
    $this->token = $token;
  }

  /**
   * Returns the contents of a Dropbox folder.
   *
   * {@link https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder Dropbox API /list_folder}
   *
   * @param string $folder
   *
   * @return object[]
   *   A list of file metadata of the files in the folder.
   *
   * @throws \RuntimeException
   */
  public function list_folder($folder) {

    // Note, I once got this message: Dropbox error: Error in call to API
    // function "files/list_folder": request body: path: Specify the root folder
    // as an empty string rather than as "/". So we'handle that case here.
    if ($folder === '/') {
      $folder = '';
    }

    // Simple listing: using Dropbox defaults:
    // - Not recursive.
    // - No deleted files.
    // - Include mounted files (the app folder is a mounted folder).
    $parameters = array(
      'path' => $folder,
      'include_non_downloadable_files' => TRUE,
    );
    $response = $this
      ->sendMessage('api', 'files/list_folder', $parameters);
    $files = $response->entries;
    while ($response->has_more) {
      $parameters = [
        'cursor' => $response->cursor,
      ];
      $response = $this
        ->sendMessage('api', 'files/list_folder/continue', $parameters);
      $files = array_merge($files, $response->entries);
    }
    return $files;
  }

  /**
   * Creates a folder on Dropbox.
   *
   * {@link https://www.dropbox.com/developers/documentation/http/documentation#files-create_folder}
   *
   * @param string $folder
   *   The folder to create.
   *
   * @return object[]
   *   A list of folder metadata for the created folder.
   *
   * @throws \RuntimeException
   *   The folder could not be created. If that is because it already exists,
   *   the exception message will contain something like
   *   '... path/conflict/folder/.. ...'.
   */
  public function create_folder($folder) {
    if ($folder[0] !== '/') {
      $folder = '/' . $folder;
    }
    $parameters = array(
      'path' => $folder,
      'autorename' => FALSE,
    );
    return $this
      ->sendMessage('api', 'files/create_folder', $parameters);
  }

  /**
   * Downloads the file from the given Dropbox $path.
   *
   * {@link https://www.dropbox.com/developers/documentation/http/documentation#files-download Dropbox API /download}
   *
   * @param string $path
   *   Path of file to download.
   * @return string
   *   The contents of the requested file.
   *
   * @throws \RuntimeException
   */
  public function file_download($path) {
    $parameters = array(
      'path' => $path,
    );
    return $this
      ->sendMessage('content', 'files/download', $parameters);
  }

  /**
   * Uploads a file to the given path.
   *
   * If the upload is larger then:
   * - what Dropbox can handle per request.
   * - or the internal memory available to PHP
   * The upload is split into multiple smaller chunks, otherwise it is uploaded
   * in 1 part.
   *
   * @param string $file
   *   Name of local file to upload its contents from.
   * @param string $path
   *   Path on Dropbox (including the file name) to upload the file contents to.
   *
   * @return object
   *   The json decoded response.
   *
   * @throws \RuntimeException
   */
  public function file_upload($file, $path) {

    // Cut PHP memory limit by 10% to allow for other in memory data.
    $php_memory_limit = intval($this
      ->byte_size(ini_get('memory_limit')) * 0.9);

    // Dropbox currently has a 150M upload limit per transaction.
    $dropbox_upload_limit = $this
      ->byte_size('150M');

    // For testing or in case the 10% leeway isn't enough allow a smaller upload
    // limit as an advanced setting. This variable has no ui but can be set with
    // drush or through the variable module.
    $manual_upload_limit = $this
      ->byte_size(variable_get('backup_migrate_dropbox_upload_limit', '150M'));

    // Use the smallest value for the max file size.
    $max_file_size = min($php_memory_limit, $dropbox_upload_limit, $manual_upload_limit);

    // File.
    $file_size = filesize($file);

    // If the file size is greater than
    if ($file_size > $max_file_size) {

      // Open file.
      $file_handle = fopen($file, 'rb');
      if (!$file_handle) {
        throw new RuntimeException('Cannot open backup file (1).');
      }

      // Start.
      $content = fread($file_handle, $max_file_size);
      if (!$content) {
        throw new RuntimeException('Cannot read backup file (2).');
      }
      $this
        ->_file_upload_session_start($content);

      // Append.
      while (!feof($file_handle)) {

        // Get content.
        $content = fread($file_handle, $max_file_size);
        if (!$content) {
          throw new RuntimeException('Cannot read backup file (3).');
        }
        $this
          ->_file_upload_session_append($content);
      }

      // Finish.
      $result = $this
        ->_file_upload_session_finish($path);
    }
    else {
      $content = file_get_contents($file);
      if (!$content) {
        throw new RuntimeException('Cannot open backup file (4).');
      }
      $result = $this
        ->_file_upload_upload($path, $content);
    }
    return $result;
  }

  /**
   * Starts a multi-request upload.
   *
   * {@link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-start Dropbox API /upload_session/start}
   *
   * @param string $content
   *
   * @return object
   *   The json decoded response.
   *
   * @throws \RuntimeException
   */
  private function _file_upload_session_start($content) {
    $result = $this
      ->sendMessage('content', 'files/upload_session/start', array(), $content);
    if (!isset($result->session_id)) {
      throw new RuntimeException('No session id returned.');
    }
    $this->upload_session['session_id'] = $result->session_id;
    $this->upload_session['offset'] = strlen($content);
    return $result;
  }

  /**
   * Uploads 1 part of a multi-request upload.
   *
   * {@link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-append Dropbox API /upload_session/append}
   * @param string $content
   *
   * @return object
   *   The json decoded response.
   *
   * @throws \RuntimeException
   */
  private function _file_upload_session_append($content) {
    $parameters = array(
      'cursor' => $this->upload_session,
    );
    $result = $this
      ->sendMessage('content', 'files/upload_session/append_v2', $parameters, $content);
    $this->upload_session['offset'] += strlen($content);
    return $result;
  }

  /**
   * Ends a multi-request upload.
   *
   * {@link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-finish Dropbox API /upload_session/finish}
   *
   * @param string $path
   *
   * @return object
   *   The json decoded response.
   *
   * @throws \RuntimeException
   */
  private function _file_upload_session_finish($path) {
    $parameters = array(
      'cursor' => $this->upload_session,
      'commit' => array(
        'path' => $path,
        'mode' => 'add',
        'autorename' => TRUE,
        'mute' => TRUE,
      ),
    );
    return $this
      ->sendMessage('content', 'files/upload_session/finish', $parameters);
  }

  /**
   * Uploads the $contents of a file (with 1 request) to the indicated $path.
   *
   * {@link https://www.dropbox.com/developers/documentation/http/documentation#files-upload Dropbox API /upload}
   *
   * @param string $path
   * @param string $content
   *
   * @return object
   *   The json decoded response.
   *
   * @throws \RuntimeException
   */
  private function _file_upload_upload($path, $content) {

    // Simple upload.
    $parameters = array(
      'path' => $path,
      'mode' => 'add',
      'autorename' => TRUE,
      'mute' => FALSE,
    );
    return $this
      ->sendMessage('content', 'files/upload', $parameters, $content);
  }

  /**
   * Deletes the file at the given $path.
   *
   * {@link https://www.dropbox.com/developers/documentation/http/documentation#files-delete Dropbox API /delete}
   *
   * @param string $path
   *
   * @return object
   *   The json decoded response.
   *
   * @throws \RuntimeException
   */
  public function file_delete($path) {
    $parameters = array(
      'path' => $path,
    );
    return $this
      ->sendMessage('api', 'files/delete_v2', $parameters);
  }

  /**
   * Sends a request to Dropbox and returns the response.
   *
   * @param string $endpointType
   *   This type determines the url to use and how to process parameters. It can
   *   be one of:
   *   - 'api'
   *   - 'content'
   *   More info about end points can be found at:
   *   https://www.dropbox.com/developers/documentation/http/documentation#formats
   * @param string $command
   * @param array|null $parameters
   * @param string|null $content
   *   File contents for the request.
   *
   * @return object
   *   The json decoded response.
   *
   * @throws \RuntimeException
   */
  private function sendMessage($endpointType, $command, $parameters = null, $content = null) {

    // Prepare the request: url, headers and the body
    $url = "https://{$endpointType}.dropboxapi.com/2/{$command}";
    $headers = array();
    $headers[] = 'Content-type: ' . ($endpointType === 'api' ? 'application/json; charset=utf-8' : 'application/octet-stream');
    $headers[] = 'Authorization: Bearer ' . $this->token;
    $headers[] = 'Accept: application/json, application/octet-stream';

    // Content end points may have real content in the body and therefore expect
    // the parameters in the 'Dropbox-API-Arg' header.
    if ($endpointType === 'content' && !empty($parameters)) {
      $headers[] = 'Dropbox-API-Arg: ' . json_encode($parameters);
    }

    // Api endpoints expect the parameters in the body as a json encoded string,
    // otherwise any passed in $contents is placed in the body.
    $body = $endpointType === 'api' && !empty($parameters) ? json_encode($parameters) : $content;
    $response = $this
      ->sendHttpRequest($url, $headers, $body);
    if ($this
      ->isJsonResponse($endpointType, $command)) {
      $result = json_decode($response, FALSE);
      if ($result === NULL) {

        // No json was returned, but (probably) a plain error message. E.g, I
        // once got the string 'Incorrect host for API function
        // "files/list_folder". You must issue the request to
        // "api.dropboxapi.com".' as response.
        throw new RuntimeException("Dropbox error: {$response}");
      }
      elseif (isset($result->error_summary)) {
        throw new RuntimeException("Dropbox error: {$result->error_summary}");
      }
    }
    else {

      // Plain result: no decoding needed.
      $result = $response;
    }
    return $result;
  }

  /**
   * Executes a curl request.
   *
   * @param string $url
   * @param array $headers
   * @param string $body
   *
   * @return string
   *   The response.
   *
   * @throws \RuntimeException
   *   On any error at the curl level, an exception will be thrown.
   */
  private function sendHttpRequest($url, $headers, $body) {
    $options = [
      CURLOPT_URL => $url,
      CURLOPT_HTTPHEADER => $headers,
      CURLOPT_POST => TRUE,
      CURLOPT_RETURNTRANSFER => TRUE,
    ];
    if (!empty($body)) {
      $options[CURLOPT_POSTFIELDS] = $body;
    }
    $request = curl_init();
    curl_setopt_array($request, $options);
    $response = curl_exec($request);
    if ($response === FALSE) {
      $response_code = curl_getinfo($request, CURLINFO_HTTP_CODE);
      if (curl_error($request)) {
        throw new RuntimeException('Curl error: ' . curl_error($request));
      }
      elseif ($response_code >= 500) {
        throw new RuntimeException('Dropbox server error. Try later or check status.dropbox.com for outages.');
      }
      else {
        throw new RuntimeException("Error: http response code: {$response_code}");
      }
    }
    curl_close($request);
    return $response;
  }

  /**
   * Returns whether the response should be a json string or file contents.
   *
   * - 'api' endpoints always return json.
   * - 'content' endpoints may return:
   *   - File contents (files/download).
   *   - A json encoded object.
   *   - Nothing (files/upload_session/append_v2).
   *
   * @param string $endpointType
   * @param string $command
   *
   * @return bool
   *   True if the response is expected to be a json string, false otherwise.
   */
  private function isJsonResponse($endpointType, $command) {
    return $endpointType === 'api' || !in_array($command, [
      'files/download',
      'files/upload_session/append_v2',
    ]);
  }

  // Pulled from Stack Overflow: http://stackoverflow.com/q/1336581/819883.
  private function byte_size($byteString) {
    preg_match('/^\\s*([0-9.]+)\\s*([KMGT])B?\\s*$/i', $byteString, $matches);
    if (!(count($matches) >= 3)) {
      return 0;
    }
    $num = (double) $matches[1];
    switch (strtoupper($matches[2])) {

      /** @noinspection PhpMissingBreakStatementInspection */
      case 'T':
        $num *= DRUPAL_KILOBYTE;

      /** @noinspection PhpMissingBreakStatementInspection */
      case 'G':
        $num *= DRUPAL_KILOBYTE;

      /** @noinspection PhpMissingBreakStatementInspection */
      case 'M':
        $num *= DRUPAL_KILOBYTE;
      case 'K':
        $num *= DRUPAL_KILOBYTE;
    }
    return intval($num);
  }

}

Classes

Namesort descending Description
BackupMigrateDropboxAPI @file backup_migrate_dropbox.api.inc