Module: Maid::Tools

Includes:
Deprecated
Defined in:
lib/maid/tools.rb

Overview

These "tools" are methods available in the Maid DSL.

In general, methods are expected to:

  • Automatically expand paths (that is, '~/Downloads/foo.zip' becomes '/home/username/Downloads/foo.zip')
  • Respect the noop (dry-run) option if it is set

Some methods are not available on all platforms. An ArgumentError is raised when a command is not available. See tags such as: [Mac OS X]

Instance Method Summary (collapse)

Instance Method Details

- (Object) accessed_at(path)

Get the time that a file was last accessed.

In Unix speak, atime.

Examples

accessed_at('foo.zip') # => Sat Apr 09 10:50:01 -0400 2011


349
350
351
# File 'lib/maid/tools.rb', line 349

def accessed_at(path)
  File.atime(expand(path))
end

- (Object) created_at(path)

Get the creation time of a file.

In Unix speak, ctime.

Examples

created_at('foo.zip') # => Sat Apr 09 10:50:01 -0400 2011


338
339
340
# File 'lib/maid/tools.rb', line 338

def created_at(path)
  File.ctime(expand(path))
end

- (Object) dir(globs)

Give all files matching the given glob.

Note that the globs are not regexps (they're closer to shell globs). However, some regexp-like notation can be used, e.g. ?, [a-z], {tgz,zip}. For more details, see Ruby's documentation on Dir.glob.

The matches are sorted lexically to aid in readability when using --dry-run.

Examples

Single glob:

dir('~/Downloads/*.zip')

Specifying multiple extensions succinctly:

dir('~/Downloads/*.{exe,deb,dmg,pkg,rpm}')

Multiple glob (all are equivalent):

dir(['~/Downloads/*.zip', '~/Dropbox/*.zip'])
dir(%w(~/Downloads/*.zip ~/Dropbox/*.zip))
dir('~/{Downloads,Dropbox}/*.zip')

Recursing into subdirectories (see also: find):

dir('~/Music/**/*.m4a')


204
205
206
207
208
209
# File 'lib/maid/tools.rb', line 204

def dir(globs)
  expand_all(globs).
    map { |glob| Dir.glob(glob) }.
    flatten.
    sort
end

- (Object) disk_usage(path)

Calculate disk usage of a given path in kilobytes.

See also: Maid::NumericExtensions::SizeToKb.

Examples

disk_usage('foo.zip') # => 136


319
320
321
322
323
324
325
326
327
328
329
# File 'lib/maid/tools.rb', line 319

def disk_usage(path)
  raw = cmd("du -s #{ sh_escape(path) }")
  # FIXME: This reports in kilobytes, but should probably report in bytes.
  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

- (Object) downloaded_from(path)

[Mac OS X] Use Spotlight metadata to determine the site from which a file was downloaded.

Examples

downloaded_from('foo.zip') # => ['http://www.site.com/foo.zip', 'http://www.site.com/']


287
288
289
290
291
# File 'lib/maid/tools.rb', line 287

def downloaded_from(path)
  raw = cmd("mdls -raw -name kMDItemWhereFroms #{ sh_escape(path) }")
  clean = raw[1, raw.length - 2]
  clean.split(/,\s+/).map { |s| t = s.strip; t[1, t.length - 2] }
end

- (Object) duration_s(path)

[Mac OS X] Use Spotlight metadata to determine audio length.

Examples

duration_s('foo.mp3') # => 235.705


298
299
300
# File 'lib/maid/tools.rb', line 298

def duration_s(path)
  cmd("mdls -raw -name kMDItemDurationSeconds #{ sh_escape(path) }").to_f
end

- (Object) find(path, &block)

Find matching files, akin to the Unix utility find.

If no block is given, it will return an array. Otherwise, it acts like Find.find.

Examples

Without a block:

find('~/Downloads/') # => [...]

Recursing and filtering using a regular expression:

find('~/Downloads/').grep(/\.pdf$/)

(Note: It's just Ruby, so any methods in Array and Enumerable can be used.)

Recursing with a block:

find('~/Downloads/') do |path|
  # ...
end


259
260
261
262
263
264
265
266
267
268
269
# File 'lib/maid/tools.rb', line 259

def find(path, &block)
  expanded_path = expand(path)

  if block.nil?
    files = []
    Find.find(expanded_path) { |file_path| files << file_path }
    files
  else
    Find.find(expanded_path, &block)
  end
end

- (Object) git_piston(path)

Deprecated.

Pull and push the git repository at the given path.

Since this is deprecated, you might also be interested in SparkleShare, a great git-based file syncronization project.

Examples

git_piston('~/code/projectname')


383
384
385
386
387
# File 'lib/maid/tools.rb', line 383

def git_piston(path)
  full_path = expand(path)
  stdout = cmd("cd #{ sh_escape(full_path) } && git pull && git push 2>&1")
  log("Fired git piston on #{ sh_escape(full_path) }.  STDOUT:\n\n#{ stdout }")
end

- (Object) last_accessed(path)

Deprecated.

Alias of accessed_at.



356
357
358
359
# File 'lib/maid/tools.rb', line 356

def last_accessed(path)
  # Not a normal `alias` so the deprecation notice shows in the docs.
  accessed_at(path)
end

- (Object) locate(name)

[Mac OS X] Use Spotlight to locate all files matching the given filename.

[Ubuntu] Not currently supported. See issue #67.

Examples

locate('foo.zip') # => ['/a/foo.zip', '/b/foo.zip']


278
279
280
# File 'lib/maid/tools.rb', line 278

def locate(name)
  cmd("mdfind -name #{ sh_escape(name) }").split("\n")
end

- (Object) mkdir(path, options = {})

Create a directory and all of its parent directories.

The path of the created directory is returned, which allows for chaining (see examples).

Options

:mode

The symbolic and absolute mode can both be used, for example: 0700, 'u=wr,go=rr'

Examples

Creating a directory with a specific mode:

mkdir('~/Music/Pink Floyd/', :mode => 0644)

Ensuring a directory exists when moving:

move('~/Downloads/Pink Floyd*.mp3', mkdir('~/Music/Pink Floyd/'))


230
231
232
233
234
235
# File 'lib/maid/tools.rb', line 230

def mkdir(path, options = {})
  path = expand(path)
  log("mkdir -p #{ sh_escape(path) }")
  FileUtils.mkdir_p(path, @file_options.merge(options))
  path
end

- (Object) modified_at(path)

Get the modification time of a file.

In Unix speak, mtime.

Examples

modified_at('foo.zip') # => Sat Apr 09 10:50:01 -0400 2011


369
370
371
# File 'lib/maid/tools.rb', line 369

def modified_at(path)
  File.mtime(expand(path))
end

- (Object) move(sources, destination)

Move sources to a destination directory.

Movement is only allowed to directories that already exist. If your intention is to rename, see the rename method.

Examples

Single path:

move('~/Downloads/foo.zip', '~/Archive/Software/Mac OS X/')

Multiple paths:

move(['~/Downloads/foo.zip', '~/Downloads/bar.zip'], '~/Archive/Software/Mac OS X/')
move(dir('~/Downloads/*.zip'), '~/Archive/Software/Mac OS X/')


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

def move(sources, destination)
  destination = expand(destination)

  if File.directory?(destination)
    expand_all(sources).each do |source|
      log("move #{ sh_escape(source) } #{ sh_escape(destination) }")
      FileUtils.mv(source, destination, @file_options)
    end
  else
    # Unix `mv` warns about the target not being a directory with multiple sources.  Maid checks the same.
    warn("skipping move because #{ sh_escape(destination) } is not a directory (use 'mkdir' to create first, or use 'rename')")
  end
end

- (Object) remove(paths, options = {})

Delete the files at the given path recursively.

NOTE: In most cases, trash is a safer choice, since the files will be recoverable by retreiving them from the trash. Once you delete a file using remove, it's gone! Please use trash whenever possible and only use remove when necessary.

Options

:force => boolean

Force deletion (no error is raised if the file does not exist).

:secure => boolean

Infrequently needed. See FileUtils.remove_entry_secure

Examples

Single path:

remove('~/Downloads/foo.zip')

Multiple path:

remove(['~/Downloads/foo.zip', '~/Downloads/bar.zip'])
remove(dir('~/Downloads/*.zip'))


168
169
170
171
172
173
174
175
# File 'lib/maid/tools.rb', line 168

def remove(paths, options = {})
  expand_all(paths).each do |path|
    options = @file_options.merge(options)

    log("Removing #{ sh_escape(path) }")
    FileUtils.rm_r(path, options)
  end
end

- (Object) rename(source, destination)

Rename a single file.

Any directories needed in order to complete the rename are made automatically.

Overwriting is not allowed; it logs a warning. If overwriting is desired, use remove to delete the file first, then use rename.

Examples

Simple rename:

rename('foo.zip', 'baz.zip') # "foo.zip" becomes "baz.zip"

Rename needing directories:

rename('foo.zip', 'bar/baz.zip') # "bar" is created, "foo.zip" becomes "baz.zip" within "bar"

Attempting to overwrite:

rename('foo.zip', 'existing.zip') # "skipping move of..."


65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/maid/tools.rb', line 65

def rename(source, destination)
  source = expand(source)
  destination = expand(destination)

  mkdir(File.dirname(destination))

  if File.exist?(destination)
    warn("skipping rename of #{ sh_escape(source) } to #{ sh_escape(destination) } because it would overwrite")
  else
    log("rename #{ sh_escape(source) } #{ sh_escape(destination) }")
    FileUtils.mv(source, destination, @file_options)
  end
end

- (Object) sync(from, to, options = {})

Simple sync two files/folders using rsync.

The host OS must provide rsync. See the rsync man page for a detailed description.

man rsync

Options

:delete => boolean :verbose => boolean :archive => boolean (default true) :update => boolean (default true) :exclude => string :prune_empty => boolean

Examples

Syncing a directory to a backup:

sync('~/music', '/backup/music')

Excluding a path:

sync('~/code', '/backup/code', :exclude => '.git')

Excluding multiple paths:

sync('~/code', '/backup/code', :exclude => ['.git', '.rvmrc'])


419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'lib/maid/tools.rb', line 419

def sync(from, to, options = {})
  # expand removes trailing slash
  # cannot use str[-1] due to ruby 1.8.7 restriction
  from = expand(from) + (from.end_with?('/') ? '/' : '')
  to = expand(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=#{ sh_escape(path) }"
  end

  ops << '--delete' if options[:delete]
  stdout = cmd("rsync #{ ops.join(' ') } #{ sh_escape(from) } #{ sh_escape(to) } 2>&1")
  log("Fired sync from #{ sh_escape(from) } to #{ sh_escape(to) }.  STDOUT:\n\n#{ stdout }")
end

- (Object) trash(paths, options = {})

Move the given paths to the user's trash.

The path is still 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. (See issue #63.) 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)

Delete files over the given size rather than moving to the trash.

See also Maid::NumericExtensions::SizeToKb

Examples

Single path:

trash('~/Downloads/foo.zip')

Multiple paths:

trash(['~/Downloads/foo.zip', '~/Downloads/bar.zip'])
trash(dir('~/Downloads/*.zip'))


106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/maid/tools.rb', line 106

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.

  expand_all(paths).each do |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)
        rename(path, safe_trash_path)
      else
        move(path, @trash_path)
      end
    end
  end
end

- (Object) zipfile_contents(path)

List the contents of a zip file.

Examples

zipfile_contents('foo.zip') # => ['foo/foo.exe', 'foo/README.txt']


307
308
309
310
# File 'lib/maid/tools.rb', line 307

def zipfile_contents(path)
  raw = cmd("unzip -Z1 #{ sh_escape(path) }")
  raw.split("\n")
end