dreamback 0.0.4 → 0.0.5

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.
@@ -39,20 +39,24 @@ module Dreamback
39
39
  end
40
40
  end
41
41
 
42
- # Get the newest folder for linking
42
+ # Get yesterday's folder to link against
43
43
  backup_to_link = backup_folders.first[0]
44
+ # If this would link us to the same folder, don't do that. Try yesterday's instead.
45
+ if backup_to_link.eql? "dreamback." + Date.today.strftime("%Y%m%d")
46
+ backup_to_link = "dreamback." + (Date.today - 1).strftime("%Y%m%d")
47
+ end
44
48
 
45
49
  # Delete any folders older than our limit
46
- # Subtract one to account for the folder we're about to create
47
- # Normally we remove a folder so that our count is one less than the "days to keep"
48
- # However, if today's folder already exists then there's no need
49
- offset = backup_folders.include?("dreamback.#{Time.now.strftime("%Y%m%d")}") ? 0 : 1
50
- if backup_folders.length >= Dreamback.settings[:days_to_keep] - offset
51
- folders_to_delete = backup_folders.slice(Dreamback.settings[:days_to_keep] - offset, backup_folders.length)
52
- folders_to_delete.map! {|f| f[0]}
53
- rsync_delete(folders_to_delete)
50
+ if Dreamback.settings[:days_to_keep]
51
+ folders_to_delete = rotate_daily(backup_folders)
52
+ elsif Dreamback.settings[:keep_time_machine]
53
+ folders_to_delete = rotate_time_machine(backup_folders)[:delete]
54
+ else
55
+ folders_to_delete = nil
54
56
  end
57
+ rsync_delete(folders_to_delete)
55
58
  end
59
+
56
60
  backup_to_link
57
61
  end
58
62
 
@@ -60,6 +64,7 @@ module Dreamback
60
64
  # We do this because sftp has no recursive delete method
61
65
  # @params [Array[String]] list of backup directories
62
66
  def self.rsync_delete(directories)
67
+ return if directories.nil? || directories.empty?
63
68
  empty_dir_path = File.expand_path("../.dreamback_empty_dir", __FILE__)
64
69
  empty_dir = Dir.mkdir(empty_dir_path) unless File.exists?(empty_dir_path)
65
70
  begin
@@ -78,6 +83,8 @@ module Dreamback
78
83
  # @param [String] name of the most recent backup folder prior to starting this run to link against
79
84
  def self.rsync_backup(link_dir)
80
85
  tmp_dir_path = "~/.dreamback_tmp"
86
+ tmp_dir = File.expand_path(tmp_dir_path)
87
+ Dir.mkdir(tmp_dir) unless File.exists?(tmp_dir)
81
88
  user_exclusions_path = File.expand_path("~/.dreamback_exclusions")
82
89
  default_exclusions_path = File.expand_path("../exclusions.txt", __FILE__)
83
90
  exclusions_path = File.exists?(user_exclusions_path) ? user_exclusions_path : default_exclusions_path
@@ -90,17 +97,84 @@ module Dreamback
90
97
  user = dreamhost[:user]
91
98
  server = dreamhost[:server]
92
99
  # rsync won't do remote<->remote syncing, so we stage everything here first
93
- tmp_dir = File.expand_path(tmp_dir_path)
94
- Dir.mkdir(tmp_dir) unless File.exists?(tmp_dir)
95
100
  `rsync -e ssh -av --keep-dirlinks --exclude-from #{exclusions_path} --copy-links #{user}@#{server}:~/ #{tmp_dir}/#{user}@#{server}`
96
- # Now we can sync local to remote. Only use link-dest if a previous folder to link to exists.
97
- link_dest = link_dir.nil? ? "" : "--link-dest=~#{BACKUP_FOLDER.gsub(".", "")}/#{link_dir}"
98
- `rsync -e ssh -av --delete --copy-links --keep-dirlinks #{link_dest} #{tmp_dir}/ #{backup_server_user}@#{backup_server}:#{BACKUP_FOLDER}/dreamback.#{today}`
99
101
  end
102
+ # Now we can sync local to remote. Only use link-dest if a previous folder to link to exists.
103
+ link_dest = link_dir.nil? ? "" : "--link-dest=~#{BACKUP_FOLDER.gsub(".", "")}/#{link_dir}"
104
+ `rsync -e ssh -av --delete --copy-links --keep-dirlinks #{link_dest} #{tmp_dir}/ #{backup_server_user}@#{backup_server}:#{BACKUP_FOLDER}/dreamback.#{today}`
100
105
  ensure
101
106
  # Remove the staging directory
102
107
  `rm -rf #{File.expand_path(tmp_dir_path)}`
103
108
  end
104
109
  end
110
+
111
+ # Rotate folders based on the number of days to keep
112
+ # @param [Array] folders to rotate in Dreamback format (dreamback.20120521)
113
+ # @return [Array] list of folders to delete
114
+ def self.rotate_daily(backup_folders)
115
+ # Subtract one to account for the folder we're about to create
116
+ # Normally we remove a folder so that our count is one less than the "days to keep"
117
+ # However, if today's folder already exists then there's no need
118
+ offset = backup_folders.include?("dreamback.#{Time.now.strftime("%Y%m%d")}") ? 0 : 1
119
+ if backup_folders.length >= Dreamback.settings[:days_to_keep] - offset
120
+ folders_to_delete = backup_folders.slice(Dreamback.settings[:days_to_keep] - offset, backup_folders.length)
121
+ folders_to_delete.map! {|f| f[0]}
122
+ end
123
+ folders_to_delete ||= nil
124
+ end
125
+
126
+ # Use a time machine-like algorithm to rotate backups. This will keep daily backups for seven days, weeklies for three months, and monthlies forever
127
+ # @param [Date] the day to count as "today", where the rotation will start
128
+ # @param [Array] folders to rotate in Dreamback format (dreamback.20120521)
129
+ # @return [Hash] :keep => list of folders to retain, :delete => folders marked for deletion
130
+ def self.rotate_time_machine(folders, today = nil)
131
+ today ||= Date.today
132
+ ymd = "%Y%m%d"
133
+
134
+ folder_data = {}
135
+ folders.each do |f|
136
+ folder_data[f] = {
137
+ :date => Date.strptime(f[1].to_s, ymd)
138
+ }
139
+ end
140
+
141
+ keep = []
142
+ one_week_ago = today - 7
143
+ three_months_ago = today << 3
144
+
145
+ week_hash = {}
146
+ month_hash = {}
147
+ folder_data.each do |k, v|
148
+ if v[:date] >= one_week_ago
149
+ keep << k
150
+ elsif v[:date] >= three_months_ago
151
+ week_hash[v[:date].cweek] = [] if week_hash[v[:date].cweek].nil?
152
+ week_hash[v[:date].cweek] << [k, v]
153
+ else
154
+ ym = v[:date].strftime("%Y%m").to_i
155
+ month_hash[ym] = [] if month_hash[ym].nil?
156
+ month_hash[ym] << [k, v]
157
+ end
158
+ end
159
+
160
+ week_hash.each do |week, dates|
161
+ dates.sort! {|a, b| a[1][:date] <=> b[1][:date]}
162
+ end
163
+
164
+ week_hash.each do |week, dates|
165
+ keep << dates.shift[0] unless dates.empty?
166
+ end
167
+
168
+ month_hash.each do |month, dates|
169
+ dates.sort! {|a, b| a[1][:date] <=> b[1][:date]}
170
+ end
171
+
172
+ month_hash.each do |month, dates|
173
+ keep << dates.shift[0] unless dates.empty?
174
+ end
175
+
176
+ keep.sort!.compact!
177
+ { :keep => keep, :delete => folders - keep}
178
+ end
105
179
  end
106
180
  end
@@ -10,4 +10,5 @@ lib
10
10
  logs
11
11
  mail
12
12
  Maildir
13
- tmp
13
+ tmp
14
+ src
@@ -1,6 +1,7 @@
1
1
  require 'json'
2
2
  require 'net/sftp'
3
3
  require 'cronedit'
4
+ require 'highline/import'
4
5
 
5
6
  module Dreamback
6
7
  @settings
@@ -35,7 +36,20 @@ module Dreamback
35
36
  save_settings(SETTINGS_LOCATION)
36
37
  else
37
38
  if direct
38
- say(bold("You have already setup Dreamback. Please run \"dreamback backup\" to start a backup."))
39
+ say(bold("You have already setup Dreamback."))
40
+
41
+ choose("From here you can: \n") do |menu|
42
+ menu.prompt = "Select an option: "
43
+
44
+ menu.choice("Run setup again") {
45
+ create_new_settings
46
+ save_settings(SETTINGS_LOCATION)
47
+ }
48
+
49
+ menu.choice("Run a backup right now") {
50
+ Dreamback::Backup.start
51
+ }
52
+ end
39
53
  end
40
54
  end
41
55
 
@@ -125,6 +139,7 @@ module Dreamback
125
139
  def self.create_new_settings
126
140
  settings = {}
127
141
  say("#{bold("Server Where We Should Store Your Backup")}\nYour dreamhost backup-specific account will work best, but any POSIX server with rsync should work\n<%= color('Note:', BOLD)%> dreamhost does not allow you to store non-webhosted data except your BACKUP-SPECIFIC account")
142
+ say("You can configure your Dreamhost backup user here: https://panel.dreamhost.com/index.cgi?tree=users.backup")
128
143
  settings[:backup_server] = ask("Server name: ")
129
144
  settings[:backup_server_user] = ask(bold("Username for the backup server: "))
130
145
  settings[:dreamhost_users] = []
@@ -136,7 +151,25 @@ module Dreamback
136
151
  settings[:dreamhost_users] << dreamhost
137
152
  another_user = agree(bold("Add another dreamhost account? [y/n]"))
138
153
  end
139
- settings[:days_to_keep] = ask(bold("How many days of backups do you want to keep [1-30]? "), Integer) {|q| q.in = 1..30}
154
+
155
+ choose("What method should we use to keep backups? \n") do |menu|
156
+ menu.prompt = "Select an option "
157
+
158
+ menu.choice("Number of days") {
159
+ settings[:days_to_keep] = ask(bold("How many days of backups do you want to keep [1-9999]? "), Integer) {|q| q.in = 1..9999}
160
+ say("#{bold("We will keep #{settings[:days_to_keep]} days of backups")}")
161
+ }
162
+
163
+ menu.choice("Time machine-inspired\n (daily backups for seven days, weekly backups for three months, monthly backups forever)") {
164
+ settings[:keep_time_machine] = true
165
+ say("#{bold("We will keep backups based on a time machine-inspired method")}")
166
+ }
167
+
168
+ menu.choice("Keep everything") {
169
+ say("#{bold("We will keep everything. This may take a lot of space.")}")
170
+ }
171
+ end
172
+
140
173
  @settings = settings
141
174
  end
142
175
 
@@ -1,3 +1,3 @@
1
1
  module Dreamback
2
- VERSION = "0.0.4"
2
+ VERSION = "0.0.5"
3
3
  end
@@ -1,7 +1,8 @@
1
1
  require "rubygems"
2
+ require "json"
2
3
  require "test/unit"
3
4
  require File.expand_path('../../lib/dreamback/initializer', __FILE__)
4
- require "json"
5
+ require File.expand_path('../../lib/dreamback/backup', __FILE__)
5
6
 
6
7
  # Used for testing private methods
7
8
  class Class
@@ -55,4 +56,76 @@ class DreambackTest < Test::Unit::TestCase
55
56
  assert_equal settings_file, JSON.parse(settings_test, :symbolize_names => true)
56
57
  end
57
58
 
59
+ def test_time_machine_rotate
60
+ folders = [
61
+ "dreamback.20120514",
62
+ "dreamback.20120513",
63
+ "dreamback.20120512",
64
+ "dreamback.20120511",
65
+ "dreamback.20120510",
66
+ "dreamback.20120509",
67
+ "dreamback.20120508",
68
+ "dreamback.20120501",
69
+ "dreamback.20120424",
70
+ "dreamback.20120413",
71
+ "dreamback.20120417",
72
+ "dreamback.20120411",
73
+ "dreamback.20120410",
74
+ "dreamback.20120403",
75
+ "dreamback.20120327",
76
+ "dreamback.20120325",
77
+ "dreamback.20120320",
78
+ "dreamback.20120321",
79
+ "dreamback.20120313",
80
+ "dreamback.20120312",
81
+ "dreamback.20120309",
82
+ "dreamback.20120308",
83
+ "dreamback.20120301",
84
+ "dreamback.20120227",
85
+ "dreamback.20120215",
86
+ "dreamback.20120207",
87
+ "dreamback.20120208"
88
+ ]
89
+
90
+ folders_sorted = {
91
+ :keep=>
92
+ [
93
+ "dreamback.20120207",
94
+ "dreamback.20120215",
95
+ "dreamback.20120227",
96
+ "dreamback.20120308",
97
+ "dreamback.20120312",
98
+ "dreamback.20120320",
99
+ "dreamback.20120327",
100
+ "dreamback.20120403",
101
+ "dreamback.20120410",
102
+ "dreamback.20120417",
103
+ "dreamback.20120424",
104
+ "dreamback.20120501",
105
+ "dreamback.20120508",
106
+ "dreamback.20120509",
107
+ "dreamback.20120510",
108
+ "dreamback.20120511",
109
+ "dreamback.20120512",
110
+ "dreamback.20120513",
111
+ "dreamback.20120514"
112
+ ],
113
+ :delete=>
114
+ [
115
+ "dreamback.20120413",
116
+ "dreamback.20120411",
117
+ "dreamback.20120325",
118
+ "dreamback.20120321",
119
+ "dreamback.20120313",
120
+ "dreamback.20120309",
121
+ "dreamback.20120301",
122
+ "dreamback.20120208"
123
+ ]
124
+ }
125
+
126
+ today = Date.strptime("20120514", "%Y%m%d")
127
+
128
+ assert folders_sorted == Dreamback::Backup.rotate_time_machine(today, folders)
129
+ end
130
+
58
131
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dreamback
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
4
+ hash: 21
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 4
10
- version: 0.0.4
9
+ - 5
10
+ version: 0.0.5
11
11
  platform: ruby
12
12
  authors:
13
13
  - Paul R Alexander
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-05-30 00:00:00 -07:00
18
+ date: 2012-06-25 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency