#!/usr/local/bin/perl

eval 'exec /usr/local/bin/perl  -S $0 ${1+"$@"}'
    if 0; # not running under some shell

use strict;
use Cwd 'abs_path';
use Data::Dumper;
use DateTime;
use DateTime::Format::Strptime;
use Encode;
use Encode::Locale;
use File::Basename qw(dirname basename);
use File::Spec::Functions qw(abs2rel catfile);
use File::Temp;
use Getopt::Std;
use JSON;
use Path::Class;
use WebService::Dropbox;

our $VERSION = '1.13';

my $limit = 10 * 1024 * 1024; # files_put_chunked method has large file support.

if ($^O eq 'darwin') {
    require Encode::UTF8Mac;
    $Encode::Locale::ENCODING_LOCALE_FS = 'utf-8-mac';
}

binmode STDOUT, ':utf8';
binmode STDERR, ':utf8';

my $config_file = file( $ENV{DROPBOX_CONF} || ($ENV{HOME} || $ENV{HOMEPATH}, '.dropbox-api-config') );

my $command = shift;
my @args;
for (@{[@ARGV]}) {
    last if $_=~/^-/;
    push @args, shift;
}

my %opts;
getopts('ndvDsherp:', \%opts);

push @args, @ARGV;

my $dry       = $opts{n};
my $delete    = $opts{d};
my $verbose   = $opts{v};
my $debug     = $opts{D};
my $human     = $opts{h};
my $printf    = $opts{p};
my $context   = $opts{s} ? 'sandbox' : 'dropbox';
my $env_proxy = $opts{e};

if ($command eq '-v') {
    &help('version');
    exit(0);
}

if ($command eq 'setup' || !-f $config_file) {
    &setup();
}

# connect dropbox
my $config = decode_json($config_file->slurp);
$config->{key} or die 'please set config key.';
$config->{secret} or die 'please set config secret.';
$config->{access_token} or die 'please set config access_token.';
$config->{access_secret} or die 'please set config access_secret.';
if ( my $access_level = delete $config->{access_level} ) {
    if ($access_level eq 'a') {
        $context = 'sandbox';
    }
}

$ENV{HTTP_PROXY} = $ENV{http_proxy} if !$ENV{HTTP_PROXY} && $ENV{http_proxy};
$ENV{NO_PROXY} = $ENV{no_proxy} if !$ENV{NO_PROXY} && $ENV{no_proxy};

my $box = WebService::Dropbox->new($config);
$box->root($context);
$box->env_proxy if $env_proxy;

# printf option
my $strp;
my $size = $human ? 'size' : 'bytes';
my $format = {
    i => 'icon',
    b => 'bytes',
    e => 'thumb_exists',
    d => '_is_dir',
    p => 'path',
    s => '_size',
    M => 'mime_type',
    t => 'modified',
    c => 'client_mtime', # For files, this is the modification time set by the desktop client when the file was added to Dropbox.
    r => 'revision', # A deprecated field that semi-uniquely identifies a file. Use rev instead.
    R => 'rev', # A unique identifier for the current revision of a file. This field is the same rev as elsewhere in the API and can be used to detect changes and avoid conflicts.
};

# ProgressBar
my $cols = 50;
if ($verbose) {
    eval {
        my $stty = `stty -a 2>/dev/null`;
        if ($stty =~ m|columns (\d+)| || $stty =~ m|(\d+) columns|) {
            $cols = $1;
        }
    };
}

my $exit_code = 0;

if ($command eq 'ls' or $command eq 'list') {
    &list(@args);
} elsif ($command eq 'find') {
    &find(@args);
} elsif ($command eq 'uid') {
    &uid();
} elsif ($command eq 'copy' or $command eq 'cp') {
    &copy(@args);
} elsif ($command eq 'move' or $command eq 'mv') {
    &move(@args);
} elsif ($command eq 'mkdir' or $command eq 'mkpath') {
    &mkdir(@args);
} elsif ($command eq 'delete' or $command eq 'rm' or $command eq 'rmtree') {
    &delete(@args);
} elsif ($command eq 'upload' or $command eq 'up' or $command eq 'put') {
    &upload(@args);
} elsif ($command eq 'download' or $command eq 'dl' or $command eq 'get') {
    &download(@args);
} elsif ($command eq 'sync') {
    &sync(@args);
} elsif ($command eq 'help' or (not length $command)) {
    &help(@args);
} else {
    die "unknown command $command";
}

exit($exit_code);

sub help {
    my ($command) = @_;

    my $help;
    if ($command eq 'ls' or $command eq 'list') {
        $help = q{
        Name
            dropbox-api-ls - list directory contents

        SYNOPSIS
            dropbox-api ls <dropbox_path> [options]

        Example
            dropbox-api ls Public
            dropbox-api ls Public -h
            dropbox-api ls Public -p "%d\t%s\t%TY/%Tm/%Td %TH:%TM:%TS\t%p\n"

        Options
            -h print sizes in human readable format (e.g., 1K 234M 2G)
            -p print format.
                %d ... is_dir ( d: dir, -: file )
                %p ... path
                %b ... bytes
                %s ... size (e.g., 1K 234M 2G)
                %i ... icon
                %e ... thumb_exists
                %M ... mime_type
                %t ... modified time
                %c ... client_mtime
                %r ... revision (A deprecated field that semi-uniquely identifies a file. Use rev instead)
                %R ... rev
                %Tk ... DateTime ‘strftime’ function (modified time)
                %Ck ... DateTime ‘strftime’ function (client_mtime)
        };
    } elsif ($command eq 'find') {
        $help = q{
        Name
            dropbox-api-find - walk a file hierarchy

        SYNOPSIS
            dropbox-api find <dropbox_path> [options]

        Example
            dropbox-api find Public
            dropbox-api find Public -h
            dropbox-api find Public -p "%d\t%s\t%TY/%Tm/%Td %TH:%TM:%TS\t%p\n"

        Options
            -h print sizes in human readable format (e.g., 1K 234M 2G)
            -p print format.
                %d ... is_dir ( d: dir, -: file )
                %p ... path
                %b ... bytes
                %s ... size (e.g., 1K 234M 2G)
                %i ... icon
                %e ... thumb_exists
                %M ... mime_type
                %t ... modified time
                %c ... client_mtime
                %r ... revision (A deprecated field that semi-uniquely identifies a file. Use rev instead)
                %R ... rev
                %Tk ... DateTime ‘strftime’ function (modified time)
                %Ck ... DateTime ‘strftime’ function (client_mtime)
        };
    } elsif ($command eq 'copy' or $command eq 'cp') {
        $help = q{
        Name
            dropbox-api-cp - copy file or directory

        SYNOPSIS
            dropbox-api cp <source_file> <target_file>

        Example
            dropbox-api cp Public/hoge.txt Public/foo.txt
            dropbox-api cp Public/work Public/work_bak
        };
    } elsif ($command eq 'move' or $command eq 'mv') {
        $help = q{
        Name
            dropbox-api-mv - move file or directory

        SYNOPSIS
            dropbox-api mv <source_file> <target_file>

        Example
            dropbox-api mv Public/hoge.txt Public/foo.txt
            dropbox-api mv Public/work Public/work_bak
        };
    } elsif ($command eq 'mkdir' or $command eq 'mkpath') {
        $help = q{
        Name
            dropbox-api-mkdir - make directory (Create intermediate directories as required)

        SYNOPSIS
            dropbox-api mkdir <directory>

        Example
            dropbox-api mkdir Public/product/chrome-extentions/foo
        };
    } elsif ($command eq 'uid') {
        $help = q{
        Name
            dropbox-api-uid - Get your account uid

        SYNOPSIS
            dropbox-api uid

        Example
            dropbox-api uid
        };
    } elsif ($command eq 'delete' or $command eq 'rm' or $command eq 'rmtree') {
        $help = q{
        Name
            dropbox-api-rm - remove file or directory (Attempt to remove the file hierarchy rooted in each file argument)

        SYNOPSIS
            dropbox-api rm <file_or_directory>

        Example
            dropbox-api rm Public/work_bak/hoge.tmp
            dropbox-api rm Public/work_bak
        };
    } elsif ($command eq 'upload' or $command eq 'up' or $command eq 'put') {
        $help = q{
        Name
            dropbox-api-put - upload file

        SYNOPSIS
            dropbox-api put <file> dropbox:<dropbox_file>

        Example
            dropbox-api put README.md dropbox:/Public/product/dropbox-api/
        };
    } elsif ($command eq 'download' or $command eq 'dl' or $command eq 'get') {
        $help = q{
        Name
            dropbox-api-get - download file

        SYNOPSIS
            dropbox-api get dropbox:<dropbox_file> <file>

        Example
            dropbox-api get dropbox:/Public/product/dropbox-api/README.md README.md
        };
    } elsif ($command eq 'sync') {
        $help = q{
        Name
            dropbox-api-sync - sync directory

        SYNOPSIS
            dropbox-api sync dropbox:<source_dir> <target_dir> [options]
            dropbox-api sync <source_dir> dropbox:<target_dir> [options]

        Example
            dropbox-api sync dropbox:/Public/product/dropbox-api/ ~/work/dropbox-api/
            dropbox-api sync ~/work/dropbox-api/ dropbox:/Public/product/dropbox-api/ -vdn
            dropbox-api sync ~/work/dropbox-api/ dropbox:/Public/product/dropbox-api/ -d

        Options
            -v increase verbosity
            -n show what would have been transferred (dry-run)
            -d delete files that don't exist on sender
        };
    } elsif ($command eq 'version') {
        $help = qq{
        This is dropbox-api-command, version $VERSION

        Copyright 2011, Shinichiro Aska

        Released under the MIT license.

        Documentation
            this system using "dropbox-api help".
            If you have access to the Internet, point your browser at
            https://github.com/s-aska/dropbox-api-command,
            the dropbox-api-command Repository.
        };
    } else {
        $help = qq{
        Usage: dropbox-api <command> [args] [options]

        Available commands:
            setup get access_key and access_secret
            ls    list directory contents
            find  walk a file hierarchy
            cp    copy file or directory
            mv    move file or directory
            mkdir make directory (Create intermediate directories as required)
            rm    remove file or directory (Attempt to remove the file hierarchy rooted in each file argument)
            put   upload file
            get   download file
            sync  sync directory (local => dropbox or dropbox => local)
            uid   get accound uid

        Common Options
            -e enable env_proxy ( HTTP_PROXY, NO_PROXY )
            -D enable debug

        See 'dropbox-api help <command>' for more information on a specific command.
        };
    }
    $help=~s|^ {8}||mg;
    $help=~s|^\s*\n||;
    print "\n$help\n";
}

sub setup {
    my $config = {};

    print "Please Input API Key: ";
    chomp( my $key = <STDIN> );
    die 'Get API Key from https://www.dropbox.com/developers' unless $key;
    $config->{key} = $key;

    print "Please Input API Secret: ";
    chomp( my $secret = <STDIN> );
    die 'Get API Secret from https://www.dropbox.com/developers' unless $secret;
    $config->{secret} = $secret;

    print "Please Input Access type\n";
    print "  a ... App folder - Your app only needs access to a single folder within the user's Dropbox\n";
    print "  f ... Full Dropbox - Your app needs access to the user's entire Dropbox\n";
    print "[a or f]: ";
    chomp( my $access_level = <STDIN> );
    if ($access_level=~m|^[af]$|i) {
        $config->{access_level} = lc($access_level) eq 'a' ? 'a' : 'f';
    } else {
        die 'invalid access level.';
    }

    $config->{callback_url} = '';
    my $box = WebService::Dropbox->new($config);
    $box->env_proxy if $env_proxy;
    my $login_link = $box->login;
    die $box->error if $box->error;
    print "URL: $login_link\n";
    print "Please Access URL and press Enter\n";
    print "OK?";
    <STDIN>;
    $box->auth;
    die $box->error if $box->error;
    $config->{access_token} = $box->access_token;
    $config->{access_secret} = $box->access_secret;
    print "success! try\n";
    print "> dropbox-api ls\n";
    print "> dropbox-api find /\n";

    $config_file->openw->print(encode_json($config));

    chmod 0600, $config_file;

    exit(0);
}

sub uid {
    my $info = $box->account_info or die $box->error;
    print $info->{uid}, "\n";
}

sub list {
    my $remote_base = decode('locale_fs', shift);
    my $list = $box->metadata($remote_base) or die $box->error;
    for my $content (@{$list->{contents}}) {
        print &_line($content);
    }
}

sub _line {
    my ($content) = @_;
    $strp ||= new DateTime::Format::Strptime( pattern => '%a, %d %b %Y %T %z' );
    my $dt;
    my $ct;
    if ($printf) {
        my $line = eval qq{"$printf"};
        $content->{_is_dir} = $content->{is_dir} ? 'd' : '-';
        $content->{_size} = $content->{is_dir} ? '-' : $content->{size};
        $line=~s/\%T([^\%])/
            $dt ||= $strp->parse_datetime($content->{modified});
            $dt->strftime('%'.$1);
        /egx;
        if ($content->{client_mtime}) {
            $line=~s/\%C([^\%])/
                $ct ||= $strp->parse_datetime($content->{client_mtime});
                $ct->strftime('%'.$1);
            /egx;
        } else {
            $line=~s/\%C([^\%])/-/g;
        }
        $line=~s|\%([^\%])|$content->{$format->{$1}}|eg;
        return $line;
    } else {
        return sprintf "%s %8s %s %s\n",
            ($content->{is_dir} ? 'd' : '-'),
            ($content->{is_dir} ? '-' : $content->{$size}),
            $content->{modified},
            $content->{path};
    }
}

sub find {
    my $remote_base = decode('locale_fs', shift);
    $printf ||= "%p\n";
    &_find($remote_base, sub { print &_line(shift); });
}

sub _find {
    my ($remote_path, $callback) = @_;
    my $list = $box->metadata($remote_path) or die $box->error;
    die "is not dir $remote_path" unless $list->{is_dir};
    if ($list->{is_deleted}) {
        warn "is deleted $remote_path";
        $exit_code = 1;
        return;
    }
    for my $content (@{$list->{contents}}) {
        next if $content->{is_deleted};
        $callback->($content);
        if ($content->{is_dir}) {
            &_find($content->{path}, $callback);
        }
    }
}

sub copy {
    my ($src, $dst) = @_;
    my $res = $box->copy(decode('locale_fs', $src), decode('locale_fs', $dst)) or die $box->error;
    print Dumper($res), "\n" if $verbose;
}

sub move {
    my ($src, $dst) = @_;
    my $res = $box->move(decode('locale_fs', $src), decode('locale_fs', $dst)) or die $box->error;
    print Dumper($res), "\n" if $verbose;
}

sub mkdir {
    my ($dir) = @_;
    my $res = $box->create_folder(decode('locale_fs', $dir)) or die $box->error;
    print Dumper($res), "\n" if $verbose;
}

sub delete {
    my ($file_or_dir) = @_;
    my $res = $box->delete(decode('locale_fs', $file_or_dir)) or die $box->error;
    print Dumper($res), "\n" if $verbose;
}

sub upload {
    my ($file, $path) = @_;
    $path=~s|^dropbox:/||
        or die "Usage: \n    dropbox-api upload /tmp/local.txt dropbox:/Public/some.txt";
    my $local_path = file($file);
    $path.= basename($file) if (! length $path) or $path=~m|/$|;
    my $res = &put($local_path, decode('locale_fs', $path), { overwrite => 1 }) or die $box->error;
    print Dumper($res), "\n" if $verbose;
}

sub download {
    my ($path, $file) = @_;
    $path=~s|^dropbox:/||
        or die "Usage: \n    dropbox-api download dropbox:/Public/some.txt /tmp/local.txt";
    my $fh = file($file)->openw or die $!;
    $box->files(decode('locale_fs', $path), $fh) or die $box->error;
    $fh->close;
}

sub sync {
    my ($arg1, $arg2) = @_;

    my ($remote_base, $local_base, $command);
    if ($arg1=~/^dropbox:/ and $arg2!~/^dropbox:/) {
        ($remote_base, $local_base) = ($arg1, $arg2);
        $command = \&sync_download;
    } elsif ($arg1!~/^dropbox:/ and $arg2=~/^dropbox:/) {
        ($remote_base, $local_base) = ($arg2, $arg1);
        $command = \&sync_upload;
    } else {
        die "Usage: \n    dropbox-api sync dropbox:/Public/ /tmp/pub/\n" .
                   "or    dropbox-api sync /tmp/pub/ dropbox:/Public/";
    }

    die "missing $local_base" unless -d $local_base;

    $local_base = dir(abs_path($local_base));

    $remote_base = decode('locale_fs', $remote_base);

    $remote_base=~s|^dropbox:/|/|;

    my $content = $box->metadata($remote_base) or die $box->error;

    $remote_base = $content->{path}; # Dropbox are case insensitive!

    print "!! enable dry run !!\n" if $dry;
    print "remote_base: $remote_base\n" if $verbose;
    print "local_base: $local_base\n" if $verbose;

    $command->($remote_base, $local_base);
}

sub sync_download {
    my ($remote_base, $local_base) = @_;

    print "** download **\n" if $verbose;

    my $strp = new DateTime::Format::Strptime( pattern => '%a, %d %b %Y %T %z' );

    my $remote_map = {};
    my $remote_inode_map = {};

    &_find($remote_base, sub {
        my $content = shift;
        my $remote_path = $content->{path};
        my $rel_path = remote_abs2rel($remote_path, $remote_base);
        my $rel_path_enc = encode('locale_fs', $rel_path);
        $remote_map->{$rel_path}++;
        printf "check: %s\n", $rel_path if $debug;
        my $local_path = $content->{is_dir} ? dir($local_base, $rel_path_enc) : file($local_base, $rel_path_enc);
        if ($content->{is_dir}) {
            printf "remote: %s\n", $remote_path if $debug;
            printf "local:  %s\n", $local_path if $debug;
            if (!-d $local_path) {
                $local_path->mkpath unless $dry;
                printf "mkpath %s\n", decode('locale_fs', $local_path);
            } else {
                printf "skip %s\n", $rel_path if $verbose;
            }
        } else {
            my $remote_epoch = $strp->parse_datetime($content->{modified})->epoch;
            my $local_epoch = -f $local_path ? $local_path->stat->mtime : '-';
            my $remote_size = $content->{bytes};
            my $local_size = -f $local_path ? $local_path->stat->size : '-';
            printf "remote: %10s %10s %s\n",
                $remote_epoch, $remote_size, $remote_path if $debug;
            printf "local:  %10s %10s %s\n",
                $local_epoch, $local_size, $local_path if $debug;

            if ((!-f $local_path) or
                ($remote_size != $local_size) or
                ($remote_epoch > $local_epoch)) {
                if ($dry) {
                    printf "download %s\n", decode('locale_fs', $local_path);
                    return;
                }
                # not displayed in the dry-run for the insurance
                printf "mkpath %s\n", decode('locale_fs', $local_path->dir) unless -d $local_path->dir;
                $local_path->dir->mkpath unless -d $local_path->dir;
                my $local_path_tmp = $local_path . '.dropbox-api.tmp';
                my $fh = file($local_path_tmp)->openw;
                if ($box->files($content->{path}, $fh)) {
                    printf "download %s\n", decode('locale_fs', $local_path);
                    $fh->close;
                    unless (utime($remote_epoch, $remote_epoch, $local_path_tmp)) {
                        warn "set modification time failure " .  decode('locale_fs', $local_path);
                        $exit_code = 1;
                    }
                    unless (rename($local_path_tmp, $local_path)) {
                        unlink($local_path_tmp);
                        warn "rename failure " . decode('locale_fs', $local_path_tmp);
                        $exit_code = 1;
                    }
                } else {
                    unlink($local_path_tmp);
                    chomp( my $error = $box->error );
                    warn "download failure " . decode('locale_fs', $local_path) . " (" . $error . ")";
                    $exit_code = 1;
                }
            } else {
                printf "skip %s\n", $rel_path if $verbose;
            }
        }
        $remote_inode_map->{ &inode($local_path) } = $content;
    });

    return if $exit_code;

    return unless $delete;

    print "** delete **\n" if $verbose;

    my @deletes;
    $local_base->recurse(
        preorder => 0,
        depthfirst => 1,
        callback => sub {
            my $local_path = shift;
            next if $local_path eq $local_base;
            my $rel_path_enc = abs2rel($local_path, $local_base);
            my $rel_path = decode('locale_fs', $rel_path_enc);
            if (exists $remote_map->{$rel_path}) {
                printf "skip %s\n", $rel_path if $verbose;
            } elsif (my $content = $remote_inode_map->{ &inode($local_path) }) {
                my $remote_path = $content->{path};
                my $rel_path_remote = remote_abs2rel($remote_path, $remote_base);
                printf "skip %s\n", $rel_path if $verbose and !$debug;
                printf "skip %s ( is %s )\n", $rel_path, $rel_path_remote if $verbose and $debug;
            } elsif (-f $local_path) {
                printf "remove %s\n", $rel_path;
                push @deletes, $local_path;
            } elsif (-d $local_path) {
                printf "rmtree %s\n", $rel_path;
                push @deletes, $local_path;
            }
        }
    );

    return if $dry;

    for my $local_path (@deletes) {
        if (-f $local_path) {
            $local_path->remove;
        } elsif (-d $local_path) {
            $local_path->rmtree;
        }
    }
}

sub sync_upload {
    my ($remote_base, $local_base) = @_;

    print "** upload **\n" if $verbose;

    my $strp = new DateTime::Format::Strptime( pattern => '%a, %d %b %Y %T %z' );

    my $remote_map = {};
    my $remote_path_map = {};

    &_find($remote_base, sub {
        my $content = shift;
        delete $content->{thumb_exists};
        $content->{is_dir} = $content->{is_dir} ? 1 : 0;
        my $remote_path = $content->{path};
        my $rel_path = remote_abs2rel($remote_path, $remote_base);
        $remote_map->{ $rel_path } = $content;
        $remote_path_map->{ $content->{path} } = $content;
        printf "find: %s\n", $rel_path if $debug;
    });
    my @makedirs;
    $local_base->recurse(
        preorder => 0,
        depthfirst => 1,
        callback => sub {
            my $local_path = shift;
            next if $local_path eq $local_base;
            my $rel_path = decode('locale_fs', abs2rel($local_path, $local_base));
            my $remote_path = file($remote_base, $rel_path);
            my $content = delete $remote_map->{$rel_path} || $box->metadata($remote_path); # Dropbox are case insensitive!
            if ($content && !$content->{is_deleted}) {
                delete $remote_path_map->{ $content->{path} };
                return unless -f $local_path;
                my $remote_epoch = $strp->parse_datetime($content->{modified})->epoch;
                my $local_epoch = -f $local_path ? $local_path->stat->mtime : '-';
                my $remote_size = $content->{bytes};
                my $local_size = -f $local_path ? $local_path->stat->size : '-';
                printf "remote: %10s %10s %s\n",
                    $remote_epoch, $remote_size, $content->{path} if $debug;
                printf "local:  %10s %10s %s\n",
                    $local_epoch, $local_size, $local_path if $debug;
                if (($remote_size != $local_size) or
                    ($remote_epoch < $local_epoch)) {
                    printf "upload %s %s\n", $rel_path, $remote_path;
                    &put($local_path, $remote_path,
                        { overwrite => 1 }) or die $box->error unless $dry;
                    push @makedirs, $rel_path;
                } else {
                    printf "skip %s\n", $rel_path if $verbose;
                }
            } elsif (-f $local_path) {
                &put($local_path, $remote_path,
                    { overwrite => 1 }) unless $dry;
                if (!$dry and $box->error) {
                    warn "upload failure $rel_path $remote_path (" . $box->error . ")";
                } else {
                    printf "upload %s %s\n", $rel_path, $remote_path;
                    push @makedirs, $rel_path;
                }
            } elsif (-d $local_path) {
                return if grep { $_=~/^\Q$rel_path/ } @makedirs;
                printf "mktree %s %s\n", $rel_path, $remote_path;
                $box->create_folder($remote_path) or die $box->error unless $dry;
                push @makedirs, $rel_path;
            } else {
                printf "unknown %s\n", $rel_path;
            }
        }
    );

    return unless $delete;

    print "** delete **\n" if $verbose;

    my @deletes;
    for my $content_path ( keys %$remote_path_map ) {
        next if grep { $content_path=~/^\Q$_/ } @deletes;
        $box->delete($content_path) or die $box->error unless $dry;
        push @deletes, $content_path;
        my $rel_path = remote_abs2rel($content_path, $remote_base);
        print "delete $rel_path\n";
    }
}

sub put {
    my ($file, $path, $params) = @_;

    my $content   = $file->openr;
    my $size      = -s $file;
    my $threshold = 10 * 1024 * 1024; # files_put or files_put_chunked

    return $box->files_put($path, $content, $params) if $size < $threshold;
    return $box->files_put($path, $content, $params) unless $box->can('files_put_chunked');
    return $box->files_put_chunked($path, $content, $params) unless $verbose;

    my $limit = 4 * 1024 * 1024; # A typical chunk is 4 MB

    local $| = 1;
    my $upload;
    $upload = sub {
        my $data = shift;
        my $buf;
        my $total = $limit;
        my $chunk = 1024;
        my $tmp = File::Temp->new;
        while (my $read = read($content, $buf, $chunk)) {
            $tmp->print($buf);
            $total -= $read;
            if ($chunk > $total) {
                $chunk = $total;
            }
            last unless $chunk;
        }

        if ($total == $limit) {
            $data->{upload_id} or die $file . ' read error.';
            print "\n";
            return $box->commit_chunked_upload(
                $path, {
                    upload_id => $data->{upload_id},
                    ( $params ? %$params : () )
                }) or die $box->error;
        }

        $tmp->flush;
        $tmp->seek(0, 0);
        $data = $box->chunked_upload($tmp, {
            ( $data   ? %$data   : () ),
            ( $params ? %$params : () )
        }) or die $box->error;

        # ProgressBar
        my $rate = sprintf '%2.1d%', $data->{offset} / $size * 100;
        my $bar = '=' x int(($cols - length($rate) - 4) * $data->{offset} / $size);
        my $space = ' ' x ($cols - length($rate) - length($bar) - 4);
        printf "\r%s [%s>%s]", $rate, $bar, $space;

        $upload->({
            upload_id => $data->{upload_id},
            offset    => $data->{offset}
        });
    };
    $upload->();
}

sub inode ($) {
    my $path = shift;
    my ($dev, $inode) = stat($path);
    return $dev . ':' . $inode if $inode;
    return $path;
}

sub remote_abs2rel ($$) {
    my ($remote_path, $remote_base) = @_;
    $remote_path =~ s|^\Q$remote_base\E/?||i;
    return $remote_path;
}

exit(0);

=head1 NAME

App::dropboxapi - command line interface to access Dropbox API

=head1 SYNOPSIS

    dropbox-api put /tmp/foo.txt dropbox:/Public/

Run C<dropbox-api help> for more options.

=head1 DESCRIPTION

dropbox-api is a command line interface to access Dropbox API.

has many sub-commands ... ls, find, sync, cp, mv, rm, mkdir, get, put, uid

=head1 Sub Commands

=head2 help

disp help.

=over 4

=item syntax

dropbox-api help [<command>]

=back

=head3 Example

    > dropbox-api help
    Usage: dropbox-api <command> [args] [options]

    Available commands:
        setup get access_key and access_secret
        ls    list directory contents
        find  walk a file hierarchy
        cp    copy file or directory
        mv    move file or directory
        mkdir make directory (Create intermediate directories as required)
        rm    remove file or directory (Attempt to remove the file hierarchy rooted in each file argument)
        put   upload file
        get   download file
        sync  sync directory (local => dropbox or dropbox => local)
        uid   get accound uid

    See 'dropbox-api help <command>' for more information on a specific command.

=head3 Example ( command help )

    > dropbox-api help ls
    Name
        dropbox-api-ls - list directory contents

    SYNOPSIS
        dropbox-api ls <dropbox_path> [options]

    Example
        dropbox-api ls Public
        dropbox-api ls Public -h
        dropbox-api ls Public -p "%d\t%s\t%TY/%Tm/%Td %TH:%TM:%TS\t%p\n"

    Options
        -h print sizes in human readable format (e.g., 1K 234M 2G)
        -p print format.
            %d ... is_dir ( d: dir, -: file )
            %p ... path
            %b ... bytes
            %s ... size (e.g., 1K 234M 2G)
            %i ... icon
            %e ... thumb_exists
            %M ... mime_type
            %t ... modified time
            %r ... revision
            %Tk ... DateTime 'strftime' function

=head2 ls

file list view.

=over 4

=item alias

list

=item syntax

dropbox-api ls <dropbox_path>

=back

=head3 Example

    > dropbox-api list /product
    d        - Thu, 24 Feb 2011 06:58:00 +0000 /product/chrome-extentions
    -   294557 Sun, 26 Dec 2010 21:55:59 +0000 /product/ex.zip

=head3 human readable option ( -h )

print sizes in human readable format (e.g., 1K 234M 2G)

    > dropbox-api ls /product -h
    d        - Thu, 24 Feb 2011 06:58:00 +0000 /product/chrome-extentions
    -  287.7KB Sun, 26 Dec 2010 21:55:59 +0000 /product/ex.zip

=head3 printf option ( -p )

print format.

    > dropbox-api ls /product -p "%d\t%s\t%TY/%Tm/%Td %TH:%TM:%TS\t%p\n"
    d       -       2011/02/24 06:58:00     /product/chrome-extentions
    -       287.7KB 2010/12/26 21:55:59     /product/ex.zip

        %d ... is_dir ( d: dir, -: file )
        %p ... path
        %b ... bytes
        %s ... size (e.g., 1K 234M 2G)
        %i ... icon
        %e ... thumb_exists
        %M ... mime_type
        %t ... modified time
        %r ... revision

        %Tk ... DateTime 'strftime' function
                <http://search.cpan.org/dist/DateTime/lib/DateTime.pm#strftime_Patterns>

=head2 find

recursive file list view.

=over 4

=item syntax

dropbox-api find <dropbox_path> [options]

=back

=head3 Example

    > dropbox-api find /product/google-tasks-checker-plus
    /product/chrome-extentions/google-tasks-checker-plus/README.md
    /product/chrome-extentions/google-tasks-checker-plus/src
    /product/chrome-extentions/google-tasks-checker-plus/src/background.html
    /product/chrome-extentions/google-tasks-checker-plus/src/external.png
    /product/chrome-extentions/google-tasks-checker-plus/src/icon-32.png
    /product/chrome-extentions/google-tasks-checker-plus/src/icon-128.png
    /product/chrome-extentions/google-tasks-checker-plus/src/icon.gif
    /product/chrome-extentions/google-tasks-checker-plus/src/jquery-1.4.2.min.js
    /product/chrome-extentions/google-tasks-checker-plus/src/main.js
    /product/chrome-extentions/google-tasks-checker-plus/src/manifest.json
    /product/chrome-extentions/google-tasks-checker-plus/src/options.html
    /product/chrome-extentions/google-tasks-checker-plus/src/popup.html
    /product/chrome-extentions/google-tasks-checker-plus/src/reset.css

=head3 printf option ( -p )

see also list command's printf option.

=head2 sync ( rsync )

recursive file synchronization.

=head3 sync from dropbox

dropbox-api sync dropbox:<source_dir> <target_dir> [options]

    > dropbox-api sync dropbox:/product/google-tasks-checker-plus/src /tmp/product
    download /private/tmp/product/external.png
    download /private/tmp/product/icon-32.png
    download /private/tmp/product/icon-128.png

=head3 sync to dropbox

dropbox-api sync <source_dir> dropbox:<target_dir> [options]

    > dropbox-api sync /tmp/product dropbox:/work/src
    upload background.html /work/src/background.html
    upload external.png /work/src/external.png
    upload icon-128.png /work/src/icon-128.png

=head3 delete option ( -d )

    > dropbox-api sync dropbox:/product/google-tasks-checker-plus/src /tmp/product -d
    download /private/tmp/product/external.png
    download /private/tmp/product/icon-32.png
    download /private/tmp/product/icon-128.png
    remove background.html.tmp

=head3 dry run option ( -n )

    > dropbox-api sync dropbox:/product/google-tasks-checker-plus/src /tmp/product -dn
    !! enable dry run !!
    download /private/tmp/product/external.png
    download /private/tmp/product/icon-32.png
    download /private/tmp/product/icon-128.png
    remove background.html.tmp

=head3 verbose option ( -v )

    > dropbox-api sync dropbox:/product/google-tasks-checker-plus/src /tmp/product -dnv
    remote_base: /product/chrome-extentions/google-tasks-checker-plus/src
    local_base: /private/tmp/product
    ** download **
    skip background.html
    download /private/tmp/product/external.png
    download /private/tmp/product/icon-32.png
    download /private/tmp/product/icon-128.png
    skip icon.gif
    skip jquery-1.4.2.min.js
    skip main.js
    skip manifest.json
    skip options.html
    skip popup.html
    skip reset.css
    ** delete **
    skip background.html
    remove background.html.tmp
    skip icon.gif
    skip jquery-1.4.2.min.js
    skip main.js
    skip manifest.json
    skip options.html
    skip popup.html
    skip reset.css

=head2 cp

copy file or directory.

=over 4

=item alias

copy

=item syntax

dropbox-api cp <source_file> <target_file>

=back

=head3 Example

    dropbox-api cp memo.txt memo.txt.bak

=head2 mv

move file or directory.

=over 4

=item alias

move

=item syntax

dropbox-api mv <source_file> <target_file>

=back

=head3 Example

    dropbox-api mv memo.txt memo.txt.bak

=head2 mkdir

make directory.

*no error if existing, make parent directories as needed.*

=over 4

=item alias

mkpath

=item syntax

dropbox-api mkdir <directory>

=back

=head3 Example

    dropbox-api mkdir product/src

=head2 rm

remove file or directory.

*remove the contents of directories recursively.*

=over 4

=item alias

rmtree

=item syntax

dropbox-api rm <file_or_directory>

=back

=head3 Example

    dropbox-api rm product/src

=head2 get

download file from dropbox.

=over 4

=item alias

dl, download

=item syntax

dropbox-api get dropbox:<dropbox_file> <file>

=back

=head3 Example

    dropbox-api get dropbox:/Public/foo.txt /tmp/foo.txt

=head2 put

upload file to dropbox.

=over 4

=item alias

up, upload

=item syntax

dropbox-api put <file> dropbox:<dropbox_dir>

=back

=head3 Example

    dropbox-api put /tmp/foo.txt dropbox:/Public/

=head3 verbose option ( -v )

A progress bar is displayed.

    dropbox-api put /tmp/1GB.dat dropbox:/Public/
    100% [=====================================================================================>]

=head2 uid

Get your accound UID

=head3 Example

    dropbox-api uid

=head1 COPYRIGHT

Copyright 2012- Shinichiro Aska

The standalone executable contains the following modules embedded.

=head1 LICENSE

Released under the MIT license. http://creativecommons.org/licenses/MIT/

=head1 COMMUNITY

=over 4

=item L<https://github.com/s-aska/dropbox-api-command> - source code repository, issue tracker

=back

=cut

1;