Backup na nuvem com Dropbox e PHP

[Update 01/05/2020]
A versão 1 da API do Dropbox utilizada por esse artigo esta obsoleta. Veja a versão deste artigo utilizando a versão 2 da API, clicando aqui.

Vamos supor que você tenha um sistema que armazene documentos dos usuários em uma pasta no seu servidor, porém para manter uma estrutura mais organizada, você separa os documentos em sub-pastas de acordo com o ano. Normalmente você se preocupa em fazer backup do banco de dados, porém e os arquivos desta pasta?

É indiscutível que em qualquer sistema online que armazene arquivos no servidor possua uma estrutura de backup. Uma opção interessante é manter uma cópia desses arquivos em algum serviço de armazenamento na nuvem. Hoje em dia temos diversos serviços desse tipo, porém falaremos especificamente do Dropbox neste artigo.

Veremos neste artigo como copiar uma pasta que está no servidor para uma pasta no Dropbox de forma recursiva, ou seja, copiando todos os arquivos da pasta e de suas sub-pastas.

Registrando aplicativo no Dropbox

Primeiramente, se você não tem uma conta no Dropbox, deverá criar uma. Após criado a conta e fazer login, você deve ir até a área de desenvolvedores e registrar um novo aplicativo.

Imagem 1 – Criando aplicativo

Como precisamos de acesso aos arquivos, selecionamos a opção Dropbox API (1). No tipo de acesso selecionamos a opção App folder (2), pois não precisamos acessar todos os arquivos, apenas de uma pasta especifica para copiarmos nossos arquivos. Por último, escolhemos um nome (3) único para nosso aplicativo.

Após registrado o aplicativo, você será redirecionado para as configurações dele. Nessa página, precisaremos gerar um token, que será utilizado para termos acesso ao Dropbox a partir do nosso aplicativo.

Imagem 2 – Gerando token

Basta clicar no botão Generate e copiar o token que será gerado. Não passe esse token para ninguém, pois ele permite que uma pessoa acesse os arquivos do aplicativo no Dropbox.

Biblioteca para comunicação

Agora que já temos um token para acessar o Dropbox, vamos começar nosso aplicativo. Neste artigo, vamos utilizar a biblioteca da própria empresa para facilitar a comunicação com o serviço deles.

A biblioteca possui os seguintes requerimentos:

Para importar a biblioteca para nosso projeto, vamos utilizar o Composer (se você não estiver familiarizado com ele, dê uma olhada neste artigo). Portanto, vamos criar nosso composer.json.

{
    "require": {
        "dropbox/dropbox-sdk": "1.1.*"
    }
}

Por fim, basta rodar composer install no terminal para baixar a biblioteca na pasta vendor. Feito isso, já podemos utilizá-la.

OBS: Se o servidor que você vai rodar o aplicativo utiliza Windows 32/64 bits ou Linux 32-bits, a biblioteca não vai funcionar, pois ela necessita de 64-bit integers. Porém, você pode comentar a exceção no arquivo \lib\Dropbox\RequestUtil.php da biblioteca como nesta sugestão. Não é o ideal, mas resolve o problema.

if (strlen((string) PHP_INT_MAX) < 19) {
    // Looks like we're running on a 32-bit build of PHP.  This could cause problems because some of the numbers
    // we use (file sizes, quota, etc) can be larger than 32-bit ints can handle.
    //throw new \Exception("The Dropbox SDK uses 64-bit integers, but it looks like we're running on a version of PHP that doesn't support 64-bit integers (PHP_INT_MAX=" . ((string) PHP_INT_MAX) . ").  Library: \"" . __FILE__ . "\"");
}

Criando classe para backup

Para facilitar ainda mais as coisas vamos criar uma classe que será responsável por fazer o envio dos arquivos para o Dropbox.

class BackupDropbox
{
    /** @var \Dropbox\Client */
    private $dropbox;

    /**
     * Inicializa a biblioteca do Dropbox a partir do token e
     * do nome do aplicativo
     *
     * @param $token
     * @param $app
     */
    public function __construct($token, $app)
    {
        $this->dropbox = new \Dropbox\Client($token, $app);
    }

No construtor da nossa classe, vamos receber o token e o nome do aplicativo para que possamos instanciar um novo objeto da biblioteca do Dropbox.

Agora que já temos o objeto para comunicação com o Dropbox, vamos criar um método básico para enviar um único arquivo.

/**
 * Envia um arquivo para o Dropbox
 *
 * @param string $localFile Caminho do arquivo no servidor
 * @param string $remoteFile Caminho do arquivo no Dropbox
 *
 * @return void
 */
public function uploadFile($localFile, $remoteFile)
{
    $fp = fopen($localFile, "rb");
    $this->dropbox->uploadFile($remoteFile, \Dropbox\WriteMode::add(), $fp);
    fclose($fp);

    echo "Arquivo '{$localFile}' enviado para '{$remoteFile}'. " . PHP_EOL;
}

Armazenamos o arquivo passado por parâmetro em uma variável e então utilizamos o método uploadFile() da biblioteca para enviarmos o arquivo no caminho especificado do Dropbox. Por fim, para simplificar mostramos uma mensagem para saber que o arquivo foi copiado, porém o mais interessante seria registrar isso em um arquivo de log (utilizando monolog, por exemplo).

Agora que já temos um método para enviar um arquivo único, vamos criar o método para enviar uma pasta inteira de forma recursiva.

/**
 * Copia uma pasta do servidor para o Dropbox recursivamente
 *
 * @param string $localFolder Caminho da pasta no servidor
 * @param string $remoteFolder Caminho da pasta no Dropbox
 *
 * @return void
 */
public function uploadFolder($localFolder, $remoteFolder)
{
    // Se não for uma pasta válida, sai do método
    if (!is_dir($localFolder)) {
        return;
    }

    // Buscando itens da pasta no servidor
    $files = new \DirectoryIterator($localFolder);

    // Buscando itens da pasta no Dropbox
    $metadata = $this->dropbox->getMetadataWithChildren($remoteFolder);

    // Passando pelos itens no servidor
    foreach ($files as $file) {

        // Se o item for '.' ou '..' passamos para o próximo item
        if ($file->isDot()) {
            continue;
        }

        // Se o item for uma pasta
        if ($file->isDir()) {
            // Chamamos o método novamente passando como parâmetro inicial a pasta atual (recursividade)
            $this->uploadFolder($file->getRealPath(), $remoteFolder . "/$file");
            continue;
        }

        // Se o item for um arquivo
        if ($file->isFile()) {
            // Verificamos se o arquivo já existe no Dropbox
            $remoteFileExists = $this->checkIfRemoteFileExistsFromMetadata($metadata, $file->getFilename());
            // Se não existir
            if (!$remoteFileExists) {
                // Fazemos upload do arquivo para o Dropbox
                $remoteFile = $remoteFolder . '/' . $file->getFilename();
                $this->uploadFile($file->getPathname(), $remoteFile);
            }
        }

    }
}

Tentei detalhar o máximo possível nos comentários, porém vamos tentar detalhar ainda mais:

  • Linha 17: Utilizamos a classe DirectoryIterator do PHP, que permite listar os arquivos e pastas de um caminho;
  • Linha 20: O método getMetadataWithChildren() da biblioteca retorna os dados de uma pasta, inclusive seus arquivos. Assim podemos comparar se o arquivo que estamos enviando já existe no Dropbox, para não precisar enviar novamente. O desempenho é melhor ao utilizar esse método para conseguir todos os dados da pasta de uma vez, ao invés de verificar arquivo por arquivo;
  • Linha 26 a 28: Quando utilizamos o DirectoryIterator,  além dos arquivos e pastas ele nos retorna ‘.’ e ‘..’, portanto se for esse o caso, ignoramos e passamos para o próximo item;
  • Linha 31 a 35: É aqui que a mágica da recursividade acontece. Se o item atual for uma pasta, chamamos o nosso método novamente, porém passamos como parâmetro esta pasta, assim todo o processo será feito novamente nesta pasta. E se esta pasta possuir sub-pastas, o método será chamado novamente, e assim sucessivamente, até chegar no último nível.
  • Linha 38 a 47: Por fim, testamos se o item atual é um arquivo. Se for, verificamos se ele já existe na pasta atual do Dropbox, utilizando o método checkIfRemoteFileExistsFromMetadata() que criaremos adiante. Se não existir, fazemos o upload dele utilizando o método uploadFile() que criamos anteriormente.

OBS: Esse método apenas copia os arquivos do servidor para o Dropbox, porém não apaga os arquivos do Dropbox, caso ele tenha sido excluído no servidor. Para isso seria necessário implementar um outro método que faria o processo inverso, passaria recursivamente pelos arquivos no Dropbox e se não existisse no servidor, apagaria ele.

Para finalizar, vamos criar o método para verificar se o arquivo já existe no Dropbox.

/**
 * Verifica se o arquivo existe no Dropbox a partir do metadata
 *
 * @param array $metadata Dados da pasta do Dropbox
 * @param string $fileName Nome do arquivo no Dropbox
 *
 * @return bool
 */
private function checkIfRemoteFileExistsFromMetadata($metadata, $fileName)
{
    // Se não houver dados, o arquivo não existe
    if (empty($metadata)) {
        return false;
    }

    // Passando pelos itens
    foreach ($metadata['contents'] as $remoteFile) {
        // Se for uma pasta passamos para o próximo item
        if ($remoteFile['is_dir']) {
            continue;
        }
        // Se for o arquivo que procuramos
        if (basename($remoteFile['path']) == $fileName) {
            return true;
        }
    }

    return false;
}

Utilizando a classe

Agora que já fizemos o trabalho pesado, basta utilizar a classe.

<?php

// Incluindo o autoload do Composer para carregar a biblioteca
// do Dropbox
require_once 'vendor/autoload.php';

// Incluindo a classe que criamos
require_once 'class/BackupDropbox.php';

// Como o processo de upload pode ser demorado, retiramos
// o limite de execução do script
set_time_limit(0);

// Dados do aplicativo no Dropbox
$token = "kWj4ECFc8RFAMlaCNLL3Z6Cmh0yAFPYNXYzmJ8q5a9ZRXn8pBq8Pfg1InYxBts08";
$app = "rafaelcouto-backup";

// Instanciando objeto e copiando arquivos e sub-pastas da pasta 'documentos'
$backup = new BackupDropbox($token, $app);
$backup->uploadFolder('documentos', '/documentos');

Acredito que nenhuma novidade aqui. Incluímos o arquivo de autoload do Composer e nossa classe. Utilizamos a função set_time_limit() para que o script possa ser executado até todos os arquivos serem copiados, já que esse processo pode ser bem longo. Por fim, instanciamos um objeto de nossa classe e chamamos o método para copiar a pasta documentos do servidor para a pasta documentos na raiz do Dropbox.

Para testar, vamos executar o arquivo que criamos através do terminal (você pode abrir pelo navegador também se preferir), você deverá ver um retorno parecido com o abaixo.

$ php backup.php
Arquivo 'D:\web\public\backup_dropbox\documentos\2016\Documento 1.txt' enviado para '/documentos/2016/Documento 1.txt'.
Arquivo 'D:\web\public\backup_dropbox\documentos\2016\Documento 2.txt' enviado para '/documentos/2016/Documento 2.txt'.
Arquivo 'D:\web\public\backup_dropbox\documentos\2016\Imagem 1.jpg' enviado para '/documentos/2016/Imagem 1.jpg'.
Arquivo 'D:\web\public\backup_dropbox\documentos\2017\Documento 3.txt' enviado para '/documentos/2017/Documento 3.txt'.
Arquivo 'D:\web\public\backup_dropbox\documentos\2017\Imagem 2.jpg' enviado para '/documentos/2017/Imagem 2.jpg'.

Vamos acessar o Dropbox agora para ver se os arquivos foram realmente enviados. Se deu tudo certo, você deve ver as pastas e os arquivos assim como na Imagem 3.

Imagem 3 – Arquivos no Dropbox

Automatizando o processo

Maravilha, conseguimos fazer backup dos nossos arquivos na nuvem, porém para isso acontecer você deverá executar o arquivo backup.php toda vez manualmente. Tudo bem, você pode chamar esse trecho de código em alguma momento no seu projeto ou então criar um botão para que o próprio usuário faça o backup. Porém, o ideal neste caso seria automatizar o processo.

Se o seu projeto estiver em uma hospedagem compartilhada, verifique se no painel você não tem a opção de criar um CronjobUm Cronjob é nada mais que uma tarefa agendada para a execução de um comando. Sendo assim, você poderia agendar um comando (cURL normalmente) para chamar o backup.php uma vez por semana, por exemplo.

Caso seu projeto esteja hospedado em um servidor Linux em uma VPS (Virtual Private Server) e você tem acesso ao servidor via SSH (Secure Shell), você pode criar um Cronjob manualmente.

Enfim, não vou entrar em detalhes sobre como criar um Cronjob, pois existem diversos tutoriais excelentes na internet, porém acredito que está seja a melhor solução para automatizar o processo. Caso você tenha dificuldades em montar a sintaxe do Cronjob, este site é de grande ajuda.

Conclusão

Neste artigo foi apresentado uma forma de fazer o backup em nuvem dos arquivos que estão no servidor. Utilizamos o Dropbox, porém o conceito se aplica a qualquer serviço, o que muda é apenas a biblioteca que utilizaremos para consultar e enviar os arquivos.

Vale lembrar que você deve ficar atento as limitações do serviço, como espaço de armazenamento e limite de requisições diárias. Em um projeto simples, com poucos arquivos ou arquivos leves isso não faz diferença, porém se você tiver muitos arquivos ou arquivos muito pesados, talvez você tenha que adaptar o código para esta situação.

Espero que de alguma forma este artigo tenha sido útil para que você possa fazer backup dos seus arquivos.

Código fonte disponível em: https://github.com/rafaelcouto/backup-na-nuvem-com-dropbox-e-php/tree/v1

Referências