├── .travis.yml ├── minil.toml ├── t └── happy_cpantesters.t ├── script ├── upload-to-dropbox └── dropbox-api ├── cpanfile ├── .gitignore ├── Build.PL ├── LICENSE ├── META.json ├── Changes ├── README.md └── lib └── App └── dropboxapi.pm /.travis.yml: -------------------------------------------------------------------------------- 1 | language: perl 2 | perl: 3 | - 5.22 4 | - 5.24 5 | - 5.26 6 | - 5.28 7 | - 5.30 8 | -------------------------------------------------------------------------------- /minil.toml: -------------------------------------------------------------------------------- 1 | name = "App-dropboxapi" 2 | authority = "cpan:ASKADNA" 3 | badges = ['travis'] 4 | markdown_maker = "Pod::Markdown::Github" -------------------------------------------------------------------------------- /t/happy_cpantesters.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use Test::More tests => 1; 4 | 5 | BEGIN { require_ok 'App::dropboxapi' } 6 | 7 | diag("App::dropboxapi/$App::dropboxapi::VERSION"); -------------------------------------------------------------------------------- /script/upload-to-dropbox: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #################################################### 3 | # Upload a file to dropbox and output public address 4 | 5 | 6 | dropbox-api put $1 dropbox:/Public/ -P 7 | -------------------------------------------------------------------------------- /cpanfile: -------------------------------------------------------------------------------- 1 | requires 'perl', '5.008001'; 2 | 3 | requires 'JSON'; 4 | requires 'Path::Class', 0.26; 5 | requires 'WebService::Dropbox', 2.06; 6 | requires 'DateTime::Format::Strptime'; 7 | requires 'Encode::Locale'; 8 | 9 | if ($^O eq 'darwin') { 10 | requires 'Encode::UTF8Mac'; 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | blib 3 | Makefile 4 | Makefile.old 5 | MYMETA.json 6 | MYMETA.yml 7 | pm_to_blib 8 | *.tar.gz 9 | tool 10 | /dropbox-api-command-* 11 | /.build 12 | /_build_params 13 | /_build 14 | /Build 15 | !Build/ 16 | !META.json 17 | !LICENSE 18 | /App-dropboxapi-* 19 | local 20 | cpanfile.snapshot 21 | .envrc 22 | -------------------------------------------------------------------------------- /Build.PL: -------------------------------------------------------------------------------- 1 | # ========================================================================= 2 | # THIS FILE IS AUTOMATICALLY GENERATED BY MINILLA. 3 | # DO NOT EDIT DIRECTLY. 4 | # ========================================================================= 5 | 6 | use 5.008_001; 7 | use strict; 8 | 9 | use Module::Build::Tiny 0.035; 10 | 11 | Build_PL(); 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 by Shinichiro Aska 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /META.json: -------------------------------------------------------------------------------- 1 | { 2 | "abstract" : "command line interface to access Dropbox API", 3 | "author" : [ 4 | "- Shinichiro Aska" 5 | ], 6 | "dynamic_config" : 0, 7 | "generated_by" : "Minilla/v3.0.2, CPAN::Meta::Converter version 2.150005", 8 | "license" : [ 9 | "unknown" 10 | ], 11 | "meta-spec" : { 12 | "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", 13 | "version" : "2" 14 | }, 15 | "name" : "App-dropboxapi", 16 | "no_index" : { 17 | "directory" : [ 18 | "t", 19 | "xt", 20 | "inc", 21 | "share", 22 | "eg", 23 | "examples", 24 | "author", 25 | "builder" 26 | ] 27 | }, 28 | "prereqs" : { 29 | "configure" : { 30 | "requires" : { 31 | "Module::Build::Tiny" : "0.035" 32 | } 33 | }, 34 | "develop" : { 35 | "requires" : { 36 | "Test::CPAN::Meta" : "0", 37 | "Test::MinimumVersion::Fast" : "0.04", 38 | "Test::PAUSE::Permissions" : "0.04", 39 | "Test::Pod" : "1.41", 40 | "Test::Spellunker" : "v0.2.7" 41 | } 42 | }, 43 | "runtime" : { 44 | "requires" : { 45 | "DateTime::Format::Strptime" : "0", 46 | "Encode::Locale" : "0", 47 | "Encode::UTF8Mac" : "0", 48 | "JSON" : "0", 49 | "Path::Class" : "0.26", 50 | "WebService::Dropbox" : "2.06", 51 | "perl" : "5.008001" 52 | } 53 | } 54 | }, 55 | "release_status" : "unstable", 56 | "resources" : { 57 | "bugtracker" : { 58 | "web" : "https://github.com/s-aska/dropbox-api-command/issues" 59 | }, 60 | "homepage" : "https://github.com/s-aska/dropbox-api-command", 61 | "repository" : { 62 | "type" : "git", 63 | "url" : "git://github.com/s-aska/dropbox-api-command.git", 64 | "web" : "https://github.com/s-aska/dropbox-api-command" 65 | } 66 | }, 67 | "version" : "2.13", 68 | "x_authority" : "cpan:ASKADNA", 69 | "x_contributors" : [ 70 | "Sylvain Filteau ", 71 | "aska ", 72 | "Andrew Childs ", 73 | "aska ", 74 | "Shinichiro Aska " 75 | ], 76 | "x_serialization_backend" : "JSON::PP version 2.27300" 77 | } 78 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | Revision history for dropbox-api 2 | 3 | {{$NEXT}} 4 | 5 | 2.13 2017-05-31T11:33:47Z 6 | - Update module version 7 | 8 | 2.12 2017-02-07T09:23:46Z 9 | - fix typo 10 | - use upload_session 11 | 12 | 2.11 2017-01-01T18:58:11Z 13 | - parse_datetime is %Y-%m-%dT%TZ 14 | - format_datetime is %Y-%m-%dT%T 15 | 16 | 2.10 2017-01-01T15:58:01Z 17 | - Add Z for strformat 18 | 19 | 2.09 2016-07-18T10:12:03Z 20 | - Fix bug `sync dropbox: ~/Dropbox` 21 | 22 | 2.08 2016-07-18T10:00:42Z 23 | - Fix bug `sync dropbox:/ ~/Dropbox/` 24 | 25 | 2.07 2016-07-14T12:12:15Z 26 | - Add sync file `sync dropbox:/file.txt ~/Dropbox/` `sync ~/file.txt dropbox:/Dropbox/` 27 | 28 | 2.06 2016-07-14T08:12:56Z 29 | - Fix bug `sync dropbox:/ ~/Dropbox/` 30 | 31 | 2.05 2016-07-12T11:58:23Z 32 | - Fix bug path_lower has upper case 33 | 34 | 2.04 2016-07-12T11:30:53Z 35 | - Add `du` command 36 | 37 | 2.03 2016-07-11T16:13:38Z 38 | - Support `ls /` and `find /` 39 | 40 | 2.02 2016-07-11T15:00:38Z 41 | - Update cpanfile 42 | 43 | 2.01 2016-07-11T13:17:43Z 44 | - Fix typo and misspell. 45 | - Add `-s` option documented. 46 | - Fix setup example. 47 | Thanks mathstuf 48 | 49 | 2.00 2016-07-11T11:06:31Z 50 | - Migration API v2 51 | 52 | 1.17 Jun 18, 2014 53 | - fix bug `next` => `return` 54 | 55 | 1.16 Jun 10, 2014 56 | - fix bug `next` => `return` 57 | 58 | 1.15 Feb 18, 2013 59 | - catch open error 60 | 61 | 1.14 Oct 2, 2013 62 | - added LICENSE, README.md, cpanfile 63 | 64 | 1.13 Jul 8, 2013 65 | - Remove interpolation in printf format arguments. (thefloweringash) 66 | - Ignore stty errors when stdin isn't a terminal. (thefloweringash) 67 | 68 | 1.12 Jun 21, 2013 69 | - Dropbox are case insensitive. (zenoamaro) 70 | 71 | 1.11 Jun 20, 2013 72 | - Dropbox are case insensitive. 73 | 74 | 1.10 Jun 20, 2013 75 | - Use lowercase for HTTP_PROXY envvar (mathstuf) 76 | 77 | 1.09 Jan 8, 2013 78 | - fix "list" and "find" utf8 path bug. 79 | 80 | 1.08 Jan 8, 2013 81 | - added print option. ( %R ... rev, %c %Ck ... client_mtime ) 82 | - fix print option bug. 83 | 84 | 1.07 Jan 8, 2013 85 | - binmode STDERR, ":utf8" 86 | 87 | 1.06 Jan 8, 2013 88 | - added -e option, it is "HTTP_PROXY" and "NO_PROXY" environment variable support. 89 | - fix UTF-8-mac filename bug 90 | 91 | 1.05 Aug 28, 2012 92 | - added ProgressBar. ( need -v option ) 93 | 94 | 1.04 Aug 27, 2012 95 | - large file support. ( over 150 MB ) 96 | 97 | 1.03 Aug 14, 2012 98 | - set modification time. 99 | - support windows homepath. 100 | 101 | 1.02 Jan 10, 2012 102 | - added Exception handling 103 | 104 | 1.01 Apr 5, 2012 105 | - added sub command uid (sylvainfilteau) 106 | - added upload-to-dropbox command (sylvainfilteau) 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/s-aska/dropbox-api-command.svg?branch=master)](https://travis-ci.org/s-aska/dropbox-api-command) 2 | # NAME 3 | 4 | App::dropboxapi - command line interface to access Dropbox API 5 | 6 | # SYNOPSIS 7 | 8 | ``` 9 | dropbox-api put /tmp/foo.txt dropbox:/Public/ 10 | ``` 11 | 12 | Run `dropbox-api help` for more options. 13 | 14 | # DESCRIPTION 15 | 16 | dropbox-api is a command line interface to access Dropbox API. 17 | 18 | - ls 19 | - find 20 | - du 21 | - sync 22 | - cp 23 | - mv 24 | - rm 25 | - mkdir 26 | - get 27 | - put 28 | 29 | # Install and Setup 30 | 31 | ## 1. Install 32 | 33 | ### 1-a) FreeBSD 34 | 35 | ``` 36 | pkg install dropbox-api-command 37 | ``` 38 | 39 | ### 1-b) Ubuntu 40 | 41 | ``` 42 | sudo apt-get install make gcc libssl-dev wget 43 | wget https://raw.github.com/miyagawa/cpanminus/master/cpanm 44 | sudo perl cpanm App::dropboxapi 45 | ``` 46 | 47 | ### 1-c) CentOS 48 | 49 | ``` 50 | # CentOS 5 51 | sudo yum install gcc gcc-c++ openssl-devel wget 52 | # CentOS 6 53 | sudo yum install gcc gcc-c++ openssl-devel wget perl-devel 54 | wget https://raw.github.com/miyagawa/cpanminus/master/cpanm 55 | sudo perl cpanm App::dropboxapi 56 | ``` 57 | 58 | ### 1-d) OS X 59 | 60 | ``` 61 | # Install Command Line Tools for Xcode 62 | open https://www.google.com/search?q=Command+Line+Tools+for+Xcode 63 | 64 | curl -O https://raw.github.com/miyagawa/cpanminus/master/cpanm 65 | sudo perl cpanm App::dropboxapi 66 | ``` 67 | 68 | ## 2. Get API Key and API Secret 69 | 70 | ```perl 71 | https://www.dropbox.com/developers 72 | My Apps => Create an App 73 | ``` 74 | 75 | ## 3. Get Access Token and Access Secret 76 | 77 | ``` 78 | > dropbox-api setup 79 | Please Input API Key: *************** 80 | Please Input API Secret: *************** 81 | 1. Open the Login URL: https://www.dropbox.com/oauth2/authorize?client_id=*****&response_type=code 82 | 2. Input code and press Enter: *************** 83 | success! try 84 | > dropbox-api ls 85 | > dropbox-api find / 86 | ``` 87 | 88 | ## 4. How to use Proxy 89 | 90 | Please use -e option. 91 | 92 | ``` 93 | > HTTP_PROXY="http://127.0.0.1:8888" dropbox-api setup -e 94 | ``` 95 | 96 | # Sub Commands 97 | 98 | ## help 99 | 100 | disp help. 101 | 102 | - syntax 103 | 104 | dropbox-api help \[<command>\] 105 | 106 | ### Example 107 | 108 | ```perl 109 | > dropbox-api help 110 | Usage: dropbox-api [args] [options] 111 | 112 | Available commands: 113 | setup get access_key and access_secret 114 | ls list directory contents 115 | find walk a file hierarchy 116 | du disk usage statistics 117 | cp copy file or directory 118 | mv move file or directory 119 | mkdir make directory (Create intermediate directories as required) 120 | rm remove file or directory (Attempt to remove the file hierarchy rooted in each file argument) 121 | put upload file 122 | get download file 123 | sync sync directory (local => dropbox or dropbox => local) 124 | 125 | Common Options 126 | -e enable env_proxy ( HTTP_PROXY, NO_PROXY ) 127 | -D enable debug 128 | -v verbose 129 | -s sandbox mode, but this option has been removed. 130 | 131 | See 'dropbox-api help ' for more information on a specific command. 132 | ``` 133 | 134 | ### Example ( command help ) 135 | 136 | ``` 137 | > dropbox-api help ls 138 | Name 139 | dropbox-api-ls - list directory contents 140 | 141 | SYNOPSIS 142 | dropbox-api ls [options] 143 | 144 | Example 145 | dropbox-api ls Public 146 | dropbox-api ls Public -h 147 | dropbox-api ls Public -p "%d\t%s\t%TY/%Tm/%Td %TH:%TM:%TS\t%p\n" 148 | 149 | Options 150 | -h print sizes in human readable format (e.g., 1K 234M 2G) 151 | -p print format. 152 | %d ... is_dir ( d: dir, -: file ) 153 | %i ... id 154 | %n ... name 155 | %p ... path_display 156 | %P ... path_lower 157 | %b ... bytes 158 | %s ... size (e.g., 1K 234M 2G) 159 | %t ... server_modified 160 | %c ... client_modified 161 | %r ... rev 162 | %Tk ... DateTime 'strftime' function (server_modified) 163 | %Ck ... DateTime 'strftime' function (client_modified) 164 | ``` 165 | 166 | [http://search.cpan.org/dist/DateTime/lib/DateTime.pm#strftime\_Patterns](http://search.cpan.org/dist/DateTime/lib/DateTime.pm#strftime_Patterns) 167 | 168 | ## ls 169 | 170 | file list view. 171 | 172 | - alias 173 | 174 | list 175 | 176 | - syntax 177 | 178 | dropbox-api ls <dropbox\_path> 179 | 180 | ### Example 181 | 182 | ``` 183 | > dropbox-api list /product 184 | d - Thu, 24 Feb 2011 06:58:00 +0000 /product/chrome-extentions 185 | - 294557 Sun, 26 Dec 2010 21:55:59 +0000 /product/ex.zip 186 | ``` 187 | 188 | ### human readable option ( -h ) 189 | 190 | print sizes in human readable format (e.g., 1K 234M 2G) 191 | 192 | ``` 193 | > dropbox-api ls /product -h 194 | d - Thu, 24 Feb 2011 06:58:00 +0000 /product/chrome-extentions 195 | - 287.7KB Sun, 26 Dec 2010 21:55:59 +0000 /product/ex.zip 196 | ``` 197 | 198 | ### printf option ( -p ) 199 | 200 | print format. 201 | 202 | ``` 203 | > dropbox-api ls /product -p "%d\t%s\t%TY/%Tm/%Td %TH:%TM:%TS\t%p\n" 204 | d - 2011/02/24 06:58:00 /product/chrome-extentions 205 | - 287.7KB 2010/12/26 21:55:59 /product/ex.zip 206 | 207 | %d ... is_dir ( d: dir, -: file ) 208 | %i ... id 209 | %n ... name 210 | %p ... path_display 211 | %P ... path_lower 212 | %b ... bytes 213 | %s ... size (e.g., 1K 234M 2G) 214 | %t ... server_modified 215 | %c ... client_modified 216 | %r ... rev 217 | %Tk ... DateTime 'strftime' function (server_modified) 218 | %Ck ... DateTime 'strftime' function (client_modified) 219 | ``` 220 | 221 | [http://search.cpan.org/dist/DateTime/lib/DateTime.pm#strftime\_Patterns](http://search.cpan.org/dist/DateTime/lib/DateTime.pm#strftime_Patterns) 222 | 223 | ## find 224 | 225 | recursive file list view. 226 | 227 | - syntax 228 | 229 | dropbox-api find <dropbox\_path> \[options\] 230 | 231 | ### Example 232 | 233 | ``` 234 | > dropbox-api find /product/google-tasks-checker-plus 235 | /product/chrome-extentions/google-tasks-checker-plus/README.md 236 | /product/chrome-extentions/google-tasks-checker-plus/src 237 | /product/chrome-extentions/google-tasks-checker-plus/src/background.html 238 | /product/chrome-extentions/google-tasks-checker-plus/src/external.png 239 | /product/chrome-extentions/google-tasks-checker-plus/src/icon-32.png 240 | /product/chrome-extentions/google-tasks-checker-plus/src/icon-128.png 241 | /product/chrome-extentions/google-tasks-checker-plus/src/icon.gif 242 | /product/chrome-extentions/google-tasks-checker-plus/src/jquery-1.4.2.min.js 243 | /product/chrome-extentions/google-tasks-checker-plus/src/main.js 244 | /product/chrome-extentions/google-tasks-checker-plus/src/manifest.json 245 | /product/chrome-extentions/google-tasks-checker-plus/src/options.html 246 | /product/chrome-extentions/google-tasks-checker-plus/src/popup.html 247 | /product/chrome-extentions/google-tasks-checker-plus/src/reset.css 248 | ``` 249 | 250 | ### printf option ( -p ) 251 | 252 | see also list command's printf option. 253 | 254 | ## du 255 | 256 | display disk usage statistics. 257 | 258 | - syntax 259 | 260 | dropbox-api du <dropbox\_path> \[options\] 261 | 262 | ### Example 263 | 264 | ``` 265 | > dropbox-api du /product -h -d 1 266 | 1.1M /product 267 | 1.1M /product/chrome-extensions 268 | 0B /product/work 269 | ``` 270 | 271 | ### human readable option ( -h ) 272 | 273 | print sizes in human readable format (e.g., 1K 234M 2G) 274 | 275 | ### depth option ( -d ) 276 | 277 | Display an entry for all files and directories depth directories deep. 278 | 279 | ## sync ( rsync ) 280 | 281 | recursive file synchronization. 282 | 283 | ### sync from dropbox 284 | 285 | dropbox-api sync dropbox:<source\_dir> <target\_dir> \[options\] 286 | 287 | ``` 288 | > dropbox-api sync dropbox:/product/google-tasks-checker-plus/src /tmp/product 289 | download /private/tmp/product/external.png 290 | download /private/tmp/product/icon-32.png 291 | download /private/tmp/product/icon-128.png 292 | ``` 293 | 294 | ### sync to dropbox 295 | 296 | dropbox-api sync <source\_dir> dropbox:<target\_dir> \[options\] 297 | 298 | ``` 299 | > dropbox-api sync /tmp/product dropbox:/work/src 300 | upload background.html /work/src/background.html 301 | upload external.png /work/src/external.png 302 | upload icon-128.png /work/src/icon-128.png 303 | ``` 304 | 305 | ### delete option ( -d ) 306 | 307 | ``` 308 | > dropbox-api sync dropbox:/product/google-tasks-checker-plus/src /tmp/product -d 309 | download /private/tmp/product/external.png 310 | download /private/tmp/product/icon-32.png 311 | download /private/tmp/product/icon-128.png 312 | remove background.html.tmp 313 | ``` 314 | 315 | ### dry run option ( -n ) 316 | 317 | ``` 318 | > dropbox-api sync dropbox:/product/google-tasks-checker-plus/src /tmp/product -dn 319 | !! enable dry run !! 320 | download /private/tmp/product/external.png 321 | download /private/tmp/product/icon-32.png 322 | download /private/tmp/product/icon-128.png 323 | remove background.html.tmp 324 | ``` 325 | 326 | ### verbose option ( -v ) 327 | 328 | ``` 329 | > dropbox-api sync dropbox:/product/google-tasks-checker-plus/src /tmp/product -dnv 330 | remote_base: /product/chrome-extentions/google-tasks-checker-plus/src 331 | local_base: /private/tmp/product 332 | ** download ** 333 | skip background.html 334 | download /private/tmp/product/external.png 335 | download /private/tmp/product/icon-32.png 336 | download /private/tmp/product/icon-128.png 337 | skip icon.gif 338 | skip jquery-1.4.2.min.js 339 | skip main.js 340 | skip manifest.json 341 | skip options.html 342 | skip popup.html 343 | skip reset.css 344 | ** delete ** 345 | skip background.html 346 | remove background.html.tmp 347 | skip icon.gif 348 | skip jquery-1.4.2.min.js 349 | skip main.js 350 | skip manifest.json 351 | skip options.html 352 | skip popup.html 353 | skip reset.css 354 | ``` 355 | 356 | ## cp 357 | 358 | copy file or directory. 359 | 360 | - alias 361 | 362 | copy 363 | 364 | - syntax 365 | 366 | dropbox-api cp <source\_file> <target\_file> 367 | 368 | ### Example 369 | 370 | ``` 371 | dropbox-api cp memo.txt memo.txt.bak 372 | ``` 373 | 374 | ## mv 375 | 376 | move file or directory. 377 | 378 | - alias 379 | 380 | move 381 | 382 | - syntax 383 | 384 | dropbox-api mv <source\_file> <target\_file> 385 | 386 | ### Example 387 | 388 | ``` 389 | dropbox-api mv memo.txt memo.txt.bak 390 | ``` 391 | 392 | ## mkdir 393 | 394 | make directory. 395 | 396 | \*no error if existing, make parent directories as needed.\* 397 | 398 | - alias 399 | 400 | mkpath 401 | 402 | - syntax 403 | 404 | dropbox-api mkdir <directory> 405 | 406 | ### Example 407 | 408 | ``` 409 | dropbox-api mkdir product/src 410 | ``` 411 | 412 | ## rm 413 | 414 | remove file or directory. 415 | 416 | \*remove the contents of directories recursively.\* 417 | 418 | - alias 419 | 420 | rmtree 421 | 422 | - syntax 423 | 424 | dropbox-api rm <file\_or\_directory> 425 | 426 | ### Example 427 | 428 | ``` 429 | dropbox-api rm product/src 430 | ``` 431 | 432 | ## get 433 | 434 | download file from dropbox. 435 | 436 | - alias 437 | 438 | dl, download 439 | 440 | - syntax 441 | 442 | dropbox-api get dropbox:<dropbox\_file> <file> 443 | 444 | ### Example 445 | 446 | ``` 447 | dropbox-api get dropbox:/Public/foo.txt /tmp/foo.txt 448 | ``` 449 | 450 | ## put 451 | 452 | upload file to dropbox. 453 | 454 | - alias 455 | 456 | up, upload 457 | 458 | - syntax 459 | 460 | dropbox-api put <file> dropbox:<dropbox\_dir> 461 | 462 | ### Example 463 | 464 | ``` 465 | dropbox-api put /tmp/foo.txt dropbox:/Public/ 466 | ``` 467 | 468 | ### verbose option ( -v ) 469 | 470 | A progress bar is displayed. 471 | 472 | ```perl 473 | dropbox-api put /tmp/1GB.dat dropbox:/Public/ -v 474 | 100% [=====================================================================================>] 475 | ``` 476 | 477 | ## Tips 478 | 479 | ### Retry 480 | 481 | ``` 482 | #!/bin/bash 483 | 484 | command='dropbox-api sync dropbox:/test/ /Users/aska/test/ -vde' 485 | 486 | NEXT_WAIT_TIME=0 487 | EXIT_CODE=0 488 | until $command || [ $NEXT_WAIT_TIME -eq 4 ]; do 489 | EXIT_CODE=$? 490 | sleep $NEXT_WAIT_TIME 491 | let NEXT_WAIT_TIME=NEXT_WAIT_TIME+1 492 | done 493 | exit $EXIT_CODE 494 | ``` 495 | 496 | # COPYRIGHT 497 | 498 | Copyright 2012- Shinichiro Aska 499 | 500 | The standalone executable contains the following modules embedded. 501 | 502 | # LICENSE 503 | 504 | Released under the MIT license. http://creativecommons.org/licenses/MIT/ 505 | 506 | # COMMUNITY 507 | 508 | - [https://github.com/s-aska/dropbox-api-command](https://github.com/s-aska/dropbox-api-command) - source code repository, issue tracker 509 | -------------------------------------------------------------------------------- /lib/App/dropboxapi.pm: -------------------------------------------------------------------------------- 1 | package App::dropboxapi; 2 | use strict; 3 | use warnings; 4 | our $VERSION = '2.13'; 5 | 6 | =head1 NAME 7 | 8 | App::dropboxapi - command line interface to access Dropbox API 9 | 10 | =head1 SYNOPSIS 11 | 12 | dropbox-api put /tmp/foo.txt dropbox:/Public/ 13 | 14 | Run C for more options. 15 | 16 | =head1 DESCRIPTION 17 | 18 | dropbox-api is a command line interface to access Dropbox API. 19 | 20 | =over 4 21 | 22 | =item ls 23 | 24 | =item find 25 | 26 | =item du 27 | 28 | =item sync 29 | 30 | =item cp 31 | 32 | =item mv 33 | 34 | =item rm 35 | 36 | =item mkdir 37 | 38 | =item get 39 | 40 | =item put 41 | 42 | =back 43 | 44 | =head1 Install and Setup 45 | 46 | =head2 1. Install 47 | 48 | =head3 1-a) FreeBSD 49 | 50 | pkg_add -r dropbox-api-command 51 | 52 | =head3 1-b) Ubuntu 53 | 54 | sudo apt-get install make gcc libssl-dev wget 55 | wget https://raw.github.com/miyagawa/cpanminus/master/cpanm 56 | sudo perl cpanm App::dropboxapi 57 | 58 | =head3 1-c) CentOS 59 | 60 | # CentOS 5 61 | sudo yum install gcc gcc-c++ openssl-devel wget 62 | # CentOS 6 63 | sudo yum install gcc gcc-c++ openssl-devel wget perl-devel 64 | wget https://raw.github.com/miyagawa/cpanminus/master/cpanm 65 | sudo perl cpanm App::dropboxapi 66 | 67 | =head3 1-d) OS X 68 | 69 | # Install Command Line Tools for Xcode 70 | open https://www.google.com/search?q=Command+Line+Tools+for+Xcode 71 | 72 | curl -O https://raw.github.com/miyagawa/cpanminus/master/cpanm 73 | sudo perl cpanm App::dropboxapi 74 | 75 | =head2 2. Get API Key and API Secret 76 | 77 | https://www.dropbox.com/developers 78 | My Apps => Create an App 79 | 80 | =head2 3. Get Access Token and Access Secret 81 | 82 | > dropbox-api setup 83 | Please Input API Key: *************** 84 | Please Input API Secret: *************** 85 | 1. Open the Login URL: https://www.dropbox.com/oauth2/authorize?client_id=*****&response_type=code 86 | 2. Input code and press Enter: *************** 87 | success! try 88 | > dropbox-api ls 89 | > dropbox-api find / 90 | 91 | =head2 4. How to use Proxy 92 | 93 | Please use -e option. 94 | 95 | > HTTP_PROXY="http://127.0.0.1:8888" dropbox-api setup -e 96 | 97 | =head1 Sub Commands 98 | 99 | =head2 help 100 | 101 | disp help. 102 | 103 | =over 4 104 | 105 | =item syntax 106 | 107 | dropbox-api help [] 108 | 109 | =back 110 | 111 | =head3 Example 112 | 113 | > dropbox-api help 114 | Usage: dropbox-api [args] [options] 115 | 116 | Available commands: 117 | setup get access_key and access_secret 118 | ls list directory contents 119 | find walk a file hierarchy 120 | du disk usage statistics 121 | cp copy file or directory 122 | mv move file or directory 123 | mkdir make directory (Create intermediate directories as required) 124 | rm remove file or directory (Attempt to remove the file hierarchy rooted in each file argument) 125 | put upload file 126 | get download file 127 | sync sync directory (local => dropbox or dropbox => local) 128 | 129 | Common Options 130 | -e enable env_proxy ( HTTP_PROXY, NO_PROXY ) 131 | -D enable debug 132 | -v verbose 133 | -s sandbox mode, but this option has been removed. 134 | 135 | See 'dropbox-api help ' for more information on a specific command. 136 | 137 | =head3 Example ( command help ) 138 | 139 | > dropbox-api help ls 140 | Name 141 | dropbox-api-ls - list directory contents 142 | 143 | SYNOPSIS 144 | dropbox-api ls [options] 145 | 146 | Example 147 | dropbox-api ls Public 148 | dropbox-api ls Public -h 149 | dropbox-api ls Public -p "%d\t%s\t%TY/%Tm/%Td %TH:%TM:%TS\t%p\n" 150 | 151 | Options 152 | -h print sizes in human readable format (e.g., 1K 234M 2G) 153 | -p print format. 154 | %d ... is_dir ( d: dir, -: file ) 155 | %i ... id 156 | %n ... name 157 | %p ... path_display 158 | %P ... path_lower 159 | %b ... bytes 160 | %s ... size (e.g., 1K 234M 2G) 161 | %t ... server_modified 162 | %c ... client_modified 163 | %r ... rev 164 | %Tk ... DateTime 'strftime' function (server_modified) 165 | %Ck ... DateTime 'strftime' function (client_modified) 166 | 167 | L 168 | 169 | =head2 ls 170 | 171 | file list view. 172 | 173 | =over 4 174 | 175 | =item alias 176 | 177 | list 178 | 179 | =item syntax 180 | 181 | dropbox-api ls 182 | 183 | =back 184 | 185 | =head3 Example 186 | 187 | > dropbox-api list /product 188 | d - Thu, 24 Feb 2011 06:58:00 +0000 /product/chrome-extentions 189 | - 294557 Sun, 26 Dec 2010 21:55:59 +0000 /product/ex.zip 190 | 191 | =head3 human readable option ( -h ) 192 | 193 | print sizes in human readable format (e.g., 1K 234M 2G) 194 | 195 | > dropbox-api ls /product -h 196 | d - Thu, 24 Feb 2011 06:58:00 +0000 /product/chrome-extentions 197 | - 287.7KB Sun, 26 Dec 2010 21:55:59 +0000 /product/ex.zip 198 | 199 | =head3 printf option ( -p ) 200 | 201 | print format. 202 | 203 | > dropbox-api ls /product -p "%d\t%s\t%TY/%Tm/%Td %TH:%TM:%TS\t%p\n" 204 | d - 2011/02/24 06:58:00 /product/chrome-extentions 205 | - 287.7KB 2010/12/26 21:55:59 /product/ex.zip 206 | 207 | %d ... is_dir ( d: dir, -: file ) 208 | %i ... id 209 | %n ... name 210 | %p ... path_display 211 | %P ... path_lower 212 | %b ... bytes 213 | %s ... size (e.g., 1K 234M 2G) 214 | %t ... server_modified 215 | %c ... client_modified 216 | %r ... rev 217 | %Tk ... DateTime 'strftime' function (server_modified) 218 | %Ck ... DateTime 'strftime' function (client_modified) 219 | 220 | L 221 | 222 | =head2 find 223 | 224 | recursive file list view. 225 | 226 | =over 4 227 | 228 | =item syntax 229 | 230 | dropbox-api find [options] 231 | 232 | =back 233 | 234 | =head3 Example 235 | 236 | > dropbox-api find /product/google-tasks-checker-plus 237 | /product/chrome-extentions/google-tasks-checker-plus/README.md 238 | /product/chrome-extentions/google-tasks-checker-plus/src 239 | /product/chrome-extentions/google-tasks-checker-plus/src/background.html 240 | /product/chrome-extentions/google-tasks-checker-plus/src/external.png 241 | /product/chrome-extentions/google-tasks-checker-plus/src/icon-32.png 242 | /product/chrome-extentions/google-tasks-checker-plus/src/icon-128.png 243 | /product/chrome-extentions/google-tasks-checker-plus/src/icon.gif 244 | /product/chrome-extentions/google-tasks-checker-plus/src/jquery-1.4.2.min.js 245 | /product/chrome-extentions/google-tasks-checker-plus/src/main.js 246 | /product/chrome-extentions/google-tasks-checker-plus/src/manifest.json 247 | /product/chrome-extentions/google-tasks-checker-plus/src/options.html 248 | /product/chrome-extentions/google-tasks-checker-plus/src/popup.html 249 | /product/chrome-extentions/google-tasks-checker-plus/src/reset.css 250 | 251 | =head3 printf option ( -p ) 252 | 253 | see also list command's printf option. 254 | 255 | =head2 du 256 | 257 | display disk usage statistics. 258 | 259 | =over 4 260 | 261 | =item syntax 262 | 263 | dropbox-api du [options] 264 | 265 | =back 266 | 267 | =head3 Example 268 | 269 | > dropbox-api du /product -h -d 1 270 | 1.1M /product 271 | 1.1M /product/chrome-extensions 272 | 0B /product/work 273 | 274 | =head3 human readable option ( -h ) 275 | 276 | print sizes in human readable format (e.g., 1K 234M 2G) 277 | 278 | =head3 depth option ( -d ) 279 | 280 | Display an entry for all files and directories depth directories deep. 281 | 282 | =head2 sync ( rsync ) 283 | 284 | recursive file synchronization. 285 | 286 | =head3 sync from dropbox 287 | 288 | dropbox-api sync dropbox: [options] 289 | 290 | > dropbox-api sync dropbox:/product/google-tasks-checker-plus/src /tmp/product 291 | download /private/tmp/product/external.png 292 | download /private/tmp/product/icon-32.png 293 | download /private/tmp/product/icon-128.png 294 | 295 | =head3 sync to dropbox 296 | 297 | dropbox-api sync dropbox: [options] 298 | 299 | > dropbox-api sync /tmp/product dropbox:/work/src 300 | upload background.html /work/src/background.html 301 | upload external.png /work/src/external.png 302 | upload icon-128.png /work/src/icon-128.png 303 | 304 | =head3 delete option ( -d ) 305 | 306 | > dropbox-api sync dropbox:/product/google-tasks-checker-plus/src /tmp/product -d 307 | download /private/tmp/product/external.png 308 | download /private/tmp/product/icon-32.png 309 | download /private/tmp/product/icon-128.png 310 | remove background.html.tmp 311 | 312 | =head3 dry run option ( -n ) 313 | 314 | > dropbox-api sync dropbox:/product/google-tasks-checker-plus/src /tmp/product -dn 315 | !! enable dry run !! 316 | download /private/tmp/product/external.png 317 | download /private/tmp/product/icon-32.png 318 | download /private/tmp/product/icon-128.png 319 | remove background.html.tmp 320 | 321 | =head3 verbose option ( -v ) 322 | 323 | > dropbox-api sync dropbox:/product/google-tasks-checker-plus/src /tmp/product -dnv 324 | remote_base: /product/chrome-extentions/google-tasks-checker-plus/src 325 | local_base: /private/tmp/product 326 | ** download ** 327 | skip background.html 328 | download /private/tmp/product/external.png 329 | download /private/tmp/product/icon-32.png 330 | download /private/tmp/product/icon-128.png 331 | skip icon.gif 332 | skip jquery-1.4.2.min.js 333 | skip main.js 334 | skip manifest.json 335 | skip options.html 336 | skip popup.html 337 | skip reset.css 338 | ** delete ** 339 | skip background.html 340 | remove background.html.tmp 341 | skip icon.gif 342 | skip jquery-1.4.2.min.js 343 | skip main.js 344 | skip manifest.json 345 | skip options.html 346 | skip popup.html 347 | skip reset.css 348 | 349 | =head2 cp 350 | 351 | copy file or directory. 352 | 353 | =over 4 354 | 355 | =item alias 356 | 357 | copy 358 | 359 | =item syntax 360 | 361 | dropbox-api cp 362 | 363 | =back 364 | 365 | =head3 Example 366 | 367 | dropbox-api cp memo.txt memo.txt.bak 368 | 369 | =head2 mv 370 | 371 | move file or directory. 372 | 373 | =over 4 374 | 375 | =item alias 376 | 377 | move 378 | 379 | =item syntax 380 | 381 | dropbox-api mv 382 | 383 | =back 384 | 385 | =head3 Example 386 | 387 | dropbox-api mv memo.txt memo.txt.bak 388 | 389 | =head2 mkdir 390 | 391 | make directory. 392 | 393 | *no error if existing, make parent directories as needed.* 394 | 395 | =over 4 396 | 397 | =item alias 398 | 399 | mkpath 400 | 401 | =item syntax 402 | 403 | dropbox-api mkdir 404 | 405 | =back 406 | 407 | =head3 Example 408 | 409 | dropbox-api mkdir product/src 410 | 411 | =head2 rm 412 | 413 | remove file or directory. 414 | 415 | *remove the contents of directories recursively.* 416 | 417 | =over 4 418 | 419 | =item alias 420 | 421 | rmtree 422 | 423 | =item syntax 424 | 425 | dropbox-api rm 426 | 427 | =back 428 | 429 | =head3 Example 430 | 431 | dropbox-api rm product/src 432 | 433 | =head2 get 434 | 435 | download file from dropbox. 436 | 437 | =over 4 438 | 439 | =item alias 440 | 441 | dl, download 442 | 443 | =item syntax 444 | 445 | dropbox-api get dropbox: 446 | 447 | =back 448 | 449 | =head3 Example 450 | 451 | dropbox-api get dropbox:/Public/foo.txt /tmp/foo.txt 452 | 453 | =head2 put 454 | 455 | upload file to dropbox. 456 | 457 | =over 4 458 | 459 | =item alias 460 | 461 | up, upload 462 | 463 | =item syntax 464 | 465 | dropbox-api put dropbox: 466 | 467 | =back 468 | 469 | =head3 Example 470 | 471 | dropbox-api put /tmp/foo.txt dropbox:/Public/ 472 | 473 | =head3 verbose option ( -v ) 474 | 475 | A progress bar is displayed. 476 | 477 | dropbox-api put /tmp/1GB.dat dropbox:/Public/ -v 478 | 100% [=====================================================================================>] 479 | 480 | =head2 Tips 481 | 482 | =head3 Retry 483 | 484 | #!/bin/bash 485 | 486 | command='dropbox-api sync dropbox:/test/ /Users/aska/test/ -vde' 487 | 488 | NEXT_WAIT_TIME=0 489 | EXIT_CODE=0 490 | until $command || [ $NEXT_WAIT_TIME -eq 4 ]; do 491 | EXIT_CODE=$? 492 | sleep $NEXT_WAIT_TIME 493 | let NEXT_WAIT_TIME=NEXT_WAIT_TIME+1 494 | done 495 | exit $EXIT_CODE 496 | 497 | =head1 COPYRIGHT 498 | 499 | Copyright 2012- Shinichiro Aska 500 | 501 | The standalone executable contains the following modules embedded. 502 | 503 | =head1 LICENSE 504 | 505 | Released under the MIT license. http://creativecommons.org/licenses/MIT/ 506 | 507 | =head1 COMMUNITY 508 | 509 | =over 4 510 | 511 | =item L - source code repository, issue tracker 512 | 513 | =back 514 | 515 | =cut 516 | 517 | 1; 518 | -------------------------------------------------------------------------------- /script/dropbox-api: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | use Cwd 'abs_path'; 6 | use DateTime; 7 | use DateTime::Format::Strptime; 8 | use Encode; 9 | use Encode::Locale; 10 | use File::Basename qw(dirname basename); 11 | use File::Spec::Functions qw(abs2rel catfile); 12 | use File::Temp; 13 | use Getopt::Std; 14 | use JSON; 15 | use Path::Class; 16 | use POSIX qw(); 17 | use WebService::Dropbox 2.06; 18 | 19 | our $VERSION = '2.13'; 20 | 21 | my $limit = 10 * 1024 * 1024; # files_put_chunked method has large file support. 22 | 23 | if ($^O eq 'darwin') { 24 | require Encode::UTF8Mac; 25 | $Encode::Locale::ENCODING_LOCALE_FS = 'utf-8-mac'; 26 | } 27 | 28 | binmode STDOUT, ':utf8'; 29 | binmode STDERR, ':utf8'; 30 | 31 | my $config_file = file( $ENV{DROPBOX_CONF} || ($ENV{HOME} || $ENV{HOMEPATH}, '.dropbox-api-config') ); 32 | 33 | my $command = shift || ''; 34 | my @args; 35 | for (@{ [ @ARGV ] }) { 36 | last if $_ =~ qr{ \A - }xms; 37 | push @args, shift; 38 | } 39 | 40 | my %opts; 41 | if ($command eq 'du') { 42 | getopts('vDhed:', \%opts); 43 | } else { 44 | getopts('ndvDshePp:', \%opts); 45 | } 46 | 47 | push @args, @ARGV; 48 | 49 | my $dry = $opts{n}; 50 | my $delete = $opts{d}; 51 | my $verbose = $opts{v}; 52 | my $debug = $opts{D}; 53 | my $human = $opts{h}; 54 | my $printf = $opts{p}; 55 | my $public = $opts{P}; 56 | my $env_proxy = $opts{e}; 57 | my $max_depth = $opts{d}; 58 | 59 | if ($opts{s}) { 60 | die "-s is gone."; 61 | } 62 | 63 | if ($command eq '-v') { 64 | &help('version'); 65 | exit(0); 66 | } 67 | 68 | if ($command eq 'setup' || !-f $config_file) { 69 | &setup(); 70 | } 71 | 72 | # connect dropbox 73 | my $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.'; 77 | if ($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 | } 89 | if (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 = ); 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 | 106 | my $box = WebService::Dropbox->new($config); 107 | $box->env_proxy if $env_proxy; 108 | 109 | my $strp = new DateTime::Format::Strptime( pattern => '%Y-%m-%dT%T' ); 110 | my $strpz = new DateTime::Format::Strptime( pattern => '%Y-%m-%dT%TZ' ); 111 | 112 | my $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 128 | my $cols = 50; 129 | if ($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 | 138 | my $exit_code = 0; 139 | 140 | if ($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 | 166 | exit($exit_code); 167 | 168 | sub 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 [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 [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 [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 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 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 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 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 dropbox: 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: 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: [options] 325 | dropbox-api sync dropbox: [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 [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 ' 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 | 381 | sub setup { 382 | my $config = {}; 383 | 384 | print "Please Input API Key: "; 385 | chomp( my $key = ); 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 = ); 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 = ); 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 | 417 | sub 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 | 459 | sub _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 | 477 | sub 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 | 488 | sub _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 | 540 | sub 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 | 552 | sub _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 | 587 | sub 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 | 593 | sub 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 | 599 | sub 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 | 605 | sub 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 | 611 | sub 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 | 654 | sub 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 | 663 | sub 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 | 719 | sub 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 | 871 | sub 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 | 926 | sub 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 | 1049 | sub 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 | 1113 | sub 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 | 1133 | sub 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 | 1235 | sub inode ($) { 1236 | my $path = shift; 1237 | my ($dev, $inode) = stat($path); 1238 | return $dev . ':' . $inode if $inode; 1239 | return $path; 1240 | } 1241 | 1242 | sub remote_abs2rel ($$) { 1243 | my ($remote_path, $remote_base) = @_; 1244 | $remote_path =~ s|^\Q$remote_base\E/?||i; 1245 | return $remote_path; 1246 | } 1247 | 1248 | sub 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 | 1259 | sub chomp_slash ($) { 1260 | my $path = shift; 1261 | unless (defined $path) { 1262 | return ''; 1263 | } 1264 | $path =~ s|/$||; 1265 | $path; 1266 | } 1267 | 1268 | sub pretty($) { 1269 | JSON->new->utf8->pretty->encode($_[0]); 1270 | } 1271 | 1272 | use 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 | 1281 | sub 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 | 1299 | sub round ($) { 1300 | POSIX::floor($_[0] + 0.50000000000008); 1301 | } 1302 | 1303 | sub nearest ($) { 1304 | round($_[1] / $_[0]) * $_[0]; 1305 | } 1306 | 1307 | sub 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 | 1323 | exit(0); 1324 | --------------------------------------------------------------------------------