lsync 1.2.5 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ LSync
2
+ =====
3
+
4
+ * Author: Samuel G. D. Williams (<http://www.oriontransfer.co.nz>)
5
+ * Copyright (C) 2009, 2011 Samuel G. D. Williams.
6
+ * Released under the MIT license.
7
+
8
+ LSync is a tool for scripted synchronization and backups. It provides a custom Ruby
9
+ DSL for describing complex backup and synchronization tasks. It is designed to give
10
+ maximum flexibility while reducing the complexity of describing complex multi-server
11
+ setups.
12
+
13
+ * Single and multi-server data synchronization.
14
+ * Incremental backups both locally and remotely.
15
+ * Backup staging and coordination.
16
+ * Backup verification using `Fingerprint`.
17
+ * Data backup redundancy controlled via DNS.
18
+
19
+ For examples please see the main [project page][1].
20
+
21
+ [1]: http://www.oriontransfer.co.nz/gems/lsync
22
+
23
+ Overview
24
+ --------
25
+
26
+ LSync imposes a particular structure regarding the organisation of backup scripts:
27
+ Backup scripts involve a set of servers and directories. A server is a logical unit
28
+ where files are available or stored. Directories are specific places within servers.
29
+
30
+ License
31
+ -------
32
+
33
+ Copyright (c) 2009, 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
34
+
35
+ Permission is hereby granted, free of charge, to any person obtaining a copy
36
+ of this software and associated documentation files (the "Software"), to deal
37
+ in the Software without restriction, including without limitation the rights
38
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
39
+ copies of the Software, and to permit persons to whom the Software is
40
+ furnished to do so, subject to the following conditions:
41
+
42
+ The above copyright notice and this permission notice shall be included in
43
+ all copies or substantial portions of the Software.
44
+
45
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
46
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
47
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
48
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
49
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
50
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
51
+ THE SOFTWARE.
data/lib/lsync.rb CHANGED
@@ -15,21 +15,13 @@
15
15
 
16
16
  require 'rubygems'
17
17
 
18
- gem 'termios'
19
- gem 'net-ssh'
20
- gem 'ruleby'
21
-
22
18
  require 'yaml'
23
19
  require 'socket'
24
20
  require 'set'
25
21
  require 'logger'
26
22
 
27
23
  require 'lsync/version'
28
- require 'lsync/extensions'
29
-
30
24
  require 'lsync/script'
31
- require 'lsync/plan'
32
-
33
25
  require 'lsync/tee_logger'
34
26
 
35
27
  require 'fileutils'
@@ -38,19 +30,4 @@ require 'optparse'
38
30
  require 'open-uri'
39
31
 
40
32
  module LSync
41
- class InvalidConfigurationPath < StandardError
42
- end
43
-
44
- def self.load_from_file(path)
45
- path = Pathname.new(path)
46
-
47
- case path.extname
48
- when ".lsync-script"
49
- return Script.load_from_file(path)
50
- when ".lsync-plan"
51
- return Plan.load_from_file(path)
52
- else
53
- raise InvalidConfigurationPath.new(path)
54
- end
55
- end
56
33
  end
data/lib/lsync/action.rb CHANGED
@@ -4,97 +4,102 @@ require 'lsync/run'
4
4
  require 'lsync/error'
5
5
 
6
6
  module LSync
7
-
8
- class AbortBackupException < Exception
9
-
10
- end
11
-
12
- class Action
13
- def initialize(function)
14
- @function = function
15
-
16
- if @function.match(/^\%([a-z]+)(\s+.*)?$/)
17
- @script_name = $1
18
- @arguments = $2
19
- else
20
- @script_name = nil
21
- end
22
- end
23
-
24
- def to_s
25
- @function
26
- end
27
-
28
- def run_on_server(server, logger)
29
- logger.info "Running #{@function} on #{server}"
30
-
31
- if server.is_local?
32
- run_locally(server, logger)
33
- else
34
- run_remotely(server, logger)
35
- end
36
- end
37
-
38
- private
39
- def run_locally(server, logger)
40
- command = nil
41
-
42
- if @script_name
43
- uname = `uname`.chomp.downcase
44
-
45
- local_path = Action.script_path(uname, @script_name)
46
- command = local_path.to_cmd + @arguments
47
- else
48
- command = @function
49
- end
50
-
51
- ret = nil
52
- Dir.chdir(server.root_path) do
53
- ret = LSync.run_command(command, logger)
54
- end
55
-
56
- case(ret)
57
- when 0
58
- return
59
- when 1
60
- raise AbortBackupException
61
- else
62
- raise BackupActionError
63
- end
64
- end
65
-
66
- def run_remotely(server, logger)
67
- conn = server.connect
68
- conn.send_object([:set_working_dir, server.root_path])
69
-
70
- if @script_name
71
- uname = `uname`.chomp.downcase
72
-
73
- local_path = Action.script_path(uname, @script_name)
74
-
75
- logger.info("Sending run_script #{@script_name}...")
76
- conn.send_object([:run_script, @script_name, Pathname.new(local_path).read, @arguments])
77
- else
78
- logger.info("Sending run_command #{@function}...")
79
- conn.send_object([:run_command, @function])
80
- end
81
-
82
- conn.run do |message|
83
- break if message == :done
84
-
85
- logger.send(*message)
86
- end
87
- end
88
-
89
- def self.script_path(platform, name)
90
- exact_script_path(platform, name) || exact_script_path("generic", name)
91
- end
92
-
93
- private
94
- def self.exact_script_path(platform, name)
95
- path = (Pathname.new(__FILE__).dirname + "actions" + platform + name).expand_path
96
- path.exist? ? path : nil
97
- end
98
- end
7
+
8
+ class AbortBackupException < StandardError
9
+ end
10
+
11
+ # A runnable action, such as a shell command or action script.
12
+ #
13
+ # If the first argument is a symbol, then this will map to one of the standard
14
+ # actions in the `lsync/actions` subdirectory. These actions are sometimes implemented
15
+ # on a per-platform basis.
16
+ class Action
17
+ def initialize(function)
18
+ @function = function
19
+
20
+ case @function[0]
21
+ when Symbol
22
+ @script_name = @function[0].to_s
23
+ @arguments = @function[1,@function.size]
24
+ else
25
+ @script_name = nil
26
+ end
27
+ end
28
+
29
+ # Return a string representation of the action for logging.
30
+ def to_s
31
+ @function.to_cmd
32
+ end
33
+
34
+ # Run the action on the given server, typically in the root directory specified.
35
+ def run_on_server(server, logger)
36
+ # logger.info "Running #{@function.to_cmd} on #{server}"
37
+
38
+ if server.is_local?
39
+ run_locally(server, logger)
40
+ else
41
+ run_remotely(server, logger)
42
+ end
43
+ end
44
+
45
+ private
46
+ # Run the script locally by invoking it directly.
47
+ def run_locally(server, logger)
48
+ command = nil
49
+
50
+ if @script_name
51
+ uname = `uname`.chomp.downcase
52
+
53
+ local_path = Action.script_path(uname, @script_name)
54
+ command = [local_path] + @arguments
55
+ else
56
+ command = @function
57
+ end
58
+
59
+ result = nil
60
+ Dir.chdir(server.root) do
61
+ result = LSync.run_command(command, logger)
62
+ end
63
+
64
+ if result != 0
65
+ raise ShellScriptError.new(command, result)
66
+ end
67
+ end
68
+
69
+ # Run the script remotely by sending the data across the network and executing it.
70
+ def run_remotely(server, logger)
71
+ conn = server.connect
72
+ conn.send_object([:set_working_dir, server.root])
73
+
74
+ if @script_name
75
+ uname = `uname`.chomp.downcase
76
+
77
+ local_path = Action.script_path(uname, @script_name)
78
+
79
+ logger.info("Sending run_script #{@script_name}...")
80
+ conn.send_object([:run_script, @script_name, Pathname.new(local_path).read, @arguments])
81
+ else
82
+ logger.info("Sending run_command #{@function}...")
83
+ conn.send_object([:run_command, @function])
84
+ end
85
+
86
+ conn.run do |message|
87
+ break if message == :done
88
+
89
+ logger.send(*message)
90
+ end
91
+ end
92
+
93
+ # Figure out the path of the script, which may depend on the given platform.
94
+ def self.script_path(platform, name)
95
+ exact_script_path(platform, name) || exact_script_path("generic", name)
96
+ end
97
+
98
+ # Return the exact path of a builtin action script.
99
+ def self.exact_script_path(platform, name)
100
+ path = (Pathname.new(__FILE__).dirname + "actions" + platform + name).expand_path
101
+ path.exist? ? path : nil
102
+ end
103
+ end
99
104
 
100
105
  end
@@ -5,15 +5,15 @@ action = ARGV[0]
5
5
  disk_name = ARGV[1]
6
6
 
7
7
  def get_disk_id(name)
8
- begin
9
- `diskutil list`.match(/#{name}\s*\*?[0-9]+\.[0-9]+ .B\s+(disk[0-9]s[0-9])$/)[1]
8
+ begin
9
+ `diskutil list`.match(/#{name}\s*\*?[0-9]+\.[0-9]+ .B\s+(disk[0-9]s[0-9])$/)[1]
10
10
  rescue
11
- exit 5
12
- end
11
+ exit 5
12
+ end
13
13
  end
14
14
 
15
15
  if (action == 'mountpoint')
16
16
  puts File.join('', 'Volumes', disk_name, ARGV[2..-1])
17
17
  else
18
- system DISKUTIL, action, get_disk_id(disk_name)
18
+ system DISKUTIL, action, get_disk_id(disk_name)
19
19
  end
@@ -26,6 +26,23 @@ class Rotation
26
26
  def <=> other
27
27
  return other.time <=> @time
28
28
  end
29
+
30
+ def eql? other
31
+ case other
32
+ when Rotation
33
+ @path.eql?(other.path) # && @time.eql?(other.time)
34
+ else
35
+ @path.eql?(other.to_s)
36
+ end
37
+ end
38
+
39
+ def hash
40
+ @path.hash
41
+ end
42
+
43
+ def to_s
44
+ @path
45
+ end
29
46
  end
30
47
 
31
48
  class Period
@@ -160,7 +177,7 @@ ARGV.options do |o|
160
177
  o.on("--dry-run", "Print out what would be deleted, but don't actually delete anything.") do
161
178
  OPTIONS[:Wet] = false
162
179
  end
163
-
180
+
164
181
  o.on("-l latest", String, "Specify the symlink name that points to the latest backup, so it won't be deleted.")
165
182
 
166
183
  o.separator ""
@@ -225,7 +242,7 @@ end.parse!
225
242
 
226
243
  backups = []
227
244
 
228
- Dir.chdir(OPTIONS[:Destination]) do
245
+ Dir.chdir(OPTIONS[:Destination]) do
229
246
  Dir["*"].each do |path|
230
247
  next if path.match(OPTIONS[:Latest])
231
248
  date_string = File.basename(path)
@@ -240,12 +257,17 @@ Dir.chdir(OPTIONS[:Destination]) do
240
257
  keep, erase = OPTIONS[:Policy].filter(backups)
241
258
 
242
259
  # We need to retain the latest backup regardless of policy
243
- if OPTIONS[:Latest]
244
- latest_backup = Pathname.new(OPTIONS[:Latest]).realpath.basename.to_s
245
- puts "Retaining latest backup #{latest_backup}"
246
- erase.delete(latest_backup)
260
+ if OPTIONS[:Latest] && File.exist?(OPTIONS[:Latest])
261
+ path = Pathname.new(OPTIONS[:Latest]).realpath.basename.to_s
262
+ latest_rotation = erase.find { |rotation| rotation.path == path }
263
+
264
+ if latest_rotation
265
+ puts "Retaining latest backup #{latest_rotation}"
266
+ erase.delete(latest_rotation)
267
+ keep << latest_rotation
268
+ end
247
269
  end
248
-
270
+
249
271
  if OPTIONS[:Wet]
250
272
  erase.sort.each do |backup|
251
273
  puts "Erasing #{backup.path}..."
@@ -9,66 +9,78 @@ require 'fileutils'
9
9
  require 'optparse'
10
10
 
11
11
  OPTIONS = {
12
- :Format => "%Y.%m.%d-%H.%M.%S",
13
- :Latest => "latest",
14
- :UseGMT => true,
15
- :Destination => nil
12
+ :Format => "%Y.%m.%d-%H.%M.%S",
13
+ :Latest => "latest",
14
+ :UseGMT => true,
15
+ :Destination => nil
16
16
  }
17
17
 
18
18
  ARGV.options do |o|
19
- script_name = File.basename($0)
20
-
21
- o.set_summary_indent(' ')
22
- o.banner = "Usage: #{script_name} [options] [directory]"
23
- o.define_head "This script is used to rotate directories."
24
-
25
- o.separator ""
26
- o.separator "Help and Copyright information"
27
-
28
- o.on("-f format", String, "Set the format of the rotated directory names. See Time$strftime") do |format|
29
- OPTIONS[:Format] = format
30
- end
31
-
32
- o.on("-l latest", String, "Set the name for the latest rotation symlink.") do |latest|
33
- OPTIONS[:Latest] = latest
34
- end
35
-
36
- o.on("-d destination", String, "Set the directory to move rotated backups.") do |destination|
37
- OPTIONS[:Destination] = destination
38
- end
39
-
40
- o.on_tail("--copy", "Display copyright information") do
41
- puts "#{script_name}. Copyright (c) 2008-2009 Samuel Williams. Released under the GPLv3."
42
- puts "See http://www.oriontransfer.co.nz/ for more information."
43
-
44
- exit
45
- end
46
-
47
- o.on_tail("-h", "--help", "Show this help message.") do
48
- puts o
49
- exit
50
- end
19
+ script_name = File.basename($0)
20
+
21
+ o.set_summary_indent(' ')
22
+ o.banner = "Usage: #{script_name} [options] [directory]"
23
+ o.define_head "This script is used to rotate directories."
24
+
25
+ o.separator ""
26
+ o.separator "Help and Copyright information"
27
+
28
+ o.on("-f format", String, "Set the format of the rotated directory names. See Time$strftime") do |format|
29
+ OPTIONS[:Format] = format
30
+ end
31
+
32
+ o.on("-l latest", String, "Set the name for the latest rotation symlink.") do |latest|
33
+ OPTIONS[:Latest] = latest
34
+ end
35
+
36
+ o.on("-d destination", String, "Set the directory to move rotated backups.") do |destination|
37
+ OPTIONS[:Destination] = destination
38
+ end
39
+
40
+ o.on_tail("--copy", "Display copyright information") do
41
+ puts "#{script_name}. Copyright (c) 2008-2009 Samuel Williams. Released under the GPLv3."
42
+ puts "See http://www.oriontransfer.co.nz/ for more information."
43
+
44
+ exit
45
+ end
46
+
47
+ o.on_tail("-h", "--help", "Show this help message.") do
48
+ puts o
49
+ exit
50
+ end
51
51
  end.parse!
52
52
 
53
53
  time = Time.now
54
54
 
55
55
  if OPTIONS[:UseUTC]
56
- time = time.utc
56
+ time = time.utc
57
57
  end
58
58
 
59
- dir = Pathname.new(ARGV[0] || ".inprogress")
59
+ dir = Pathname.new(ARGV[0] || "backup.inprogress")
60
60
  rotation_dir = Pathname.new(OPTIONS[:Destination] ? OPTIONS[:Destination] : dir.dirname)
61
61
  rotated_name = time.strftime(OPTIONS[:Format])
62
62
  rotated_dir = rotation_dir + rotated_name
63
63
  latest_link = rotation_dir + OPTIONS[:Latest]
64
64
 
65
+ unless File.exist? dir
66
+ $stderr.puts "Can not find source directory to rotate: #{dir} in #{Dir.getwd}!"
67
+ exit(10)
68
+ end
69
+
70
+ if File.exist? rotated_dir
71
+ $stderr.puts "Destination rotation name #{rotated_dir} already exists in #{Dir.getwd}!"
72
+ exit(20)
73
+ end
74
+
75
+ puts "Rotating #{dir} to #{rotated_dir}..."
76
+
65
77
  # Move rotated dir
66
78
  FileUtils.mv(dir, rotated_dir)
67
79
 
68
80
  # Recreate latest symlink
69
81
  if File.symlink?(latest_link)
70
- puts "Removing old latest link..."
71
- FileUtils.rm(latest_link)
82
+ puts "Removing old latest link..."
83
+ FileUtils.rm(latest_link)
72
84
  end
73
85
 
74
86
  puts "Creating latest symlink to #{rotated_name}"