Module: Commands
- Defined in:
- lib/droxi/commands.rb
Overview
Module containing definitions for client commands.
Defined Under Namespace
Classes: Command
Constant Summary collapse
- UsageError =
Exception indicating that a client command was given the wrong number of arguments.
Class.new(ArgumentError)
- CAT =
Print the contents of remote files.
Command.new( 'cat REMOTE_FILE...', 'Print the concatenated contents of remote files.', lambda do |client, state, args| extract_flags(CAT.usage, args, {}) state.(args).each do |path| if path.is_a?(GlobError) warn "cat: #{path}: no such file or directory" else puts client.get_file(path) end end end )
- CD =
Change the remote working directory.
Command.new( 'cd [REMOTE_DIR]', "Change the remote working directory. With no arguments, changes to the \ Dropbox root. With a remote directory name as the argument, changes to \ that directory. With - as the argument, changes to the previous working \ directory.", lambda do |_client, state, args| extract_flags(CD.usage, args, {}) case when args.empty? then state.pwd = '/' when args.first == '-' then state.pwd = state.oldpwd else path = state.resolve_path(args.first) if state.directory?(path) state.pwd = path else warn "cd: #{args.first}: no such directory" end end end )
- CP =
Copy remote files.
Command.new( 'cp [-f] REMOTE_FILE... REMOTE_FILE', "When given two arguments, copies the remote file or folder at the first \ path to the second path. When given more than two arguments or when the \ final argument is a directory, copies each remote file or folder into \ that directory. Will refuse to overwrite existing files unless invoked \ with the -f option.", lambda do |client, state, args| cp_mv(client, state, args, 'cp', CP.usage) end )
- DEBUG =
Execute arbitrary code.
Command.new( 'debug STRING...', "Evaluates the given string as Ruby code and prints the result. Won't \ work unless the program was invoked with the --debug flag.", # rubocop:disable Lint/UnusedBlockArgument, Lint/Eval lambda do |client, state, args| if ARGV.include?('--debug') begin p eval(args.join(' ')) # rubocop:enable Lint/UnusedBlockArgument, Lint/Eval rescue SyntaxError => error warn error rescue => error warn error.inspect end else warn 'debug: not enabled.' end end )
- EXIT =
Terminate the session.
Command.new( 'exit', 'Exit the program.', lambda do |_client, state, args| extract_flags(EXIT.usage, args, {}) state.exit_requested = true end )
- FORGET =
Clear the cache.
Command.new( 'forget [REMOTE_DIR]...', "Clear the client-side cache of remote filesystem metadata. With no \ arguments, clear the entire cache. If given directories as arguments, \ (recursively) clear the cache of those directories only.", lambda do |_client, state, args| extract_flags(FORGET.usage, args, {}) if args.empty? state.cache.clear else args.each do |arg| state.forget_contents(arg) { |line| warn line } end end end )
- GET =
Download remote files.
Command.new( 'get [-f] REMOTE_FILE...', "Download each specified remote file to a file of the same name in the \ local working directory. Will refuse to overwrite existing files unless \ invoked with the -f option.", lambda do |client, state, args| flags = extract_flags(GET.usage, args, '-f' => 0) state.(args).each do |path| if path.is_a?(GlobError) warn "get: #{path}: no such file or directory" else basename = File.basename(path) try_and_handle(DropboxError) do if flags.include?('-f') || !File.exist?(basename) contents = client.get_file(path) IO.write(basename, contents, mode: 'wb') puts "#{basename} <- #{path}" else warn "get: #{basename}: local file already exists" end end end end end )
- HELP =
List commands, or print information about a specific command.
Command.new( 'help [COMMAND]', "Print usage and help information about a command. If no command is \ given, print a list of commands instead.", lambda do |_client, _state, args| extract_flags(HELP.usage, args, {}) if args.empty? Text.table(NAMES).each { |line| puts line } else cmd_name = args.first if NAMES.include?(cmd_name) cmd = const_get(cmd_name.upcase.to_s) puts cmd.usage Text.wrap(cmd.description).each { |line| puts line } else warn "help: #{cmd_name}: no such command" end end end )
- HISTORY =
Get remote file revisions.
Command.new( 'history REMOTE_FILE', "Print a list of revisions for a remote file. The file can be restored to \ a previous revision using the 'restore' command and a revision ID given \ by this command.", lambda do |client, state, args| extract_flags(HISTORY.usage, args, {}) path = state.resolve_path(args.first) if !state.(path) || state.directory?(path) warn "history: #{args.first}: no such file" else try_and_handle(DropboxError) do client.revisions(path).each do |rev| size = rev['size'].sub(/ (.)B/, '\1').sub(' bytes', '').rjust(7) mtime = Time.parse(rev['modified']) current_year = (mtime.year == Time.now.year) format_str = current_year ? '%b %e %H:%M' : '%b %e %Y' puts "#{size} #{mtime.strftime(format_str)} #{rev['rev']}" end end end end )
- LCD =
Change the local working directory.
Command.new( 'lcd [LOCAL_DIR]', "Change the local working directory. With no arguments, changes to the \ home directory. With a local directory name as the argument, changes to \ that directory. With - as the argument, changes to the previous working \ directory.", lambda do |_client, state, args| extract_flags(LCD.usage, args, {}) path = case when args.empty? then File.('~') when args.first == '-' then state.local_oldpwd else begin File.(args.first) rescue ArgumentError args.first end end if Dir.exist?(path) state.local_oldpwd = Dir.pwd Dir.chdir(path) else warn "lcd: #{args.first}: no such file or directory" end end )
- LS =
List remote files.
Command.new( 'ls [-l] [REMOTE_FILE]...', "List information about remote files. With no arguments, list the \ contents of the working directory. When given remote directories as \ arguments, list the contents of the directories. When given remote files \ as arguments, list the files. If the -l option is given, display \ information about the files.", lambda do |_client, state, args| long = extract_flags(LS.usage, args, '-l' => 0).include?('-l') files, dirs = [], [] state.(args, true).each do |path| if path.is_a?(GlobError) warn "ls: #{path}: no such file or directory" else type = state.directory?(path) ? dirs : files type << path end end dirs << state.pwd if args.empty? # First list files. list(state, files, files, long) { |line| puts line } puts unless dirs.empty? || files.empty? # Then list directory contents. dirs.each_with_index do |dir, i| puts "#{dir}:" if dirs.size + files.size > 1 contents = state.contents(dir) names = contents.map { |path| File.basename(path) } list(state, contents, names, long) { |line| puts line } puts if i < dirs.size - 1 end end )
- MEDIA =
Get temporary links to remote files.
Command.new( 'media REMOTE_FILE...', "Create Dropbox links to publicly share remote files. The links are \ time-limited and link directly to the files themselves.", lambda do |client, state, args| extract_flags(MEDIA.usage, args, {}) state.(args).each do |path| if path.is_a?(GlobError) warn "media: #{path}: no such file or directory" else try_and_handle(DropboxError) do url = client.media(path)['url'] puts "#{File.basename(path)} -> #{url}" end end end end )
- MKDIR =
Create a remote directory.
Command.new( 'mkdir REMOTE_DIR...', 'Create remote directories.', lambda do |client, state, args| extract_flags(MKDIR.usage, args, {}) args.each do |arg| try_and_handle(DropboxError) do path = state.resolve_path(arg) = client.file_create_folder(path) state.cache.add() end end end )
- MV =
Move/rename remote files.
Command.new( 'mv [-f] REMOTE_FILE... REMOTE_FILE', "When given two arguments, moves the remote file or folder at the first \ path to the second path. When given more than two arguments or when the \ final argument is a directory, moves each remote file or folder into \ that directory. Will refuse to overwrite existing files unless invoked \ with the -f option.", lambda do |client, state, args| cp_mv(client, state, args, 'mv', MV.usage) end )
- PUT =
Upload a local file.
Command.new( 'put [-f] [-q] [-O REMOTE_DIR] [-t COUNT] LOCAL_FILE...', "Upload local files to the remote working directory. If a remote file of \ the same name already exists, Dropbox will rename the upload unless the \ the -f option is given, in which case the remote file will be \ overwritten. If the -O option is given, the files will be uploaded to \ the given directory instead of the current directory. The -q option \ prevents progress from being printed. The -t option specifies the \ number of tries in case of error. The default is 5; -t 0 will retry \ infinitely.", lambda do |client, state, args| flags = extract_flags(PUT.usage, args, '-f' => 0, '-q' => 0, '-O' => 1, '-t' => 1) dest_index = flags.find_index('-O') dest_path = nil unless dest_index.nil? dest_path = flags[dest_index + 1] if state.directory?(dest_path) state.pwd = state.resolve_path(dest_path) else warn "put: #{dest_path}: no such directory" return end end tries_index = flags.find_index('-t') tries = tries_index ? flags[tries_index + 1].to_i : 5 args.each do |arg| to_path = state.resolve_path(File.basename(arg)) try_and_handle(StandardError) do File.open(File.(arg), 'rb') do |file| if flags.include?('-f') && state.(to_path) client.file_delete(to_path) state.cache.remove(to_path) end # Chunked upload if file is more than 1M. if file.size > 1024 * 1024 data = chunked_upload(client, to_path, file, flags.include?('-q'), tries) else data = client.put_file(to_path, file) end state.cache.add(data) puts "#{arg} -> #{data['path']}" end end end state.pwd = state.oldpwd unless dest_path.nil? end )
- RESTORE =
Restore a remove file to a previous version.
Command.new( 'restore REMOTE_FILE REVISION_ID', "Restore a remote file to a previous version. Use the 'history' command \ to get a list of IDs for previous revisions of the file.", lambda do |client, state, args| extract_flags(RESTORE.usage, args, {}) path = state.resolve_path(args.first) if !state.(path) || state.directory?(path) warn "restore: #{args.first}: no such file" else try_and_handle(DropboxError) do client.restore(path, args.last) end end end )
- RM =
Remove remote files.
Command.new( 'rm [-r] REMOTE_FILE...', "Remove each specified remote file. If the -r option is given, will \ also remove directories recursively.", lambda do |client, state, args| flags = extract_flags(RM.usage, args, '-r' => 0) state.(args).each do |path| if path.is_a?(GlobError) warn "rm: #{path}: no such file or directory" else if state.directory?(path) && !flags.include?('-r') warn "rm: #{path}: is a directory" next end try_and_handle(DropboxError) do client.file_delete(path) state.cache.remove(path) end end end check_pwd(state) end )
- RMDIR =
Remove remote directories.
Command.new( 'rmdir REMOTE_DIR...', 'Remove each specified empty remote directory.', lambda do |client, state, args| extract_flags(RMDIR.usage, args, {}) state.(args).each do |path| if path.is_a?(GlobError) warn "rmdir: #{path}: no such file or directory" else unless state.directory?(path) warn "rmdir: #{path}: not a directory" next end contents = state.(path)['contents'] if contents && !contents.empty? warn "rmdir: #{path}: directory not empty" next end try_and_handle(DropboxError) do client.file_delete(path) state.cache.remove(path) end end end check_pwd(state) end )
- SEARCH =
Search for remote files.
Command.new( 'search REMOTE_DIR SUBSTRING...', "List remote files in a directory or its subdirectories with names that \ contain all given substrings.", lambda do |client, state, args| extract_flags(SEARCH.usage, args, {}) path = state.resolve_path(args.first) unless state.directory?(path) warn "search: #{args.first}: no such directory" return end query = args.drop(1).join(' ') try_and_handle(DropboxError) do client.search(path, query).each { |result| puts result['path'] } end end )
- SHARE =
Get permanent links to remote files.
Command.new( 'share REMOTE_FILE...', "Create Dropbox links to publicly share remote files. The links are \ shortened and direct to 'preview' pages of the files. Links created by \ this method are set to expire far enough in the future so that \ expiration is effectively not an issue.", lambda do |client, state, args| extract_flags(SHARE.usage, args, {}) state.(args).each do |path| if path.is_a?(GlobError) warn "share: #{path}: no such file or directory" else try_and_handle(DropboxError) do url = client.shares(path)['url'] puts "#{File.basename(path)} -> #{url}" end end end end )
- NAMES =
Arrayof all command names. names
Class Method Summary collapse
-
.check_pwd(state) ⇒ Object
If the remote working directory does not exist, move up the directory tree until at a real location.
-
.chunked_upload(client, to_path, file, quiet, tries) ⇒ Object
Attempts to upload a file to the server in chunks, displaying progress.
-
.copy_move(method, args, flags, client, state) ⇒ Object
Copies or moves a file.
-
.cp_mv(client, state, args, cmd, usage) ⇒ Object
Execute a ‘mv’ or ‘cp’ operation depending on arguments given.
-
.cp_mv_to_dir(args, flags, client, state, cmd) ⇒ Object
Copies or moves files into a directory.
-
.exec(input, client, state) ⇒ Object
Parse and execute a line of user input in the given context.
-
.expand(state, paths, preserve_root, output, cmd) ⇒ Object
Return an
Arrayof paths from anArrayof globs, printing error messages ifoutputis true. -
.extract_flag(usage, args, flags, arg, index) ⇒ Object
Removes a flag and its arugments from the
Arrayand returns anArrayof the flag and its arguments. -
.extract_flags(usage, args, flags) ⇒ Object
Removes flags (e.g. -f) from the
Arrayand returns anArrayof the removed flags. -
.list(state, paths, names, long) ⇒ Object
Yield lines of output for the ls command executed on the given file paths and names.
-
.long_info(state, path, name) ⇒ Object
Return a
Stringof information about a remote file for ls -l. -
.loop_upload(uploader, monitor_thread, tries) ⇒ Object
Continuously try to upload until successful or interrupted.
-
.monitor_upload(uploader, to_path) ⇒ Object
Displays real-time progress for the a being uploaded.
-
.names ⇒ Object
Return an
Arrayof all command names. - .overwrite(path, client, state) ⇒ Object
-
.shell(cmd) ⇒ Object
Run a command in the system shell and yield lines of output.
-
.try_and_handle(exception_class) ⇒ Object
Attempt to run the associated block, handling the given type of
Exceptionby issuing a warning using itsStringrepresentation. -
.try_command(command_name, args, client, state) ⇒ Object
Run a command with the given name, or print an error message if usage is incorrect or no such command exists.
Class Method Details
.check_pwd(state) ⇒ Object
If the remote working directory does not exist, move up the directory tree until at a real location.
669 670 671 |
# File 'lib/droxi/commands.rb', line 669 def self.check_pwd(state) (state.pwd = File.dirname(state.pwd)) until state.(state.pwd) end |
.chunked_upload(client, to_path, file, quiet, tries) ⇒ Object
Attempts to upload a file to the server in chunks, displaying progress.
704 705 706 707 708 709 710 711 712 713 714 715 |
# File 'lib/droxi/commands.rb', line 704 def self.chunked_upload(client, to_path, file, quiet, tries) uploader = DropboxClient::ChunkedUploader.new(client, file, file.size) thread = quiet ? nil : Thread.new { monitor_upload(uploader, to_path) } tries = -1 if tries == 0 loop_upload(uploader, thread, tries) data = uploader.finish(to_path) if thread thread.join print "\r" + (' ' * (18 + to_path.rpartition('/')[2].size)) + "\r" end data end |
.copy_move(method, args, flags, client, state) ⇒ Object
Copies or moves a file.
628 629 630 631 632 633 634 635 636 637 |
# File 'lib/droxi/commands.rb', line 628 def self.copy_move(method, args, flags, client, state) from_path, to_path = args.map { |p| state.resolve_path(p) } try_and_handle(DropboxError) do overwrite(to_path, client, state) if flags.include?('-f') = client.send(method, from_path, to_path) state.cache.remove(from_path) if method == :file_move state.cache.add() puts "#{args.first} -> #{args[1]}" end end |
.cp_mv(client, state, args, cmd, usage) ⇒ Object
Execute a ‘mv’ or ‘cp’ operation depending on arguments given.
640 641 642 643 644 645 646 647 648 649 650 651 |
# File 'lib/droxi/commands.rb', line 640 def self.cp_mv(client, state, args, cmd, usage) flags = extract_flags(usage, args, '-f' => 0) sources = (state, args.take(args.size - 1), true, true, cmd) method = (cmd == 'cp') ? :file_copy : :file_move dest = state.resolve_path(args.last) if sources.size == 1 && !state.directory?(dest) copy_move(method, [sources.first, args.last], flags, client, state) else cp_mv_to_dir(args, flags, client, state, cmd) end end |
.cp_mv_to_dir(args, flags, client, state, cmd) ⇒ Object
Copies or moves files into a directory.
654 655 656 657 658 659 660 661 662 663 664 665 |
# File 'lib/droxi/commands.rb', line 654 def self.cp_mv_to_dir(args, flags, client, state, cmd) sources = (state, args.take(args.size - 1), true, false, cmd) method = (cmd == 'cp') ? :file_copy : :file_move if state.(state.resolve_path(args.last)) sources.each do |source| to_path = args.last.chomp('/') + '/' + File.basename(source) copy_move(method, [source, to_path], flags, client, state) end else warn "#{cmd}: #{args.last}: no such directory" end end |
.exec(input, client, state) ⇒ Object
Parse and execute a line of user input in the given context.
542 543 544 545 546 547 548 549 550 |
# File 'lib/droxi/commands.rb', line 542 def self.exec(input, client, state) if input.start_with?('!') shell(input[1, input.size - 1]) { |line| puts line } elsif !input.empty? tokens = Text.tokenize(input) cmd, args = tokens.first, tokens.drop(1) try_command(cmd, args, client, state) end end |
.expand(state, paths, preserve_root, output, cmd) ⇒ Object
Return an Array of paths from an Array of globs, printing error messages if output is true.
610 611 612 613 614 615 616 617 618 619 |
# File 'lib/droxi/commands.rb', line 610 def self.(state, paths, preserve_root, output, cmd) state.(paths, preserve_root).map do |item| if item.is_a?(GlobError) warn "#{cmd}: #{item}: no such file or directory" if output nil else item end end.compact end |
.extract_flag(usage, args, flags, arg, index) ⇒ Object
Removes a flag and its arugments from the Array and returns an Array of the flag and its arguments. Prints warnings if the given flag is invalid.
691 692 693 694 695 696 697 698 699 700 701 |
# File 'lib/droxi/commands.rb', line 691 def self.extract_flag(usage, args, flags, arg, index) num_args = flags[arg] if num_args.nil? fail UsageError, usage else if index + num_args < args.size return (num_args + 1).times.map { args.delete_at(index) } end fail UsageError, usage end end |
.extract_flags(usage, args, flags) ⇒ Object
Removes flags (e.g. -f) from the Array and returns an Array of the removed flags. Prints warnings if the flags are not in the given String of valid flags (e.g. ‘-rf’).
676 677 678 679 680 681 682 683 684 685 686 687 |
# File 'lib/droxi/commands.rb', line 676 def self.extract_flags(usage, args, flags) extracted, index = [], 0 while index < args.size arg = args[index] extracted_flags = arg[/^-\w/] ? extract_flag(usage, args, flags, arg, index) : nil extracted += extracted_flags unless extracted_flags.nil? index += 1 if extracted_flags.nil? || extracted_flags.empty? end args.delete_if { |a| a[/^-\w/] } extracted end |
.list(state, paths, names, long) ⇒ Object
Yield lines of output for the ls command executed on the given file paths and names.
589 590 591 592 593 594 595 |
# File 'lib/droxi/commands.rb', line 589 def self.list(state, paths, names, long) if long paths.zip(names).each { |path, name| yield long_info(state, path, name) } else Text.table(names).each { |line| yield line } end end |
.long_info(state, path, name) ⇒ Object
Return a String of information about a remote file for ls -l.
578 579 580 581 582 583 584 585 |
# File 'lib/droxi/commands.rb', line 578 def self.long_info(state, path, name) = state.(state.resolve_path(path), false) is_dir = ['is_dir'] ? 'd' : '-' size = ['size'].sub(/ (.)B/, '\1').sub(' bytes', '').rjust(7) mtime = Time.parse(['modified']) format_str = (mtime.year == Time.now.year) ? '%b %e %H:%M' : '%b %e %Y' "#{is_dir} #{size} #{mtime.strftime(format_str)} #{name}" end |
.loop_upload(uploader, monitor_thread, tries) ⇒ Object
Continuously try to upload until successful or interrupted. rubocop:disable Style/MethodLength
719 720 721 722 723 724 725 726 727 728 729 730 731 |
# File 'lib/droxi/commands.rb', line 719 def self.loop_upload(uploader, monitor_thread, tries) while tries != 0 && uploader.offset < uploader.total_size begin uploader.upload(1024 * 1024) rescue DropboxError => error puts "\n" + error.to_s --tries end end rescue Interrupt => error monitor_thread.kill if monitor_thread raise error end |
.monitor_upload(uploader, to_path) ⇒ Object
Displays real-time progress for the a being uploaded.
735 736 737 738 739 740 741 742 743 |
# File 'lib/droxi/commands.rb', line 735 def self.monitor_upload(uploader, to_path) filename = to_path.rpartition('/')[2] loop do percent = 100.0 * uploader.offset / uploader.total_size printf("\rUploading %s: %.1f%%", filename, percent) break if uploader.offset == uploader.total_size sleep 1 end end |
.names ⇒ Object
Return an Array of all command names.
533 534 535 536 |
# File 'lib/droxi/commands.rb', line 533 def self.names symbols = constants.select { |sym| const_get(sym).is_a?(Command) } symbols.map { |sym| sym.to_s.downcase } end |
.overwrite(path, client, state) ⇒ Object
621 622 623 624 625 |
# File 'lib/droxi/commands.rb', line 621 def self.overwrite(path, client, state) return unless state.(path) client.file_delete(path) state.cache.remove(path) end |
.shell(cmd) ⇒ Object
Run a command in the system shell and yield lines of output.
598 599 600 601 602 603 604 605 606 |
# File 'lib/droxi/commands.rb', line 598 def self.shell(cmd) IO.popen(cmd) do |pipe| pipe.each_line { |line| yield line.chomp if block_given? } end rescue Interrupt yield '' rescue Errno::ENOENT => error yield error.to_s if block_given? end |
.try_and_handle(exception_class) ⇒ Object
Attempt to run the associated block, handling the given type of Exception by issuing a warning using its String representation.
556 557 558 559 560 |
# File 'lib/droxi/commands.rb', line 556 def self.try_and_handle(exception_class) yield rescue exception_class => error warn error end |
.try_command(command_name, args, client, state) ⇒ Object
Run a command with the given name, or print an error message if usage is incorrect or no such command exists.
564 565 566 567 568 569 570 571 572 573 574 575 |
# File 'lib/droxi/commands.rb', line 564 def self.try_command(command_name, args, client, state) if NAMES.include?(command_name) begin command = const_get(command_name.upcase.to_sym) command.exec(client, state, *args) { |line| puts line } rescue UsageError => error warn "Usage: #{error}" end else warn "droxi: #{command_name}: command not found" end end |