morhekil-prisync 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/README +23 -0
  2. data/bin/prisync +62 -0
  3. data/lib/inotify.rb +211 -0
  4. data/lib/prisync.rb +99 -0
  5. data/prisync.gemspec +26 -0
  6. metadata +57 -0
data/README ADDED
@@ -0,0 +1,23 @@
1
+ prisync is a pseudo-realtime file synchronization daemon. It uses rsync utility in background
2
+ to transfer changes and inotify kernel events to monitor changes - thus it can be run only
3
+ on Linux with inotify-capable kernel. Can be used to keep two local folders or (usually)
4
+ to keep files on two servers in sync.
5
+
6
+ Usage: ssync [options]
7
+ -p, --local-path PATH Local path to synchronize and monitor for changes,
8
+ defaults to current directory if omitted
9
+ -r, --target-path PATH Target (remote) path to sync with. If you want to sync with
10
+ a remote host it should be given as user@host:/remote/path/
11
+ --ssh-port [PORT] SSH port to use if you want to sync with remote host via SSH tunnel,
12
+ you HAVE to specify remote host in the target-path option
13
+ -t, --timeout [SECONDS] Time to wait for any new changes to arrive before
14
+ starting the actual synchronization, use to not trigger
15
+ multiple synchronizations for repetitive event sources like
16
+ file uploading, default value is 5 seconds
17
+ -x, --exclude [REGEXP] Exclusion mask, should be specified as a valid Regexp. Items
18
+ matching this regexp will be excluded from monitoring and will not
19
+ trigger synchronization but will still be synced by rsync
20
+ -s, --sync-on-start Triggers initial synchronization on start
21
+ -v, --verbose Run verbosely with debug output
22
+ --dry Only print the rsync command without actually executing it
23
+ -h, --help Show help
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require File.dirname(__FILE__) + '/../lib/prisync'
5
+
6
+ # get a list of files in the specified path
7
+
8
+ options = { :local_path => Dir::pwd }
9
+
10
+ OptionParser.new do |opts|
11
+ opts.banner = "Usage: prisync [options]"
12
+
13
+ opts.on('-p', '--local-path PATH', 'Local path to synchronize and monitor for changes,',
14
+ 'defaults to current directory if omitted') do |path|
15
+ options[:local_path] = path
16
+ end
17
+
18
+ opts.on('-r', '--target-path PATH', 'Target (remote) path to sync with. If you want to sync with',
19
+ 'a remote host it should be given as user@host:/remote/path/') do |path|
20
+ options[:remote_path] = path
21
+ end
22
+
23
+ opts.on('--ssh-port [PORT]', Integer, 'SSH port to use if you want to sync with remote host via SSH tunnel,',
24
+ 'you HAVE to specify remote host in the target-path option') do |port|
25
+ options[:ssh_port] = port
26
+ end
27
+
28
+ opts.on('-t', '--timeout [SECONDS]', Integer, 'Time to wait for any new changes to arrive before',
29
+ 'starting the actual synchronization, use to not trigger',
30
+ 'multiple synchronizations for repetitive event sources like',
31
+ 'file uploading, default value is '+Prisync::DEFAULT_TIMEOUT.to_s+' seconds') do |secs|
32
+ options[:timeout] = secs
33
+ end
34
+
35
+ opts.on('-x', '--exclude [REGEXP]', Regexp, 'Exclusion mask, should be specified as a valid Regexp. Items',
36
+ 'matching this regexp will be excluded from monitoring and will not',
37
+ 'trigger synchronization but will still be synced by rsync') do |exclude|
38
+ options[:exclude] = exclude
39
+ end
40
+
41
+ opts.on('-s', '--sync-on-start', 'Triggers initial synchronization on start') do
42
+ options[:sync_on_start] = true
43
+ end
44
+
45
+ opts.on('-v', '--verbose', 'Run verbosely with debug output') do
46
+ options[:debug] = true
47
+ end
48
+
49
+ opts.on('--dry', 'Only print the rsync command without actually executing it') do
50
+ options[:dryrun] = true
51
+ end
52
+
53
+ opts.on_tail('-h', '--help', 'Show help') do
54
+ puts opts
55
+ exit
56
+ end
57
+
58
+ end.parse!
59
+
60
+ sync = Prisync.new
61
+ sync.start(options)
62
+
@@ -0,0 +1,211 @@
1
+ # Version 0.3.0 (2005-09-27) by James Le Cuirot <chewi@ffaura.com>
2
+ # masks updated
3
+ # syscalls instead of /dev/inotify for linux-2.6.13 (are the archs correct?)
4
+ # start/stop methods added for threading
5
+ # ignore_dir_recursively method added
6
+ # Events class removed : not necessary
7
+ # (wd <=> dir) hashed both ways : needed for ignore
8
+ # default watch mask is IN_ALL_EVENTS
9
+ # UnsupportedPlatformError class added to deal with unsupported CPUs and OSes
10
+ #
11
+ # Version 0.2.3 (2005-01-18) by oxman
12
+ # function ignore_dir : was added
13
+ #
14
+ # Version 0.2.2 (2005-01-18) by oxman
15
+ # cleaning code (big thanks to gnome at #ruby-lang)
16
+ # rename next_event in each_event (thanks kig)
17
+ #
18
+ # Version 0.2.1 (2005-01-18) by oxman
19
+ # class Events : use real mask
20
+ #
21
+ # Version 0.2.0 (2005-01-18) by oxman
22
+ # function watch_dir : only watch
23
+ # function next_event : was added
24
+ # function watch_dir_recursively : was added
25
+ #
26
+ # Version 0.1.1 (2005-01-17) by oxman
27
+ # Correct IN_ var for inotify 0.18
28
+
29
+ module INotify
30
+ require 'rbconfig'
31
+ require 'io/nonblock'
32
+
33
+ class UnsupportedPlatformError < RuntimeError
34
+ end
35
+
36
+ case Config::CONFIG["arch"]
37
+
38
+ when /i[3-6]86-linux/
39
+ INOTIFY_INIT = 291
40
+ INOTIFY_ADD_WATCH = 292
41
+ INOTIFY_RM_WATCH = 293
42
+
43
+ when /x86_64-linux/
44
+ INOTIFY_INIT = 253
45
+ INOTIFY_ADD_WATCH = 254
46
+ INOTIFY_RM_WATCH = 255
47
+
48
+ when /powerpc(64)?-linux/
49
+ INOTIFY_INIT = 275
50
+ INOTIFY_ADD_WATCH = 276
51
+ INOTIFY_RM_WATCH = 277
52
+
53
+ when /ia64-linux/
54
+ INOTIFY_INIT = 1277
55
+ INOTIFY_ADD_WATCH = 1278
56
+ INOTIFY_RM_WATCH = 1279
57
+
58
+ when /s390-linux/
59
+ INOTIFY_INIT = 284
60
+ INOTIFY_ADD_WATCH = 285
61
+ INOTIFY_RM_WATCH = 286
62
+
63
+ when /alpha-linux/
64
+ INOTIFY_INIT = 444
65
+ INOTIFY_ADD_WATCH = 445
66
+ INOTIFY_RM_WATCH = 446
67
+
68
+ when /sparc(64)?-linux/
69
+ INOTIFY_INIT = 151
70
+ INOTIFY_ADD_WATCH = 152
71
+ INOTIFY_RM_WATCH = 156
72
+
73
+ when /arm-linux/
74
+ INOTIFY_INIT = 316
75
+ INOTIFY_ADD_WATCH = 317
76
+ INOTIFY_RM_WATCH = 318
77
+
78
+ when /sh-linux/
79
+ INOTIFY_INIT = 290
80
+ INOTIFY_ADD_WATCH = 291
81
+ INOTIFY_RM_WATCH = 292
82
+
83
+ else raise UnsupportedPlatformError, Config::CONFIG["arch"]
84
+
85
+ end
86
+
87
+ Mask = Struct::new(:value, :name)
88
+
89
+ Masks = {
90
+ :IN_ACCESS => Mask::new(0x00000001, 'access'),
91
+ :IN_MODIFY => Mask::new(0x00000002, 'modify'),
92
+ :IN_ATTRIB => Mask::new(0x00000004, 'attrib'),
93
+ :IN_CLOSE_WRITE => Mask::new(0x00000008, 'close_write'),
94
+ :IN_CLOSE_NOWRITE => Mask::new(0x00000010, 'close_nowrite'),
95
+ :IN_OPEN => Mask::new(0x00000020, 'open'),
96
+ :IN_MOVED_FROM => Mask::new(0x00000040, 'moved_from'),
97
+ :IN_MOVED_TO => Mask::new(0x00000080, 'moved_to'),
98
+ :IN_CREATE => Mask::new(0x00000100, 'create'),
99
+ :IN_DELETE => Mask::new(0x00000200, 'delete'),
100
+ :IN_DELETE_SELF => Mask::new(0x00000400, 'delete_self'),
101
+ :IN_UNMOUNT => Mask::new(0x00002000, 'unmount'),
102
+ :IN_Q_OVERFLOW => Mask::new(0x00004000, 'q_overflow'),
103
+ :IN_IGNORED => Mask::new(0x00008000, 'ignored'),
104
+ }
105
+
106
+ Masks.each {|key, value|
107
+ const_set(key, value)
108
+ }
109
+
110
+ OrMasks = {
111
+ :IN_CLOSE => Mask::new(IN_CLOSE_WRITE.value | IN_CLOSE_NOWRITE.value, 'close'),
112
+ :IN_MOVE => Mask::new(IN_MOVED_FROM.value | IN_MOVED_TO.value, 'moved'),
113
+ :IN_ALL_EVENTS => Mask::new(IN_ACCESS.value | IN_MODIFY.value | IN_ATTRIB.value | IN_CLOSE_WRITE.value | IN_CLOSE_NOWRITE.value | IN_OPEN.value | IN_MOVED_FROM.value | IN_MOVED_TO.value | IN_DELETE.value | IN_CREATE.value | IN_DELETE_SELF.value, 'all_events')
114
+ }
115
+
116
+ OrMasks.each {|key, value|
117
+ const_set(key, value)
118
+ }
119
+
120
+ AllMasks = Masks.merge OrMasks
121
+
122
+ require 'find'
123
+
124
+ class INotify
125
+ def initialize
126
+ @wd_dir = Hash.new
127
+ @dir_wd = Hash.new
128
+ @io = IO.open(syscall(INOTIFY_INIT))
129
+ end
130
+
131
+ def close
132
+ @io.close
133
+ end
134
+
135
+ def watch_dir (dir, option = IN_ALL_EVENTS)
136
+ wd = syscall(INOTIFY_ADD_WATCH, @io.fileno, dir, option.value)
137
+
138
+ if wd >= 0
139
+ @dir_wd[dir] = wd
140
+ @wd_dir[wd] = dir
141
+ end
142
+
143
+ return wd
144
+ end
145
+
146
+ def ignore_dir (dir)
147
+ syscall(INOTIFY_RM_WATCH, @io.fileno, @dir_wd[dir])
148
+ end
149
+
150
+ def watch_dir_recursively (dir, option = IN_ALL_EVENTS)
151
+ Find.find(dir) { |sub_dir| watch_dir(sub_dir, option) if (File::directory?(sub_dir) == true) }
152
+ end
153
+
154
+ def ignore_dir_recursively (dir)
155
+ Find.find(dir) { |sub_dir| ignore_dir(sub_dir) if (File::directory?(sub_dir) == true) }
156
+ end
157
+
158
+ def next_events( is_nonblocking = false )
159
+
160
+ events = Array.new
161
+
162
+ begin
163
+ @io.nonblock = is_nonblocking
164
+ begin
165
+ read_cnt = @io.read(16)
166
+ wd, mask, cookie, len = read_cnt.unpack('lLLL')
167
+ read_cnt = @io.read(len)
168
+ filename = read_cnt.unpack('Z*')
169
+ rescue Errno::EAGAIN
170
+ return events
171
+ ensure
172
+ @io.nonblock = false
173
+ end
174
+ end while (mask & IN_Q_OVERFLOW.value) != 0
175
+
176
+ AllMasks.each_value do |m|
177
+ next if m.value == IN_ALL_EVENTS.value
178
+ events.push Event.new(@wd_dir[wd].to_s, filename.to_s, m.name.to_s, cookie) if (m.value & mask) != 0
179
+ end
180
+
181
+ return events
182
+ end
183
+
184
+ def each_event
185
+ loop { next_events.each { |event| yield event } }
186
+ end
187
+
188
+ def start
189
+ @thread = Thread.new { loop { next_events.each { |event| yield event } } }
190
+ end
191
+
192
+ def stop
193
+ @thread.exit
194
+ end
195
+ end
196
+
197
+ class Event
198
+ attr_reader :path, :filename, :type, :cookie
199
+
200
+ def initialize (path, filename, type, cookie)
201
+ @path = path
202
+ @filename = filename
203
+ @type = type
204
+ @cookie = cookie
205
+ end
206
+
207
+ def dump
208
+ "path: " + @path.to_s + ", filename: " + @filename.to_s + ", type: " + @type.to_s + ", cookie: " + @cookie.to_s
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,99 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'inotify.rb'
4
+ require 'pp'
5
+
6
+ class Prisync
7
+
8
+ DEFAULT_TIMEOUT = 5
9
+
10
+ # Initializes INotify events listener object
11
+ def initialize
12
+ @inotify = INotify::INotify.new
13
+ @inotify_mask = INotify::Mask.new(INotify::IN_MODIFY.value | INotify::IN_DELETE.value | INotify::IN_CREATE.value | INotify::IN_MOVED_TO.value, 'filechange')
14
+ end
15
+
16
+ # Starts the synchronization process, options are
17
+ # :local_path - local path to watch for changes,
18
+ # :target_path - target (remote) path to sync with,
19
+ # :ssh_port - SSH port to use if syncing remotely over the ssh tunnel
20
+ # :timeout - seconds to wait for any new changes to arrive before starting the actual synchronization
21
+ # :exclude - regexp for excluding files, if omitted or nil - no excluding,
22
+ # :sync_on_start - trigger synchronization on start if set,
23
+ # :dryrun - run rsync with --dry-run option to not perform any actual changes if set,
24
+ # :debug - print out extra debug information if set
25
+ def start(opts)
26
+
27
+ # init runtime parameters
28
+ @exclude = opts[:exclude].kind_of?(Regexp) ? opts[:exclude] : nil
29
+ @pending_changes = opts[:sync_on_start] ? true : false
30
+ @timeout = opts[:timeout] || DEFAULT_TIMEOUT
31
+ @is_debug = opts[:debug]
32
+ @is_dryrun = opts[:dryrun]
33
+ # build the rsync command
34
+ @rsync_cmd = 'rsync -azcC --force --delete '
35
+ @rsync_cmd += '--dry-run ' if opts[:dryrun]
36
+ @rsync_cmd += '--progress ' if @is_debug
37
+ @rsync_cmd += "-e \"ssh -p#{opts[:ssh_port]}\" " if opts[:ssh_port]
38
+ @rsync_cmd += "#{opts[:local_path]} #{opts[:remote_path]}"
39
+
40
+ # Start inotify watcher
41
+ @inotify.watch_dir_recursively(opts[:local_path], @inotify_mask)
42
+ # Catch INT and TERM signals to perform a clean up after ourselves
43
+ %w{INT TERM}.each do |signal_name|
44
+ Signal.trap(signal_name, proc { self.cleanup })
45
+ end
46
+
47
+ print "Started synchronization with the following options:\n#{opts.pretty_inspect}\n\n" if @is_debug
48
+
49
+ while (true) do
50
+ begin
51
+ self.monitor_changes
52
+ rescue
53
+ print "Exception: ",$!, "\n"
54
+ end
55
+ end
56
+
57
+ end
58
+
59
+ # Monitors for changes and initiates syncing in case syncable changes were detected
60
+ def monitor_changes
61
+ # Check to see if we've got any new changes
62
+ new_changes = false
63
+ while ( (events = @inotify.next_events(true)).size > 0 ) do
64
+ print "Detected #{events.size} new events\n" if @is_debug
65
+ new_changes = true if events.find { |event| @exclude.nil? || event.filename !~ @exclude }
66
+ print "Has synchronizable changes\n" if new_changes && @is_debug
67
+ end
68
+
69
+ # Check if we've got any pending changes those need to be synced and timeout has passed indicating that we're ready to go
70
+ if (@pending_changes && !new_changes) then
71
+ print "Synchronizing...\n" if @is_debug
72
+ self.synchronize
73
+ @pending_changes = false
74
+ end
75
+
76
+ # Update pending status and wait for a timeout
77
+ @pending_changes = true if new_changes
78
+ print "Pending changes\n" if @pending_changes && @is_debug
79
+ sleep @timeout
80
+
81
+ end
82
+
83
+ # Actually synchronizes the files by calling rsync using the pre-built command line
84
+ def synchronize
85
+ print "#{@rsync_cmd}\n" if @is_debug
86
+ system @rsync_cmd
87
+ errorvalue = $?
88
+ errorvalue = errorvalue.exitstatus if errorvalue.kind_of?(Process::Status)
89
+ $stderr.print "rsync error status #{errorvalue}\n" if @is_debug
90
+ self.cleanup if errorvalue == 20 || errorvalue == 255
91
+ end
92
+
93
+ # Cleans up before exit, closing inotify connection
94
+ def cleanup
95
+ print "Cleaning up...\n" if @is_debug
96
+ @inotify.close
97
+ exit 0
98
+ end
99
+ end
@@ -0,0 +1,26 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'prisync'
3
+ s.version = "0.2.1"
4
+ s.date = "2008-10-01"
5
+ s.summary = "Pseudo-realtime file synchronization daemon."
6
+ s.description = %{Prisync allows to synchronize to local or remote directories in a pseudo-realtime fashion.
7
+ Filesystem changes are monitored with inotify kernel events, rsync utility is used for the actual
8
+ synchronization and can be run over SSH.%}
9
+ s.has_rdoc = false
10
+
11
+ s.files = ['lib/inotify.rb',
12
+ 'lib/prisync.rb',
13
+ 'bin/prisync',
14
+ 'prisync.gemspec',
15
+ 'README'
16
+ ]
17
+
18
+ s.bindir = 'bin'
19
+ s.executables = ['prisync']
20
+
21
+ s.require_path = 'lib'
22
+
23
+ s.author = "Oleg Ivanov"
24
+ s.email = "morhekil@morhekil.net"
25
+ s.homepage = "http://morhekil.net"
26
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: morhekil-prisync
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Oleg Ivanov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-10-01 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Prisync allows to synchronize to local or remote directories in a pseudo-realtime fashion. Filesystem changes are monitored with inotify kernel events, rsync utility is used for the actual synchronization and can be run over SSH.%
17
+ email: morhekil@morhekil.net
18
+ executables:
19
+ - prisync
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - lib/inotify.rb
26
+ - lib/prisync.rb
27
+ - bin/prisync
28
+ - prisync.gemspec
29
+ - README
30
+ has_rdoc: false
31
+ homepage: http://morhekil.net
32
+ post_install_message:
33
+ rdoc_options: []
34
+
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: "0"
42
+ version:
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ requirements: []
50
+
51
+ rubyforge_project:
52
+ rubygems_version: 1.2.0
53
+ signing_key:
54
+ specification_version: 2
55
+ summary: Pseudo-realtime file synchronization daemon.
56
+ test_files: []
57
+