backup_organizer 0.0.1

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.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.3
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ # - jruby-18mode # JRuby in 1.8 mode
7
+ # - jruby-19mode # JRuby in 1.9 mode
8
+ - rbx-18mode
9
+ script: bundle exec rspec spec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in backup_organizer.gemspec
4
+ gemspec
@@ -0,0 +1,8 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2, :cli => "--color --format doc" do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Thorben Schröder
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,73 @@
1
+ # BackupOrganizer [![Build Status](https://secure.travis-ci.org/walski/backup_organizer.png)](http://travis-ci.org/walski/backup_organizer)
2
+
3
+ This gem helps you to keep your backups organized and to let the density of kept files to fade out over time.
4
+
5
+ The rules of how you want your backups organized are described in a Pattern like this:
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'backup_organizer'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install backup_organizer
20
+
21
+ ## Usage
22
+
23
+ ### Scenario
24
+ * You are doing daily backups.
25
+ * The goal is to have a script that you can run daily and that ensures, that
26
+ * all the backups of the last 30 days are kept
27
+ * one file per month for the last year is kept
28
+ * one file per year is kept forever
29
+ * all other files are deleted
30
+
31
+ ### Solution
32
+
33
+ #### How it looks like
34
+
35
+ ```ruby
36
+ # Load ActiveSupport to make date wrangling a bit more convenient
37
+ require 'rubygems'
38
+ require 'active_support/core_ext'
39
+
40
+ # Define the pattern
41
+ BackupOrganizer.organize('/basepath/to/your/backups') do |files|
42
+ files.stored_in('daily').if {|file| file.age < 30.days}
43
+
44
+ files.stored_in('monthly').if do |file|
45
+ file.age < 1.year &&
46
+ file.most_recent_in_its_month?
47
+ end
48
+
49
+ files.stored_in('yearly').if {|file| file.most_recent_in_its_year?}
50
+ end
51
+ ````
52
+
53
+ #### What it does
54
+
55
+ Given that pattern you should drop all your backups in ``/basepath/to/your/backups/daily``. The organizer then does the following:
56
+
57
+ 1. Move all files in ``/basepath/to/your/backups/daily`` which
58
+ * are not created in the current month
59
+
60
+ to ``/basepath/to/your/backups/monthly``
61
+
62
+ 2. Move all files in ``/basepath/to/your/backups/monthly`` which
63
+ * are not created in the current year
64
+ * are among all the files created in the same month **not** most recent file
65
+
66
+ to ``/basepath/to/your/backups/yearly``
67
+
68
+ 3. **Delete** (as this is the last rule) all files in ``/basepath/to/your/backups/monthly`` which
69
+ * are among all the files created in the same year **not** the most recent file
70
+
71
+ #### What else?
72
+
73
+ If you run the script make sure at least the ``/basepath/to/your/backups`` directory exists, the BackupOrganizer will take care of getting everything else in place.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/backup_organizer/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Thorben Schröder"]
6
+ gem.email = ["stillepost@gmail.com"]
7
+ gem.description = %q{Organizes backup files in folders in patterns that can be defined by the user.}
8
+ gem.summary = %q{If backups should only be kept around for a certain time and be stored sparsely (weekly, monthly, yearly, whatever) after that backup_organizer can do that for you.}
9
+ gem.homepage = "http://github.com/walski/backup_organizer"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "backup_organizer"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = BackupOrganizer::VERSION
17
+
18
+ gem.add_development_dependency 'rspec', '>= 2'
19
+ gem.add_development_dependency 'guard-rspec'
20
+ gem.add_development_dependency 'ruby_gntp'
21
+ end
@@ -0,0 +1,22 @@
1
+ require "backup_organizer/version"
2
+ require "backup_organizer/file_age"
3
+ require "backup_organizer/extensions/file_extensions"
4
+ require "backup_organizer/error/base_error"
5
+ Dir[File.expand_path('../backup_organizer/error/**/*.rb', __FILE__)].each {|f| require f}
6
+ require "backup_organizer/file_utils"
7
+ require "backup_organizer/rule"
8
+ require "backup_organizer/pattern"
9
+ require "backup_organizer/configuration"
10
+ require "backup_organizer/setup"
11
+ require "backup_organizer/file_mover"
12
+
13
+ module BackupOrganizer
14
+ def self.organize(path)
15
+ pattern = Pattern.new do |files|
16
+ yield files
17
+ end
18
+ configuration = Configuration.new(:path => path, :pattern => pattern)
19
+ Setup.create_structure(configuration)
20
+ configuration.move_all_files
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ require "backup_organizer/file_mover"
2
+
3
+ module BackupOrganizer
4
+ class Configuration
5
+ attr_reader :path, :pattern
6
+ def initialize(options)
7
+ @path, @pattern = options[:path], options[:pattern]
8
+
9
+ raise Error::ConfigurationError.new('No path set') unless @path
10
+ raise Error::ConfigurationError.new('No pattern set') unless @path
11
+ end
12
+
13
+ def directories
14
+ pattern.directories.map {|directory| File.expand_path("./#{directory}", @path)}
15
+ end
16
+
17
+ def move_all_files
18
+ sources = directories
19
+ destinations = (directories << :delete)[1..-1]
20
+ sources.each_with_index do |source, i|
21
+ FileMover.move_files(source, @pattern.rules[i], destinations[i])
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ module BackupOrganizer
2
+ module Error
3
+ class BaseError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ require "backup_organizer/error/error_with_custom_message"
2
+
3
+ module BackupOrganizer
4
+ module Error
5
+ class ConfigurationError < ErrorWithCustomMessage
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ module BackupOrganizer
2
+ module Error
3
+ class ErrorWithCustomMessage < BaseError
4
+ attr_reader :message
5
+
6
+ def initialize(message)
7
+ @message = message
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module BackupOrganizer
2
+ module Error
3
+ class InvalidPathError < BaseError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ require "backup_organizer/error/error_with_custom_message"
2
+
3
+ module BackupOrganizer
4
+ module Error
5
+ class SetupError < ErrorWithCustomMessage
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,66 @@
1
+ require "backup_organizer/file_age"
2
+ require 'backup_organizer/file_utils'
3
+
4
+ module BackupOrganizer
5
+ module Extensions
6
+ module FileExtensions
7
+ def age
8
+ FileAge.new(BackupOrganizer::FileUtils.age_of(self))
9
+ end
10
+
11
+ def most_recent_in_its_hour?
12
+ mtime = BackupOrganizer::FileUtils.mtime(self)
13
+ hour = Time.local(mtime.year, mtime.month, mtime.day, mtime.hour)
14
+ one_hour = 60 * 60
15
+
16
+ most_recent_in?(hour...(hour + one_hour))
17
+ end
18
+
19
+ def most_recent_in_its_day?
20
+ mtime = BackupOrganizer::FileUtils.mtime(self)
21
+ midnight = Time.local(mtime.year, mtime.month, mtime.day)
22
+ one_day = 24 * 60 * 60
23
+
24
+ most_recent_in?(midnight...(midnight + one_day))
25
+ end
26
+
27
+ def most_recent_in_its_week?
28
+ mtime = BackupOrganizer::FileUtils.mtime(self)
29
+ midnight = Time.local(mtime.year, mtime.month, mtime.day)
30
+ one_day = 24 * 60 * 60
31
+ monday = midnight - ((midnight.wday - 1) * one_day)
32
+ monday = monday - (7 * one_day) if midnight.wday == 0
33
+
34
+ most_recent_in?(monday...(monday + (7 * one_day)))
35
+ end
36
+
37
+ def most_recent_in_its_month?
38
+ mtime = BackupOrganizer::FileUtils.mtime(self)
39
+ this_month = Time.local(mtime.year, mtime.month, 1)
40
+ mtime = this_month + 32.days
41
+ next_month = Time.local(mtime.year, mtime.month, 1)
42
+
43
+ most_recent_in?(this_month...next_month)
44
+ end
45
+
46
+ def most_recent_in_its_year?
47
+ mtime = BackupOrganizer::FileUtils.mtime(self)
48
+ this_year = Time.local(mtime.year, 1, 1)
49
+ next_year = Time.local(this_year.year + 1, 1, 1)
50
+
51
+ most_recent_in?(this_year...next_year)
52
+ end
53
+
54
+ protected
55
+ def most_recent_in?(period)
56
+ dir = File.dirname(self)
57
+
58
+ files_in_dir = Dir[File.expand_path('./*', dir)]
59
+ method = Range.instance_methods.include?(:cover?) ? :cover? : :include?
60
+ files_this_month = files_in_dir.select {|f| period.send(method, FileUtils.mtime(f))}
61
+
62
+ self == files_this_month.sort {|a,b| File.mtime(a) <=> File.mtime(b)}.last
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,31 @@
1
+ module BackupOrganizer
2
+ class BasicObject #:nodoc:
3
+ instance_methods.each { |m| undef_method m unless m =~ /^__|instance_eval/ }
4
+ end unless defined?(BasicObject)
5
+
6
+ class FileAge < BasicObject
7
+ include ::Comparable
8
+
9
+ def initialize(age)
10
+ @age = age
11
+ end
12
+
13
+ def <=>(b)
14
+ b = b.to_f if b.respond_to?(:to_f)
15
+ @age.<=>(b)
16
+ end
17
+
18
+ def respond_to?(method)
19
+ @age.respond_to?(method)
20
+ end
21
+
22
+ def is_a?(klass)
23
+ FileAge == klass || @age.is_a?(klass)
24
+ end
25
+ alias :kind_of? :is_a?
26
+
27
+ def method_missing(name, *attrs)
28
+ @age.send(name, *attrs)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ require 'backup_organizer/file_utils'
2
+
3
+ module BackupOrganizer
4
+ class FileMover
5
+ def self.move_files(source, rule, destination)
6
+ Dir[File.expand_path('./*', source)].each do |file|
7
+ move_file(file, rule, destination)
8
+ end
9
+ end
10
+
11
+ def self.move_file(file, rule, destination)
12
+ return if rule.applies_for?(file)
13
+ return FileUtils.rm_rf(file) if destination == :delete
14
+
15
+ FileUtils.mv(file, destination)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ module BackupOrganizer
2
+ class FileUtils
3
+ def self.mv(from, to)
4
+ ::FileUtils.mv(from, to)
5
+ end
6
+
7
+ def self.rm_rf(path)
8
+ ::FileUtils.rm_rf(path)
9
+ end
10
+
11
+ def self.touch(path)
12
+ ::FileUtils.touch(path)
13
+ end
14
+
15
+ def self.mkdir(path)
16
+ Dir.mkdir(path)
17
+ end
18
+
19
+ def self.age_of(path)
20
+ Time.now - mtime(path)
21
+ end
22
+
23
+ def self.mtime(path)
24
+ File.mtime(path)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ module BackupOrganizer
2
+ class Pattern
3
+ def initialize
4
+ yield self
5
+ end
6
+
7
+ def stored_in(directory)
8
+ rule = Rule.new(directory)
9
+ rules << rule
10
+ rule
11
+ end
12
+
13
+ def directories
14
+ rules.map &:directory
15
+ end
16
+
17
+ def rules
18
+ @rules ||= []
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ module BackupOrganizer
2
+ class Rule
3
+ def initialize(directory)
4
+ @directory = directory
5
+ end
6
+
7
+ def if(&rule)
8
+ @rule = rule
9
+ end
10
+
11
+ def applies_for?(file)
12
+ file.extend(Extensions::FileExtensions)
13
+ @rule.call(file)
14
+ end
15
+
16
+ def directory
17
+ @directory
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ require 'backup_organizer/file_utils'
2
+
3
+ module BackupOrganizer
4
+ class Setup
5
+ def self.create_structure(configuration)
6
+ @configuration = configuration
7
+ raise Error::InvalidPathError unless File.exist?(@configuration.path) && File.directory?(@configuration.path)
8
+
9
+ @configuration.directories.each do |necessary_directory|
10
+ next if File.directory?(necessary_directory)
11
+ raise Error::SetupError.new('`#{necessary_directory}` should be a directory but is a file.') if File.exist?(necessary_directory)
12
+ BackupOrganizer::FileUtils.mkdir(necessary_directory)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module BackupOrganizer
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+ require 'tempfile'
3
+
4
+ describe BackupOrganizer::Configuration do
5
+ it "returns the full path to all the directories of it's pattern" do
6
+ path = Tempdir.new
7
+ pattern = mock(BackupOrganizer::Pattern, :directories => %w{daily weekly yearly})
8
+ configuration = BackupOrganizer::Configuration.new(:path => path, :pattern => pattern)
9
+ configuration.directories.should eq pattern.directories.map {|d| File.expand_path("./#{d}", path)}
10
+ end
11
+
12
+ it "runs the file mover for all rules/destinations" do
13
+ path = Tempdir.new
14
+ rule1 = mock(BackupOrganizer::Rule, :path => 'daily')
15
+ rule2 = mock(BackupOrganizer::Rule, :path => 'weekly')
16
+ rule3 = mock(BackupOrganizer::Rule, :path => 'yearly')
17
+ pattern = mock(BackupOrganizer::Pattern, :directories => %w{daily weekly yearly}, :rules => [rule1, rule2, rule3])
18
+ configuration = BackupOrganizer::Configuration.new(:path => path, :pattern => pattern)
19
+
20
+ BackupOrganizer::FileMover.should_receive(:move_files).with(File.expand_path("./#{rule1.path}", path), rule1, File.expand_path("./#{rule2.path}", path)).ordered
21
+ BackupOrganizer::FileMover.should_receive(:move_files).with(File.expand_path("./#{rule2.path}", path), rule2, File.expand_path("./#{rule3.path}", path)).ordered
22
+ BackupOrganizer::FileMover.should_receive(:move_files).with(File.expand_path("./#{rule3.path}", path), rule3, :delete).ordered
23
+
24
+ configuration.move_all_files
25
+ end
26
+ end
@@ -0,0 +1,123 @@
1
+ require 'spec_helper'
2
+
3
+ def check_expectations(expectations, method)
4
+ dir = Tempdir.new
5
+ files = []
6
+
7
+ # Create all the files first
8
+ expectations.each do |date, expectation|
9
+ path = temp_file_at(date, dir).extend(BackupOrganizer::Extensions::FileExtensions)
10
+ files << [date, path, expectation]
11
+ end
12
+
13
+ # Check for the expectations
14
+ files.each do |date, path, expectation|
15
+ path.send(method).should be(expectation), "File with mtime of #{date} should return #{expectation} to #{method} but didn't"
16
+ end
17
+ end
18
+
19
+ describe BackupOrganizer::Extensions::FileExtensions do
20
+ it "can tell a file's age" do
21
+ file = '/tmp/some/path'
22
+ file.extend BackupOrganizer::Extensions::FileExtensions
23
+ BackupOrganizer::FileUtils.should_receive(:age_of).with(file).and_return(1.234)
24
+ age = file.age
25
+ age.kind_of?(BackupOrganizer::FileAge).should be true
26
+ age.should eq 1.234
27
+ end
28
+
29
+ it "can tell if a file is the most recent one in the same hour of it's last modification (mtime)" do
30
+ time = Time.now
31
+
32
+ first_of_this_hour = Time.local(time.year, time.month, time.day, time.hour)
33
+ last_of_last_hours = first_of_this_hour - 1
34
+ mid_of_this_hour = first_of_this_hour + 30.minutes
35
+ first_of_next_hour = first_of_this_hour + 1.hour
36
+
37
+ expectations = {
38
+ first_of_this_hour => false,
39
+ last_of_last_hours => true,
40
+ mid_of_this_hour => true,
41
+ first_of_next_hour => true
42
+ }
43
+
44
+ check_expectations(expectations, :most_recent_in_its_hour?)
45
+ end
46
+
47
+ it "can tell if a file is the most recent one in the same day of it's last modification (mtime)" do
48
+ time = Time.now
49
+
50
+ midnight = Time.local(time.year, time.month, time.day)
51
+
52
+ first_of_this_day = midnight
53
+ last_of_last_day = first_of_this_day - 1
54
+ mid_of_this_day = first_of_this_day + 12.hours
55
+ first_of_next_day = first_of_this_day + 24.hours
56
+
57
+ expectations = {
58
+ first_of_this_day => false,
59
+ last_of_last_day => true,
60
+ mid_of_this_day => true,
61
+ first_of_next_day => true
62
+ }
63
+
64
+ check_expectations(expectations, :most_recent_in_its_day?)
65
+ end
66
+
67
+ it "can tell if a file is the most recent one in the same week of it's last modification (mtime)" do
68
+ time = Time.now
69
+
70
+ midnight = Time.local(time.year, time.month, time.day)
71
+ first_of_this_week = midnight - (midnight.wday - 1).days
72
+ first_of_this_week -= 7.days if midnight.wday == 0
73
+ last_of_last_week = first_of_this_week - 1
74
+ mid_of_this_week = first_of_this_week + 3.days
75
+ first_of_next_week = first_of_this_week + 7.days
76
+
77
+ expectations = {
78
+ first_of_this_week => false,
79
+ last_of_last_week => true,
80
+ mid_of_this_week => true,
81
+ first_of_next_week => true
82
+ }
83
+
84
+ check_expectations(expectations, :most_recent_in_its_week?)
85
+ end
86
+
87
+ it "can tell if a file is the most recent one in the same month of it's last modification (mtime)" do
88
+ time = Time.now
89
+
90
+ first_of_this_month = Time.local(time.year, time.month, 1)
91
+ last_of_last_month = first_of_this_month - 1
92
+ mid_of_this_month = first_of_this_month + 15.days
93
+ next_month = (first_of_this_month + 32.days)
94
+ first_of_next_month = Time.local(next_month.year, next_month.month, 1)
95
+
96
+ expectations = {
97
+ first_of_this_month => false,
98
+ last_of_last_month => true,
99
+ mid_of_this_month => true,
100
+ first_of_next_month => true
101
+ }
102
+
103
+ check_expectations(expectations, :most_recent_in_its_month?)
104
+ end
105
+
106
+ it "can tell if a file is the most recent one in the same year of it's last modification (mtime)" do
107
+ time = Time.now
108
+
109
+ first_of_this_year = Time.local(time.year, 1, 1)
110
+ last_of_last_year = first_of_this_year - 1
111
+ mid_of_this_year = first_of_this_year + 180.days
112
+ first_of_next_year = Time.local(first_of_this_year.year + 1, 1, 1)
113
+
114
+ expectations = {
115
+ first_of_this_year => false,
116
+ last_of_last_year => true,
117
+ mid_of_this_year => true,
118
+ first_of_next_year => true
119
+ }
120
+
121
+ check_expectations(expectations, :most_recent_in_its_year?)
122
+ end
123
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe BackupOrganizer::FileAge do
4
+ it "can be compared with Time" do
5
+ age = BackupOrganizer::FileAge.new(Time.now - 2.days)
6
+ age.should < (Time.now - 1.day)
7
+ age.should_not > (Time.now - 1.day)
8
+ age.should > (Time.now - 3.days)
9
+ age.should_not < (Time.now - 3.days)
10
+ end
11
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe BackupOrganizer::FileMover do
4
+ describe "moving a single file to a destination" do
5
+ before do
6
+ @destination = Tempdir.new
7
+ @path = Tempfile.new('backup_organizer_file').path
8
+ @expected_path = File.expand_path("./#{File.basename(@path)}", @destination)
9
+ File.exist?(@path).should be true
10
+ File.exist?(@expected_path).should be false
11
+ end
12
+
13
+ it "is not carried out when the rule applies" do
14
+ rule = mock(BackupOrganizer::Rule, :applies_for? => true)
15
+ BackupOrganizer::FileMover.move_file(@path, rule, @destination)
16
+ File.exist?(@path).should be true
17
+ File.exist?(@expected_path).should be false
18
+ end
19
+
20
+ it "is carried out when the does not rule applies" do
21
+ rule = mock(BackupOrganizer::Rule, :applies_for? => false)
22
+ BackupOrganizer::FileMover.move_file(@path, rule, @destination)
23
+ File.exist?(@path).should be false
24
+ File.exist?(@expected_path).should be true
25
+ end
26
+
27
+ it "deletes the file if the destination is :delete" do
28
+ rule = mock(BackupOrganizer::Rule, :applies_for? => false)
29
+ BackupOrganizer::FileMover.move_file(@path, rule, :delete)
30
+ File.exist?(@path).should be false
31
+ File.exist?(@expected_path).should be false
32
+ end
33
+ end
34
+
35
+ it "moves all files in a directory that match a rule" do
36
+ directory = Tempdir.new
37
+ %w{some test-1 files with different-1 looks}.each {|f| BackupOrganizer::FileUtils.touch(File.expand_path("./#{f}", directory))}
38
+ rule = mock(BackupOrganizer::Rule)
39
+ def rule.applies_for?(file)
40
+ file !~ /-1$/
41
+ end
42
+ destination = Tempdir.new
43
+
44
+ BackupOrganizer::FileMover.move_files(directory, rule, destination)
45
+
46
+ File.exist?(File.expand_path("./some", directory) ).should be true
47
+ File.exist?(File.expand_path("./files", directory) ).should be true
48
+ File.exist?(File.expand_path("./with", directory) ).should be true
49
+ File.exist?(File.expand_path("./looks", directory) ).should be true
50
+ File.exist?(File.expand_path("./test-1", directory) ).should be false
51
+ File.exist?(File.expand_path("./different-1", directory)).should be false
52
+
53
+ File.exist?(File.expand_path("./some", destination) ).should be false
54
+ File.exist?(File.expand_path("./files", destination) ).should be false
55
+ File.exist?(File.expand_path("./with", destination) ).should be false
56
+ File.exist?(File.expand_path("./looks", destination) ).should be false
57
+ File.exist?(File.expand_path("./test-1", destination) ).should be true
58
+ File.exist?(File.expand_path("./different-1", destination)).should be true
59
+ end
60
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe BackupOrganizer::FileUtils do
4
+ it "knows the age of a file" do
5
+ path = temp_file_at(Time.now - 3.days)
6
+ BackupOrganizer::FileUtils.age_of(path).should be_within(1).of(3.days)
7
+ path = temp_file_at(Time.now - 99.days)
8
+ BackupOrganizer::FileUtils.age_of(path).should be_within(1).of(99.days)
9
+ path = temp_file_at(Time.now - 17.years)
10
+ BackupOrganizer::FileUtils.age_of(path).should be_within(1).of(17.years)
11
+ end
12
+ end
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+ require 'find'
3
+
4
+ def include_files(dir_name, actual, expected, expectation)
5
+ expected.each do |file|
6
+ basename = File.basename(file)
7
+ actual.include?(basename).should be(expectation), "#{dir_name} should#{expectation ? '' : ' not'} include #{basename} but does #{expectation ? 'not' : ''}."
8
+ end
9
+ end
10
+
11
+ describe "integration of the whole system" do
12
+ before do
13
+ @now = Time.local(Time.now.year, 12, 30)
14
+ Time.stub(:now).and_return(@now)
15
+
16
+ @base_path = Tempdir.new
17
+ @daily_destination = File.expand_path("./daily", @base_path)
18
+ @monthly_destination = File.expand_path("./monthly", @base_path)
19
+ @yearly_destination = File.expand_path("./yearly", @base_path)
20
+ BackupOrganizer::FileUtils.mkdir(@daily_destination)
21
+ BackupOrganizer::FileUtils.mkdir(@yearly_destination)
22
+
23
+ @file_01_day_ago = temp_file_at(Time.now - 1.day + 2, @daily_destination)
24
+ @file_04_days_ago = temp_file_at(Time.now - 10.days + 2, @yearly_destination)
25
+ @file_10_days_ago = temp_file_at(Time.now - 10.days + 2, @daily_destination)
26
+ @file_30_days_ago = temp_file_at(Time.now - 30.days + 2, @daily_destination)
27
+ @file_31_days_ago = temp_file_at(Time.now - 31.days + 2, @daily_destination)
28
+ @file_02_month_ago = temp_file_at(Time.now - 60.days + 2, @daily_destination)
29
+ @file_02_month_and_a_bit_ago = temp_file_at(Time.now - 65.days + 2, @daily_destination)
30
+ @file_06_month_ago = temp_file_at(Time.now - 180.days + 2, @daily_destination)
31
+ @file_07_month_ago = temp_file_at(Time.now - 240.days + 2, @yearly_destination)
32
+ @file_365_days_ago = temp_file_at(Time.now - 365.days + 2, @daily_destination)
33
+ @file_366_days_ago = temp_file_at(Time.now - 366.days + 2, @daily_destination)
34
+ time_366_days_ago = Time.now - 366.days
35
+ @file_a_year_and_a_bit_ago = temp_file_at(Time.local(time_366_days_ago.year, 1, 1) + 2, @daily_destination)
36
+ @file_three_years_ago = temp_file_at(Time.now - 3.years + 2, @yearly_destination)
37
+ @file_ten_years_ago = temp_file_at(Time.now - 10.years + 2, @yearly_destination)
38
+ end
39
+
40
+ after do
41
+ Time.unstub(:now)
42
+ BackupOrganizer::FileUtils.rm_rf @base_path
43
+ end
44
+
45
+ it "works fine" do
46
+ BackupOrganizer.organize(@base_path) do |files|
47
+ files.stored_in('daily').if {|file| file.age < 30.days}
48
+
49
+ files.stored_in('monthly').if do |file|
50
+ file.age < 1.year &&
51
+ file.most_recent_in_its_month?
52
+ end
53
+
54
+ files.stored_in('yearly').if {|file| file.most_recent_in_its_year?}
55
+ end
56
+
57
+ expected_in_daily = [
58
+ @file_01_day_ago,
59
+ @file_10_days_ago,
60
+ @file_30_days_ago
61
+ ]
62
+
63
+ expected_in_monthly = [
64
+ @file_31_days_ago,
65
+ @file_02_month_ago,
66
+ @file_06_month_ago,
67
+ @file_365_days_ago
68
+ ]
69
+
70
+ expected_in_yearly = [
71
+ @file_04_days_ago,
72
+ @file_366_days_ago,
73
+ @file_three_years_ago,
74
+ @file_ten_years_ago
75
+ ]
76
+
77
+ expected_deleted = [
78
+ @file_07_month_ago,
79
+ @file_a_year_and_a_bit_ago,
80
+ @file_02_month_and_a_bit_ago
81
+ ]
82
+
83
+ dailies = files_in(File.expand_path('./daily', @base_path))
84
+ monthlies = files_in(File.expand_path('./monthly', @base_path))
85
+ yearlies = files_in(File.expand_path('./yearly', @base_path))
86
+
87
+ include_files(:dailies, dailies, expected_in_daily, true)
88
+ include_files(:dailies, dailies, expected_in_monthly + expected_in_yearly + expected_deleted, false)
89
+
90
+ include_files(:monthlies, monthlies, expected_in_monthly, true)
91
+ include_files(:monthlies, monthlies, expected_in_daily + expected_in_yearly + expected_deleted, false)
92
+
93
+ include_files(:yearlies, yearlies, expected_in_yearly, true)
94
+ include_files(:yearlies, yearlies, expected_in_daily + expected_in_monthly + expected_deleted, false)
95
+ end
96
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe BackupOrganizer::Pattern do
4
+ subject {
5
+ BackupOrganizer::Pattern.new do |files|
6
+ files.stored_in('daily' ).if {|f| true}
7
+ files.stored_in('weekly').if {|f| true}
8
+ files.stored_in('yearly').if {|f| true}
9
+ end
10
+ }
11
+
12
+ it "can list all directories it needs" do
13
+ subject.directories.should eq %w{daily weekly yearly}
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe BackupOrganizer::Rule do
4
+ subject {BackupOrganizer::Rule.new('/tmp/dir')}
5
+
6
+ it "knows it's directory" do
7
+ subject.directory.should eq '/tmp/dir'
8
+ end
9
+
10
+ it "can tell if the rule apply to a certain file" do
11
+ subject.if {|f| f.applies}
12
+
13
+ file = mock(File, :applies => false)
14
+ subject.applies_for?(file).should be false
15
+
16
+ file = mock(File, :applies => true)
17
+ subject.applies_for?(file).should be true
18
+ end
19
+
20
+ it "mixes the FileExtensions in to each yielded path" do
21
+ subject.if{|f| (class << f; self; end).included_modules.include?(BackupOrganizer::Extensions::FileExtensions).should be true}
22
+ file = mock(File, :applies => true)
23
+ subject.applies_for?(file)
24
+ end
25
+ end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+ require 'tempfile'
3
+
4
+ describe BackupOrganizer::Setup do
5
+ before do
6
+ path = Tempdir.new
7
+ @configuration = mock(BackupOrganizer::Configuration, :path => path, :directories => %w{daily weekly yearly}.map {|e| File.expand_path("./#{e}", path)})
8
+ end
9
+
10
+ describe "creating the structure" do
11
+ describe "raises an error when" do
12
+ it "the path is not a directory" do
13
+ file = Tempfile.new('backup_organizer_fake_dir')
14
+ path = file.path
15
+ @configuration.stub(:path).and_return(path)
16
+ file.unlink
17
+ lambda {
18
+ BackupOrganizer::Setup.create_structure(@configuration)
19
+ }.should raise_error(BackupOrganizer::Error::InvalidPathError)
20
+ end
21
+
22
+ it "the path is a file" do
23
+ file = Tempfile.new('backup_organizer_fake_dir')
24
+ path = file.path
25
+ @configuration.stub(:path).and_return(path)
26
+ lambda {
27
+ BackupOrganizer::Setup.create_structure(@configuration)
28
+ }.should raise_error(BackupOrganizer::Error::InvalidPathError)
29
+ file.unlink
30
+ end
31
+ end
32
+
33
+ it "results in having all the needed directories present" do
34
+ BackupOrganizer::Setup.create_structure(@configuration)
35
+ @configuration.directories.each do |necessary_directory|
36
+ File.directory?(necessary_directory).should be true
37
+ end
38
+ end
39
+
40
+ it "leaves an existing structure intact" do
41
+ BackupOrganizer::Setup.create_structure(@configuration)
42
+ files = %w{some test files}
43
+ require 'backup_organizer/file_utils'
44
+ @configuration.directories.each do |directory|
45
+ files.each do |file|
46
+ BackupOrganizer::FileUtils.touch(File.expand_path("./#{file}", directory))
47
+ end
48
+ end
49
+
50
+ BackupOrganizer::Setup.create_structure(@configuration)
51
+
52
+ @configuration.directories.each do |directory|
53
+ files.each do |file|
54
+ File.exist?(File.expand_path("./#{file}", directory)).should be true
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,7 @@
1
+ Bundler.require
2
+
3
+ require 'backup_organizer'
4
+ Dir[File.expand_path('../support/**/*.rb', __FILE__)].each {|f| require f}
5
+
6
+ RSpec.configure do |config|
7
+ end
@@ -0,0 +1,43 @@
1
+ def temp_file_at(time, destination = nil)
2
+ path = Tempfile.new('backup_organizer_integration_spec').path
3
+ File.utime(time, time, path)
4
+ if destination
5
+ BackupOrganizer::FileUtils.mv path, destination
6
+ File.expand_path("./#{File.basename(path)}", destination)
7
+ else
8
+ path
9
+ end
10
+ end
11
+
12
+ def files_in(path)
13
+ path = "#{File.expand_path('./', path)}/"
14
+ files = []
15
+ Find.find(path) {|file| files << file.gsub(/^#{Regexp.escape(path)}/, '')}
16
+ files.select {|e| !e.empty?}
17
+ end
18
+
19
+ class Fixnum
20
+ def minutes
21
+ self * 60
22
+ end
23
+ alias minute minutes
24
+
25
+ def hours
26
+ self * 60.minutes
27
+ end
28
+ alias hour hours
29
+
30
+ def days
31
+ self * 24.hours
32
+ end
33
+ alias day days
34
+
35
+ def years
36
+ self * 365.days
37
+ end
38
+ alias year years
39
+
40
+ def ago
41
+ Time.now - self
42
+ end
43
+ end
@@ -0,0 +1,12 @@
1
+ require 'tempfile'
2
+ require 'backup_organizer/file_utils'
3
+
4
+ class Tempdir
5
+ def self.new
6
+ file = Tempfile.new("backup_spec_temp_dir")
7
+ path = file.path
8
+ file.unlink
9
+ BackupOrganizer::FileUtils.mkdir(path)
10
+ path
11
+ end
12
+ end
metadata ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: backup_organizer
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - "Thorben Schr\xC3\xB6der"
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-01-24 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 7
29
+ segments:
30
+ - 2
31
+ version: "2"
32
+ type: :development
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: guard-rspec
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: ruby_gntp
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :development
61
+ version_requirements: *id003
62
+ description: Organizes backup files in folders in patterns that can be defined by the user.
63
+ email:
64
+ - stillepost@gmail.com
65
+ executables: []
66
+
67
+ extensions: []
68
+
69
+ extra_rdoc_files: []
70
+
71
+ files:
72
+ - .gitignore
73
+ - .rvmrc
74
+ - .travis.yml
75
+ - Gemfile
76
+ - Guardfile
77
+ - LICENSE
78
+ - README.md
79
+ - Rakefile
80
+ - backup_organizer.gemspec
81
+ - lib/backup_organizer.rb
82
+ - lib/backup_organizer/configuration.rb
83
+ - lib/backup_organizer/error/base_error.rb
84
+ - lib/backup_organizer/error/configuration_error.rb
85
+ - lib/backup_organizer/error/error_with_custom_message.rb
86
+ - lib/backup_organizer/error/invalid_path_error.rb
87
+ - lib/backup_organizer/error/setup_error.rb
88
+ - lib/backup_organizer/extensions/file_extensions.rb
89
+ - lib/backup_organizer/file_age.rb
90
+ - lib/backup_organizer/file_mover.rb
91
+ - lib/backup_organizer/file_utils.rb
92
+ - lib/backup_organizer/pattern.rb
93
+ - lib/backup_organizer/rule.rb
94
+ - lib/backup_organizer/setup.rb
95
+ - lib/backup_organizer/version.rb
96
+ - spec/configuration_spec.rb
97
+ - spec/extensions/file_extensions_spec.rb
98
+ - spec/file_age_spec.rb
99
+ - spec/file_mover_spec.rb
100
+ - spec/file_utils_spec.rb
101
+ - spec/integration_spec.rb
102
+ - spec/pattern_spec.rb
103
+ - spec/rule_spec.rb
104
+ - spec/setup_spec.rb
105
+ - spec/spec_helper.rb
106
+ - spec/support/helpers.rb
107
+ - spec/support/temp_dir.rb
108
+ homepage: http://github.com/walski/backup_organizer
109
+ licenses: []
110
+
111
+ post_install_message:
112
+ rdoc_options: []
113
+
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ hash: 3
122
+ segments:
123
+ - 0
124
+ version: "0"
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ hash: 3
131
+ segments:
132
+ - 0
133
+ version: "0"
134
+ requirements: []
135
+
136
+ rubyforge_project:
137
+ rubygems_version: 1.8.10
138
+ signing_key:
139
+ specification_version: 3
140
+ summary: If backups should only be kept around for a certain time and be stored sparsely (weekly, monthly, yearly, whatever) after that backup_organizer can do that for you.
141
+ test_files:
142
+ - spec/configuration_spec.rb
143
+ - spec/extensions/file_extensions_spec.rb
144
+ - spec/file_age_spec.rb
145
+ - spec/file_mover_spec.rb
146
+ - spec/file_utils_spec.rb
147
+ - spec/integration_spec.rb
148
+ - spec/pattern_spec.rb
149
+ - spec/rule_spec.rb
150
+ - spec/setup_spec.rb
151
+ - spec/spec_helper.rb
152
+ - spec/support/helpers.rb
153
+ - spec/support/temp_dir.rb