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/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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'maid'
4
+
5
+ Maid::App.start
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
@@ -0,0 +1,6 @@
1
+ class Maid::Rule < Struct.new(:description, :instructions)
2
+ # Follow the instructions of the rule.
3
+ def follow
4
+ instructions.call
5
+ end
6
+ end
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
@@ -0,0 +1,3 @@
1
+ module Maid
2
+ VERSION = "0.1.0.beta1"
3
+ end
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