maid 0.1.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +20 -0
- data/.rspec +0 -0
- data/Gemfile +4 -0
- data/LICENSE +339 -0
- data/README.rdoc +215 -0
- data/Rakefile +16 -0
- data/bin/maid +5 -0
- data/lib/maid.rb +31 -0
- data/lib/maid/app.rb +40 -0
- data/lib/maid/maid.rb +67 -0
- data/lib/maid/numeric_extensions.rb +76 -0
- data/lib/maid/rule.rb +6 -0
- data/lib/maid/tools.rb +119 -0
- data/lib/maid/version.rb +3 -0
- data/maid.gemspec +26 -0
- data/spec/lib/maid/app_spec.rb +54 -0
- data/spec/lib/maid/maid_spec.rb +151 -0
- data/spec/lib/maid/rule_spec.rb +10 -0
- data/spec/lib/maid/tools_spec.rb +118 -0
- data/spec/lib/maid_spec.rb +26 -0
- data/spec/spec_helper.rb +9 -0
- metadata +160 -0
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
require 'bundler'
|
5
|
+
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
Bundler::GemHelper.install_tasks
|
9
|
+
|
10
|
+
Rake::RDocTask.new do |rd|
|
11
|
+
rd.rdoc_dir = 'doc'
|
12
|
+
rd.main = 'README.rdoc'
|
13
|
+
rd.rdoc_files.include('README.rdoc', 'lib/**/*.rb')
|
14
|
+
end
|
15
|
+
|
16
|
+
RSpec::Core::RakeTask.new(:spec)
|
data/bin/maid
ADDED
data/lib/maid.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Maid
|
2
|
+
autoload :App, 'maid/app'
|
3
|
+
autoload :Maid, 'maid/maid'
|
4
|
+
autoload :Tools, 'maid/tools'
|
5
|
+
autoload :NumericExtensions, 'maid/numeric_extensions'
|
6
|
+
autoload :Rule, 'maid/rule'
|
7
|
+
autoload :VERSION, 'maid/version'
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# Execute the block with the Maid instance set to <tt>instance</tt>.
|
11
|
+
def with_instance(instance)
|
12
|
+
@instance = instance
|
13
|
+
result = yield
|
14
|
+
@instance = nil
|
15
|
+
result
|
16
|
+
end
|
17
|
+
|
18
|
+
# Define rules for the Maid instance.
|
19
|
+
def rules(&block)
|
20
|
+
@instance.instance_eval(&block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Numeric
|
26
|
+
include Maid::NumericExtensions
|
27
|
+
end
|
28
|
+
|
29
|
+
if __FILE__ == $PROGRAM_NAME
|
30
|
+
Maid::App.start
|
31
|
+
end
|
data/lib/maid/app.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'thor'
|
3
|
+
|
4
|
+
class Maid::App < Thor
|
5
|
+
default_task 'clean'
|
6
|
+
|
7
|
+
desc 'clean', 'Clean based on rules'
|
8
|
+
method_option :rules, :type => :string, :aliases => %w[-r]
|
9
|
+
method_option :noop, :type => :boolean, :aliases => %w[-n --dry-run]
|
10
|
+
method_option :silent, :type => :boolean, :aliases => %w[-s]
|
11
|
+
def clean
|
12
|
+
maid = Maid::Maid.new(maid_options(options))
|
13
|
+
say "Logging actions to #{maid.log_path.inspect}" unless options.silent? || options.noop?
|
14
|
+
maid.clean
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'version', 'print version number'
|
18
|
+
def version
|
19
|
+
say Maid::VERSION
|
20
|
+
end
|
21
|
+
|
22
|
+
no_tasks do
|
23
|
+
def maid_options(options)
|
24
|
+
h = {}
|
25
|
+
|
26
|
+
if options['noop']
|
27
|
+
# You're testing, so a simple log goes to STDOUT and no actions are taken
|
28
|
+
h[:file_options] = {:noop => true}
|
29
|
+
h[:log_path] = STDOUT
|
30
|
+
h[:log_formatter] = lambda { |_, _, _, msg| "#{msg}\n" }
|
31
|
+
end
|
32
|
+
|
33
|
+
if options['rules']
|
34
|
+
h[:rules_path] = options['rules']
|
35
|
+
end
|
36
|
+
|
37
|
+
h
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/maid/maid.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
# Maid cleans up according to the given rules, logging what it does.
|
5
|
+
class Maid::Maid
|
6
|
+
DEFAULTS = {
|
7
|
+
:progname => 'Maid',
|
8
|
+
:log_path => File.expand_path('~/.maid/maid.log'),
|
9
|
+
:rules_path => File.expand_path('~/.maid/rules.rb'),
|
10
|
+
:trash_path => File.expand_path('~/.Trash'),
|
11
|
+
:file_options => {:noop => true}, # for FileUtils
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
include ::Maid::Tools
|
15
|
+
attr_reader :file_options, :log_path, :rules, :rules_path, :trash_path
|
16
|
+
|
17
|
+
# Make a new Maid, setting up paths for the log and trash.
|
18
|
+
#
|
19
|
+
# Sane defaults for a log and trash path are set for Mac OS X, but they can easily be overridden like so:
|
20
|
+
#
|
21
|
+
# Maid::Maid.new(:log_path => '/home/username/log/maid.log', :trash_path => '/home/username/.local/share/Trash/files/')
|
22
|
+
#
|
23
|
+
def initialize(options = {})
|
24
|
+
options = DEFAULTS.merge(options.reject { |k, v| v.nil? })
|
25
|
+
|
26
|
+
@log_path = options[:log_path]
|
27
|
+
FileUtils.mkdir_p(File.dirname(@log_path)) unless @log_path.kind_of?(IO)
|
28
|
+
@logger = Logger.new(@log_path)
|
29
|
+
@logger.progname = options[:progname]
|
30
|
+
@logger.formatter = options[:log_formatter] if options[:log_formatter]
|
31
|
+
|
32
|
+
@rules_path = options[:rules_path]
|
33
|
+
@trash_path = options[:trash_path]
|
34
|
+
@file_options = options[:file_options]
|
35
|
+
|
36
|
+
@rules = []
|
37
|
+
end
|
38
|
+
|
39
|
+
# Start cleaning, based on the rules defined at rules_path.
|
40
|
+
def clean
|
41
|
+
@logger.info 'Started'
|
42
|
+
add_rules(@rules_path)
|
43
|
+
follow_rules
|
44
|
+
@logger.info 'Finished'
|
45
|
+
end
|
46
|
+
|
47
|
+
# Add the rules at path.
|
48
|
+
def add_rules(path)
|
49
|
+
Maid.with_instance(self) do
|
50
|
+
# Using 'Kernel' here to help with testability
|
51
|
+
Kernel.require(path)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Register a rule with a description and instructions (lambda function).
|
56
|
+
def rule(description, &instructions)
|
57
|
+
@rules << ::Maid::Rule.new(description, instructions)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Follow all registered rules.
|
61
|
+
def follow_rules
|
62
|
+
@rules.each do |rule|
|
63
|
+
@logger.info("Rule: #{rule.description}")
|
64
|
+
rule.follow
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# From https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/numeric/time.rb, with some modifications since active_support ruins Logger by overriding its functionality.
|
2
|
+
module Maid::NumericExtensions
|
3
|
+
# Enables the use of time calculations and declarations, like 45.minutes + 2.hours + 4.years.
|
4
|
+
#
|
5
|
+
# These methods use Time#advance for precise date calculations when using from_now, ago, etc.
|
6
|
+
# as well as adding or subtracting their results from a Time object. For example:
|
7
|
+
#
|
8
|
+
# # equivalent to Time.now.advance(:months => 1)
|
9
|
+
# 1.month.from_now
|
10
|
+
#
|
11
|
+
# # equivalent to Time.now.advance(:years => 2)
|
12
|
+
# 2.years.from_now
|
13
|
+
#
|
14
|
+
# # equivalent to Time.now.advance(:months => 4, :years => 5)
|
15
|
+
# (4.months + 5.years).from_now
|
16
|
+
#
|
17
|
+
# While these methods provide precise calculation when used as in the examples above, care
|
18
|
+
# should be taken to note that this is not true if the result of `months', `years', etc is
|
19
|
+
# converted before use:
|
20
|
+
#
|
21
|
+
# # equivalent to 30.days.to_i.from_now
|
22
|
+
# 1.month.to_i.from_now
|
23
|
+
#
|
24
|
+
# # equivalent to 365.25.days.to_f.from_now
|
25
|
+
# 1.year.to_f.from_now
|
26
|
+
#
|
27
|
+
# In such cases, Ruby's core
|
28
|
+
# Date[http://stdlib.rubyonrails.org/libdoc/date/rdoc/index.html] and
|
29
|
+
# Time[http://stdlib.rubyonrails.org/libdoc/time/rdoc/index.html] should be used for precision
|
30
|
+
# date and time arithmetic
|
31
|
+
def seconds
|
32
|
+
self
|
33
|
+
end
|
34
|
+
alias :second :seconds
|
35
|
+
|
36
|
+
def minutes
|
37
|
+
self * 60
|
38
|
+
end
|
39
|
+
alias :minute :minutes
|
40
|
+
|
41
|
+
def hours
|
42
|
+
self * 3600
|
43
|
+
end
|
44
|
+
alias :hour :hours
|
45
|
+
|
46
|
+
def days
|
47
|
+
self * 24.hours
|
48
|
+
end
|
49
|
+
alias :day :days
|
50
|
+
|
51
|
+
def weeks
|
52
|
+
self * 7.days
|
53
|
+
end
|
54
|
+
alias :week :weeks
|
55
|
+
|
56
|
+
def fortnights
|
57
|
+
self * 2.weeks
|
58
|
+
end
|
59
|
+
alias :fortnight :fortnights
|
60
|
+
|
61
|
+
# Reads best without arguments: 10.minutes.ago
|
62
|
+
def ago(time = ::Time.now)
|
63
|
+
time - self
|
64
|
+
end
|
65
|
+
|
66
|
+
# Reads best with argument: 10.minutes.until(time)
|
67
|
+
alias :until :ago
|
68
|
+
|
69
|
+
# Reads best with argument: 10.minutes.since(time)
|
70
|
+
def since(time = ::Time.now)
|
71
|
+
time + self
|
72
|
+
end
|
73
|
+
|
74
|
+
# Reads best without arguments: 10.minutes.from_now
|
75
|
+
alias :from_now :since
|
76
|
+
end
|
data/lib/maid/rule.rb
ADDED
data/lib/maid/tools.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'find'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
# Collection of utility methods included in Maid::Maid (and thus available in the rules DSL).
|
6
|
+
#
|
7
|
+
# In general, all paths are automatically expanded (e.g. '~/Downloads/foo.zip' becomes '/home/username/Downloads/foo.zip').
|
8
|
+
module Maid::Tools
|
9
|
+
# Move from <tt>from</tt> to <tt>to</tt>.
|
10
|
+
#
|
11
|
+
# The path is not moved if a file already exists at the destination with the same name. A warning is logged instead.
|
12
|
+
#
|
13
|
+
# This method delegates to FileUtils. The instance-level <tt>file_options</tt> hash is passed to control the <tt>:noop</tt> option.
|
14
|
+
#
|
15
|
+
# move('~/Downloads/foo.zip', '~/Archive/Software/Mac OS X/')
|
16
|
+
def move(from, to)
|
17
|
+
from = File.expand_path(from)
|
18
|
+
to = File.expand_path(to)
|
19
|
+
target = File.join(to, File.basename(from))
|
20
|
+
|
21
|
+
unless File.exist?(target)
|
22
|
+
@logger.info "mv #{from.inspect} #{to.inspect}"
|
23
|
+
FileUtils.mv(from, to, @file_options)
|
24
|
+
else
|
25
|
+
@logger.warn "skipping #{from.inspect} because #{target.inspect} already exists"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Move the given path to the trash (as set by <tt>trash_path</tt>).
|
30
|
+
#
|
31
|
+
# 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.
|
32
|
+
#
|
33
|
+
# trash('~/Downloads/foo.zip')
|
34
|
+
def trash(path)
|
35
|
+
target = File.join(@trash_path, File.basename(path))
|
36
|
+
safe_trash_path = File.join(@trash_path, "#{File.basename(path)} #{Time.now.strftime('%Y-%m-%d-%H-%M-%S')}")
|
37
|
+
|
38
|
+
if File.exist?(target)
|
39
|
+
move(path, safe_trash_path)
|
40
|
+
else
|
41
|
+
move(path, @trash_path)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Give all files matching the given glob.
|
46
|
+
#
|
47
|
+
# Delgates to Dir.
|
48
|
+
#
|
49
|
+
# dir('~/Downloads/*.zip')
|
50
|
+
def dir(glob)
|
51
|
+
Dir[File.expand_path(glob)]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Find matching files, akin to the Unix utility <tt>find</tt>.
|
55
|
+
#
|
56
|
+
# Delegates to Find.find.
|
57
|
+
#
|
58
|
+
# find '~/Downloads/' do |path|
|
59
|
+
# # ...
|
60
|
+
# end
|
61
|
+
def find(path, &block)
|
62
|
+
Find.find(File.expand_path(path), &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Run a shell command.
|
66
|
+
#
|
67
|
+
# Delegates to Kernel.`. Made primarily for testing other commands.
|
68
|
+
def cmd(command) #:nodoc:
|
69
|
+
%x(#{command})
|
70
|
+
end
|
71
|
+
|
72
|
+
# Use Spotlight metadata to determine the site from which a file was downloaded.
|
73
|
+
#
|
74
|
+
# downloaded_from('foo.zip') # => ['http://www.site.com/foo.zip', 'http://www.site.com/']
|
75
|
+
def downloaded_from(path)
|
76
|
+
raw = cmd("mdls -raw -name kMDItemWhereFroms #{path.inspect}")
|
77
|
+
clean = raw[1, raw.length - 2]
|
78
|
+
clean.split(/,\s+/).map { |s| t = s.strip; t[1, t.length - 2] }
|
79
|
+
end
|
80
|
+
|
81
|
+
# Use Spotlight metadata to determine audio length.
|
82
|
+
#
|
83
|
+
# duration_s('foo.mp3') # => 235.705
|
84
|
+
def duration_s(path)
|
85
|
+
cmd("mdls -raw -name kMDItemDurationSeconds #{path.inspect}").to_f
|
86
|
+
end
|
87
|
+
|
88
|
+
# Use Spotlight to locate all files matching the given filename
|
89
|
+
#
|
90
|
+
# locate('foo.zip') # => ['/a/foo.zip', '/b/foo.zip']
|
91
|
+
def locate(name)
|
92
|
+
cmd("mdfind -name #{name.inspect}").split("\n")
|
93
|
+
end
|
94
|
+
|
95
|
+
# Inspect the contents of a .zip file.
|
96
|
+
#
|
97
|
+
# zipfile_contents('foo.zip') # => ['foo/foo.exe', 'foo/README.txt']
|
98
|
+
def zipfile_contents(path)
|
99
|
+
raw = cmd("unzip -Z1 #{path.inspect}")
|
100
|
+
raw.split("\n")
|
101
|
+
end
|
102
|
+
|
103
|
+
# Calculate disk usage of a given path.
|
104
|
+
#
|
105
|
+
# disk_usage('foo.zip') # => 136
|
106
|
+
def disk_usage(path)
|
107
|
+
raw = cmd("du -s #{path.inspect}")
|
108
|
+
raw.split(/\s+/).first.to_i
|
109
|
+
end
|
110
|
+
|
111
|
+
# Pulls and pushes the given git repository.
|
112
|
+
#
|
113
|
+
# git_piston('~/code/projectname')
|
114
|
+
def git_piston(path)
|
115
|
+
full_path = File.expand_path(path)
|
116
|
+
stdout = cmd("cd #{full_path.inspect} && git pull && git push 2>&1")
|
117
|
+
@logger.info "Fired piston on #{full_path.inspect}. STDOUT:\n\n#{stdout}"
|
118
|
+
end
|
119
|
+
end
|
data/lib/maid/version.rb
ADDED
data/maid.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "maid/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "maid"
|
7
|
+
s.version = Maid::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Benjamin Oakes"]
|
10
|
+
s.email = ["hello@benjaminoakes.com"]
|
11
|
+
s.homepage = "http://www.benjaminoakes.com/"
|
12
|
+
s.summary = %q{Maid cleans up after you, based on rules you define.}
|
13
|
+
s.description = s.summary
|
14
|
+
|
15
|
+
s.rubyforge_project = "maid"
|
16
|
+
|
17
|
+
s.add_dependency('thor', '~> 0.14.6')
|
18
|
+
s.add_development_dependency('rspec', '~> 2.5.0')
|
19
|
+
s.add_development_dependency('timecop', '~> 0.3.5')
|
20
|
+
s.add_development_dependency('ZenTest', '~> 4.4.2')
|
21
|
+
|
22
|
+
s.files = `git ls-files`.split("\n")
|
23
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
24
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
25
|
+
s.require_paths = ["lib"]
|
26
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Maid
|
4
|
+
describe App, '#clean' do
|
5
|
+
before :each do
|
6
|
+
@app = App.new
|
7
|
+
@app.stub!(:maid_options)
|
8
|
+
@app.stub!(:say)
|
9
|
+
|
10
|
+
@maid = mock('Maid')
|
11
|
+
@maid.stub!(:clean)
|
12
|
+
@maid.stub!(:log_path)
|
13
|
+
Maid.stub!(:new).and_return(@maid)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should make a new Maid with the options' do
|
17
|
+
opts = {:foo => 'bar'}
|
18
|
+
@app.stub!(:maid_options).and_return(opts)
|
19
|
+
Maid.should_receive(:new).with(opts).and_return(@maid)
|
20
|
+
@app.clean
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should tell the Maid to clean' do
|
24
|
+
@maid.should_receive(:clean)
|
25
|
+
@app.clean
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe App, '#version' do
|
30
|
+
it 'should print out the gem version' do
|
31
|
+
app = App.new
|
32
|
+
app.should_receive(:say).with(VERSION)
|
33
|
+
app.version
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe App, '#maid_options' do
|
38
|
+
before :each do
|
39
|
+
@app = App.new
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should log to STDOUT for testing purposes when given noop' do
|
43
|
+
opts = @app.maid_options('noop' => true)
|
44
|
+
opts[:file_options][:noop].should be_true
|
45
|
+
opts[:log_path].should == STDOUT
|
46
|
+
opts[:log_formatter].call(nil, nil, nil, 'hello').should == "hello\n"
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should set the rules path when given rules' do
|
50
|
+
opts = @app.maid_options('rules' => 'maid_rules.rb')
|
51
|
+
opts[:rules_path].should match(/maid_rules.rb$/)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|