morhekil-prisync 0.2.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.
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
+