1#!/usr/bin/env perl 2 3use strict; 4use warnings; 5use Cwd 'abs_path'; 6use DateTime; 7use DateTime::Format::Strptime; 8use Encode; 9use Encode::Locale; 10use File::Basename qw(dirname basename); 11use File::Spec::Functions qw(abs2rel catfile); 12use File::Temp; 13use Getopt::Std; 14use JSON; 15use Path::Class; 16use POSIX qw(); 17use WebService::Dropbox 2.06; 18 19our $VERSION = '2.13'; 20 21my $limit = 10 * 1024 * 1024; # files_put_chunked method has large file support. 22 23if ($^O eq 'darwin') { 24 require Encode::UTF8Mac; 25 $Encode::Locale::ENCODING_LOCALE_FS = 'utf-8-mac'; 26} 27 28binmode STDOUT, ':utf8'; 29binmode STDERR, ':utf8'; 30 31my $config_file = file( $ENV{DROPBOX_CONF} || ($ENV{HOME} || $ENV{HOMEPATH}, '.dropbox-api-config') ); 32 33my $command = shift || ''; 34my @args; 35for (@{ [ @ARGV ] }) { 36 last if $_ =~ qr{ \A - }xms; 37 push @args, shift; 38} 39 40my %opts; 41if ($command eq 'du') { 42 getopts('vDhed:', \%opts); 43} else { 44 getopts('ndvDshePp:', \%opts); 45} 46 47push @args, @ARGV; 48 49my $dry = $opts{n}; 50my $delete = $opts{d}; 51my $verbose = $opts{v}; 52my $debug = $opts{D}; 53my $human = $opts{h}; 54my $printf = $opts{p}; 55my $public = $opts{P}; 56my $env_proxy = $opts{e}; 57my $max_depth = $opts{d}; 58 59if ($opts{s}) { 60 die "-s is gone."; 61} 62 63if ($command eq '-v') { 64 &help('version'); 65 exit(0); 66} 67 68if ($command eq 'setup' || !-f $config_file) { 69 &setup(); 70} 71 72# connect dropbox 73my $config = decode_json($config_file->slurp); 74$config->{key} or die 'please set config key.'; 75$config->{secret} or die 'please set config secret.'; 76$config->{access_token} or die 'please set config access_token.'; 77if ($config->{access_secret}) { 78 warn "Auto migration OAuth1 Token to OAuth2 token..."; 79 my $oauth2_access_token = &token_from_oauth1($config->{key}, $config->{secret}, $config->{access_token}, $config->{access_secret}); 80 if ($oauth2_access_token) { 81 delete $config->{access_secret}; 82 $config->{access_token} = $oauth2_access_token; 83 $config_file->openw->print(encode_json($config)); 84 warn "=> Suucess."; 85 } else { 86 die "please setup."; 87 } 88} 89if (my $access_level = delete $config->{access_level}) { 90 if ($access_level eq 'a') { 91 print "sandbox is gone, Are you sure you want to delete from the config the access_level? [y/n]: "; 92 chomp( my $y = <STDIN> ); 93 if ($y =~ qr{ [yY] }xms) { 94 delete $config->{access_level}; 95 $config_file->openw->print(encode_json($config)); 96 warn "=> Suucess."; 97 } else { 98 die "cancelled."; 99 } 100 } 101} 102 103$ENV{HTTP_PROXY} = $ENV{http_proxy} if !$ENV{HTTP_PROXY} && $ENV{http_proxy}; 104$ENV{NO_PROXY} = $ENV{no_proxy} if !$ENV{NO_PROXY} && $ENV{no_proxy}; 105 106my $box = WebService::Dropbox->new($config); 107$box->env_proxy if $env_proxy; 108 109my $strp = new DateTime::Format::Strptime( pattern => '%Y-%m-%dT%T' ); 110my $strpz = new DateTime::Format::Strptime( pattern => '%Y-%m-%dT%TZ' ); 111 112my $format = { 113 i => 'id', 114 n => 'name', 115 b => 'size', 116 e => 'thumb_exists', # jpg, jpeg, png, tiff, tif, gif or bmp 117 d => 'is_dir', # Check if .tag = "folder" 118 p => 'path_display', 119 P => 'path_lower', 120 s => 'format_size', 121 t => 'server_modified', 122 c => 'client_modified', # For files, this is the modification time set by the desktop client when the file was added to Dropbox. 123 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. 124 R => 'rev', 125}; 126 127# ProgressBar 128my $cols = 50; 129if ($verbose) { 130 eval { 131 my $stty = `stty -a 2>/dev/null`; 132 if ($stty =~ m|columns (\d+)| || $stty =~ m|(\d+) columns|) { 133 $cols = $1; 134 } 135 }; 136} 137 138my $exit_code = 0; 139 140if ($command eq 'ls' or $command eq 'list') { 141 &list(@args); 142} elsif ($command eq 'find') { 143 &find(@args); 144} elsif ($command eq 'du') { 145 &du(@args); 146} elsif ($command eq 'copy' or $command eq 'cp') { 147 ©(@args); 148} elsif ($command eq 'move' or $command eq 'mv') { 149 &move(@args); 150} elsif ($command eq 'mkdir' or $command eq 'mkpath') { 151 &mkdir(@args); 152} elsif ($command eq 'delete' or $command eq 'rm' or $command eq 'rmtree') { 153 &delete(@args); 154} elsif ($command eq 'upload' or $command eq 'up' or $command eq 'put') { 155 &upload(@args); 156} elsif ($command eq 'download' or $command eq 'dl' or $command eq 'get') { 157 &download(@args); 158} elsif ($command eq 'sync') { 159 &sync(@args); 160} elsif ($command eq 'help' or (not length $command)) { 161 &help(@args); 162} else { 163 die "unknown command $command"; 164} 165 166exit($exit_code); 167 168sub help { 169 my ($command) = @_; 170 171 $command ||= ''; 172 173 my $help; 174 if ($command eq 'ls' or $command eq 'list') { 175 $help = q{ 176 Name 177 dropbox-api-ls - list directory contents 178 179 SYNOPSIS 180 dropbox-api ls <dropbox_path> [options] 181 182 Example 183 dropbox-api ls /Public 184 dropbox-api ls /Public -h 185 dropbox-api ls /Public -p "%d\t%s\t%TY/%Tm/%Td %TH:%TM:%TS\t%p\n" 186 187 Options 188 -h print sizes in human readable format (e.g., 1K 234M 2G) 189 -p print format. 190 %d ... is_dir ( d: dir, -: file ) 191 %i ... id 192 %n ... name 193 %p ... path_display 194 %P ... path_lower 195 %b ... bytes 196 %s ... size (e.g., 1K 234M 2G) 197 %t ... server_modified 198 %c ... client_modified 199 %r ... rev 200 %Tk ... DateTime 'strftime' function (server_modified) 201 %Ck ... DateTime 'strftime' function (client_modified) 202 }; 203 } elsif ($command eq 'find') { 204 $help = q{ 205 Name 206 dropbox-api-find - walk a file hierarchy 207 208 SYNOPSIS 209 dropbox-api find <dropbox_path> [options] 210 211 Example 212 dropbox-api find /Public 213 dropbox-api find /Public -h 214 dropbox-api find /Public -p "%d\t%s\t%TY/%Tm/%Td %TH:%TM:%TS\t%p\n" 215 216 Options 217 -h print sizes in human readable format (e.g., 1K 234M 2G) 218 -p print format. 219 %d ... is_dir ( d: dir, -: file ) 220 %i ... id 221 %n ... name 222 %p ... path_display 223 %P ... path_lower 224 %b ... bytes 225 %s ... size (e.g., 1K 234M 2G) 226 %t ... server_modified 227 %c ... client_modified 228 %r ... rev 229 %Tk ... DateTime 'strftime' function (server_modified) 230 %Ck ... DateTime 'strftime' function (client_modified) 231 }; 232 } elsif ($command eq 'du') { 233 $help = q{ 234 Name 235 dropbox-api-du - list directory contents 236 237 SYNOPSIS 238 dropbox-api du <dropbox_path> [options] 239 240 Example 241 dropbox-api du /Public 242 dropbox-api du / -h 243 dropbox-api du / -d 1 244 245 Options 246 -h print sizes in human readable format (e.g., 1K 234M 2G) 247 -d depth. 248 }; 249 } elsif ($command eq 'copy' or $command eq 'cp') { 250 $help = q{ 251 Name 252 dropbox-api-cp - copy file or directory 253 254 SYNOPSIS 255 dropbox-api cp <source_file> <target_file> 256 257 Example 258 dropbox-api cp /Public/hoge.txt /Public/foo.txt 259 dropbox-api cp /Public/work /Public/work_bak 260 }; 261 } elsif ($command eq 'move' or $command eq 'mv') { 262 $help = q{ 263 Name 264 dropbox-api-mv - move file or directory 265 266 SYNOPSIS 267 dropbox-api mv <source_file> <target_file> 268 269 Example 270 dropbox-api mv /Public/hoge.txt /Public/foo.txt 271 dropbox-api mv /Public/work /Public/work_bak 272 }; 273 } elsif ($command eq 'mkdir' or $command eq 'mkpath') { 274 $help = q{ 275 Name 276 dropbox-api-mkdir - make directory (Create intermediate directories as required) 277 278 SYNOPSIS 279 dropbox-api mkdir <directory> 280 281 Example 282 dropbox-api mkdir /Public/product/chrome-extentions/foo 283 }; 284 } elsif ($command eq 'delete' or $command eq 'rm' or $command eq 'rmtree') { 285 $help = q{ 286 Name 287 dropbox-api-rm - remove file or directory (Attempt to remove the file hierarchy rooted in each file argument) 288 289 SYNOPSIS 290 dropbox-api rm <file_or_directory> 291 292 Example 293 dropbox-api rm /Public/work_bak/hoge.tmp 294 dropbox-api rm /Public/work_bak 295 }; 296 } elsif ($command eq 'upload' or $command eq 'up' or $command eq 'put') { 297 $help = q{ 298 Name 299 dropbox-api-put - upload file 300 301 SYNOPSIS 302 dropbox-api put <file> dropbox:<dropbox_file> 303 304 Example 305 dropbox-api put README.md dropbox:/Public/product/dropbox-api/ 306 }; 307 } elsif ($command eq 'download' or $command eq 'dl' or $command eq 'get') { 308 $help = q{ 309 Name 310 dropbox-api-get - download file 311 312 SYNOPSIS 313 dropbox-api get dropbox:<dropbox_file> <file> 314 315 Example 316 dropbox-api get dropbox:/Public/product/dropbox-api/README.md README.md 317 }; 318 } elsif ($command eq 'sync') { 319 $help = q{ 320 Name 321 dropbox-api-sync - sync directory 322 323 SYNOPSIS 324 dropbox-api sync dropbox:<source_dir> <target_dir> [options] 325 dropbox-api sync <source_dir> dropbox:<target_dir> [options] 326 327 Example 328 dropbox-api sync dropbox:/Public/product/dropbox-api/ ~/work/dropbox-api/ 329 dropbox-api sync ~/work/dropbox-api/ dropbox:/Public/product/dropbox-api/ -vdn 330 dropbox-api sync ~/work/dropbox-api/ dropbox:/Public/product/dropbox-api/ -d 331 332 Options 333 -v increase verbosity 334 -n show what would have been transferred (dry-run) 335 -d delete files that don't exist on sender 336 }; 337 } elsif ($command eq 'version') { 338 $help = qq{ 339 This is dropbox-api-command, version $VERSION 340 341 Copyright 2016, Shinichiro Aska 342 343 Released under the MIT license. 344 345 Documentation 346 this system using "dropbox-api help". 347 If you have access to the Internet, point your browser at 348 https://github.com/s-aska/dropbox-api-command, 349 the dropbox-api-command Repository. 350 }; 351 } else { 352 $help = qq{ 353 Usage: dropbox-api <command> [args] [options] 354 355 Available commands: 356 setup get access_key and access_secret 357 ls list directory contents 358 find walk a file hierarchy 359 du disk usage statistics 360 cp copy file or directory 361 mv move file or directory 362 mkdir make directory (Create intermediate directories as required) 363 rm remove file or directory (Attempt to remove the file hierarchy rooted in each file argument) 364 put upload file 365 get download file 366 sync sync directory (local => dropbox or dropbox => local) 367 368 Common Options 369 -e enable env_proxy ( HTTP_PROXY, NO_PROXY ) 370 -D enable debug 371 -v verbose 372 373 See 'dropbox-api help <command>' for more information on a specific command. 374 }; 375 } 376 $help =~ s|^ {8}||mg; 377 $help =~ s|^\s*\n||; 378 print "\n$help\n"; 379} 380 381sub setup { 382 my $config = {}; 383 384 print "Please Input API Key: "; 385 chomp( my $key = <STDIN> ); 386 die 'Get API Key from https://www.dropbox.com/developers' unless $key; 387 $config->{key} = $key; 388 389 print "Please Input API Secret: "; 390 chomp( my $secret = <STDIN> ); 391 die 'Get API Secret from https://www.dropbox.com/developers' unless $secret; 392 $config->{secret} = $secret; 393 394 my $box = WebService::Dropbox->new($config); 395 $box->env_proxy if $env_proxy; 396 my $login_link = $box->authorize; 397 die $box->error if $box->error; 398 print "1. Open the Login URL: $login_link\n"; 399 print "2. Input code and press Enter: "; 400 chomp( my $code = <STDIN> ); 401 unless ($box->token($code)) { 402 die $box->error; 403 } 404 405 $config->{access_token} = $box->access_token; 406 print "success! try\n"; 407 print "> dropbox-api ls\n"; 408 print "> dropbox-api find /\n"; 409 410 $config_file->openw->print(encode_json($config)); 411 412 chmod 0600, $config_file; 413 414 exit(0); 415} 416 417sub du { 418 my $remote_base = decode('locale_fs', slash(shift)); 419 $remote_base =~ s|/$||; 420 my $entries = _find($remote_base); 421 my $dir_map = {}; 422 for my $content (@{ $entries }) { 423 if ($content->{'.tag'} eq 'folder') { 424 next; 425 } 426 my @paths = _paths($remote_base, $content->{path_lower}); 427 for my $path (@paths) { 428 $dir_map->{ lc $path } ||= 0; 429 $dir_map->{ lc $path } += $content->{size}; 430 } 431 } 432 if (!$remote_base) { 433 my $size = $dir_map->{'/'} || 0; 434 if ($human) { 435 $size = format_bytes($size); 436 } 437 printf("%s\t%s\n", $size, '/'); 438 } 439 for my $content (@{ $entries }) { 440 if ($content->{'.tag'} ne 'folder') { 441 next; 442 } 443 if (defined $max_depth) { 444 my $path = $content->{path_lower}; 445 $path =~ s|^\Q$remote_base\E/?||i; 446 my $depth = $path ? scalar(split('/', $path)) : 0; 447 if ($depth > $max_depth) { 448 next; 449 } 450 } 451 my $size = $dir_map->{ lc $content->{path_lower} } || 0; 452 if ($human) { 453 $size = format_bytes($size); 454 } 455 printf("%s\t%s\n", $size, $content->{path_display}); 456 } 457} 458 459sub _paths ($$) { 460 my ($base_path, $path) = @_; 461 $path =~ s|^\Q$base_path\E/?||i; 462 my @paths; 463 my $dir = $base_path || '/'; 464 push @paths, $dir; 465 my @names = split '/', $path; 466 pop @names; 467 for my $name (@names) { 468 if ($dir ne '/') { 469 $dir .= '/'; 470 } 471 $dir .= $name; 472 push @paths, $dir; 473 } 474 return @paths; 475} 476 477sub list { 478 my $remote_base = decode('locale_fs', slash(shift)); 479 if ($remote_base eq '/') { 480 $remote_base = ''; 481 } 482 my $list = $box->list_folder($remote_base) or die $box->error; 483 for my $entry (@{ $list->{entries} }) { 484 print &_line($entry); 485 } 486} 487 488sub _line { 489 my ($content) = @_; 490 $strp ||= new DateTime::Format::Strptime( pattern => '%Y-%m-%dT%T' ); 491 my $dt; 492 my $ct; 493 my $get = sub { 494 my $key = $format->{ $_[0] }; 495 if ($key eq 'format_size') { 496 return exists $content->{size} ? format_bytes($content->{size}) : ' -'; 497 } elsif ($key eq 'is_dir') { 498 $content->{'.tag'} eq 'folder' ? 'd' : '-'; 499 } elsif ($key eq 'thumb_exists') { 500 if ($content->{path_display} =~ qr{ \.(?:jpg|jpeg|png|tiff|tif|gif|bmp) \z }xms && $content->{size} < 20 * 1024 * 1024) { 501 return 'true'; 502 } else { 503 return 'false'; 504 } 505 } else { 506 return exists $content->{ $key } ? $content->{ $key } : '-'; 507 } 508 }; 509 if ($printf) { 510 my $line = eval qq{"$printf"}; 511 if ($content->{server_modified}) { 512 $line=~s/\%T([^\%])/ 513 $dt ||= $strpz->parse_datetime($content->{server_modified}); 514 $dt->strftime('%'.$1); 515 /egx; 516 } else { 517 $line=~s/\%TY/----/g; 518 $line=~s/\%T([^\%])/--/g; 519 } 520 if ($content->{client_modified}) { 521 $line=~s/\%C([^\%])/ 522 $ct ||= $strpz->parse_datetime($content->{client_modified}); 523 $ct->strftime('%'.$1); 524 /egx; 525 } else { 526 $line=~s/\%CY/----/g; 527 $line=~s/\%C([^\%])/--/g; 528 } 529 $line=~s|\%([^\%])|$get->($1)|eg; 530 return $line; 531 } else { 532 return sprintf "%s %8s %s %s\n", 533 ($content->{'.tag'} eq 'folder' ? 'd' : '-'), 534 $get->($human ? 's' : 'b'), 535 $get->('t'), 536 $content->{path_display}; 537 } 538} 539 540sub find { 541 my $remote_base = decode('locale_fs', slash(shift)); 542 if ($remote_base eq '/') { 543 $remote_base = ''; 544 } 545 $printf ||= "%p\n"; 546 my $entries = _find($remote_base); 547 for my $entry (@{ $entries }) { 548 print &_line($entry); 549 } 550} 551 552sub _find ($) { 553 my $remote_base = decode('locale_fs', slash(shift)); 554 if ($remote_base eq '/') { 555 $remote_base = ''; 556 } 557 my @entries; 558 my $fetch; 559 my $count = 0; 560 $fetch = sub { 561 my $cursor = shift; 562 my $list; 563 if ($cursor) { 564 $list = $box->list_folder_continue($cursor) or die $box->error; 565 } else { 566 $list = $box->list_folder($remote_base, { 567 recursive => JSON::true, 568 }) or die $box->error; 569 } 570 push @entries, @{ $list->{entries} }; 571 if ($list->{has_more}) { 572 if ($verbose) { 573 $| = 1; 574 $count++; 575 printf("\r" . (('.') x $count)); 576 } 577 $fetch->($list->{cursor}); 578 } 579 }; 580 $fetch->(); 581 if ($verbose) { 582 print "\n"; 583 } 584 [ sort { $a->{path_lower} cmp $b->{path_lower} } @entries ]; 585} 586 587sub copy { 588 my ($src, $dst) = @_; 589 my $res = $box->copy(decode('locale_fs', slash($src)), decode('locale_fs', slash($dst))) or die $box->error; 590 print pretty($res) if $verbose; 591} 592 593sub move { 594 my ($src, $dst) = @_; 595 my $res = $box->move(decode('locale_fs', slash($src)), decode('locale_fs', slash($dst))) or die $box->error; 596 print pretty($res) if $verbose; 597} 598 599sub mkdir { 600 my ($dir) = @_; 601 my $res = $box->create_folder(decode('locale_fs', slash($dir))) or die $box->error; 602 print pretty($res) if $verbose; 603} 604 605sub delete { 606 my ($file_or_dir) = @_; 607 my $res = $box->delete(decode('locale_fs', slash($file_or_dir))) or die $box->error; 608 print pretty($res) if $verbose; 609} 610 611sub upload { 612 my ($file, $path) = @_; 613 $path =~ s|^dropbox:/|/| 614 or die "Usage: \n dropbox-api upload /tmp/local.txt dropbox:/Public/some.txt"; 615 my $local_path = file($file); 616 if ((! length $path) or $path =~ m|/$|) { 617 $path.= basename($file); 618 } 619 my $res = &put($local_path, decode('locale_fs', $path)) or die $box->error; 620 621 if ($verbose) { 622 print pretty($res); 623 } 624 625 my $id = $res->{id}; 626 627 if ($public) { 628 my $list_shared_links = $box->api({ 629 url => 'https://api.dropboxapi.com/2/sharing/list_shared_links', 630 params => { 631 path => $id, 632 } 633 }) or die $box->error; 634 for (@{ $list_shared_links->{links} }) { 635 if ($id eq $_->{id} && $_->{link_permissions}{resolved_visibility}{'.tag'} eq 'public') { 636 print $_->{url}, "\n"; 637 return; 638 } 639 } 640 641 my $res = $box->api({ 642 url => 'https://api.dropboxapi.com/2/sharing/create_shared_link_with_settings', 643 params => { 644 path => $path, 645 settings => { 646 requested_visibility => 'public', 647 } 648 } 649 }) or die $box->error; 650 print $res->{url}, "\n"; 651 } 652} 653 654sub download { 655 my ($path, $file) = @_; 656 $path =~ s|^dropbox:/|/| 657 or die "Usage: \n dropbox-api download dropbox:/Public/some.txt /tmp/local.txt"; 658 my $fh = file($file)->openw or die $!; 659 $box->download(decode('locale_fs', $path), $fh) or die $box->error; 660 $fh->close; 661} 662 663sub sync { 664 my ($arg1, $arg2) = @_; 665 666 if ($dry) { 667 print "!! enable dry run !!\n"; 668 } 669 670 # download 671 if ($arg1 =~ qr{ \A dropbox: }xms and $arg2 !~ qr{ \A dropbox: }xms) { 672 673 my ($remote_base, $local_base) = ($arg1, $arg2); 674 $remote_base = decode('locale_fs', $remote_base); 675 $remote_base =~ s|^dropbox:||; 676 677 if ($remote_base eq '/' || $remote_base eq '') { 678 unless (-d $local_base) { 679 die "missing $local_base"; 680 } 681 &sync_download('/', dir(abs_path($local_base))); 682 } else { 683 my $content = $box->get_metadata(chomp_slash($remote_base)) or die $box->error; 684 if ($content->{'.tag'} eq 'folder') { 685 unless (-d $local_base) { 686 die "missing $local_base"; 687 } 688 &sync_download($content->{path_display}, dir(abs_path($local_base))); 689 } else { 690 $local_base = -d $local_base ? dir(abs_path($local_base)) : -f $local_base ? file(abs_path($local_base)) : file($local_base); 691 &sync_download_file($content, file($local_base)); 692 } 693 } 694 } 695 696 # upload 697 elsif ($arg1 !~ qr{ \A dropbox: }xms and $arg2 =~ qr{ \A dropbox: }xms) { 698 699 my ($local_base, $remote_base) = ($arg1, $arg2); 700 $remote_base = decode('locale_fs', $remote_base); 701 $remote_base =~ s|^dropbox:||; 702 703 if (-d $local_base) { 704 &sync_upload($remote_base, dir(abs_path($local_base))); 705 } elsif (-f $local_base) { 706 &sync_upload_file($remote_base, file(abs_path($local_base))); 707 } else { 708 die "missing $local_base"; 709 } 710 } 711 712 # invalid command 713 else { 714 die "Usage: \n dropbox-api sync dropbox:/Public/ /tmp/pub/\n" . 715 "or dropbox-api sync /tmp/pub/ dropbox:/Public/"; 716 } 717} 718 719sub sync_download { 720 my ($remote_base, $local_base) = @_; 721 722 if ($verbose) { 723 print "remote_base: $remote_base\n"; 724 print "local_base: $local_base\n"; 725 } 726 727 print "** download **\n" if $verbose; 728 729 my $entries = _find($remote_base); 730 unless (@{ $entries }) { 731 return; 732 } 733 734 my $remote_map = {}; 735 my $remote_inode_map = {}; 736 737 for my $content (@{ $entries }) { 738 my $remote_path = $content->{path_display}; 739 my $rel_path = remote_abs2rel($remote_path, $remote_base); 740 unless (length $rel_path) { 741 if ($content->{'.tag'} eq 'folder') { 742 next; 743 } else { 744 $rel_path = $content->{name}; 745 } 746 } 747 my $rel_path_enc = encode('locale_fs', $rel_path); 748 $remote_map->{$rel_path}++; 749 printf "check: %s\n", $rel_path if $debug; 750 my $is_dir = $content->{'.tag'} eq 'folder' ? 1 : 0; 751 my $local_path = $is_dir ? dir($local_base, $rel_path_enc) : file($local_base, $rel_path_enc); 752 if ($is_dir) { 753 printf "remote: %s\n", $remote_path if $debug; 754 printf "local: %s\n", $local_path if $debug; 755 if (!-d $local_path) { 756 $local_path->mkpath unless $dry; 757 printf "mkpath %s\n", decode('locale_fs', $local_path); 758 } else { 759 printf "skip %s\n", $rel_path if $verbose; 760 } 761 } else { 762 763 if ((!-f $local_path) || has_change($local_path, $content)) { 764 765 if ($dry) { 766 printf "download %s\n", decode('locale_fs', $local_path); 767 next; 768 } 769 770 # not displayed in the dry-run for the insurance 771 unless (-d $local_path->dir) { 772 printf "mkpath %s\n", decode('locale_fs', $local_path->dir); 773 $local_path->dir->mkpath; 774 } 775 776 my $local_path_tmp = $local_path . '.dropbox-api.tmp'; 777 my $fh; 778 unless (open($fh, '>', $local_path_tmp)) { 779 warn "open failure " . decode('locale_fs', $local_path) . " (" . $! . ")"; 780 $exit_code = 1; 781 next; 782 } 783 if ($box->download($content->{path_display}, $fh)) { 784 printf "download %s\n", decode('locale_fs', $local_path); 785 close($fh); 786 my $remote_epoch = $strpz->parse_datetime($content->{client_modified})->epoch; 787 unless (utime($remote_epoch, $remote_epoch, $local_path_tmp)) { 788 warn "set modification time failure " . decode('locale_fs', $local_path); 789 $exit_code = 1; 790 } 791 unless (rename($local_path_tmp, $local_path)) { 792 unlink($local_path_tmp); 793 warn "rename failure " . decode('locale_fs', $local_path_tmp); 794 $exit_code = 1; 795 } 796 } else { 797 unlink($local_path_tmp); 798 chomp( my $error = $box->error ); 799 warn "download failure " . decode('locale_fs', $local_path) . " (" . $error . ")"; 800 $exit_code = 1; 801 } 802 } else { 803 printf "skip %s\n", $rel_path if $verbose; 804 } 805 } 806 $remote_inode_map->{ &inode($local_path) } = $content; 807 } 808 809 if ($exit_code) { 810 return; 811 } 812 813 unless ($delete) { 814 return; 815 } 816 817 if ($verbose) { 818 print "** delete **\n"; 819 } 820 821 my @deletes; 822 $local_base->recurse( 823 preorder => 0, 824 depthfirst => 1, 825 callback => sub { 826 my $local_path = shift; 827 if ($local_path eq $local_base) { 828 return; 829 } 830 831 my $rel_path_enc = abs2rel($local_path, $local_base); 832 my $rel_path = decode('locale_fs', $rel_path_enc); 833 834 if (exists $remote_map->{$rel_path}) { 835 if ($verbose) { 836 printf "skip %s\n", $rel_path; 837 } 838 } elsif (my $content = $remote_inode_map->{ &inode($local_path) }) { 839 my $remote_path = $content->{path_display}; 840 my $rel_path_remote = remote_abs2rel($remote_path, $remote_base); 841 if ($verbose) { 842 if ($debug) { 843 printf "skip %s ( is %s )\n", $rel_path, $rel_path_remote; 844 } else { 845 printf "skip %s\n", $rel_path; 846 } 847 } 848 } elsif (-f $local_path) { 849 printf "remove %s\n", $rel_path; 850 push @deletes, $local_path; 851 } elsif (-d $local_path) { 852 printf "rmtree %s\n", $rel_path; 853 push @deletes, $local_path; 854 } 855 } 856 ); 857 858 if ($dry) { 859 return; 860 } 861 862 for my $local_path (@deletes) { 863 if (-f $local_path) { 864 $local_path->remove; 865 } elsif (-d $local_path) { 866 $local_path->rmtree; 867 } 868 } 869} 870 871sub sync_download_file { 872 my ($content, $local_path) = @_; 873 874 if ($verbose) { 875 print "remote_base: " . $content->{name} . "\n"; 876 print "local_base: $local_path\n"; 877 } 878 879 if (-d $local_path) { 880 $local_path = file($local_path, $content->{name}); 881 } 882 883 if ((!-f $local_path) || has_change($local_path, $content)) { 884 885 if ($dry) { 886 printf "download %s\n", decode('locale_fs', $local_path); 887 return; 888 } 889 890 unless (-d $local_path->dir) { 891 printf "mkpath %s\n", decode('locale_fs', $local_path->dir); 892 $local_path->dir->mkpath; 893 } 894 895 my $local_path_tmp = $local_path . '.dropbox-api.tmp'; 896 my $fh; 897 unless (open($fh, '>', $local_path_tmp)) { 898 warn "open failure " . decode('locale_fs', $local_path) . " (" . $! . ")"; 899 $exit_code = 1; 900 return; 901 } 902 if ($box->download($content->{path_display}, $fh)) { 903 printf "download %s\n", decode('locale_fs', $local_path); 904 close($fh); 905 my $remote_epoch = $strpz->parse_datetime($content->{client_modified})->epoch; 906 unless (utime($remote_epoch, $remote_epoch, $local_path_tmp)) { 907 warn "set modification time failure " . decode('locale_fs', $local_path); 908 $exit_code = 1; 909 } 910 unless (rename($local_path_tmp, $local_path)) { 911 unlink($local_path_tmp); 912 warn "rename failure " . decode('locale_fs', $local_path_tmp); 913 $exit_code = 1; 914 } 915 } else { 916 unlink($local_path_tmp); 917 chomp( my $error = $box->error ); 918 warn "download failure " . decode('locale_fs', $local_path) . " (" . $error . ")"; 919 $exit_code = 1; 920 } 921 } else { 922 printf "skip %s\n", $content->{path_display} if $verbose; 923 } 924} 925 926sub sync_upload { 927 my ($remote_base, $local_base) = @_; 928 929 930 if ($verbose) { 931 print "remote_base: $remote_base\n"; 932 print "local_base: $local_base\n"; 933 } 934 935 print "** upload **\n" if $verbose; 936 937 my $remote_map = {}; 938 my $remote_path_map = {}; 939 940 my $entries = _find($remote_base); 941 for my $content (@{ $entries }) { 942 my $remote_path = $content->{path_display}; 943 my $rel_path = remote_abs2rel($remote_path, $remote_base); 944 unless (length $rel_path) { 945 next; 946 } 947 $remote_map->{ lc $rel_path } = $content; 948 $remote_path_map->{ $content->{path_display} } = $content; 949 if ($debug) { 950 printf "find: %s\n", $rel_path; 951 } 952 } 953 954 my @makedirs; 955 $local_base->recurse( 956 preorder => 0, 957 depthfirst => 1, 958 callback => sub { 959 my $local_path = shift; 960 if ($local_path eq $local_base) { 961 return; 962 } 963 my $rel_path = decode('locale_fs', abs2rel($local_path, $local_base)); 964 my $remote_path = file($remote_base, $rel_path); 965 my $content = delete $remote_map->{ lc $rel_path }; 966 967 # exists file or directory 968 if ($content) { 969 delete $remote_path_map->{ $content->{path_display} }; 970 971 unless (-f $local_path) { 972 return; 973 } 974 975 if (has_change($local_path, $content)) { 976 printf "upload %s %s\n", $rel_path, $remote_path; 977 unless ($dry) { 978 if ($content->{size} == -s $local_path) { 979 $box->delete("$remote_path"); 980 } 981 my $local_epoch = $local_path->stat->mtime; 982 &put($local_path, "$remote_path", { client_modified => $strp->format_datetime(DateTime->from_epoch( epoch => $local_epoch )) . 'Z' }) or die $box->error; 983 } 984 push @makedirs, $rel_path; 985 } elsif ($verbose) { 986 printf "skip %s\n", $rel_path; 987 } 988 } 989 990 # new file 991 elsif (-f $local_path) { 992 unless ($dry) { 993 my $local_epoch = $local_path->stat->mtime; 994 &put($local_path, "$remote_path", { client_modified => $strp->format_datetime(DateTime->from_epoch( epoch => $local_epoch )) . 'Z' }); 995 } 996 if (!$dry && $box->error) { 997 warn "upload failure $rel_path $remote_path (" . $box->error . ")"; 998 } else { 999 printf "upload %s %s\n", $rel_path, $remote_path; 1000 push @makedirs, $rel_path; 1001 } 1002 } 1003 1004 # new directory 1005 elsif (-d $local_path) { 1006 1007 if (grep { $_ =~ qr{ \A\Q$rel_path }xms } @makedirs) { 1008 return; 1009 } 1010 1011 printf "mktree %s %s\n", $rel_path, $remote_path; 1012 1013 unless ($dry) { 1014 $box->create_folder("$remote_path") or die $box->error; 1015 } 1016 1017 push @makedirs, $rel_path; 1018 } else { 1019 printf "unknown %s\n", $rel_path; 1020 } 1021 } 1022 ); 1023 1024 return unless $delete; 1025 1026 print "** delete **\n" if $verbose; 1027 1028 my @deletes; 1029 for my $content_path ( keys %$remote_path_map ) { 1030 1031 if (chomp_slash($content_path) eq chomp_slash($remote_base)) { 1032 next; 1033 } 1034 1035 if (grep { $content_path =~ qr{ \A\Q$_ }xms } @deletes) { 1036 next; 1037 } 1038 1039 unless ($dry) { 1040 $box->delete($content_path) or die $box->error; 1041 } 1042 1043 push @deletes, $content_path; 1044 1045 printf "delete %s\n", remote_abs2rel($content_path, $remote_base); 1046 } 1047} 1048 1049sub sync_upload_file { 1050 my ($remote_base, $local_path) = @_; 1051 1052 if ($verbose) { 1053 print "remote_base: $remote_base\n"; 1054 print "local_path: $local_path\n"; 1055 } 1056 1057 my $remote_path; 1058 my $content; 1059 { 1060 local $SIG{__WARN__} = sub {}; 1061 1062 $content = $box->get_metadata(chomp_slash($remote_base)); 1063 1064 # exists folder 1065 if ($content && $content->{'.tag'} eq 'folder') { 1066 $remote_path = file($remote_base, basename($local_path)); 1067 my $remote_file_content = $box->get_metadata(chomp_slash("$remote_path")); 1068 if ($remote_file_content) { 1069 if ($remote_file_content->{'.tag'} eq 'folder') { 1070 die "$remote_path is folder."; 1071 } 1072 $content = $remote_file_content; 1073 } 1074 } else { 1075 if ($remote_base =~ qr{ / \z }xms) { 1076 $remote_path = file($remote_base, basename($local_path)); 1077 } else { 1078 $remote_path = $remote_base; 1079 } 1080 } 1081 } 1082 1083 # exists file 1084 if ($content && $content->{'.tag'} ne 'folder') { 1085 if ($debug) { 1086 printf "find: %s\n", $content->{path_display}; 1087 } 1088 $remote_path = $content->{path_display}; 1089 1090 if (has_change($local_path, $content)) { 1091 printf "upload %s %s\n", $local_path, $remote_path; 1092 unless ($dry) { 1093 if ($content->{size} == -s $local_path) { 1094 $box->delete("$remote_path"); 1095 } 1096 my $local_epoch = $local_path->stat->mtime; 1097 &put($local_path, "$remote_path", { client_modified => $strp->format_datetime(DateTime->from_epoch( epoch => $local_epoch )) . 'Z' }) or die $box->error; 1098 } 1099 } elsif ($verbose) { 1100 printf "skip %s\n", $local_path; 1101 } 1102 return; 1103 } 1104 1105 unless ($dry) { 1106 my $local_epoch = $local_path->stat->mtime; 1107 &put($local_path, "$remote_path", { client_modified => $strp->format_datetime(DateTime->from_epoch( epoch => $local_epoch )) . 'Z' }); 1108 } 1109 1110 printf "upload %s %s\n", $local_path, $remote_path; 1111} 1112 1113sub has_change ($$) { 1114 my ($local_path, $content) = @_; 1115 1116 my $remote_epoch = $strpz->parse_datetime($content->{client_modified})->epoch; 1117 my $local_epoch = $local_path->stat->mtime; 1118 my $remote_size = $content->{size}; 1119 my $local_size = $local_path->stat->size; 1120 1121 if ($debug) { 1122 printf "remote: %10s %10s %s\n", $remote_epoch, $remote_size, $content->{path_display}; 1123 printf "local: %10s %10s %s\n", $local_epoch, $local_size, decode('locale_fs', $local_path); 1124 } 1125 1126 if (($remote_size != $local_size) || ($remote_epoch != $local_epoch)) { 1127 return 1; 1128 } 1129 1130 return; 1131} 1132 1133sub put { 1134 my ($file, $path, $optional_params) = @_; 1135 1136 my $commit_params = { 1137 path => "$path", 1138 mode => 'overwrite', 1139 %{ $optional_params || +{} }, 1140 }; 1141 1142 my $content = $file->openr; 1143 my $size = -s $file; 1144 my $threshold = 10 * 1024 * 1024; 1145 1146 if ($size < $threshold) { 1147 return $box->upload("$path", $content, $commit_params); 1148 } 1149 1150 my $session_id; 1151 my $offset = 0; 1152 1153 my $limit = 4 * 1024 * 1024; 1154 1155 $| = 1; 1156 1157 my $upload; 1158 $upload = sub { 1159 my $buf; 1160 my $total = 0; 1161 my $chunk = 1024; 1162 my $tmp = File::Temp->new; 1163 my $is_last; 1164 while (my $read = read($content, $buf, $chunk)) { 1165 $tmp->print($buf); 1166 $total += $read; 1167 my $remaining = $limit - $total; 1168 if ($chunk > $remaining) { 1169 $chunk = $remaining; 1170 } 1171 unless ($chunk) { 1172 last; 1173 } 1174 } 1175 1176 $tmp->flush; 1177 $tmp->seek(0, 0); 1178 1179 # finish or small file 1180 if ($total < $limit) { 1181 if ($session_id) { 1182 my $params = { 1183 cursor => { 1184 session_id => $session_id, 1185 offset => $offset, 1186 }, 1187 commit => $commit_params, 1188 }; 1189 return $box->upload_session_finish($tmp, $params); 1190 } else { 1191 return $box->upload("$path", $tmp, $commit_params); 1192 } 1193 } 1194 1195 # append 1196 elsif ($session_id) { 1197 my $params = { 1198 cursor => { 1199 session_id => $session_id, 1200 offset => $offset, 1201 }, 1202 }; 1203 unless ($box->upload_session_append_v2($tmp, $params)) { 1204 # some error 1205 return; 1206 } 1207 $offset += $total; 1208 } 1209 1210 # start 1211 else { 1212 my $res = $box->upload_session_start($tmp); 1213 if ($res && $res->{session_id}) { 1214 $session_id = $res->{session_id}; 1215 $offset = $total; 1216 } else { 1217 # some error 1218 return; 1219 } 1220 } 1221 1222 # ProgressBar 1223 if ($verbose) { 1224 my $rate = sprintf('%2.1d%%', $offset / $size * 100); 1225 my $bar = '=' x int(($cols - length($rate) - 4) * $offset / $size); 1226 my $space = ' ' x ($cols - length($rate) - length($bar) - 4); 1227 printf "\r%s [%s>%s]", $rate, $bar, $space; 1228 } 1229 1230 $upload->(); 1231 }; 1232 $upload->(); 1233} 1234 1235sub inode ($) { 1236 my $path = shift; 1237 my ($dev, $inode) = stat($path); 1238 return $dev . ':' . $inode if $inode; 1239 return $path; 1240} 1241 1242sub remote_abs2rel ($$) { 1243 my ($remote_path, $remote_base) = @_; 1244 $remote_path =~ s|^\Q$remote_base\E/?||i; 1245 return $remote_path; 1246} 1247 1248sub slash ($) { 1249 my $path = shift; 1250 unless (defined $path) { 1251 return '/'; 1252 } 1253 if ($path !~ qr{ \A / }xms) { 1254 $path = '/' . $path; 1255 } 1256 $path; 1257} 1258 1259sub chomp_slash ($) { 1260 my $path = shift; 1261 unless (defined $path) { 1262 return ''; 1263 } 1264 $path =~ s|/$||; 1265 $path; 1266} 1267 1268sub pretty($) { 1269 JSON->new->utf8->pretty->encode($_[0]); 1270} 1271 1272use constant UNITS => [ 1273 [ 'P', 1024 ** 4 * 1000, 1024 ** 5 ], 1274 [ 'T', 1024 ** 3 * 1000, 1024 ** 4 ], 1275 [ 'G', 1024 ** 2 * 1000, 1024 ** 3 ], 1276 [ 'M', 1024 * 1000, 1024 ** 2 ], 1277 [ 'K', 1000, 1024 ], 1278 [ 'B', 0, 1 ], 1279]; 1280 1281sub format_bytes ($) { 1282 my $size = shift; 1283 if ($size > 0) { 1284 for my $unit (@{ UNITS() }) { 1285 my ($unit_label, $unit_min, $unit_value) = @{ $unit }; 1286 if ($size >= $unit_min) { 1287 my $size_unit = $size / $unit_value; 1288 if (round($size_unit) < 10) { 1289 return sprintf('%1.1f%s', nearest(.1, $size_unit), $unit_label); 1290 } else { 1291 return sprintf('%3s%s', round($size_unit), $unit_label); 1292 } 1293 } 1294 } 1295 } 1296 return ' 0B'; 1297} 1298 1299sub round ($) { 1300 POSIX::floor($_[0] + 0.50000000000008); 1301} 1302 1303sub nearest ($) { 1304 round($_[1] / $_[0]) * $_[0]; 1305} 1306 1307sub token_from_oauth1 { 1308 my $key = shift; 1309 my $secret = shift; 1310 my $access_token = shift; 1311 my $access_secret = shift; 1312 1313 require WebService::Dropbox::TokenFromOAuth1; 1314 1315 WebService::Dropbox::TokenFromOAuth1->token_from_oauth1({ 1316 consumer_key => $key, 1317 consumer_secret => $secret, 1318 access_token => $access_token, # OAuth1 access_token 1319 access_secret => $access_secret, # OAuth2 access_secret 1320 }); 1321} 1322 1323exit(0); 1324