require 'fileutils'
require 'find'
require 'time'
# Collection of utility methods included in Maid::Maid (and thus available in the rules DSL).
#
# In general, all paths are automatically expanded (e.g. '~/Downloads/foo.zip' becomes '/home/username/Downloads/foo.zip').
#
# Some methods are not available on all platforms. An ArgumentError is raised when a command is not available. See tags: [Mac OS X]
module Maid::Tools
include Deprecated
# Move from from to to.
#
# The path is not moved if a file already exists at the destination with the same name. A warning is logged instead.
#
# This method delegates to FileUtils. The instance-level file_options hash is passed to control the :noop option.
#
# move('~/Downloads/foo.zip', '~/Archive/Software/Mac OS X/')
#
# This method can handle multiple from paths.
#
# move(['~/Downloads/foo.zip', '~/Downloads/bar.zip'], '~/Archive/Software/Mac OS X/')
# move(dir('~/Downloads/*.zip'), '~/Archive/Software/Mac OS X/')
def move(froms, to)
Array(froms).each do |from|
from = File.expand_path(from)
to = File.expand_path(to)
target = File.join(to, File.basename(from))
unless File.exist?(target)
@logger.info "mv #{from.inspect} #{to.inspect}"
FileUtils.mv(from, to, @file_options)
else
@logger.warn "skipping #{from.inspect} because #{target.inspect} already exists"
end
end
end
# Move the given path to the trash (as set by trash_path).
#
# The path is moved if a file already exists in the trash with the same name. However, the current date and time is appended to the filename.
#
# Note: the OS native "restore" or "put back" functionality for trashed files is not currently supported. However, they can be restored manually, and the Maid log can help assist with this.
#
# Options:
#
# - :remove_over => Fixnum (e.g. 1.gigabyte, 1024.megabytes)
# Remove files over the given size rather than moving to the trash.
# See also Maid::NumericExtensions::SizeToKb
#
# trash('~/Downloads/foo.zip')
#
# This method can also handle multiple paths.
#
# trash(['~/Downloads/foo.zip', '~/Downloads/bar.zip'])
# trash(dir('~/Downloads/*.zip'))
def trash(paths, options = {})
# ## Implementation Notes
#
# Trashing files correctly is surprisingly hard. What Maid ends up doing is one the easiest, most foolproof solutions: moving the file.
#
# Unfortunately, that means it's not possile to restore files automatically in OSX or Ubuntu. The previous location of the file is lost.
#
# OSX support depends on AppleScript or would require a not-yet-written C extension to interface with the OS. The AppleScript solution is less than ideal: the user has to be logged in, Finder has to be running, and it makes the "trash can sound" every time a file is moved.
#
# Ubuntu makes it easy to implement, and there's a Python library for doing so (see `trash-cli`). However, there's not a Ruby equivalent yet.
Array(paths).each do |path|
path = File.expand_path(path)
target = File.join(@trash_path, File.basename(path))
safe_trash_path = File.join(@trash_path, "#{File.basename(path)} #{Time.now.strftime('%Y-%m-%d-%H-%M-%S')}")
if options[:remove_over] &&
File.exist?(path) &&
disk_usage(path) > options[:remove_over]
remove(path)
end
if File.exist?(path)
if File.exist?(target)
move(path, safe_trash_path)
else
move(path, @trash_path)
end
end
end
end
# Remove the given path recursively.
#
# Options:
#
# - :force => boolean
# - :secure => boolean (See FileUtils.remove_entry_secure for further details)
#
# remove('~/Downloads/foo.zip')
#
# This method can handle multiple remove paths.
#
# remove(['~/Downloads/foo.zip', '~/Downloads/bar.zip'])
# remove(dir('~/Downloads/*.zip'))
def remove(paths, options = {})
Array(paths).each do |path|
path = File.expand_path(path)
options = @file_options.merge(options)
@logger.info "Removing #{path.inspect}"
FileUtils.rm_r(path,options)
end
end
# Give all files matching the given glob.
#
# dir('~/Downloads/*.zip')
def dir(glob)
Dir[File.expand_path(glob)]
end
# Creates a directory and all its parent directories.
#
# Options:
#
# - :mode, the symbolic and absolute mode both can be used.
# eg. 0700, 'u=wr,go=rr'
#
# mkdir('~/Downloads/Music/Pink.Floyd/', :mode => 0644)
def mkdir(path, options = {})
FileUtils.mkdir_p(File.expand_path(path), options)
end
# Find matching files, akin to the Unix utility find.
#
# If no block is given, it will return an array.
#
# find '~/Downloads/' # => [...]
#
# or delegates to Find.find.
#
# find '~/Downloads/' do |path|
# # ...
# end
#
def find(path, &block)
expanded_path = File.expand_path(path)
if block.nil?
files = []
Find.find(expanded_path) { |file_path| files << file_path }
files
else
Find.find(expanded_path, &block)
end
end
# [Mac OS X] Use Spotlight to locate all files matching the given filename.
#
# locate('foo.zip') # => ['/a/foo.zip', '/b/foo.zip']
#--
# TODO use `locate` elsewhere -- it isn't available by default on OS X starting with OS X Leopard.
def locate(name)
cmd("mdfind -name #{name.inspect}").split("\n")
end
# [Mac OS X] Use Spotlight metadata to determine the site from which a file was downloaded.
#
# downloaded_from('foo.zip') # => ['http://www.site.com/foo.zip', 'http://www.site.com/']
def downloaded_from(path)
raw = cmd("mdls -raw -name kMDItemWhereFroms #{path.inspect}")
clean = raw[1, raw.length - 2]
clean.split(/,\s+/).map { |s| t = s.strip; t[1, t.length - 2] }
end
# [Mac OS X] Use Spotlight metadata to determine audio length.
#
# duration_s('foo.mp3') # => 235.705
def duration_s(path)
cmd("mdls -raw -name kMDItemDurationSeconds #{path.inspect}").to_f
end
# Inspect the contents of a .zip file.
#
# zipfile_contents('foo.zip') # => ['foo/foo.exe', 'foo/README.txt']
def zipfile_contents(path)
raw = cmd("unzip -Z1 #{path.inspect}")
raw.split("\n")
end
# Calculate disk usage of a given path.
#
# FIXME: This reports in kilobytes, but should probably report in bytes.
#
# disk_usage('foo.zip') # => 136
def disk_usage(path)
raw = cmd("du -s #{path.inspect}")
usage_kb = raw.split(/\s+/).first.to_i
if usage_kb.zero?
raise "Stopping pessimistically because of unexpected value from du (#{raw.inspect})"
else
usage_kb
end
end
# In Unix speak, "ctime".
#
# created_at('foo.zip') # => Sat Apr 09 10:50:01 -0400 2011
def created_at(path)
File.ctime(File.expand_path(path))
end
# In Unix speak, "atime".
#
# accessed_at('foo.zip') # => Sat Apr 09 10:50:01 -0400 2011
def accessed_at(path)
File.atime(File.expand_path(path))
end
alias :last_accessed :accessed_at
deprecated :last_accessed, :accessed_at
# In Unix speak, "mtime".
#
# modified_at('foo.zip') # => Sat Apr 09 10:50:01 -0400 2011
def modified_at(path)
File.mtime(File.expand_path(path))
end
# Pulls and pushes the given git repository.
#
# Since this is deprecated, you might also be interested in SparkleShare (http://sparkleshare.org/), a great git-based file syncronization project.
#
# git_piston('~/code/projectname')
#
# @deprecated
def git_piston(path)
full_path = File.expand_path(path)
stdout = cmd("cd #{full_path.inspect} && git pull && git push 2>&1")
@logger.info "Fired git piston on #{full_path.inspect}. STDOUT:\n\n#{stdout}"
end
deprecated :git_piston, 'SparkleShare (http://sparkleshare.org/)'
# [Rsync] Simple sync of two files/folders using rsync.
#
# See rsync man page for a detailed description.
#
# Options:
#
# - :delete => boolean
# - :verbose => boolean
# - :archive => boolean (default true)
# - :update => boolean (default true)
# - :exclude => string EXE :exclude => ".git" or :exclude => [".git", ".rvmrc"]
# - :prune_empty => boolean
#
# sync('~/music', '/backup/music')
def sync(from, to, options = {})
# expand path removes trailing slash
# cannot use str[-1] due to ruby 1.8.7 restriction
from = File.expand_path(from) + (from.end_with?('/') ? '/' : '')
to = File.expand_path(to) + (to.end_with?('/') ? '/' : '')
# default options
options = { :archive => true, :update => true }.merge(options)
ops = []
ops << '-a' if options[:archive]
ops << '-v' if options[:verbose]
ops << '-u' if options[:update]
ops << '-m' if options[:prune_empty]
ops << '-n' if @file_options[:noop]
Array(options[:exclude]).each do |path|
ops << "--exclude=#{path.inspect}"
end
ops << '--delete' if options[:delete]
stdout = cmd("rsync #{ops.join(' ')} #{from.inspect} #{to.inspect} 2>&1")
@logger.info "Fired sync from #{from.inspect} to #{to.inspect}. STDOUT:\n\n#{stdout}"
end
end