Module: Can

Defined in:
lib/can.rb,
lib/info.rb,
lib/list.rb,
lib/empty.rb,
lib/trash.rb,
lib/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.1.1'
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'],
}
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']
}
ALL_FLAGS =
MODES.merge(OPTIONS)

Class Method Summary collapse

Class Method Details

.canObject



26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/can.rb', line 26

def self.can
  ArgParse.init_args

  mode = ArgParse.get_mode

  self.init_dirs

  self.send mode

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

.emptyObject



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

def self.empty
  # Remove everything in the files and info directory
  if ARGV.length == 0
    FileUtils.rm_r Dir.glob("#{HOME_TRASH_INFO_DIRECTORY}/*"), secure: true
    FileUtils.rm_r Dir.glob("#{HOME_TRASH_FILES_DIRECTORY}/*"), secure: true
  else
    ARGV.map { |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

.infoObject

TODO: Parse the .trashinfo files to make them more human readable. Also, display the filename above the information with empty lines between consecutive info blocks.



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
33
# File 'lib/info.rb', line 5

def self.info
  # Fails with a fatal error even with --force, intended
  # behavior.
  if ARGV.length == 0
    Error.fatal 'missing operand'
  else
    ARGV.each_with_index { |file, i|
      trashinfo_filename = file + '.trashinfo'
      trashinfo_path = File.join(HOME_TRASH_INFO_DIRECTORY, trashinfo_filename)

      if not 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

.init_dirsObject



21
22
23
24
# File 'lib/can.rb', line 21

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

.listObject



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

def self.list
  # Given no args, show every trashed file
  if ARGV.length == 0
    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 { |file|
      regex =~ file
    }

  else
    raise StandardError.new(
      "can: mode --list expects 0 to 1 arguments, given #{ARGV.length}"
    )
  end
end

.trashObject



24
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
78
79
80
81
82
83
84
85
86
87
# File 'lib/trash.rb', line 24

def self.trash
  if ARGV.length == 0 and not $options.include? :force
      Error.fatal 'missing operand'
  end

  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.
    if not File.exist?(path)
      if not $options.include? :force
        Error.nonfatal "cannot trash '#{path}': No such file or directory"
      end
      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 and not File.symlink? path
      if not $options.include? :recursive
        Error.nonfatal "cannot remove '#{path}': Is a directory"
      end
      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`.
    if $options.include? :prompt
      unless HighLine.agree "can: remove file '#{path}'?"
        next
      end
    end

    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



2
3
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
# File 'lib/untrash.rb', line 2

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

    if not File.exist? file_path
        if not $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]

    # TODO: Implement more thorough error handling
    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