#!/usr/bin/perl
# --------------------------------------------------------------
# Libreria Dropbox.
# Para crear una app Dropbox ir aqui: https://www.dropbox.com/developers/apps
# - Hacer click en Create
# - Marcar Scoped access en el paso 1. Choose an API.
# - Elegir tipo de acceso App folder
# - Darle un nombre a la app (este será tb el nombre de la carpeta en que se hará el backup)
# Al crear la app generará un id y secret, y podremos generar los tokens de autenticación desde Prontus.
# Los cambios en la API significan que:
# - no existen más OAUTH_TOKEN de larga duración. Duran 4hh, lo que es de larga duración es el REFRESH_TOKEN,
# y con éste se deben regenerar los OAUTH_TOKEN.
# --------------------------------------------------------------
# Prontus CMS
# http://www.prontus.cl
# by Altavoz.net
#
# licensed under LGPL license.
# http://www.prontus.cl/license.html
# --------------------------------------------------------------

package lib_dropbox;

use strict;
use utf8;

our $DEBUG = 0; # 1 activa el debug.
our $ERROR = '';

# Token de la aplicacion en Dropbox.
our $ATOKEN = '';

# endpoint para obtener y refrescar tokens.
our $DROPBOX_URL_OAUTH_TOKEN = 'https://api.dropboxapi.com/oauth2/token';

# URLs de Dropbox.
our $SPACEUSAGEURL   = 'https://api.dropboxapi.com/2/users/get_space_usage'; # 2.0.0
our $DIRLISTINGURL   = 'https://api.dropboxapi.com/2/files/list_folder'; # 2.0.0 'https://api.dropbox.com/1/metadata/auto';
our $DIRLISTCONTURL  = 'https://api.dropboxapi.com/2/files/list_folder/continue'; # 2.0.0
our $FILEUPLOADURL   = 'https://content.dropboxapi.com/2/files/upload'; # 2.0.0
our $FILEDOWNLOADURL = 'https://content.dropboxapi.com/2/files/download'; # 2.0.0
our $DELETEURL       = 'https://api.dropboxapi.com/2/files/delete'; # 2.0.0 'https://api.dropbox.com/1/fileops/delete';
our $UPLOADSTARTURL  = 'https://content.dropboxapi.com/2/files/upload_session/start';
our $UPLOADAPPENDURL = 'https://content.dropboxapi.com/2/files/upload_session/append_v2';
our $UPLOADFINISHURL = 'https://content.dropboxapi.com/2/files/upload_session/finish';
our $CREATEFOLDERURL = "https://api.dropboxapi.com/2/files/create_folder_v2";

# Tamano maximo de un archivo para ser subido de una sola vez.
our $MAXFILESIZE = 100000000; # App 100 MB.

# User Agent.
our $UA = undef;

# ------------------------------------------------------------------------#
# Inicializa la interfaz a Dropbox.
# Setea la token de acceso
# Inicializa el user-agent.
# Revisa la validez del access_token.
sub init {
    $DEBUG = 0;
    my $REFRESH_TOKEN = shift;

    my $margin_seconds = 300; # 5 minutos
    my $current_time = time;
    my $data_dir = $prontus_varglb::DIR_SERVER . "/$prontus_varglb::PRONTUS_ID/cpan/data/dropbox_post/_token/";
    my $must_refresh_token = 0;

    if (-f $data_dir."access_token.json") {
        my $json_str = glib_fildir_03::read_file($data_dir."access_token.json");
        # print STDERR "Json_str: [$json_str]\n";
        my $json = JSON::decode_json($json_str);
        my $token_exp = $json->{'current_timestamp'} + $json->{'expires_in'};

        # si el ts de expiración del token es más que el ts actual menos un margen, intentamos renovar.
        # print STDERR "curtime: $current_time; token_exp: $token_exp; margen: $margin_seconds; diferencia: ". ($current_time - $token_exp)."\n";
        if (($token_exp - $current_time) < $margin_seconds) {
            $must_refresh_token = 1;
        } else {
            $ATOKEN = $json->{access_token};
        }
    } else {
        $must_refresh_token = 1;
    }
    if ($must_refresh_token) {
        # print STDERR "diferencia: ".($token_exp - $current_time)."\n";
        # print STDERR "ATOKEN:              [$ATOKEN]\n";
        &printDebug("Dropbox Access Token cercano a expiración, intentamos refrescar.\n");
        $ATOKEN = &getAccessToken($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DROPBOX_CLIENT_ID'}, $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DROPBOX_CLIENT_SECRET'}, $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DROPBOX_REFRESH_TOKEN'});
        if (! $ATOKEN) {
            &printError("No se logró refrescar el Dropbox Access Token!\n");
            die;
        }
    }

    &initUA();

} # init.

# ------------------------------------------------------------------------#
# Inicializa el user agent y lee la información del access_token vigente.
sub initUA {
    $UA = LWP::UserAgent->new();
    $UA->default_header('Authorization' => "Bearer $ATOKEN" );
    $UA->timeout(30);
} # initUA.

# ------------------------------------------------------------------------#
# 2.0.0 Obtiene y entrega el espacio utilizado.
# {"used": 284062049740, "allocation": {".tag": "individual", "allocated": 1101659111424}}
sub getSpaceUsage {
    my $data = &postHttp($SPACEUSAGEURL);
    if ($data =~ /"used":\s*(\d+)(,|})/s) {
        return $1;
    } else {
        return 0;
    }
} # getSpaceUsage.

# ------------------------------------------------------------------------#
# Obtiene y entrega informacion sobre la cuenta del usuario.
# Por ahora solo la cuota es de interes.
# 2.0.0 Hace wrp a la funcion getSpaceUsage.
sub getInfo {
    return &getSpaceUsage();
} # getInfo.

# ------------------------------------------------------------------------#
# 2.0.0 Obtiene el listado de un directorio.
# Entrega un arreglo de strings de la forma
# <path>\t<es dir>\t<tamano en bytes>\t<client_modified timestamp>\t<hash>
sub getDir {
    my $dir = $_[0];
    my (@lines, @entries);
    my ($path,$isdir,$bytes,$client_modified,$cursor,$content_hash);
    my $jsonData = '{"path": "'.$dir.'","recursive": false,"include_media_info": false,"include_deleted": false,"include_has_explicit_shared_members": false}';
    my $data = &postHttpJson($DIRLISTINGURL,$jsonData);

    do {
        # Hace un match para obtener el arreglo completo de entries.
        if ($data =~ /"entries": \[(.+)\]/s) {
            # @lines = split(/}/,$1);
            @lines = &getObjects($1);
            foreach my $line (@lines) {
                ($path,$isdir,$bytes,$client_modified) = ();
                # ".tag": "folder", "name": "site", "path_lower": "/noticias/site", "path_display": "/noticias/site", "id": "id:tOyNRss-k0AAAAAAAAAABg"}
                # ".tag": "file", "name": "release_notes.txt", "path_lower": "/noticias/release_notes.txt", "path_display": "/noticias/release_notes.txt", "id": "id:tOyNRss-k0AAAAAAAAKNQw", "client_modified": "2015-08-01T07:19:14Z", "server_modified": "2015-08-01T07:19:14Z", "rev": "2eb8a33e4ef7a", "size": 852, "content_hash": "df3bdd100f896e415dcc24e653e92566eef9201051d9ee5492dbfe92693039f5"
                if ($line =~ /"path_display": "(.+?)"/s) {
                    $path = $1;
                }
                if ($line =~ /".tag": "([^,}"]+)"/s) {
                    $isdir = ($1 eq 'file') ? 0:1;
                }
                if ($line =~ /"size": ([^,}]+)/s) {
                    $bytes = $1;
                }
                if ($line =~ /"client_modified": "([^,}"]+)"/s) {
                    $client_modified = $1;
                }
                if ($line =~ /"content_hash": "([^,}"]+)"/s) {
                    $content_hash = $1;
                }
                if ($path ne '') {
                    push @entries, "$path\t$isdir\t$bytes\t$client_modified\t$content_hash";
                }
            }
        }
        if ($data =~ /"has_more": true/s) {
            # Hay que pedir mas datos.
            if ($data =~ /"cursor": "([^,}"]+)"/s) {
                $cursor = $1;
                $jsonData = '{"cursor": "'.$cursor.'"}';
                $data = &postHttpJson($DIRLISTCONTURL,$jsonData);
            } else {
                $data = '';
            }
        } else {
            $data = '';
        }

    }while ($data ne '');

    return @entries;
} # getDir.

# ------------------------------------------------------------------------#
# 2.0.0 Obtiene el archivo especificado y lo escribe en el path indicado.
# Retorna 1 si tuvo exito y 0 si no. La metadata viene en el header 'Dropbox-Api-Result'.
sub getFile {
    my ($srcPath,$dstPath) = @_;
    my ($req,$jsonArg,$content);
    my $res = 0;

    $jsonArg = "{\"path\": \"$srcPath\"}";

    $req = HTTP::Request->new('POST',$FILEDOWNLOADURL);
    $req->header( 'Dropbox-API-Arg' => $jsonArg );
    my $response = $UA->request($req);

    if ($response->is_success) {
       &printDebug("getFile content: [" . $response->headers->as_string . "]\n");
       $content = $response->decoded_content;
    } else {
       &printError("getFile error: [" . $response->status_line . "][".length($content)."]\n");
    }

    if ($content ne '') {
       &writeFile($dstPath,$content);
       $res = 1;
    }
    return $res
} # getFile.

# ------------------------------------------------------------------------#
# 2.0.0 Sube el archivo especificado en el path indicado.
# El path de destino debe incluir el nombre del archivo de destino.
# Retorna 1 si tuvo exito, 0 si no, -1 si el archivo es rechazado localmente
# -2 si se recibio un error 400. El error 400 indica que el nombre del
# archivo no le gusto a dropbox.
# -3 si se recibió 401 indica que nuestra autenticación no es válida por algún motivo.
# Si el archivo excede el tamano $MAXFILESIZE, entonces es subido mediante
# la funcion putLargeFile.
sub putFile {
    my ($srcPath,$dstPath) = @_;
    my ($req,$jsonArg,$json,$content,$client_modified);
    my $res = 0;

    if ( -s $srcPath > $MAXFILESIZE) {
        # Si el archivo excede el tamano $MAXFILESIZE, entonces
        # es subido mediante la funcion putLargeFile.
        $res = &putLargeFile($srcPath,$dstPath);
    } else {
        # No permite archivos con caracteres extranos. Asi nunca habra problemas con el parseo del JSON.
        if ($dstPath !~ /^[_ 0-9a-zA-Z\.\-\/]+$/) {
            &printError("Caracteres no validos en path: [$dstPath]\n");
            return -1;
        }
        $content = &readFile($srcPath);
        $client_modified = &epochToIso8601(time);
        $jsonArg = "{\"path\": \"$dstPath\",\"mode\": \"overwrite\",\"autorename\": true,\"client_modified\": \"$client_modified\",\"mute\": false}";
        $req = HTTP::Request->new('POST',$FILEUPLOADURL);
        $req->content_type('application/octet-stream');
        $req->header( 'Dropbox-API-Arg' => $jsonArg );
        $req->content($content);
        my $response = $UA->request($req);
        if ($response->is_success) {
            &printDebug("putFile content: [" . $response->decoded_content . "]\n");
            # {"name": "avatar.jpg", "path_lower": "/noticias/avatar.jpg", "path_display": "/noticias/avatar.jpg",
            #  "id": "id:tOyNRss-k0AAAAAAAApLzQ", "client_modified": "2017-02-25T21:59:43Z",
            #  "server_modified": "2017-02-25T21:59:43Z", "rev": "e347c33e4ef7a", "size": 159552,
            #  "content_hash": "89ce72c2460a60217b2e53da3979fa9c1ea119a40c3f0b0e4a12cae55f2dfc15"}
            $json = $response->decoded_content;
            if ($json =~ /"size": ([^,}]+)/) {
                if (length($content) == $1) {
                    $res = 1;
                }
            }
        } else {
            # Codigos de error que no ameritan un reintento.
            if ($response->status_line =~ /400/) {
                if ($response->status_line =~ /Invalid authorization value/i) {
                    &printError("putFile error: [" . $response->status_line . "][". $response->decoded_content ."]\n");
                    $res = -3;
                } else {
                    $res = -2;
                }
            } elsif ($response->status_line =~ /401|403|503/) {
                &printError("putFile error: [" . $response->status_line . "][". $response->decoded_content ."]\n");
                $res = -3;
            # Códigos de error que permiten reintentar la operación
            } elsif ($response->status_line =~ /429/) {
                # en el caso de un error http 429, debemos esperar el tiempo especificado en el error.
                if ($response->decoded_content =~ /"retry_after":\s*(\d+)/) {
                    my $seconds = $1;
                    sleep $seconds;
                } else {
                    sleep 1;
                }
                $res = 1;
            } else {
                # capturamos el código de error pero no enviamos mail, porque el script seguirá reintentando.
                print STDERR "putFile error: [" . $response->status_line . "][". $response->decoded_content ."]\n";
            }
        }
    }

    return $res
} # putFile.

# ------------------------------------------------------------------------#
# 2.0.0 Sube el archivo especificado en el path indicado, en pedazos de a 100 MB.
# El path de destino debe incluir el nombre del archivo de destino.
# Retorna 1 si tuvo exito, 0 si no, -1 si el archivo es rechazado localmente
# -2 si se recibio un error 400.
# -3 si se recibió un error 401.
# Si el resultado es 0, se deberia reintentar hasta lograrlo.
sub putLargeFile {
    my ($srcPath,$dstPath) = @_;
    my ($req,$jsonArg,$json,$content,$index,$bytesRead,$response,$sessionId,$client_modified);
    my $chunkSize = $MAXFILESIZE;
    my $res = 0;
    # No permite archivos con caracteres extranos. Asi nunca habra problemas con el parseo del JSON.
    if ($dstPath !~ /^[_ 0-9a-zA-Z\.\-\/]+$/) {
        return -1;
    }

    # Abre el archivo y lo lee en trozos de a $chunkSize.
    open my $IN,'<', $srcPath;
    $index = 0;
    $bytesRead = read($IN, $content, $chunkSize);
    if ($bytesRead > 0) {
        # Inicia la sesion de upload.
        $jsonArg = "{\"close\": false}";
        $req = HTTP::Request->new('POST',$UPLOADSTARTURL);
        $req->content_type('application/octet-stream');
        $req->header( 'Dropbox-API-Arg' => $jsonArg );
        $req->content($content);
        $response = $UA->request($req);
        if ($response->is_success) {
            $json = $response->decoded_content;
            &printDebug("putLargeFile content 1: [$json][$bytesRead][$index]\n");
            if ($json =~ /"session_id": "([^,}"]+)"/) {
                $sessionId = $1;
                # Con el id de sesion se continua la carga del archivo.
                do {
                    # Normalmente $bytesRead == $chunkSize, excepto al final.
                    $index += $bytesRead;
                    seek($IN, $index, 0);
                    $bytesRead = read($IN, $content, $chunkSize);
                    if ($bytesRead == $chunkSize) {
                        $jsonArg = "{\"cursor\": {\"session_id\": \"$sessionId\",\"offset\": $index},\"close\": false}";
                        $req = HTTP::Request->new('POST',$UPLOADAPPENDURL);
                        $req->content_type('application/octet-stream');
                        $req->header( 'Dropbox-API-Arg' => $jsonArg );
                        $req->content($content);
                        $response = $UA->request($req);
                        &printDebug("putLargeFile content 2: [" . $response->decoded_content . "][$bytesRead][$index][$sessionId]\n");
                        if (! $response->is_success) {
                            # Si el upload falla, sale indicando error -2 para no reintentar.
                            # (Falta un analisis mas exhaustivo del error aqui).
                            close $IN;
                            return -2;
                        }
                    }
                } while ($bytesRead == $chunkSize);
                # Cierra la sesion de upload.
                $client_modified = &epochToIso8601(time);
                $jsonArg = "{\"cursor\": {\"session_id\": \"$sessionId\",\"offset\": $index},\"commit\": {\"path\": \"$dstPath\",\"mode\": \"overwrite\",\"autorename\": true,\"client_modified\": \"$client_modified\",\"mute\": false}}";
                $req = HTTP::Request->new('POST',$UPLOADFINISHURL);
                $req->content_type('application/octet-stream');
                $req->header( 'Dropbox-API-Arg' => $jsonArg );
                $req->content($content);
                $response = $UA->request($req);
                &printDebug("putLargeFile content 3: [" . $response->decoded_content . "][$bytesRead][$index][$sessionId]\n");
                if ($response->decoded_content =~ /"path_display": "$dstPath"/s) {
                    $res = 1;
                }
            }
        } else {
            # Codigos de error que no ameritan un reintento (resultado no es 1, pero tampoco es 0).
            if ($response->status_line =~ /400/) {
                &printError("putLargeFile error: [" . $response->status_line . "][". $response->decoded_content ."]\n");
                $res = -2;
            } elsif ($response->status_line =~ /401|403|503/) {
                &printError("putLargeFile error: [" . $response->status_line . "][". $response->decoded_content ."]\n");
                $res = -3;
            # Códigos de error que permiten reintentar la operación
            } elsif ($response->status_line =~ /429/) {
                # en el caso de un error http 429, debemos esperar el tiempo especificado en el error.
                if ($response->decoded_content =~ /"retry_after":\s*(\d+)/) {
                    my $seconds = $1;
                    sleep $seconds;
                } else {
                    sleep 1;
                }
                $res = 1; # el error 429 no indica necesariamente que debamos matar el proceso, solamente esperar.
            } else {
                # capturamos el código de error pero no enviamos mail, porque el script seguirá reintentando.
                print STDERR "putLargeFile error: [" . $response->status_line . "][". $response->decoded_content ."]\n";
            }
        }
    }
    close $IN;
    return $res
} # putLargeFile.

# ------------------------------------------------------------------------#
# 2.0.0 Elimina el archivo o directorio especificado en el path indicado.
# Retorna 1 si tuvo exito y 0 si no.
# Ojo que Dropbox en realidad nunca elimina los archivos; los deja en el trash.
# Para borrar en forma permanente hay que hacerlo en la GUI web.
sub deletePath {
    my $path = $_[0];
    my $res = 0;
    my $jsonData = "{\"path\": \"$path\"}";
    my $response = &postHttpJson($DELETEURL,$jsonData);
    if ($response =~ /"path_display": "$path"/s) {
        $res = 1;
    }
    return $res;
} # deletePath.

# ------------------------------------------------------------------------#
# Hace un POST sin parametros.
# Retorna la respuesta si tuvo exito y '' si no.
sub postHttp {
    my ($url) = $_[0];
    my $req = HTTP::Request->new( 'POST', $url );
    my $response = $UA->request( $req );
    if ($response->is_success) {
        &printDebug("postHttp $url content: [" . $response->decoded_content . "]\n");
        return $response->decoded_content;
    } else {
        &printError("postHttp $url error: [" . $response->status_line . " " . $response->content . "]\n");
        return '';
    }
} # postHttp.

# ------------------------------------------------------------------------#
# Hace un POST de json.
# Retorna la respuesta si tuvo exito y '' si no.
sub postHttpJson {
    my ($url,$json) = @_;
    my $req = HTTP::Request->new('POST',$url);
    $req->content_type('application/json');
    $req->content($json);
    my $response = $UA->request($req);
    if ($response->is_success) {
        &printDebug("postHttpJson $url content: [" . $response->decoded_content . "]\n");
        return $response->decoded_content;
    } else {
        if ($response->content !~ /not_found/) {
            &printError("postHttpJson $url error: [" . $response->status_line . " ][$json][" . $response->content . "]\n");
        } else {
            &printDebug("postHttpJson $url error: [" . $response->status_line . " ][$json][" . $response->content . "]\n");
        }
        return '';
    }
} # postHttpJson.

# ---------------------------------------------------------------
# hace una request POST recibiendo datos de un hash perl.
sub postHttpHash {
    my $url = $_[0];
    my $form = $_[1];
    my $access_token = $_[2];
    my $content_type = $_[3];
    my $ua = LWP::UserAgent->new();

    &printDebug("postHttpHash access token: [$access_token]\n");
    if ($access_token) {
        $ua->default_header('Authorization' => "Bearer $access_token");
    }
    $ua->default_header('Content-Type' => 'application/json');
    if ($content_type) {
        $ua->default_header('Content-Type' => $content_type);
    }

    $ua->timeout(60);

    my $response = $ua->post($url, $form);

    &printDebug("RESPONSE[".$response->as_string."]\n");

    if ($response->is_success) {
        #print "[$url] status_line[" . $response->status_line . "]\n";
        return $response->decoded_content;  # or whatever
    } else {
        &printError("ERROR: [$url] status_line[" . $response->status_line. "]\n");
        return '';
    }
}

# ------------------------------------------------------------------------#
# Obtiene datos via GET.
# Retorna la respuesta si tuvo exito y '' si no.
sub getHttp {
    my $url = $_[0];
    my $response = $UA->get($url);
    if ($response->is_success) {
        &printDebug("getHttp $url content: [" . $response->decoded_content . "]\n");
        return $response->decoded_content;
    } else {
        &printError("getHttp $url error: [" . $response->status_line . " " . $response->content . "]\n");
        return '';
    }
} # getHttp.

# -------------------------------------------------------------------------#
# Lee un archivo por completo. Si el archivo no existe retorna ''.
sub readFile {
    my($archivo) = $_[0];
    my($buffer) = '';
    if (-e $archivo) {
        open (my $ARCHIVO, '<', $archivo);
        binmode $ARCHIVO;
        read $ARCHIVO,$buffer,-s $archivo;
        close $ARCHIVO;
    }
    return $buffer;
} # readFile

# ------------------------------------------------------------------------#
# Escribe un archivo. Si no puede retorna un mensaje de error.
sub writeFile {
    my($archivo,$buffer) = ($_[0],$_[1]);
    my($dir);
    if (open (my $ARCHIVO, '>', $archivo) ) {
        binmode $ARCHIVO;
        print $ARCHIVO $buffer; # Escribe buffer completo
        close $ARCHIVO;
        return '';
    } else {
        return "Fail writing file $archivo $!";
    }
} # writeFile

# ------------------------------------------------------------------------#
# Obtiene los objetos de primer nivel de un arreglo json.
# Para eso matches los cierres de } y corta el string en ellos.
sub getObjects {
    my $json = $_[0];
    my $i = 0;
    my $largo = length $json;
    my @entries;
    my ($car,$depth,$lastIndex);
    $depth = 0;
    $lastIndex = 0;
    while ($i < $largo) {
        $car = substr($json, $i, 1);
        if ($car eq '{') {
            $depth++;
            if ($depth == 1) {
                $lastIndex = $i;
            }
        }elsif ($car eq '}') {
            $depth--;
        }
        if (($car eq '}') && ($depth == 0)) {
            # Agrega el trozo de string como elemento del arreglo.
            push @entries, substr($json,$lastIndex,($i - $lastIndex + 1));
        }
        $i++;
    }
    return @entries;
} # getObjects

# ----------------------------------------------------------------------------- #
# Transforma un epoch al formato ISO 8601, UTC: "%Y-%m-%dT%H:%M:%SZ"
sub epochToIso8601 {
    my $epoch = $_[0];
    my @utc = gmtime($epoch);
    return sprintf('%04d-%02d-%02dT%02d:%02d:%02dZ',(1900 + $utc[5]),(1 + $utc[4]),$utc[3],$utc[2],$utc[1],$utc[0]);
} # epochToIso8601

# ----------------------------------------------------------------------------- #
# Convierte una fecha en formato iso8601 (UTC) en epoch. Ejemplo: 2017-02-27T12:05:28Z
sub iso8601ToEpoch {
    my $utc = $_[0];
    my ($year,$mon,$mday,$hour,$min,$sec,$epoch);
    if ($utc =~ /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z/) {
        ($year,$mon,$mday,$hour,$min,$sec) = ($1,$2,$3,$4,$5,$6);
        $year -= 1900;
        $mon--;
        $epoch = Time::Local::timegm( $sec, $min, $hour, $mday, $mon, $year );
    }
    return $epoch;
}

# ----------------------------------------------------------------------------- #
# Calcula un hash de contenido al estilo Dropbox.
sub computeContentHash {
    my $file = $_[0];
    my $chunkSize = 4194304;
    my ($i,$index,$bytesRead,$computedHash,$content);
    my ($hexHash);
    open my $IN, '<', $file;
    $index = 0;
    $bytesRead = read($IN, $content, $chunkSize);
    while ($bytesRead > 0) {
        $computedHash .= Digest::SHA::sha256($content);
        $index += $bytesRead;
        seek($IN, $index, 0);
        $bytesRead = read($IN, $content, $chunkSize);
    }
    $computedHash = Digest::SHA::sha256($computedHash);
    for ($i=0;$i<length($computedHash);$i++) {
        $hexHash .= unpack('H2',substr($computedHash,$i,1));
    }
    close $IN;
    return $hexHash;
} # computeContentHash

# ------------------------------------------------------------------------#
# Hace print siempre que el flag debug este activo.
sub printDebug {
    print STDERR $_[0] if $DEBUG;
} # printDebug.

# imprime y guarda los errores ocurridos
sub printError {
    $ERROR .= $_[0];
    print STDERR $_[0];
} # printError.

# TODO: estas dos subrutinas que siguen deberían homologarse al resto, usando HTTP::Request y la variable global $UA.

# Dado un authorization code, obtiene un refresh_token de larga duración
sub getRefreshToken {
    my $code = shift; # code    El código de autorización para la API Dropbox.
    # client_id   El ID de cliente de OAuth 2.0 para la aplicación. Este valor aparece en la consola de apps de dropbox.
    # client_secret   El secreto de cliente asociado con el ID de cliente. Este valor aparece en la consola de apps de dropbox.
    # grant_type  Establece este valor en authorization_code.
    my %form = (
        code => $code, #authorization_code obtenido antes
        client_id => $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DROPBOX_CLIENT_ID'},
        client_secret => $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DROPBOX_CLIENT_SECRET'},
        grant_type => 'authorization_code' # necesario para obtener un refresh_token
    );
    my $strjson = &postHttpHash($DROPBOX_URL_OAUTH_TOKEN, \%form);
    #print STDERR "json: $strjson\n";
    if ($strjson) {
        utf8::encode($strjson);
        my $hashtemp = JSON::decode_json($strjson);

        if (defined $hashtemp->{'refresh_token'}) {
            return $hashtemp->{'refresh_token'};
        }
    }

    return;
}

# Dado un refresh_token de larga duración, obtiene un access_token.
sub getAccessToken {
    my $client_id = $_[0];
    my $client_secret = $_[1];
    my $refresh_token = $_[2];

    my %form = (
        client_id => $client_id,
        client_secret => $client_secret,
        refresh_token => $refresh_token,
        grant_type => 'refresh_token'
    );

    my $strjson = &postHttpHash($DROPBOX_URL_OAUTH_TOKEN, \%form);

    if ($strjson) {
        utf8::encode($strjson);
        my $hashtemp = JSON::decode_json($strjson);

        if (defined $hashtemp->{'access_token'}) {
            # guardamos los datos en un archivo json para uso del upload.
            $hashtemp->{'refresh_token'} = $refresh_token;
            $hashtemp->{'current_timestamp'} = time;
            my $data_dir = $prontus_varglb::DIR_SERVER . "/$prontus_varglb::PRONTUS_ID/cpan/data/dropbox_post/_token/";
            glib_fildir_03::check_dir($data_dir);
            my $json = JSON::encode_json($hashtemp);
            utf8::decode($json);
            glib_fildir_03::write_file($data_dir."access_token.json", $json);

            return $hashtemp->{'access_token'};
        }
    }
}

1;
