maid 0.1.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|