@smurf e @heemayl certamente estão corretos, mas descobri que, no meu caso, era mais lento do que eu queria que fosse; Eu simplesmente tinha muitos arquivos para processar. Por isso, escrevi uma pequena ferramenta de linha de comando que, acredito, pode ajudá-lo também. ( link ; ruby; sem dependências externas)
Basicamente, meu script adia o cálculo de hash: ele só executará o cálculo quando os tamanhos dos arquivos estiverem correspondentes. Desde porque eu iria querer transmitir o conteúdo de vários MP4s multi-gigabyte ou iso-arquivos através de um algoritmo de hash quando eu sei que estou procurando por um jpg de 4 KB !? O restante do script é principalmente formatação de saída.
Edit: (obrigado @Serg) Aqui está o código-fonte de todo o script. Você deve salvá-lo em ~/bin/find-dups
ou talvez /usr/local/bin/find-dups
e, em seguida, usar chmod +x
para torná-lo executável. Ele precisa ter o Ruby instalado, mas caso contrário não haverá outras dependências.
#!/usr/bin/env ruby
require 'digest/md5'
require 'fileutils'
require 'optparse'
def glob_from_argument(arg)
if File.directory?(arg)
arg + '/**/*'
elsif File.file?(arg)
arg
else # it's already a glob
arg
end
end
# Wrap text at 80 chars. (configurable)
def wrap_text(*args)
width = args.last.is_a?(Integer) ? args.pop : 80
words = args.flatten.join(' ').split(' ')
if words.any? { |word| word.size > width }
raise NotImplementedError, 'cannot deal with long words'
end
lines = []
line = []
until words.empty?
word = words.first
if line.size + line.map(&:size).inject(0, :+) + word.size > width
lines << line.join(' ')
line = []
else
line << words.shift
end
end
lines << line.join(' ') unless line.empty?
lines.join("\n")
end
ALLOWED_PRINT_OPTIONS = %w(hay needle separator)
def parse_options(args)
options = {}
options[:print] = %w(hay needle)
opt_parser = OptionParser.new do |opts|
opts.banner = "Usage: #{$0} [options] HAYSTACK NEEDLES"
opts.separator ''
opts.separator 'Search for duplicate files (needles) in a directory (the haystack).'
opts.separator ''
opts.separator 'HAYSTACK should be the directory (or one file) that you want to search in.'
opts.separator ''
opts.separator wrap_text(
'NEEDLES are the files you want to search for.',
'A NEEDLE can be a file or a directory,',
'in which case it will be recursively scanned.',
'Note that NEEDLES may overlap the HAYSTACK.')
opts.separator ''
opts.on("-p", "--print PROPERTIES", Array,
"When a match is found, print needle, or",
"hay, or both. PROPERTIES is a comma-",
"separated list with one or more of the",
"words 'needle', 'hay', or 'separator'.",
"'separator' prints an empty line.",
"Default: 'needle,hay'") do |list|
options[:print] = list
end
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
options[:verbose] = v
end
opts.on_tail("-h", "--help", "Show this message") do
puts opts
exit
end
end
opt_parser.parse!(args)
options[:haystack] = ARGV.shift
options[:needles] = ARGV.shift(ARGV.size)
raise ArgumentError, "Missing HAYSTACK" if options[:haystack].nil?
raise ArgumentError, "Missing NEEDLES" if options[:needles].empty?
unless options[:print].all? { |option| ALLOWED_PRINT_OPTIONS.include? option }
raise ArgumentError, "Allowed print options are 'needle', 'hay', 'separator'"
end
options
rescue OptionParser::InvalidOption, ArgumentError => error
puts error, nil, opt_parser.banner
exit 1
end
options = parse_options(ARGV)
VERBOSE = options[:verbose]
PRINT_HAY = options[:print].include? 'hay'
PRINT_NEEDLE = options[:print].include? 'needle'
PRINT_SEPARATOR = options[:print].include? 'separator'
HAYSTACK_GLOB = glob_from_argument options[:haystack]
NEEDLES_GLOB = options[:needles].map { |arg| glob_from_argument(arg) }
def info(*strings)
return unless VERBOSE
STDERR.puts strings
end
def info_with_ellips(string)
return unless VERBOSE
STDERR.print string + '... '
end
def all_files(*globs)
globs
.map { |glob| Dir.glob(glob) }
.flatten
.map { |filename| File.expand_path(filename) } # normalize filenames
.uniq
.sort
.select { |filename| File.file?(filename) }
end
def index_haystack(glob)
all_files(glob).group_by { |filename| File.size(filename) }
end
@checksums = {}
def checksum(filename)
@checksums[filename] ||= calculate_checksum(filename)
end
def calculate_checksum(filename)
Digest::MD5.hexdigest(File.read(filename))
end
def find_needle(needle, haystack)
straws = haystack[File.size(needle)] || return
checksum_needle = calculate_checksum(needle)
straws.detect do |straw|
straw != needle &&
checksum(straw) == checksum_needle &&
FileUtils.identical?(needle, straw)
end
end
BOLD = "3[1m"
NORMAL = "3[22m"
def print_found(needle, found)
if PRINT_NEEDLE
print BOLD if $stdout.tty?
puts needle
print NORMAL if $stdout.tty?
end
puts found if PRINT_HAY
puts if PRINT_SEPARATOR
end
info "Searching #{HAYSTACK_GLOB} for files equal to #{NEEDLES_GLOB}."
info_with_ellips 'Indexing haystack by file size'
haystack = index_haystack(HAYSTACK_GLOB)
haystack[0] = nil # ignore empty files
info "#{haystack.size} files"
info 'Comparing...'
all_files(*NEEDLES_GLOB).each do |needle|
info " examining #{needle}"
found = find_needle(needle, haystack)
print_found(needle, found) unless found.nil?
end