maid 0.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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