paparazzi 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -10,8 +10,8 @@ to previously existing unchanged files, allowing for frequent backups to be made
10
10
  directories (see 'How it works' below). Older snapshots maintain versions of files as they existed
11
11
  at the time that snapshot was made.
12
12
 
13
- Paparazzi automatically purges out old hourly, daily, weekly, and monthly snapshots. Yearly
14
- snapshots are not automatically purged and need to be removed manually when necessary.
13
+ Paparazzi automatically purges out old snapshots, and allows you to define how many of each snapshot
14
+ to keep in reserve.
15
15
 
16
16
  Installation
17
17
  ------------
@@ -30,11 +30,36 @@ Create a ruby script that you'll run hourly from a cron.
30
30
  settings = {
31
31
  :source => '/full/path/to/source/directory/', # note the trailing '/'
32
32
  :destination => '/mnt/external_drive/backup_folder',
33
- :rsync_flags => '-L --exclude lost+found' # see 'man rsync' for available options.
34
- } # Paparazzi sends '-aq --delete', plus whatever you add.
33
+ :rsync_flags => '-L --exclude lost+found'
34
+ }
35
35
 
36
36
  Paparazzi::Camera.trigger(settings)
37
37
 
38
+
39
+ Available Settings
40
+ ------------------
41
+
42
+ * `:source` : **required** The source folder to be backed up. Trailing '/' recommended. See rsync manpage
43
+ for explanation of trailing '/'
44
+ * `:destination` : **required** The destination folder for backups to be written to, preferably on a different
45
+ physical drive.
46
+ * `:reserves` : A hash of snapshot intervals and number of snapshots of each to keep before purging.
47
+ default: {:hourly => 24, :daily => 7, :weekly => 5, :monthly => 12, :yearly => 9999}
48
+ * `:rsync_flags` : additional flags to pass to rsync. Paparazzi uses -aq, --delete, & --link_dest, plus
49
+ whatever you add. The author suggests considering -L and --exclude.
50
+
51
+
52
+ Supported Operating Systems
53
+ ---------------------------
54
+
55
+ Paparazzi is developed and tested on Ubuntu Linux and should work fine on all other flavors. As of version 0.1.1, all
56
+ tests reportedly passed on Max OSX, and it is expected that Paparazzi will probably run very well in that environment
57
+ (although it is not actively tested, so run at your own risk).
58
+
59
+ It is highly unlikely that Paparazzi will run on an MS Windows based machine without a whole lot of TLC by the user,
60
+ if it runs at all. At a minimum, the user will need to get rsync installed on the machine, possibly through cygwin,
61
+ but this is not supported at all by the author and users are entirely on their own.
62
+
38
63
 
39
64
  How it works.
40
65
  -------------
@@ -1,19 +1,19 @@
1
1
  require 'admit_one'
2
2
  require 'yaml'
3
3
  require 'fileutils'
4
+ require 'digest/md5'
4
5
 
5
6
  module Paparazzi
6
7
  class Camera
7
- FREQUENCIES = [:hourly,:daily,:weekly,:monthly,:yearly]
8
8
  REQUIRED_SETTINGS = [:source,:destination]
9
9
 
10
10
  class << self
11
- attr_accessor :source, :destination, :rsync_flags
11
+ attr_accessor :source, :destination, :rsync_flags, :reserves
12
12
 
13
13
  def trigger(settings = {})
14
14
  validate_and_cache_settings(settings)
15
- AdmitOne::LockFile.new(:paparazzi) do
16
- initialize
15
+ AdmitOne::LockFile.new("paparazzi-#{Digest::MD5.hexdigest(destination)}") do
16
+ setup
17
17
  purge_old_snapshots
18
18
  make_snapshots
19
19
  end
@@ -25,7 +25,7 @@ module Paparazzi
25
25
  #######
26
26
 
27
27
  def validate_and_cache_settings(settings)
28
- [:source,:destination,:rsync_flags].each do |setting_name|
28
+ [:source,:destination,:rsync_flags,:reserves].each do |setting_name|
29
29
  if REQUIRED_SETTINGS.include?(setting_name) and settings[setting_name].nil?
30
30
  raise MissingSettingError, "#{setting_name} is required"
31
31
  else
@@ -40,18 +40,18 @@ module Paparazzi
40
40
  raise MissingFolderError, destination unless File.exist?(destination) and File.directory?(destination)
41
41
  end
42
42
 
43
- def initialize
43
+ def setup
44
44
  @previous_snapshot_name = {}
45
- FREQUENCIES.each do |frequency|
45
+ frequencies.each do |frequency|
46
46
  Dir.mkdir(destination(frequency)) unless File.exists?(destination(frequency)) && File.directory?(destination(frequency))
47
47
 
48
48
  full_path = Dir[destination(frequency)+'/*'].sort{|a,b| File.ctime(b) <=> File.ctime(a) }.first
49
49
  @previous_snapshot_name[frequency] = full_path ? File.basename(full_path) : ''
50
50
  end
51
51
 
52
- if @previous_snapshot_name[:hourly] != last_successful_hourly_snapshot and !last_successful_hourly_snapshot.nil? and File.exists?(destination(:hourly) + '/' + last_successful_hourly_snapshot)
53
- File.rename(previous_snapshot(:hourly), current_snapshot(:hourly))
54
- @previous_snapshot_name[:hourly] = last_successful_hourly_snapshot
52
+ if @previous_snapshot_name[frequencies.first] != last_successful_snapshot and !last_successful_snapshot.nil? and File.exists?(destination(frequencies.first) + '/' + last_successful_snapshot)
53
+ File.rename(previous_snapshot(frequencies.first), current_snapshot(frequencies.first))
54
+ @previous_snapshot_name[frequencies.first] = last_successful_snapshot
55
55
  end
56
56
  end
57
57
 
@@ -59,20 +59,20 @@ module Paparazzi
59
59
  frequency.nil? ? @destination : "#{@destination}/#{frequency}"
60
60
  end
61
61
 
62
- def last_successful_hourly_snapshot
63
- @last_successful_hourly_snapshot ||= File.exists?("#{destination}/.paparazzi.yml") ? YAML.load_file("#{destination}/.paparazzi.yml")[:last_successful_snapshot] : nil
62
+ def last_successful_snapshot
63
+ @last_successful_snapshot ||= File.exists?("#{destination}/.paparazzi.yml") ? YAML.load_file("#{destination}/.paparazzi.yml")[:last_successful_snapshot] : nil
64
64
  end
65
65
 
66
- def last_successful_hourly_snapshot=(string)
66
+ def last_successful_snapshot=(string)
67
67
  cached_data ||= File.exists?("#{destination}/.paparazzi.yml") ? YAML.load_file("#{destination}/.paparazzi.yml") : {}
68
68
  cached_data[:last_successful_snapshot] = string
69
69
  File.open("#{destination}/.paparazzi.yml", 'w') {|file| file.write(cached_data.to_yaml) }
70
70
  end
71
71
 
72
72
  def purge_old_snapshots
73
- keepers = {:hourly => 24, :daily => 7, :weekly => 5, :monthly => 12}
74
- keepers.keys.each do |frequency|
75
- while Dir[destination(frequency)+'/*'].size > keepers[frequency]-1
73
+ frequencies.each do |frequency|
74
+ #raise RuntimeError, frequency if reserves[frequency].nil?
75
+ while Dir[destination(frequency)+'/*'].size > (reserves[frequency]-1)
76
76
  full_path = Dir[destination(frequency)+'/*'].sort{|a,b| File.ctime(a) <=> File.ctime(b) }.first
77
77
  FileUtils.rm_rf(full_path)
78
78
  end
@@ -80,14 +80,14 @@ module Paparazzi
80
80
  end
81
81
 
82
82
  def make_snapshots
83
- FREQUENCIES.each do |frequency|
83
+ frequencies.each do |frequency|
84
84
  Dir.mkdir(current_snapshot(frequency)) unless File.exists?(current_snapshot(frequency))
85
- if frequency == :hourly and previous_snapshot_name(frequency) == ''
85
+ if frequency == frequencies.first and previous_snapshot_name(frequency) == ''
86
86
  system 'rsync', *(['-aq', '--delete'] + rsync_flags + [source, current_snapshot(frequency)])
87
- self.last_successful_hourly_snapshot = current_snapshot_name(:hourly)
87
+ self.last_successful_snapshot = current_snapshot_name(frequency)
88
88
  elsif previous_snapshot_name(frequency) != current_snapshot_name(frequency)
89
89
  system 'rsync', *(['-aq', '--delete', "--link-dest=#{link_destination(frequency)}"] + rsync_flags + [source, current_snapshot(frequency)])
90
- self.last_successful_hourly_snapshot = current_snapshot_name(:hourly)
90
+ self.last_successful_snapshot = current_snapshot_name(frequencies.first)
91
91
  end
92
92
  end
93
93
  end
@@ -116,7 +116,17 @@ module Paparazzi
116
116
  end
117
117
 
118
118
  def link_destination(frequency)
119
- frequency == :hourly ? "../#{previous_snapshot_name(:hourly)}" : "../../hourly/#{current_snapshot_name(:hourly)}"
119
+ frequency == frequencies.first ? "../#{previous_snapshot_name(frequencies.first)}" : "../../#{frequencies.first}/#{current_snapshot_name(frequencies.first)}"
120
+ end
121
+
122
+ def reserves
123
+ @reserves ||= {:hourly => 24, :daily => 7, :weekly => 5, :monthly => 12, :yearly => 9999}
124
+ end
125
+
126
+ def frequencies
127
+ reserves.keys.select{ |key| reserves[key] > 0 }.sort{ |a,b|
128
+ [:hourly,:daily,:weekly,:monthly,:yearly].find_index(a) <=> [:hourly,:daily,:weekly,:monthly,:yearly].find_index(b)
129
+ }
120
130
  end
121
131
 
122
132
  end
@@ -1,3 +1,3 @@
1
1
  module Paparazzi
2
- VERSION = '0.1.1'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -3,9 +3,10 @@ require 'test/unit'
3
3
  require 'fileutils'
4
4
 
5
5
  class CameraTest < Test::Unit::TestCase
6
+ FREQUENCIES = [:hourly,:daily,:weekly,:monthly,:yearly]
6
7
 
7
8
  def setup
8
- Paparazzi::Camera::FREQUENCIES.each do |frequency|
9
+ FREQUENCIES.each do |frequency|
9
10
  FileUtils.rm_rf("#{destination}/#{frequency}")
10
11
  end
11
12
  FileUtils.rm_rf("#{destination}/.paparazzi.yml")
@@ -31,18 +32,18 @@ class CameraTest < Test::Unit::TestCase
31
32
  end
32
33
 
33
34
  def test_should_create_frequency_folders_on_initialization
34
- Paparazzi::Camera::FREQUENCIES.each do |frequency|
35
+ FREQUENCIES.each do |frequency|
35
36
  assert(!File.exists?("#{destination}/#{frequency}"))
36
37
  end
37
38
  Paparazzi::Camera.trigger(default_test_settings)
38
- Paparazzi::Camera::FREQUENCIES.each do |frequency|
39
+ FREQUENCIES.each do |frequency|
39
40
  assert(File.exists?("#{destination}/#{frequency}"))
40
41
  end
41
42
  end
42
43
 
43
44
  def test_should_make_all_first_snapshots_of_source
44
45
  Paparazzi::Camera.trigger(default_test_settings)
45
- Paparazzi::Camera::FREQUENCIES.each do |frequency|
46
+ FREQUENCIES.each do |frequency|
46
47
  assert(Dir["#{destination}/#{frequency}/*"].size > 0,"#{destination}/#{frequency}/ is empty")
47
48
  file = File.open(%Q{#{Dir["#{destination}/#{frequency}/*"].first}/test.txt})
48
49
  assert_equal('This is a test, this is only a test.',file.gets)
@@ -54,20 +55,22 @@ class CameraTest < Test::Unit::TestCase
54
55
  assert(YAML.load_file("#{destination}/.paparazzi.yml")[:last_successful_snapshot].match(/\d{4}-\d{2}-\d{2}\.\d{2}/))
55
56
  end
56
57
 
57
- def test_purges_out_expired_snapshots
58
+ def test_should_purge_out_expired_snapshots
58
59
  Dir.mkdir("#{destination}/weekly")
59
- (1..5).each do |x|
60
+ Dir.mkdir("#{destination}/weekly/1")
61
+ sleep 1;
62
+ (2..5).each do |x|
60
63
  Dir.mkdir("#{destination}/weekly/#{x}")
61
- sleep 1;
62
64
  end
63
65
  assert_equal(5,Dir["#{destination}/weekly/*"].size)
64
66
  previous_folder_contents = Dir["#{destination}/weekly/*"]
65
67
  Paparazzi::Camera.trigger(default_test_settings)
66
68
  assert_equal(5,Dir["#{destination}/weekly/*"].size)
69
+ assert(!File.exists?("#{destination}/weekly/1"))
67
70
  assert_not_equal(previous_folder_contents,Dir["#{destination}/weekly/*"])
68
71
  end
69
72
 
70
- def test_gracefully_recovers_if_last_hourly_snapshot_ended_prematurely
73
+ def test_should_gracefully_recover_if_last_hourly_snapshot_ended_prematurely
71
74
  Paparazzi::Camera.instance_variable_set('@start_time',Time.now - 7200)
72
75
  Paparazzi::Camera.trigger(default_test_settings)
73
76
  successful_snapshot_name = Paparazzi::Camera.send(:current_snapshot_name,:hourly)
@@ -76,7 +79,7 @@ class CameraTest < Test::Unit::TestCase
76
79
  Paparazzi::Camera.trigger(default_test_settings)
77
80
  failed_snapshot_name = Paparazzi::Camera.send(:current_snapshot_name,:hourly)
78
81
 
79
- Paparazzi::Camera.send(:last_successful_hourly_snapshot=,successful_snapshot_name)
82
+ Paparazzi::Camera.send(:last_successful_snapshot=,successful_snapshot_name)
80
83
  assert_equal(2,Dir["#{destination}/hourly/*"].size)
81
84
  assert_equal(successful_snapshot_name,YAML.load_file("#{destination}/.paparazzi.yml")[:last_successful_snapshot])
82
85
 
@@ -88,16 +91,27 @@ class CameraTest < Test::Unit::TestCase
88
91
  assert(!Dir["#{destination}/hourly/*"].include?("#{destination}/hourly/#{failed_snapshot_name}"))
89
92
  end
90
93
 
91
- def test_correct_hard_links_are_created_to_multiple_snapshots_of_same_file
94
+ def test_should_create_hard_links_to_multiple_snapshots_of_same_file
92
95
  Paparazzi::Camera.trigger(default_test_settings)
93
- assert_equal(5,File.stat("#{destination}/hourly/#{Paparazzi::Camera.send(:current_snapshot_name,:hourly)}/test.txt").nlink)
96
+ inode = File.stat("#{destination}/hourly/#{Paparazzi::Camera.send(:current_snapshot_name,:hourly)}/test.txt").ino
97
+ FREQUENCIES.each do |frequency|
98
+ assert_equal(5,File.stat("#{destination}/#{frequency}/#{Paparazzi::Camera.send(:current_snapshot_name,frequency)}/test.txt").nlink)
99
+ assert_equal(inode,File.stat("#{destination}/#{frequency}/#{Paparazzi::Camera.send(:current_snapshot_name,frequency)}/test.txt").ino)
100
+ end
94
101
  end
95
102
 
96
- def test_excluded_files_are_not_backed_up
103
+ def test_should_not_backup_excluded_files
97
104
  Paparazzi::Camera.trigger(default_test_settings)
98
105
  assert(!File.exists?("#{destination}/hourly/#{Paparazzi::Camera.send(:current_snapshot_name,:hourly)}/test.exclude"))
99
106
  end
100
107
 
108
+ def test_should_not_make_un_requested_frequency_snapshots
109
+ my_settings = default_test_settings
110
+ my_settings[:reserves][:hourly] = 0
111
+ Paparazzi::Camera.trigger(my_settings)
112
+ assert(!File.exists?("#{destination}/hourly"))
113
+ end
114
+
101
115
  #######
102
116
  private
103
117
  #######
@@ -110,7 +124,9 @@ class CameraTest < Test::Unit::TestCase
110
124
  {
111
125
  :source => "#{File.expand_path('../../source', __FILE__)}/",
112
126
  :destination => destination,
127
+ :reserves => {:hourly => 24, :daily => 7, :weekly => 5, :monthly => 12, :yearly => 9999},
113
128
  :rsync_flags => '-L --exclude test.exclude'
129
+
114
130
  }
115
131
  end
116
132
  end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 1
8
- - 1
9
- version: 0.1.1
7
+ - 2
8
+ - 0
9
+ version: 0.2.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Jonathan S. Garvin
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-05-22 00:00:00 -06:00
17
+ date: 2011-05-23 00:00:00 -06:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency