Module: Can

Defined in:
lib/can.rb,
lib/can/info.rb,
lib/can/list.rb,
lib/can/empty.rb,
lib/can/trash.rb,
lib/can/untrash.rb,
lib/can/version.rb,
lib/can/argparse.rb

Defined Under Namespace

Modules: ArgParse

Constant Summary collapse

XDG_DATA_HOME_DEFAULT =
File.join(ENV['HOME'], '.local/share')
XDG_DATA_HOME =
ENV['XDG_DATA_HOME'] || XDG_DATA_HOME_DEFAULT
HOME_TRASH_DIRECTORY =
File.join(XDG_DATA_HOME, 'Trash')
HOME_TRASH_INFO_DIRECTORY =
File.join(HOME_TRASH_DIRECTORY, 'info')
HOME_TRASH_FILES_DIRECTORY =
File.join(HOME_TRASH_DIRECTORY, 'files')
VERSION =
'0.2.0'
USAGE =
'Usage: can [OPTION] [FILE]...'
MODES =
{
  list: ['-l', '--list',
         'list files in the trash'],
  info: ['-n', '--info',
         'see information about a trashed file'],
  untrash: ['-u', '--untrash',
            'restore a trashed file'],
  empty: ['-e', '--empty',
          'permanently remove a file from the trash;
                use with no arguments to empty entire
                trashcan']
}.freeze
OPTIONS =
{
  force: ['-f', '--force',
          'ignore nonexistent files and arguments,
              never prompt'],
  prompt: ['-i', nil, 'prompt before every trashing'],
  recursive: ['-r', '--recursive',
              'trash directories and their contents
                 recursively']
}.freeze
ALL_FLAGS =
MODES.merge(OPTIONS).freeze

Class Method Summary collapse

Class Method Details

.can(argv) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
# File 'lib/can.rb', line 33

def self.can(argv)
  @options, @argv = ArgParse.init_args(argv)

  mode = ArgParse.mode @options

  init_dirs

  send mode

  $exit = EXIT_SUCCESS if @options.include?(:force)
end

.cliObject



28
29
30
# File 'lib/can.rb', line 28

def self.cli
  can ARGV
end

.emptyObject



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# File 'lib/can/empty.rb', line 4

def self.empty
  # Remove everything in the files and info directory
  if ARGV.empty?
    FileUtils.rm_r Dir.glob("#{HOME_TRASH_INFO_DIRECTORY}/*"), secure: true
    FileUtils.rm_r Dir.glob("#{HOME_TRASH_FILES_DIRECTORY}/*"), secure: true
  else
    @argv.each do |filename|
      trashinfo_filename = "#{filename}.trashinfo"

      file_path = File.join(HOME_TRASH_FILES_DIRECTORY, filename)
      trashinfo_file_path = File.join(HOME_TRASH_INFO_DIRECTORY, trashinfo_filename)

      FileUtils.remove_entry_secure file_path
      FileUtils.remove_entry_secure trashinfo_file_path
    end
  end
end

.infoObject



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/can/info.rb', line 4

def self.info
  # Fails with a fatal error even with --force, intended
  # behavior.
  if @argv.empty?
    Error.fatal 'missing operand'
  else
    @argv.each_with_index do |file, i|
      trashinfo_filename = "#{file}.trashinfo"
      trashinfo_path = File.join(HOME_TRASH_INFO_DIRECTORY, trashinfo_filename)

      unless File.exist? trashinfo_path
        Error.nonfatal "no such file in trashcan: '#{file}'"
        next
      end

      trashinfo = Trashinfo.parse(File.read(trashinfo_path))

      # TODO: Checking if i is not zero every single
      # iteration is a little inefficient. Maybe there is a
      # better way to do this?
      puts if i != 0
      puts <<~INFO
        #{file}:
        Path: #{trashinfo[:path]}
        Deletion Date: #{trashinfo[:deletion_date]}
      INFO
    end
  end
end

.init_dirsObject



23
24
25
26
# File 'lib/can.rb', line 23

def self.init_dirs
  FileUtils.mkpath HOME_TRASH_FILES_DIRECTORY
  FileUtils.mkpath HOME_TRASH_INFO_DIRECTORY
end

.listObject



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/can/list.rb', line 4

def self.list
  # Given no args, show every trashed file
  if @argv.empty?
    puts Dir.children(HOME_TRASH_FILES_DIRECTORY)

  # Given a regex pattern as an arg, print trashed files
  # that fit
  elsif @argv.length == 1
    regex = Regexp.new(@argv[0])
    puts(
      Dir.children(HOME_TRASH_FILES_DIRECTORY).select do |file|
        regex =~ file
      end
    )

  else
    raise StandardError, "can: mode --list expects 0 to 1 arguments, given #{@argv.length}"
  end
end

.trashObject



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/can/trash.rb', line 25

def self.trash
  Error.fatal 'missing operand' if @argv.empty? && !@options.include?(:force)

  @argv.each do |path|
    # TODO: If both `-f` and `-i` are used, can should
    # prompt if `-i` is used last. If `-f` is used last,
    # can should not prompt trashings. This follows the
    # behavior of rm.
    unless File.exist?(path)
      Error.nonfatal "cannot trash '#{path}': No such file or directory" unless @options.include? :force
      next
    end

    # If --recursive is not used and a directory is given as an
    # argument, a non-zero error code should be returned
    # regardless if --force is used.
    if File.directory?(path) && !File.symlink?(path)
      Error.nonfatal "cannot remove '#{path}': Is a directory" unless @options.include? :recursive
      next
    end

    # TODO: Highline.agree prints to stdout, when it should
    # print to stderr. It also uses `puts`, while this use
    # case should use `print`.
    next if @options.include?(:prompt) && !(HighLine.agree "can: remove file '#{path}'?")

    filename = File.basename path

    trashinfo_string = Trashinfo.new path

    existing_trash_files = Dir.children HOME_TRASH_FILES_DIRECTORY

    # The File.basename function only strips the last
    # extension. These functions are needed to support files
    # with multiple extensions, like file.txt.bkp
    basename = strip_extensions(filename)
    exts = gather_extensions(filename)

    # Most implementations add a number as the first
    # extension to prevent file conflicts
    i = 0
    while existing_trash_files.include?(filename)
      i += 1
      filename = "#{basename}.#{i}#{exts}"
    end

    FileUtils.mv(path, File.join(HOME_TRASH_FILES_DIRECTORY, filename))

    trashinfo_filename = "#{filename}.trashinfo"
    trashinfo_out_path = File.join(HOME_TRASH_INFO_DIRECTORY, trashinfo_filename)
    File.new(trashinfo_out_path, 'w').syswrite(trashinfo_string)
  end
end

.untrashObject



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/can/untrash.rb', line 4

def self.untrash
  @argv.each do |filename|
    file_path = File.join(HOME_TRASH_FILES_DIRECTORY, filename)

    unless File.exist? file_path
      unless @options.include? :force
        Error.nonfatal "cannot untrash '#{filename}': No such file or directory in trash"
      end
      next
    end

    trashinfo_filename = "#{filename}.trashinfo"
    trashinfo_path = File.join(HOME_TRASH_INFO_DIRECTORY, trashinfo_filename)
    trashinfo = Trashinfo.parse(File.read(trashinfo_path))

    original_path = trashinfo[:path]

    if File.exist? original_path
      Error.nonfatal "cannot untrash '#{filename}' to '#{original_path}': File exists"
      next
    end

    # TODO: Make sure ctime, atime, mtime, do not change
    FileUtils.mv file_path, original_path
    FileUtils.rm trashinfo_path
  end
end