morhekil-prisync 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +23 -0
- data/bin/prisync +62 -0
- data/lib/inotify.rb +211 -0
- data/lib/prisync.rb +99 -0
- data/prisync.gemspec +26 -0
- 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
|
data/bin/prisync
ADDED
@@ -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
|
+
|
data/lib/inotify.rb
ADDED
@@ -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
|
data/lib/prisync.rb
ADDED
@@ -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
|
data/prisync.gemspec
ADDED
@@ -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
|
+
|