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

#accessed_at(path) ⇒ Object

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


545
546
547
# File 'lib/maid/tools.rb', line 545

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

#add_tag(path, tag) ⇒ Object

Add a Finder label or a list of labels to a file or directory. Only available on OS X when you have tag installed.

Example

add_tag("~/Downloads/a.dmg.download", "Unfinished")


836
837
838
839
840
841
842
843
844
845
# File 'lib/maid/tools.rb', line 836

def add_tag(path, tag)
  return unless has_tag_available_and_warn?

  path = expand(path)
  ts = Array(tag).join(',')
  log "add tags #{ts} to #{path}"
  return if @file_options[:noop]

  cmd("tag -a #{sh_escape(ts)} #{sh_escape(path)}")
end

#added_at(path) ⇒ Object

The added time of a file on OS X, or ctime on Linux.

Example

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


950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
# File 'lib/maid/tools.rb', line 950

def added_at(path)
  if Maid::Platform.osx?
    path = expand(path)
    raw = cmd("mdls -raw -name kMDItemDateAdded #{sh_escape(path)}")

    if raw == '(null)'
      1.second.ago
    else
      begin
        DateTime.parse(raw).to_time
      rescue ArgumentError => e
        created_at(path)
      end
    end
  else
    created_at(path)
  end
end

#checksum_of(path) ⇒ Object

Get a checksum for a file.

Examples

checksum_of('foo.zip') # => "67258d750ca654d5d3c7b06bd2a1c792ced2003e"


583
584
585
# File 'lib/maid/tools.rb', line 583

def checksum_of(path)
  Digest::SHA1.hexdigest(File.read(path))
end

#contains_tag?(path, tag) ⇒ Boolean

Tell if a file or directory has a certain Finder labels. Only available on OS X when you have tag installed.

Example

contains_tag?("~/Downloads/a.dmg.download", "Unfinished") # => true

Returns:

  • (Boolean)


820
821
822
823
824
825
826
827
828
# File 'lib/maid/tools.rb', line 820

def contains_tag?(path, tag)
  if has_tag_available_and_warn?
    path = expand(path)
    ts = tags(path)
    ts.include?(tag)
  else
    false
  end
end

#content_types(path) ⇒ Object

Get the content types of a path.

Content types can be MIME types, Internet media types or Spotlight content types (OS X only).

Examples

content_types('foo.zip') # => ["public.zip-archive", "com.pkware.zip-archive",
                               "public.archive", "application/zip", "application"]
content_types('bar.jpg') # => ["public.jpeg", "public.image", "image/jpeg", "image"]


674
675
676
# File 'lib/maid/tools.rb', line 674

def content_types(path)
  [spotlight_content_types(path), mime_type(path), media_type(path)].flatten
end

#copy(sources, destination) ⇒ Object

Copy from sources to destination

The path is not copied if a file already exists at the destination with the same name. A warning is logged instead. Note: Similar functionality is provided by the sync tool, but this requires installation of the rsync binary

Examples

Single path:

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

Multiple paths:

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


188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/maid/tools.rb', line 188

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

  expand_all(sources).each do |source|
    target = File.join(destination, File.basename(source))

    if File.exist?(target)
      warn("skipping copy because #{sh_escape(source)} because #{sh_escape(target)} already exists")
    else
      log("cp #{sh_escape(source)} #{sh_escape(destination)}")
      FileUtils.cp(source, destination, **@file_options)
    end
  end
end

#created_at(path) ⇒ Object

Get the creation time of a file.

In Unix speak, ctime.

Examples

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


534
535
536
# File 'lib/maid/tools.rb', line 534

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

#dimensions_px(path) ⇒ Object

Determine the dimensions of GIF, PNG, JPEG, or TIFF images.

Value returned is [width, height].

Examples

dimensions_px('image.jpg') # => [1024, 768]
width, height = dimensions_px('image.jpg')
dimensions_px('image.jpg').join('x') # => "1024x768"


470
471
472
# File 'lib/maid/tools.rb', line 470

def dimensions_px(path)
  Dimensions.dimensions(path)
end

#dir(globs) ⇒ Object

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')


268
269
270
271
272
273
# File 'lib/maid/tools.rb', line 268

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

#dir_safe(globs) ⇒ Object

Same as dir, but excludes files that are (possibly) being downloaded.

Example

Move Debian/Ubuntu packages that are finished downloading into a software directory.

move dir_safe('~/Downloads/*.deb'), '~/Archive/Software'


284
285
286
287
# File 'lib/maid/tools.rb', line 284

def dir_safe(globs)
  dir(globs)
    .reject { |path| downloading?(path) }
end

#disk_usage(path) ⇒ Object

Calculate disk usage of a given path in kilobytes.

See also: Maid::NumericExtensions::SizeToKb.

Examples

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


517
518
519
520
521
522
523
524
525
# File 'lib/maid/tools.rb', line 517

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

  raise "Stopping pessimistically because of unexpected value from du (#{raw.inspect})" if usage_kb.zero?

  usage_kb
end

#downloaded_from(path) ⇒ Object

[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/']


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

def downloaded_from(path)
  mdls_to_array(path, 'kMDItemWhereFroms')
end

#downloading?(path) ⇒ Boolean

Detect whether the path is currently being downloaded in Chrome, Firefox or Safari.

See also: dir_safe

Returns:

  • (Boolean)


390
391
392
# File 'lib/maid/tools.rb', line 390

def downloading?(path)
  Maid::Downloading.downloading?(path)
end

#dupes_in(globs) ⇒ Object

Find all duplicate files in the given globs.

More often than not, you'll want to use newest_dupes_in or verbose_dupes_in instead of using this method directly.

Globs are expanded as in dir, then all non-files are filtered out. The remaining files are compared by size, and non-dupes are filtered out. The remaining candidates are then compared by checksum. Dupes are returned as an array of arrays.

Examples

dupes_in('~/{Downloads,Desktop}/*') # => [
                                           ['~/Downloads/foo.zip', '~/Downloads/foo (1).zip'],
                                           ['~/Desktop/bar.txt', '~/Desktop/bar copy.txt']
                                         ]

Keep the newest dupe:

dupes_in('~/Desktop/*', '~/Downloads/*').each do |dupes|
  trash dupes.sort_by { |p| File.mtime(p) }[0..-2]
end


417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/maid/tools.rb', line 417

def dupes_in(globs)
  dupes = []
  files(globs) # Start by filtering out non-files
    .group_by { |f| size_of(f) } # ... then grouping by size, since that's fast
    .reject { |_s, p| p.length < 2 } # ... and filter out any non-dupes
    .map do |_size, candidates|
      dupes += candidates
               .group_by { |p| checksum_of(p) } # Now group our candidates by a slower checksum calculation
               .reject { |_c, p| p.length < 2 } # ... and filter out any non-dupes
               .values
    end
  dupes
end

#duration_s(path) ⇒ Object

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

Examples

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


494
495
496
# File 'lib/maid/tools.rb', line 494

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

#escape_glob(glob) ⇒ Object

Escape characters that have special meaning as a part of path global patterns.

Useful when using dir with file names that may contain { } [ ] characters.

Example

escape_glob('test [tmp]') # => 'test \\[tmp\\]'


305
306
307
# File 'lib/maid/tools.rb', line 305

def escape_glob(glob)
  glob.gsub(/[{}\[\]]/) { |s| '\\' + s }
end

#files(globs) ⇒ Object

Give only files matching the given glob.

This is the same as dir but only includes actual files (no directories or symlinks).



293
294
295
296
# File 'lib/maid/tools.rb', line 293

def files(globs)
  dir(globs)
    .select { |f| File.file?(f) }
end

#find(path, &block) ⇒ Object

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


357
358
359
360
361
362
363
364
365
# File 'lib/maid/tools.rb', line 357

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

  if block.nil?
    Find.find(expanded_path).to_a
  else
    Find.find(expanded_path, &block)
  end
end

#git_piston(path) ⇒ Object

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')


597
598
599
600
601
# File 'lib/maid/tools.rb', line 597

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

#has_been_used?(path) ⇒ Boolean

Tell if a file has been used since added

Example

has_been_used?("~/Downloads/downloading.download") # => false

Returns:

  • (Boolean)


901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
# File 'lib/maid/tools.rb', line 901

def has_been_used?(path)
  if Maid::Platform.osx?
    path = expand(path)
    raw = cmd("mdls -raw -name kMDItemLastUsedDate #{sh_escape(path)}")

    if raw == '(null)'
      false
    else
      begin
        DateTime.parse(raw).to_time
        true
      rescue ArgumentError => e
        false
      end
    end
  else
    used_at(path) <=> added_at(path) > 0
  end
end

#has_tags?(path) ⇒ Boolean

Tell if a file or directory has any Finder labels. Only available on OS X when you have tag installed.

Example

has_tags?("~/Downloads/a.dmg.download") # => true

Returns:

  • (Boolean)


805
806
807
808
809
810
811
812
# File 'lib/maid/tools.rb', line 805

def has_tags?(path)
  if has_tag_available_and_warn?
    ts = tags(path)
    ts && ts.count > 0
  else
    false
  end
end

#hidden?(path) ⇒ Boolean

Tell if a file is hidden

Example

hidden?("~/.maid") # => true

Returns:

  • (Boolean)


886
887
888
889
890
891
892
893
894
# File 'lib/maid/tools.rb', line 886

def hidden?(path)
  if Maid::Platform.osx?
    raw = cmd("mdls -raw -name kMDItemFSInvisible #{sh_escape(path)}")
    raw == '1'
  else
    p = Pathname.new(expand(path))
    p.basename =~ /^\./
  end
end

#ignore_child_dirs(arr) ⇒ Object

Given an array of directories, return a new array without any child directories whose parent is already present in that array.

Example

ignore_child_dirs(["foo", "foo/a", "foo/b", "bar"]) # => ["foo", "bar"]


776
777
778
779
780
781
782
# File 'lib/maid/tools.rb', line 776

def ignore_child_dirs(arr)
  arr.sort do |x, y|
    y.count('/') - x.count('/')
  end.select do |d|
    !arr.include?(File.dirname(d))
  end
end

#last_accessed(path) ⇒ Object

Deprecated.

Alias of accessed_at.



552
553
554
555
# File 'lib/maid/tools.rb', line 552

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

#locate(name) ⇒ Object

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

[Ubuntu] Use locate to locate all files matching the given filename.

Examples

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


374
375
376
# File 'lib/maid/tools.rb', line 374

def locate(name)
  cmd("#{Maid::Platform::Commands.locate} #{sh_escape(name)}").split("\n")
end

#location_city(path) ⇒ Object

Determine the city of the given JPEG image.

Examples

loation_city('old_capitol.jpg') # => "Iowa City, IA, US"


479
480
481
482
483
484
485
486
487
# File 'lib/maid/tools.rb', line 479

def location_city(path)
  case mime_type(path)
  when 'image/jpeg'
    gps = EXIFR::JPEG.new(path).gps
    coordinates_string = [gps.latitude, gps.longitude]
    location = Geocoder.search(coordinates_string).first
    [location.city, location.province, location.country_code.upcase].join(', ')
  end
end

#media_type(path) ⇒ Object

Get the Internet media type of the file.

In other words, the first part of mime_type.

Examples

media_type('bar.jpg') # => "image"


698
699
700
701
702
703
704
# File 'lib/maid/tools.rb', line 698

def media_type(path)
  type = MIME::Types.type_for(path)[0]

  return unless type

  type.media_type
end

#mime_type(path) ⇒ Object

Get the MIME type of the file.

Examples

mime_type('bar.jpg') # => "image/jpeg"


683
684
685
686
687
688
689
# File 'lib/maid/tools.rb', line 683

def mime_type(path)
  type = MIME::Types.type_for(path)[0]

  return unless type

  [type.media_type, type.sub_type].join('/')
end

#mkdir(path, options = {}) ⇒ Object

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/'))


328
329
330
331
332
333
# File 'lib/maid/tools.rb', line 328

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

#modified_at(path) ⇒ Object

Get the modification time of a file.

In Unix speak, mtime.

Examples

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


565
566
567
# File 'lib/maid/tools.rb', line 565

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

#move(sources, destination_dir, clobber: true) ⇒ Object

Moves sources file(s) to a destination directory.

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

move sources to file if it exists, false to skip moving file if it exists

Examples:

Single source file

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

Multiple source files

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

Overwrite destination file if it already exists

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

Skip file if it already exists at destination

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

Parameters:

  • sources (String, Array<String>)

    the paths to the source files to

  • destination_dir (String)

    path of the directory where to move

  • kwargs (Hash)

    the arguments to modify behaviour



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/maid/tools.rb', line 56

def move(sources, destination_dir, clobber: true)
  expanded_destination_dir = expand(destination_dir)

  if File.directory?(expanded_destination_dir)
    expand_all(sources).each do |source|
      log("move #{sh_escape(source)} #{sh_escape(expanded_destination_dir)}")

      unless skip_move?(source, expanded_destination_dir, clobber)
        FileUtils.mv(source, expanded_destination_dir, **@file_options)
      end
    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(expanded_destination_dir)} " \
         "is not a directory (use 'mkdir' to create first, or use 'rename')")
  end
end

#newest_dupes_in(globs) ⇒ Object

Convenience method that is like dupes_in but excludes the oldest dupe.

Example

Keep the oldest dupe (trash the others):

trash newest_dupes_in('~/Downloads/*')


439
440
441
442
443
# File 'lib/maid/tools.rb', line 439

def newest_dupes_in(globs)
  dupes_in(globs)
    .map { |dupes| dupes.sort_by { |p| File.mtime(p) }[1..-1] }
    .flatten
end

#remove(paths, options = {}) ⇒ Object

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'))


232
233
234
235
236
237
238
239
# File 'lib/maid/tools.rb', line 232

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

#remove_tag(path, tag) ⇒ Object

Remove a Finder label or a list of labels from a file or directory. Only available on OS X when you have tag installed.

Example

remove_tag("~/Downloads/a.dmg", "Unfinished")


853
854
855
856
857
858
859
860
861
862
# File 'lib/maid/tools.rb', line 853

def remove_tag(path, tag)
  return unless has_tag_available_and_warn?

  path = expand(path)
  ts = Array(tag).join(',')
  log "remove tags #{ts} from #{path}"
  return if @file_options[:noop]

  cmd("tag -r #{sh_escape(ts)} #{sh_escape(path)}")
end

#rename(source, destination) ⇒ Object

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..."


94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/maid/tools.rb', line 94

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

#set_tag(path, tag) ⇒ Object

Set Finder label of a file or directory to a label or a list of labels. Only available on OS X when you have tag installed.

Example

set_tag("~/Downloads/a.dmg.download", "Unfinished")


870
871
872
873
874
875
876
877
878
879
# File 'lib/maid/tools.rb', line 870

def set_tag(path, tag)
  return unless has_tag_available_and_warn?

  path = expand(path)
  ts = Array(tag).join(',')
  log "set tags #{ts} to #{path}"
  return if @file_options[:noop]

  cmd("tag -s #{sh_escape(ts)} #{sh_escape(path)}")
end

#size_of(path) ⇒ Object

Get the size of a file.

Examples

size_of('foo.zip') # => 2193


574
575
576
# File 'lib/maid/tools.rb', line 574

def size_of(path)
  File.size(path)
end

#spotlight_content_types(path) ⇒ Object

[Mac OS X] Use Spotlight metadata to determine which content types a file has.

Examples

spotlight_content_types('foo.zip') # => ['public.zip-archive', 'public.archive']


661
662
663
# File 'lib/maid/tools.rb', line 661

def spotlight_content_types(path)
  mdls_to_array(path, 'kMDItemContentTypeTree')
end

#sync(from, to, options = {}) ⇒ Object

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'])


633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
# File 'lib/maid/tools.rb', line 633

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

#tags(path) ⇒ Object

Get a list of Finder labels of a file or directory. Only available on OS X when you have tag installed.

Example

tags("~/Downloads/a.dmg.download") # => ["Unfinished"]


789
790
791
792
793
794
795
796
797
# File 'lib/maid/tools.rb', line 789

def tags(path)
  if has_tag_available_and_warn?
    path = expand(path)
    raw = cmd("tag -lN #{sh_escape(path)}")
    raw.strip.split(',')
  else
    []
  end
end

#trash(paths, options = {}) ⇒ Object

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'))


135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/maid/tools.rb', line 135

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

#tree_empty?(root) ⇒ Boolean

Test whether a directory is either empty, or contains only empty directories/subdirectories.

Example

if tree_empty?(dir('~/Downloads/foo'))
  trash('~/Downloads/foo')
end

Returns:

  • (Boolean)


741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
# File 'lib/maid/tools.rb', line 741

def tree_empty?(root)
  return nil if File.file?(root)
  return true if Dir.glob(root + '/*').length == 0

  ignore = []

  # Look for files.
  return false if Dir.glob(root + '/*').select { |f| File.file?(f) }.length > 0

  empty_dirs = Dir.glob(root + '/**/*').select do |d|
    File.directory?(d)
  end.reverse.select do |d|
    # `.reverse` sorts deeper directories first.

    # If the directory is empty, its parent should ignore it.
    should_ignore = Dir.glob(d + '/*').select do |n|
      !ignore.include?(n)
    end.length == 0

    ignore << d if should_ignore

    should_ignore
  end

  Dir.glob(root + '/*').select do |n|
    !empty_dirs.include?(n)
  end.length == 0
end

#used_at(path) ⇒ Object

The last used time of a file on OS X, or atime on Linux.

Example

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


926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
# File 'lib/maid/tools.rb', line 926

def used_at(path)
  if Maid::Platform.osx?
    path = expand(path)
    raw = cmd("mdls -raw -name kMDItemLastUsedDate #{sh_escape(path)}")

    if raw == '(null)'
      nil
    else
      begin
        DateTime.parse(raw).to_time
      rescue ArgumentError => e
        accessed_at(path)
      end
    end
  else
    accessed_at(path)
  end
end

#verbose_dupes_in(globs) ⇒ Object

Convenience method for dupes_in that excludes the dupe with the shortest name.

This is ideal for dupes like foo.zip, foo (1).zip, foo copy.zip.

Example

Keep the dupe with the shortest name (trash the others):

trash verbose_dupes_in('~/Downloads/*')


455
456
457
458
459
# File 'lib/maid/tools.rb', line 455

def verbose_dupes_in(globs)
  dupes_in(globs)
    .map { |dupes| dupes.sort_by { |p| File.basename(p).length }[1..-1] }
    .flatten
end

#where_content_type(paths, filter_types) ⇒ Object

Filter an array by content types.

Content types can be MIME types, internet media types or Spotlight content types (OS X only).

If you need your rules to work on multiple platforms, it's recommended to avoid using Spotlight content types.

Examples

Using media types

where_content_type(dir('~/Downloads/*'), 'video')
where_content_type(dir('~/Downloads/*'), ['image', 'audio'])

Using MIME types

where_content_type(dir('~/Downloads/*'), 'image/jpeg')

Using Spotlight content types

Less portable, but richer data in some cases.

where_content_type(dir('~/Downloads/*'), 'public.image')


728
729
730
731
# File 'lib/maid/tools.rb', line 728

def where_content_type(paths, filter_types)
  filter_types = Array(filter_types)
  Array(paths).select { |p| !(filter_types & content_types(p)).empty? }
end

#zipfile_contents(path) ⇒ Object

List the contents of a zip file.

Examples

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


503
504
505
506
507
508
# File 'lib/maid/tools.rb', line 503

def zipfile_contents(path)
  # It might be nice to use `glob` from `Zip::FileSystem`, but it seems buggy.  (Subdirectories aren't included.)
  Zip::File.open(path) do |zip_file|
    zip_file.entries.map { |entry| entry.name }.sort
  end
end