loft-harmony 1.1.0
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.
- checksums.yaml +7 -0
- data/bin/harmony +644 -0
- metadata +44 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 357be3211af77da5e3cd8a20d084446311bf1743
|
4
|
+
data.tar.gz: 526c22da20b924a191a59e51005e5eaa8ba13e36
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 63b4cba6a29257c401496a11adc42ec9e485b8e8d2db027d01b0f9415b299bf12f17509fe2518d8070b22ceb381ac6b73c9d49564449a3586669d7043904a40d
|
7
|
+
data.tar.gz: f508d0854a236ea048c6444d6fae9389a9d2bbfdd6ccc6cd80442cee14df4b43350f302e3862dc5bd845311f78dee2a8374ab53659959630348d0dccc1a2f6b1
|
data/bin/harmony
ADDED
@@ -0,0 +1,644 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'net/ftp'
|
3
|
+
require 'terrun'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
class Harmony < TerminalRunner
|
7
|
+
name "Harmony"
|
8
|
+
|
9
|
+
param "server", "The FTP server to connect to"
|
10
|
+
param "user", "The FTP user to use."
|
11
|
+
param "password", "Password for the given user."
|
12
|
+
param "directory", "The directory to watch."
|
13
|
+
|
14
|
+
option "--help", 0, "", "Show this help document."
|
15
|
+
option "--remote", 1, "path", "Remote path to use."
|
16
|
+
option "--timeout", 1, "seconds", "Length of time to allow files to transfer. (default 2)"
|
17
|
+
option "--coffee", 1, "target", "Automatically compile saved coffeescript files to the given directory."
|
18
|
+
option "--eco", 2, "target identifier", "Automatically compile saved eco files to the given directory."
|
19
|
+
option "--auto", 0, "", "Start up auto mode automatically."
|
20
|
+
|
21
|
+
help ""
|
22
|
+
|
23
|
+
def self.run
|
24
|
+
if @@options.include? "--help"
|
25
|
+
show_usage
|
26
|
+
exit
|
27
|
+
end
|
28
|
+
|
29
|
+
@modified = []
|
30
|
+
@active = false
|
31
|
+
|
32
|
+
@server = @@params["server"]
|
33
|
+
@user = @@params["user"]
|
34
|
+
@password = @@params["password"]
|
35
|
+
@directory = Dir.new(@@params["directory"])
|
36
|
+
@remote_path = @@options.include?("--remote") ? @@options["--remote"][0] : ""
|
37
|
+
@timeout = @@options.include?("--timeout") ? @@options["--timeout"][0].to_i : 2
|
38
|
+
@compile_coffeescript = @@options.include?("--coffee") ? @@options["--coffee"][0] : nil
|
39
|
+
@ignored = []
|
40
|
+
|
41
|
+
@compile_eco = nil
|
42
|
+
if @@options.include?("--eco")
|
43
|
+
@compile_eco = @@options["--eco"][0]
|
44
|
+
@eco_ident = @@options["--eco"][1]
|
45
|
+
end
|
46
|
+
|
47
|
+
@watcher = Dir::DirectoryWatcher.new(@directory)
|
48
|
+
|
49
|
+
@modified_proc = Proc.new do |file, info|
|
50
|
+
unless @ignored.include?(file.path)
|
51
|
+
@modified << file.path
|
52
|
+
if @active
|
53
|
+
if file.path.end_with?(".coffee") && @compile_coffeescript
|
54
|
+
`coffee -o #{@compile_coffeescript} -c #{file.path}`
|
55
|
+
end
|
56
|
+
if file.path.end_with?(".eco") && @compile_eco
|
57
|
+
`eco -i #{@eco_ident} -o #{@compile_eco} #{file.path}`
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
@watcher.on_add = @modified_proc
|
64
|
+
@watcher.on_modify = @modified_proc
|
65
|
+
#@watcher.on_remove = @modified_proc
|
66
|
+
|
67
|
+
@watcher.scan_now
|
68
|
+
self.clear
|
69
|
+
@active = true
|
70
|
+
|
71
|
+
puts " ## Harmony is now running".red
|
72
|
+
|
73
|
+
if File.exist?(".harmonyignore")
|
74
|
+
puts " ## Loading .harmonyignore".pink
|
75
|
+
File.readlines('.harmonyignore').each do |line|
|
76
|
+
@ignored << line[0..-2]
|
77
|
+
puts "X Ignoring #{line}".pink
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
self.start_auto if @@options.include?("--auto")
|
82
|
+
|
83
|
+
begin
|
84
|
+
while true
|
85
|
+
break if self.get_command
|
86
|
+
end
|
87
|
+
rescue => e
|
88
|
+
puts e.to_s.red
|
89
|
+
puts " ## FATAL ERROR OCCURRED. SHUTTING DOWN.".red
|
90
|
+
end
|
91
|
+
@thread.kill if @thread
|
92
|
+
self.close_connection
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.get_command
|
96
|
+
print "Harmony:: ".yellow
|
97
|
+
command, arg = gets.chomp.split(' ')
|
98
|
+
return true if command == "exit" || command == "quit"
|
99
|
+
self.show_help if command == "help"
|
100
|
+
self.send_to_remote if command == "send" || command == "s"
|
101
|
+
self.clear if command == "clear"
|
102
|
+
self.show_status if command == "status" || command == "st"
|
103
|
+
self.deploy if command == "deploy"
|
104
|
+
self.start_auto if command == "auto"
|
105
|
+
self.stop_auto if command == "stop"
|
106
|
+
@modified_proc.call(File.new(arg), nil) if command == "mark"
|
107
|
+
self.ftp if command == "ftp"
|
108
|
+
false
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.ftp
|
112
|
+
`ftp ftp://#{@user}:#{@password}@#{@server}`
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.start_auto
|
116
|
+
puts " ## Auto upload has been started. Type 'stop' to kill the thread.".red
|
117
|
+
@thread = Thread.new do
|
118
|
+
begin
|
119
|
+
while true
|
120
|
+
self.send_to_remote
|
121
|
+
sleep 2
|
122
|
+
end
|
123
|
+
rescue => e
|
124
|
+
puts e.to_s.red
|
125
|
+
puts " ## FATAL ERROR OCCURRED IN AUTO THREAD. SHUTTING DOWN.".red
|
126
|
+
self.stop_auto
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.stop_auto
|
132
|
+
return unless @thread
|
133
|
+
@thread.kill
|
134
|
+
puts " ## Auto upload thread has been killed.".red
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.deploy
|
138
|
+
@watcher.add_all
|
139
|
+
self.send_to_remote
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.send_to_remote
|
143
|
+
@watcher.scan_now
|
144
|
+
return if @modified.empty?
|
145
|
+
failed = !self.open_connection
|
146
|
+
unless failed
|
147
|
+
@modified.each do |file|
|
148
|
+
next if file.end_with? "~"
|
149
|
+
begin
|
150
|
+
Timeout::timeout(@timeout) do
|
151
|
+
rpath = self.remote_path_for(file)
|
152
|
+
|
153
|
+
if @ftp.mkdir_p rpath
|
154
|
+
puts " ## Created new directory #{rpath}".pink
|
155
|
+
end
|
156
|
+
|
157
|
+
@ftp.chdir rpath
|
158
|
+
|
159
|
+
if file.end_with? ".png", ".gif", ".jpg", ".bmp", ".svg", ".tiff", ".raw"
|
160
|
+
@ftp.putbinaryfile(file)
|
161
|
+
else
|
162
|
+
@ftp.puttextfile(file)
|
163
|
+
end
|
164
|
+
puts " ## [SUCCESS] #{file} => #{rpath}".green
|
165
|
+
end
|
166
|
+
rescue Timeout::Error
|
167
|
+
failed = true
|
168
|
+
puts " ## [ FAIL ] #{file} timed out while syncing".red
|
169
|
+
rescue Net::FTPError => e
|
170
|
+
failed = true
|
171
|
+
puts " ## [ FAIL ] #{file} failed to sync".red
|
172
|
+
puts e.message
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
if failed
|
178
|
+
puts " ## Some files failed to transfer. The dirty list has not been cleared.".pink
|
179
|
+
else
|
180
|
+
self.clear
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def self.remote_path_for(file)
|
185
|
+
extra_path = file.sub(@directory.path, "")
|
186
|
+
@remote_path + extra_path[0, extra_path.rindex("/")]
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.clear
|
190
|
+
@modified = []
|
191
|
+
end
|
192
|
+
|
193
|
+
def self.show_status
|
194
|
+
@watcher.scan_now
|
195
|
+
|
196
|
+
return puts " ## Directory is in sync".green if @modified.count == 0
|
197
|
+
|
198
|
+
puts " ## Files to be uploaded".red
|
199
|
+
@modified.each do |file|
|
200
|
+
puts "+ #{file}".pink
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.show_help
|
205
|
+
puts " ## Harmony Help".red
|
206
|
+
puts "help - Show this help file"
|
207
|
+
puts "exit (quit) - Quit Harmony"
|
208
|
+
puts "status (st) - Show a list of files that will be transfered"
|
209
|
+
puts "clear - Mark all files as synced"
|
210
|
+
puts "send (s) - Send all new and modified files to the remote server"
|
211
|
+
puts "deploy - Send all files, regardless of their state"
|
212
|
+
puts "mark [file] - Mark the specified file as changed (use format ./directory/file)"
|
213
|
+
puts "auto - Automatically run 'send' every 2 seconds"
|
214
|
+
puts "stop - reverses the 'auto' command"
|
215
|
+
end
|
216
|
+
|
217
|
+
def self.open_connection
|
218
|
+
Timeout.timeout(@timeout) do
|
219
|
+
if @ftp
|
220
|
+
begin
|
221
|
+
@ftp.list
|
222
|
+
rescue Net::FTPError
|
223
|
+
puts " ## Connection was closed by server".pink
|
224
|
+
@ftp.close
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
if @ftp.nil? || @ftp.closed?
|
229
|
+
puts " ## Connection opening".red
|
230
|
+
@ftp = BetterFTP.new(@server, @user, @password)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
true
|
234
|
+
rescue SocketError
|
235
|
+
puts " ## [FAIL] Unable to open connection to server.".red
|
236
|
+
false
|
237
|
+
rescue Timeout::Error
|
238
|
+
puts " ## [TIMEOUT] Failed to connect to server.".red
|
239
|
+
@ftp.close
|
240
|
+
@ftp = nil
|
241
|
+
false
|
242
|
+
end
|
243
|
+
|
244
|
+
def self.close_connection
|
245
|
+
if @ftp
|
246
|
+
puts " ## Connection closing".red
|
247
|
+
@ftp.close
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
end
|
252
|
+
|
253
|
+
|
254
|
+
# Monkey patching the string class for easy colors (you mad?)
|
255
|
+
class String
|
256
|
+
def colorize(color_code)
|
257
|
+
"\e[#{color_code}m#{self}\e[0m"
|
258
|
+
end
|
259
|
+
|
260
|
+
def red
|
261
|
+
colorize(31)
|
262
|
+
end
|
263
|
+
|
264
|
+
def green
|
265
|
+
colorize(32)
|
266
|
+
end
|
267
|
+
|
268
|
+
def yellow
|
269
|
+
colorize(33)
|
270
|
+
end
|
271
|
+
|
272
|
+
def pink
|
273
|
+
colorize(35)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
require 'net/ftp'
|
278
|
+
|
279
|
+
class BetterFTP < Net::FTP
|
280
|
+
|
281
|
+
attr_accessor :port
|
282
|
+
attr_accessor :public_ip
|
283
|
+
alias_method :cd, :chdir
|
284
|
+
attr_reader :home
|
285
|
+
|
286
|
+
def initialize(host = nil, user = nil, passwd = nil, acct = nil)
|
287
|
+
super
|
288
|
+
@host = host
|
289
|
+
@user = user
|
290
|
+
@passwd = passwd
|
291
|
+
@acct = acct
|
292
|
+
@home = self.pwd
|
293
|
+
initialize_caches
|
294
|
+
end
|
295
|
+
|
296
|
+
def initialize_caches
|
297
|
+
@created_paths_cache = []
|
298
|
+
@deleted_paths_cache = []
|
299
|
+
end
|
300
|
+
|
301
|
+
def connect(host, port = nil)
|
302
|
+
port ||= @port || FTP_PORT
|
303
|
+
if @debug_mode
|
304
|
+
print "connect: ", host, ", ", port, "\n"
|
305
|
+
end
|
306
|
+
synchronize do
|
307
|
+
initialize_caches
|
308
|
+
@sock = open_socket(host, port)
|
309
|
+
voidresp
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def reconnect!
|
314
|
+
if @host
|
315
|
+
connect(@host)
|
316
|
+
if @user
|
317
|
+
login(@user, @passwd, @acct)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def directory?(path)
|
323
|
+
chdir(path)
|
324
|
+
|
325
|
+
return true
|
326
|
+
rescue Net::FTPPermError
|
327
|
+
return false
|
328
|
+
end
|
329
|
+
|
330
|
+
def file?(path)
|
331
|
+
chdir(File.dirname(path))
|
332
|
+
|
333
|
+
begin
|
334
|
+
size(path)
|
335
|
+
return true
|
336
|
+
rescue Net::FTPPermError
|
337
|
+
return false
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def mkdir_p(dir)
|
342
|
+
made_path = false
|
343
|
+
|
344
|
+
parts = dir.split("/")
|
345
|
+
if parts.first == "~"
|
346
|
+
growing_path = ""
|
347
|
+
else
|
348
|
+
growing_path = "/"
|
349
|
+
end
|
350
|
+
for part in parts
|
351
|
+
next if part == ""
|
352
|
+
if growing_path == ""
|
353
|
+
growing_path = part
|
354
|
+
else
|
355
|
+
growing_path = File.join(growing_path, part)
|
356
|
+
end
|
357
|
+
unless @created_paths_cache.include?(growing_path)
|
358
|
+
begin
|
359
|
+
mkdir(growing_path)
|
360
|
+
chdir(growing_path)
|
361
|
+
made_path = true
|
362
|
+
rescue Net::FTPPermError, Net::FTPTempError
|
363
|
+
end
|
364
|
+
@created_paths_cache << growing_path
|
365
|
+
else
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
made_path
|
370
|
+
end
|
371
|
+
|
372
|
+
def rm_r(path)
|
373
|
+
return if @deleted_paths_cache.include?(path)
|
374
|
+
@deleted_paths_cache << path
|
375
|
+
if directory?(path)
|
376
|
+
chdir path
|
377
|
+
|
378
|
+
begin
|
379
|
+
files = nlst
|
380
|
+
files.each {|file| rm_r "#{path}/#{file}"}
|
381
|
+
rescue Net::FTPTempError
|
382
|
+
# maybe all files were deleted already
|
383
|
+
end
|
384
|
+
|
385
|
+
rmdir path
|
386
|
+
else
|
387
|
+
rm(path)
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def rm(path)
|
392
|
+
chdir File.dirname(path)
|
393
|
+
delete File.basename(path)
|
394
|
+
end
|
395
|
+
|
396
|
+
private
|
397
|
+
|
398
|
+
def makeport
|
399
|
+
sock = TCPServer.open(@sock.addr[3], 0)
|
400
|
+
port = sock.addr[1]
|
401
|
+
host = @public_ip || sock.addr[3]
|
402
|
+
sendport(host, port)
|
403
|
+
return sock
|
404
|
+
end
|
405
|
+
|
406
|
+
end
|
407
|
+
|
408
|
+
|
409
|
+
# *** This code is copyright 2004 by Gavin Kistner
|
410
|
+
# *** It is covered under the license viewable at http://phrogz.net/JS/_ReuseLicense.txt
|
411
|
+
class Dir
|
412
|
+
|
413
|
+
# The DirectoryWatcher class keeps an eye on all files in a directory, and calls
|
414
|
+
# methods of your making when files are added to, modified in, and/or removed from
|
415
|
+
# that directory.
|
416
|
+
#
|
417
|
+
# The +on_add+, +on_modify+ and +on_remove+ callbacks should be Proc instances
|
418
|
+
# that should be called when a file is added to, modified in, or removed from
|
419
|
+
# the watched directory.
|
420
|
+
#
|
421
|
+
# The +on_add+ and +on_modify+ Procs will be passed a File instance pointing to
|
422
|
+
# the file added/changed, as well as a Hash instance. The hash contains some
|
423
|
+
# saved statistics about the file (modification date (<tt>:date</tt>),
|
424
|
+
# file size (<tt>:size</tt>), and original path (<tt>:path</tt>)) and may also
|
425
|
+
# be used to store additional information about the file.
|
426
|
+
#
|
427
|
+
# The +on_remove+ Proc will only be passed the hash that was passed to +on_add+
|
428
|
+
# and +on_modify+ (since the file no longer exists in a way that the watcher
|
429
|
+
# knows about it). By storing your own information in the hash, you may do
|
430
|
+
# something intelligent when the file disappears.
|
431
|
+
#
|
432
|
+
# The first time the directory is scanned, the +on_add+ callback will be invoked
|
433
|
+
# for each file in the directory (since it's the first time they've been seen).
|
434
|
+
#
|
435
|
+
# Use the +onmodify_checks+ and +onmodify_requiresall+ properties to control
|
436
|
+
# what is required for the +on_modify+ callback to occur.
|
437
|
+
#
|
438
|
+
# If you do not set a Proc for any one of these callbacks; they'll simply
|
439
|
+
# be ignored.
|
440
|
+
#
|
441
|
+
# Use the +autoscan_delay+ property to set how frequently the directory is
|
442
|
+
# automatically checked, or use the #scan_now method to force it to look
|
443
|
+
# for changes.
|
444
|
+
#
|
445
|
+
# <b>You must call the #start_watching method after you create a DirectoryWatcher
|
446
|
+
# instance</b> (and after you have set the necessary callbacks) <b>to start the
|
447
|
+
# automatic scanning.</b>
|
448
|
+
#
|
449
|
+
# The DirectoryWatcher does not process sub-directories of the watched
|
450
|
+
# directory. Any item in the directory which returns +false+ for <tt>File.file?</tt>
|
451
|
+
# is ignored.
|
452
|
+
#
|
453
|
+
# Example:
|
454
|
+
#
|
455
|
+
# device_manager = Dir::DirectoryWatcher.new( 'plugins/devices', 2 )
|
456
|
+
# device_manager.name_regexp = /^[^.].*\.rb$/
|
457
|
+
#
|
458
|
+
# device_manager.on_add = Proc.new{ |the_file, stats_hash|
|
459
|
+
# puts "Hey, just found #{the_file.inspect}!"
|
460
|
+
#
|
461
|
+
# # Store something custom
|
462
|
+
# stats_hash[:blorgle] = the_file.foo
|
463
|
+
# }
|
464
|
+
#
|
465
|
+
# device_manager.on_modify = Proc.new{ |the_file, stats_hash|
|
466
|
+
# puts "Hey, #{the_file.inspect} just changed."
|
467
|
+
# }
|
468
|
+
#
|
469
|
+
# device_manager.on_remove = Proc.new{ |stats_hash|
|
470
|
+
# puts "Whoa, the following file just disappeared:"
|
471
|
+
# stats_hash.each_pair{ |k,v|
|
472
|
+
# puts "#{k} : #{v}"
|
473
|
+
# }
|
474
|
+
# }
|
475
|
+
#
|
476
|
+
# device_manager.start_watching
|
477
|
+
class DirectoryWatcher
|
478
|
+
# How long (in seconds) to wait between checks of the directory for changes.
|
479
|
+
attr_accessor :autoscan_delay
|
480
|
+
|
481
|
+
# The Dir instance or path to the directory to watch.
|
482
|
+
attr_accessor :directory
|
483
|
+
def directory=( dir ) #:nodoc:
|
484
|
+
@directory = dir.is_a?(Dir) ? dir : Dir.new( dir )
|
485
|
+
end
|
486
|
+
|
487
|
+
# Proc to call when files are added to the watched directory.
|
488
|
+
attr_accessor :on_add
|
489
|
+
|
490
|
+
# Proc to call when files are modified in the watched directory
|
491
|
+
# (see +onmodify_checks+).
|
492
|
+
attr_accessor :on_modify
|
493
|
+
|
494
|
+
# Proc to call when files are removed from the watched directory.
|
495
|
+
attr_accessor :on_remove
|
496
|
+
|
497
|
+
# Array of symbols which specify which attribute(s) to check for changes.
|
498
|
+
# Valid symbols are <tt>:date</tt> and <tt>:size</tt>.
|
499
|
+
# Defaults to <tt>:date</tt> only.
|
500
|
+
attr_accessor :onmodify_checks
|
501
|
+
|
502
|
+
# If more than one symbol is specified for +onmodify_checks+, should
|
503
|
+
# +on_modify+ be called only when *all* specified values change
|
504
|
+
# (value of +true+), or when any *one* value changes (value of +false+)?
|
505
|
+
# Defaults to +false+.
|
506
|
+
attr_accessor :onmodify_requiresall
|
507
|
+
|
508
|
+
# Should files which exist in the directory fire the +on_add+ callback
|
509
|
+
# the first time the directory is scanned? Defaults to +true+.
|
510
|
+
attr_accessor :onadd_for_existing
|
511
|
+
|
512
|
+
# Regular expression to match against file names. If +nil+, all files
|
513
|
+
# will be included, otherwise only those whose name match the regexp
|
514
|
+
# will be passed to the +on_add+/+on_modify+/+on_remove+ callbacks.
|
515
|
+
# Defaults to <tt>/^[^.].*$/</tt> (files which do not begin with a period).
|
516
|
+
attr_accessor :name_regexp
|
517
|
+
|
518
|
+
# Creates a new directory watcher.
|
519
|
+
#
|
520
|
+
# _dir_:: The path (relative to the current working directory) of the
|
521
|
+
# directory to watch, or a Dir instance.
|
522
|
+
# _delay_:: The +autoscan_delay+ value to use; defaults to 10 seconds.
|
523
|
+
def initialize( dir, delay = 10 )
|
524
|
+
self.directory = dir
|
525
|
+
@autoscan_delay = delay
|
526
|
+
@known_file_stats = {}
|
527
|
+
@onmodify_checks = [ :date ]
|
528
|
+
@onmodify_requiresall = false
|
529
|
+
@onadd_for_existing = true
|
530
|
+
@scanned_once = false
|
531
|
+
@name_regexp = /^[^.].*$/
|
532
|
+
end
|
533
|
+
|
534
|
+
# Starts the automatic scanning of the directory for changes,
|
535
|
+
# repeatedly calling #scan_now and then waiting +autoscan_delay+
|
536
|
+
# seconds before calling it again.
|
537
|
+
#
|
538
|
+
# Automatic scanning is *not* turned on when you create a new
|
539
|
+
# DirectoryWatcher; you must invoke this method (after setting
|
540
|
+
# the +on_add+/+on_modify+/+on_remove+ callbacks).
|
541
|
+
def start_watching
|
542
|
+
@thread = Thread.new{
|
543
|
+
while true
|
544
|
+
self.scan_now
|
545
|
+
sleep @autoscan_delay
|
546
|
+
end
|
547
|
+
}
|
548
|
+
end
|
549
|
+
|
550
|
+
# Stops the automatic scanning of the directory for changes.
|
551
|
+
def stop_watching
|
552
|
+
@thread.kill
|
553
|
+
end
|
554
|
+
|
555
|
+
# Scans the directory for additions/modifications/removals,
|
556
|
+
# calling the +on_add+/+on_modify+/+on_remove+ callbacks as
|
557
|
+
# appropriate.
|
558
|
+
def scan_now
|
559
|
+
#Check for add/modify
|
560
|
+
scan_dir(@directory)
|
561
|
+
|
562
|
+
# Check for removed files
|
563
|
+
if @on_remove.respond_to?( :call )
|
564
|
+
@known_file_stats.each_pair{ |path,stats|
|
565
|
+
next if File.file?( path )
|
566
|
+
stats[:path] = path
|
567
|
+
@on_remove.call( stats )
|
568
|
+
@known_file_stats.delete(path)
|
569
|
+
}
|
570
|
+
end
|
571
|
+
|
572
|
+
@scanned_once = true
|
573
|
+
end
|
574
|
+
|
575
|
+
def add_all
|
576
|
+
scan_dir(@directory, true)
|
577
|
+
end
|
578
|
+
|
579
|
+
def scan_dir(directory, override = false)
|
580
|
+
# Setup the checks
|
581
|
+
# ToDo: CRC
|
582
|
+
checks = {
|
583
|
+
:date => {
|
584
|
+
:use=>false,
|
585
|
+
:proc=>Proc.new{ |file,stats| stats.mtime }
|
586
|
+
},
|
587
|
+
:size => {
|
588
|
+
:use=>false,
|
589
|
+
:proc=>Proc.new{ |file,stats| stats.size }
|
590
|
+
},
|
591
|
+
:crc => {
|
592
|
+
:use=>false,
|
593
|
+
:proc=>Proc.new{ |file,stats| 1 }
|
594
|
+
}
|
595
|
+
}
|
596
|
+
checks.each_pair{ |check_name,check|
|
597
|
+
check[:use] = (@onmodify_checks == check_name) || ( @onmodify_checks.respond_to?( :include? ) && @onmodify_checks.include?( check_name ) )
|
598
|
+
}
|
599
|
+
|
600
|
+
directory.rewind
|
601
|
+
directory.each{ |fname|
|
602
|
+
next if fname.start_with? '.'
|
603
|
+
file_path = "#{directory.path}/#{fname}"
|
604
|
+
scan_dir(Dir.new(file_path), override) unless fname == "." || fname == ".." || File.file?(file_path)
|
605
|
+
next if (@name_regexp.respond_to?( :match ) && !@name_regexp.match( fname )) || !File.file?( file_path )
|
606
|
+
the_file = File.new( file_path )
|
607
|
+
file_stats = File.stat( file_path )
|
608
|
+
|
609
|
+
saved_stats = @known_file_stats[file_path]
|
610
|
+
new_stats = {}
|
611
|
+
checks.each_pair{ |check_name,check|
|
612
|
+
new_stats[check_name] = check[:proc].call( the_file, file_stats )
|
613
|
+
}
|
614
|
+
|
615
|
+
@on_add.call(the_file, new_stats) if override
|
616
|
+
if saved_stats
|
617
|
+
if @on_modify.respond_to?( :call )
|
618
|
+
sufficiently_modified = @onmodify_requiresall
|
619
|
+
saved_stats = @known_file_stats[file_path]
|
620
|
+
checks.each_pair{ |check_name,check|
|
621
|
+
stat_changed = check[:use] && ( saved_stats[check_name] != new_stats[check_name] )
|
622
|
+
if @onmodify_requiresall
|
623
|
+
sufficiently_modified &&= stat_changed
|
624
|
+
else
|
625
|
+
sufficiently_modified ||= stat_changed
|
626
|
+
end
|
627
|
+
saved_stats[check_name] = new_stats[check_name]
|
628
|
+
}
|
629
|
+
@on_modify.call( the_file, saved_stats ) if sufficiently_modified
|
630
|
+
end
|
631
|
+
elsif @on_add.respond_to?( :call ) && (@scanned_once || @onadd_for_existing)
|
632
|
+
@known_file_stats[file_path] = new_stats
|
633
|
+
@on_add.call( the_file, new_stats )
|
634
|
+
end
|
635
|
+
|
636
|
+
the_file.close
|
637
|
+
}
|
638
|
+
end
|
639
|
+
|
640
|
+
end
|
641
|
+
|
642
|
+
end
|
643
|
+
|
644
|
+
Harmony.start(ARGV)
|
metadata
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: loft-harmony
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Caleb Simpson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-07-10 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Watches a directory for changes to upload to a server via FTP
|
14
|
+
email: caleb@simpson.center
|
15
|
+
executables:
|
16
|
+
- harmony
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- bin/harmony
|
21
|
+
homepage:
|
22
|
+
licenses: []
|
23
|
+
metadata: {}
|
24
|
+
post_install_message:
|
25
|
+
rdoc_options: []
|
26
|
+
require_paths:
|
27
|
+
- lib
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
requirements: []
|
39
|
+
rubyforge_project:
|
40
|
+
rubygems_version: 2.2.3
|
41
|
+
signing_key:
|
42
|
+
specification_version: 4
|
43
|
+
summary: Watches a directory for changes to upload to a server via FTP
|
44
|
+
test_files: []
|