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.expand_patterns(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 [-E] [-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. If the -E option is given, remove each \
   remote file after successful download.",
  lambda do |client, state, args|
    flags = extract_flags(GET.usage, args, '-E' => 0, '-f' => 0)

    state.expand_patterns(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')
            if flags.include?('-E')
              client.file_delete(path)
              state.cache.remove(path)
            end
            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?
      puts 'Type "help <command>" for more info about a command:'
      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.expand_path('~')
           when args.first == '-' then state.local_oldpwd
           else
             begin
               File.expand_path(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 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.expand_patterns(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.expand_patterns(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 [-E] [-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 -E option is given, delete each local file after \
   successful upload. 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,
                          '-E' => 0,
                          '-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

    # Glob arguments.
    args.map! do |arg|
      array = Dir.glob(File.expand_path(arg))
      warn "put: #{arg}: no such file or directory" if array.empty?
      array.map { |path| path.sub(File.dirname(path), File.dirname(arg)) }
    end
    args = args.reduce(:+)

    args.each do |arg|
      to_path = state.resolve_path(File.basename(arg))

      try_and_handle(StandardError) do
        path = File.expand_path(arg)
        if File.directory?(path)
          warn "put: #{arg}: cannot put directory"
          next
        end

        success = false

        File.open(path, '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, success = chunked_upload(client, to_path, file,
                                           flags.include?('-q'), tries)
          else
            data = client.put_file(to_path, file)
            success = true
          end

          state.cache.add(data)
          puts "#{arg} -> #{data['path']}"
        end

        File.delete(path) if flags.include?('-E') && success
      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.expand_patterns(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.expand_patterns(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.expand_patterns(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 =

Array of all command names.

names

Class Method Summary collapse

Class Method Details

.exec(input, client, state) ⇒ Object

Parse and execute a line of user input in the given context.



568
569
570
571
572
573
574
575
576
# File 'lib/droxi/commands.rb', line 568

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

.namesObject

Return an Array of all command names.



559
560
561
562
# File 'lib/droxi/commands.rb', line 559

def self.names
  symbols = constants.select { |sym| const_get(sym).is_a?(Command) }
  symbols.map { |sym| sym.to_s.downcase }
end