six-rsync 0.3.8 → 0.4.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.
data/lib/six/rsync/lib.rb CHANGED
@@ -1,863 +1,803 @@
1
- # encoding: UTF-8
2
-
3
- # TODO: Add Rsync add, commit and push (Update should be pull?), either with staging like area like Git, or add is pack into .pack, and commit is update sum ?
4
- # TODO: Seperate command lib from custom layer over rsync?
5
-
6
- module Six
7
- module Repositories
8
- module Rsync
9
- # TODO: Configure somewhere!
10
- KEY = "C:/users/sb/documents/keys/id_rsa.ppk"
11
- RSH = "-r --rsh=\"'#{File.join(BASE_PATH, "tools", "bin", "cygnative.exe")}' plink.exe -i #{KEY}\""
12
- DIR_RSYNC = '.rsync'
13
- DIR_PACK = File.join(DIR_RSYNC, '.pack')
14
- REGEX_FOLDER = /(.*)[\\|\/](.*)/
15
-
16
- class RsyncExecuteError < StandardError
17
- end
18
-
19
- class RsyncError < StandardError
20
- end
21
-
22
- class Lib
23
- attr_accessor :verbose
24
- PROTECTED = false
25
- WINDRIVE = /\"(\w)\:/
26
- DEFAULT_CONFIG = {:hosts => [], :exclude => []}.to_yaml
27
- PARAMS = if PROTECTED
28
- "--dry-run --times -O --no-whole-file -r --delete --progress -h --exclude=.rsync"
29
- else
30
- "--times -O --no-whole-file -r --delete --progress -h --exclude=.rsync"
31
- end
32
-
33
- def initialize(base = nil, logger = nil)
34
- @rsync_dir = nil
35
- @rsync_work_dir = nil
36
- @path = nil
37
- @stats = false
38
- @verbose = true
39
-
40
- @repos_local = {:pack => Hash.new, :wd => Hash.new, :version => 0}
41
- @repos_remote = {:pack => Hash.new, :wd => Hash.new, :version => 0}
42
-
43
- if base.is_a?(Rsync::Base)
44
- @rsync_dir = base.repo.path
45
- @rsync_work_dir = base.dir.path if base.dir
46
- elsif base.is_a?(Hash)
47
- @rsync_dir = base[:repository]
48
- @rsync_work_dir = base[:working_directory]
49
- end
50
- @logger = logger
51
-
52
- etc = File.join(TOOLS_PATH, 'etc')
53
- FileUtils.mkdir_p etc
54
- fstab = File.join(etc, 'fstab')
55
- str = ""
56
- str = File.open(fstab) {|file| file.read} if FileTest.exist?(fstab)
57
- unless str[/cygdrive/]
58
- str += "\nnone /cygdrive cygdrive user,noacl,posix=0 0 0\n"
59
- File.open(fstab, 'w') {|file| file.puts str}
60
- end
61
- end
62
-
63
- def init
64
- @logger.info "Processing: #{rsync_path}"
65
- if FileTest.exist? rsync_path
66
- @logger.error "Seems to already be an Rsync repository, Aborting!"
67
- raise RsyncError
68
- end
69
- if FileTest.exist? @rsync_work_dir
70
- @logger.error "Seems to already be a folder, Aborting!"
71
- raise RsyncError
72
- end
73
- FileUtils.mkdir_p pack_path
74
- save_config(config)
75
- save_repos(:local)
76
- save_repos(:remote)
77
- end
78
-
79
- def clone(repository, name, opts = {})
80
- @path = opts[:path] || '.'
81
- @rsync_work_dir = opts[:path] ? File.join(@path, name) : name
82
-
83
- # TODO: Solve logger mess completely.
84
- @logger = opts[:log] if opts[:log]
85
-
86
- case repository
87
- when Array
88
- config[:hosts] += repository
89
- when String
90
- config[:hosts] << repository
91
- end
92
-
93
- begin
94
- init
95
-
96
- # TODO: Eval move to update?
97
- arr_opts = []
98
- arr_opts << "-I" if opts[:force]
99
- begin
100
- update('', arr_opts)
101
- rescue RsyncError
102
- @logger.error "Unable to sucessfully update, aborting..."
103
- @logger.debug "#{$!}"
104
- # Dangerous? :D
105
- FileUtils.rm_rf @rsync_work_dir if File.exists?(@rsync_work_dir)
106
- #rescue
107
- # FileUtils.rm_rf @rsync_work_dir if File.exists?(@rsync_work_dir)
108
- end
109
- rescue RsyncError
110
- @logger.error "Unable to initialize"
111
- @logger.debug "#{$!}"
112
- end
113
-
114
- opts[:bare] ? {:repository => @rsync_work_dir} : {:working_directory => @rsync_work_dir}
115
- end
116
-
117
- def update(cmd, x_opts = [], opts = {})
118
- @logger.debug "Checking for updates..."
119
- @config = load_config
120
- unless @config
121
- @logger.error "Not an Rsync repository!"
122
- raise RsyncError
123
- end
124
-
125
- unless config[:hosts].size > 0
126
- @logger.error "No hosts configured!"
127
- raise RsyncError
128
- end
129
-
130
- load_repos(:local)
131
- load_repos(:remote)
132
-
133
- hosts = config[:hosts].clone
134
-
135
- # FIXME: This does not work when not forced, as host is sampled in comparesums :)
136
- host = hosts.sample
137
-
138
- if opts[:force]
139
- done = false
140
- b = false
141
- verbose = @verbose
142
- @verbose = false
143
- while hosts.size > 0 && !done do
144
- # FIXME: Nasty
145
- host = hosts.sample if b
146
- b = true
147
- hosts -= [host]
148
- @logger.info "Trying #{host}"
149
-
150
- begin
151
- arr_opts = []
152
- arr_opts << PARAMS
153
- arr_opts += x_opts
154
- if host[/\A(\w)*\@/]
155
- arr_opts << RSH#"-e ssh"
156
- end
157
- # TODO: UNCLUSTERFUCK
158
- arr_opts << esc(File.join(host, '.pack/.'))
159
- arr_opts << esc(pack_path)
160
- command(cmd, arr_opts)
161
- calc
162
- save_repos
163
- done = true
164
- rescue
165
- @logger.debug "#{$!}"
166
- end
167
- end
168
- @verbose = verbose
169
- raise RsyncError if !done
170
- else
171
- #reset(:hard => true)
172
- calc
173
- save_repos
174
-
175
- # fetch latest sums and only update when changed
176
- compare_sums(true, host)
177
- end
178
- end
179
-
180
- # TODO: Allow local-self healing, AND remote healing. reset and fetch?
181
- def reset(opts = {})
182
- @logger.info "Resetting!"
183
- if opts[:hard]
184
- @config = load_config
185
- calc
186
- save_repos
187
- compare_sums(false)
188
- end
189
- end
190
-
191
- # TODO: WIP
192
- def add(file)
193
- @logger.error "Please use commit instead!"
194
- return
195
- @logger.info "Adding #{file}"
196
- if (file == ".")
197
- load_repos(:remote)
198
- @logger.info "Calculating Checksums..."
199
- ar = Dir[File.join(@rsync_work_dir, '/**/*')]
200
-
201
- change = false
202
- i = 0
203
- ar.each do |file|
204
- i += 1
205
- unless file[/\.gz\Z/]
206
- relative = file.clone
207
- relative.gsub!(@rsync_work_dir, '')
208
- relative.gsub!(/\A[\\|\/]/, '')
209
-
210
- checksum = md5(file)
211
- if checksum != @repos_remote[:wd][relative]
212
- change = true
213
- @logger.info "Packing #{i}/#{ar.size}: #{file}"
214
- gzip(file)
215
- @repos_remote[:wd][relative] = checksum
216
- @repos_remote[:pack]["#{relative}.gz"] = md5("#{file}.gz")
217
- FileUtils.mv("#{file}.gz", pack_path("#{relative}.gz"))
218
- end
219
- end
220
- end
221
- if change
222
- save_repos
223
- #File.open(File.join(@rsync_work_dir, '.sums.yml'), 'w') { |file| file.puts remote_wd[:list].sort.to_yaml }
224
- #File.open(pack_path('.sums.yml'), 'w') { |file| file.puts remote_pack[:list].sort.to_yaml }
225
- end
226
- else
227
-
228
- end
229
- end
230
-
231
- def commit
232
- @logger.info "Committing changes on #{@rsync_work_dir}"
233
- @config = load_config
234
- unless @config
235
- @logger.error "Not an Rsync repository!"
236
- return
237
- end
238
-
239
- unless config[:hosts].size > 0
240
- @logger.error "No hosts configured!"
241
- return
242
- end
243
-
244
- load_repos(:local)
245
- load_repos(:remote)
246
-
247
- @logger.info "Calculating Checksums..."
248
- @repos_local[:wd] = calc_sums(:wd)
249
- # Added or Changed files
250
- ar = Dir[File.join(@rsync_work_dir, '/**/*')]
251
-
252
-
253
- change = false
254
- i = 0
255
- @repos_local[:wd].each_pair do |key, value|
256
- i += 1
257
- if value != @repos_remote[:wd][key]
258
- change = true
259
- @logger.info "Packing #{i}/#{@repos_local[:wd].size}: #{key}"
260
- file = File.join(@rsync_work_dir, key)
261
- file[REGEX_FOLDER]
262
- folder = $1
263
- folder.gsub!(@rsync_work_dir, '')
264
- gzip(file)
265
- @repos_local[:pack]["#{key}.gz"] = md5("#{file}.gz")
266
- FileUtils.mkdir_p pack_path(folder) if folder
267
- FileUtils.mv("#{file}.gz", pack_path("#{key}.gz"))
268
- end
269
- end
270
-
271
- # Deleted files
272
- @logger.info "Checking for deleted files..."
273
-
274
- @repos_remote[:wd].each_pair do |key, value|
275
- i += 1
276
- if @repos_local[:wd][key].nil?
277
- packed = "#{key}.gz"
278
- change = true
279
- file = pack_path(packed)
280
- file[REGEX_FOLDER]
281
- folder = $2
282
-
283
- @logger.info "Removing #{i}/#{@repos_remote[:wd].size}: #{packed}"
284
- @repos_local[:wd].delete key
285
- @repos_local[:pack].delete packed
286
- FileUtils.rm_f(file) if File.exists?(file)
287
- end
288
- end
289
-
290
- if change
291
- @logger.info "Changes found!"
292
- cmd = ''
293
- save_repos(:local)
294
-
295
- host = config[:hosts].sample
296
-
297
- verfile_srv = File.join(".pack", ".repository.yml")
298
- verbose = @verbose
299
- @verbose = false
300
- begin
301
- fetch_file(verfile_srv, host)
302
- rescue
303
- # FIXME: Should never assume that :)
304
- @logger.warn "Unable to retrieve version file from server, repository probably doesnt exist!"
305
- @logger.debug "#{$!}"
306
- # raise RsyncExecuteError
307
- end
308
- @verbose = verbose
309
-
310
- load_repos(:remote)
311
-
312
- if @repos_local[:version] < @repos_remote[:version] # && !force
313
- @logger.warn "WARNING, version on server is NEWER, aborting!"
314
- raise RsyncError
315
- end
316
- @repos_local[:version] += 1
317
- @repos_remote[:version] = @repos_local[:version]
318
- @repos_remote[:pack] = @repos_local[:pack].clone
319
- @repos_remote[:wd] = @repos_local[:wd].clone
320
- save_repos(:remote)
321
- save_repos(:local)
322
- push(host)
323
- else
324
- @logger.info "No changes found!"
325
- end
326
- end
327
-
328
- def push(host = nil)
329
- @logger.info "Pushing..."
330
- @config = load_config
331
- host = config[:hosts].sample unless host
332
- # TODO: UNCLUSTERFUCK
333
- arr_opts = []
334
- arr_opts << PARAMS
335
-
336
- # Upload .pack changes
337
- if host[/\A(\w)*\@/]
338
- arr_opts << RSH
339
- end
340
- arr_opts << esc(pack_path('.'))
341
- arr_opts << esc(File.join(host, '.pack'))
342
-
343
- command('', arr_opts)
344
- end
345
-
346
- def compare_set(typ, host, online = true)
347
- load_repos(:local)
348
- load_repos(:remote)
349
-
350
- #if local[typ][:md5] == remote[typ][:md5]
351
- # @logger.info "#{typ} Match!"
352
- #else
353
- # @logger.info "#{typ} NOT match, updating!"
354
-
355
- mismatch = []
356
- @repos_remote[typ].each_pair do |key, value|
357
- if value == @repos_local[typ][key]
358
- #@logger.info "Match! #{key}"
359
- else
360
- @logger.debug "Mismatch! #{key}"
361
- mismatch << key
362
- end
363
- end
364
-
365
- if mismatch.size > 0
366
- case typ
367
- when :pack
368
- # direct unpack of gz into working folder
369
- # Update file
370
- if online
371
- hosts = config[:hosts].clone
372
- done = false
373
-
374
- ## Pack
375
- if online
376
- b = false
377
- while hosts.size > 0 && !done do
378
- # FIXME: Nasty
379
- if b
380
- host = hosts.sample
381
- @logger.info "Trying #{host}"
382
- end
383
- slist = nil
384
- b = true
385
- hosts -= [host]
386
- begin
387
- # TODO: Progress bar
388
- if mismatch.size > (@repos_remote[typ].size / 2)
389
- @logger.info "Many files mismatched (#{mismatch.size}), running full update on .pack folder"
390
- arr_opts = []
391
- arr_opts << PARAMS
392
- if host[/\A(\w)*\@/]
393
- arr_opts << RSH
394
- end
395
-
396
- arr_opts << esc(File.join(host, '.pack/.'))
397
- arr_opts << esc(pack_path)
398
- command('', arr_opts)
399
- else
400
- c = mismatch.size
401
- @logger.info "Fetching #{mismatch.size} files... Please wait"
402
- slist = File.join(TEMP_PATH, ".six-updater_#{rand 9999}-list")
403
- slist.gsub!("\\", "/")
404
- File.open(slist, 'w') do |f|
405
- mismatch.each { |e| f.puts e }
406
- end
407
- arr_opts = []
408
- arr_opts << PARAMS
409
-
410
- arr_opts << RSH if host[/\A(\w)*\@/]
411
-
412
- cyg_slist = "\"#{slist}\""
413
-
414
- while cyg_slist[WINDRIVE] do
415
- drive = cyg_slist[WINDRIVE]
416
- cyg_slist.gsub!(drive, "\"/cygdrive/#{$1}")
417
- end
418
- arr_opts << "--files-from=#{cyg_slist}"
419
-
420
- arr_opts << esc(File.join(host, '.pack/.'))
421
- arr_opts << esc(pack_path)
422
-
423
- command('', arr_opts)
424
- end
425
- done = true
426
- rescue
427
- @logger.warn "Failure"
428
- @logger.debug "#{$!}"
429
- ensure
430
- FileUtils.rm_f slist if slist
431
- end
432
- end
433
- @logger.warn "There was a problem during updating, please retry!" unless done
434
- end
435
- end
436
- when :wd
437
- c = mismatch.size
438
- i = 0
439
- mismatch.each do |e|
440
- # TODO: Nicer progress bar...
441
- i += 1
442
- @logger.info "Unpacking #{i}/#{c}: #{e}"
443
- unpack(:path => "#{e}.gz")
444
- end
445
- end
446
- end
447
-
448
- del = []
449
- @repos_local[typ].each_pair do |key, value|
450
- if @repos_remote[typ][key].nil?
451
- @logger.info "Removed: #{key}"
452
- del << key unless config[:exclude].include?(key)
453
- end
454
- end
455
- del.each { |e| del_file(e, typ) }
456
- @repos_local[typ] = calc_sums(typ)
457
- @repos_local[:version] = @repos_remote[:version]
458
- save_repos
459
- end
460
-
461
- def compare_sums(online = true, host = config[:hosts].sample)
462
- hosts = config[:hosts].clone
463
- done = false
464
-
465
- ## Pack
466
- if online
467
- b = false
468
- verbose = @verbose
469
- @verbose = false
470
- while hosts.size > 0 && !done do
471
- # FIXME: Nasty
472
- host = hosts.sample if b
473
- b = true
474
- hosts -= [host]
475
- @logger.info "Trying #{host}"
476
-
477
- begin
478
- FileUtils.cp(pack_path(".repository.yml"), rsync_path(".repository-pack.yml"))
479
- fetch_file(".pack/.repository.yml", host)
480
-
481
- load_repos(:remote)
482
- load_repos(:local)
483
-
484
- if @repos_local[:version] > @repos_remote[:version] # && !force
485
- @logger.warn "WARNING, version on server is OLDER, aborting!"
486
- FileUtils.cp(rsync_path(".repository-pack.yml"), pack_path(".repository.yml"))
487
- raise RsyncError
488
- end
489
- done = true
490
- rescue
491
- @logger.debug "#{$!}"
492
- ensure
493
- FileUtils.rm(rsync_path(".repository-pack.yml"))
494
- end
495
- end
496
- @verbose = verbose
497
- end
498
- if done && online
499
- # TODO: Don't do actions when not online
500
- @logger.info "Verifying Packed files..."
501
- compare_set(:pack, host)
502
- @logger.info "Verifying Unpacked files..."
503
- compare_set(:wd, host)
504
- save_repos
505
- end
506
- end
507
-
508
- private
509
- def config
510
- cfg = @config ||= YAML::load(DEFAULT_CONFIG)
511
- cfg[:exclude] = [] unless cfg[:exclude]
512
- cfg[:hosts] = [] unless cfg[:hosts]
513
- cfg
514
- end
515
-
516
- def rsync_path(path = '')
517
- p = File.join(@rsync_work_dir, DIR_RSYNC)
518
- p = File.join(p, path) unless path.size == 0
519
- p
520
- end
521
-
522
- def pack_path(path = '')
523
- p = File.join(@rsync_work_dir, DIR_PACK)
524
- p = File.join(p, path) unless path.size == 0
525
- p
526
- end
527
-
528
- def esc(val)
529
- "\"#{val}\""
530
- end
531
-
532
- def escape(s)
533
- "\"" + s.to_s.gsub('\"', '\"\\\"\"') + "\""
534
- end
535
-
536
- def fetch_file(path, host)
537
- path[/(.*)\/(.*)/]
538
- folder, file = $1, $2
539
- folder = "." unless folder
540
- file = path unless file
541
- # Only fetch a specific file
542
- @logger.debug "Fetching #{path} from #{host}"
543
- arr_opts = []
544
- arr_opts << PARAMS
545
- if host[/\A(\w)*\@/]
546
- arr_opts << RSH
547
- end
548
- arr_opts << esc(File.join(host, path))
549
- arr_opts << esc(rsync_path(folder))
550
-
551
- command('', arr_opts)
552
- end
553
-
554
- def calc
555
- @logger.info "Calculating checksums"
556
- [:pack, :wd].each { |t| @repos_local[t] = calc_sums(t) }
557
- end
558
-
559
- def calc_sums(typ)
560
- @logger.debug "Calculating checksums of #{typ} files"
561
- ar = []
562
- reg = case typ
563
- when :pack
564
- ar = Dir[pack_path('**/*')]
565
- /\A[\\|\/]\.rsync[\\|\/]\.pack[\\|\/]/
566
- when :wd
567
- ar = Dir[File.join(@rsync_work_dir, '/**/*')]
568
- /\A[\\|\/]/
569
- end
570
- h = Hash.new
571
- ar.each do |file|
572
- relative = file.clone
573
- relative.gsub!(@rsync_work_dir, '')
574
- relative.gsub!(reg, '')
575
-
576
- sum = md5(file)
577
- h[relative] = sum if sum && !config[:exclude].include?(relative)
578
- end
579
- h
580
- end
581
-
582
- def load_config
583
- load_yaml(File.join(rsync_path, 'config.yml'))
584
- end
585
-
586
- def load_yaml(file)
587
- if FileTest.exist?(file)
588
- YAML::load_file(file)
589
- else
590
- nil
591
- end
592
- end
593
-
594
- def save_default_config
595
- FileUtils.mkdir_p rsync_path
596
- save_config(config)
597
- end
598
-
599
- def save_config(config = YAML::load(DEFAULT_CONFIG))
600
- File.open(File.join(rsync_path, 'config.yml'), 'w') { |file| file.puts config.to_yaml }
601
- end
602
-
603
- def save_repos(typ = :local)
604
- file, config = nil, nil
605
- case typ
606
- when :local
607
- file = rsync_path('.repository.yml')
608
- config = @repos_local.clone
609
- when :remote
610
- file = pack_path('.repository.yml')
611
- config = @repos_remote.clone
612
- end
613
- config[:pack] = config[:pack].sort
614
- config[:wd] = config[:wd].sort
615
- File.open(file, 'w') { |file| file.puts config.to_yaml }
616
- end
617
-
618
- def load_repos(typ)
619
- config = Hash.new
620
- case typ
621
- when :local
622
- File.open(rsync_path('.repository.yml')) { |file| config = YAML::load(file) }
623
- when :remote
624
- File.open(pack_path('.repository.yml')) { |file| config = YAML::load(file) }
625
- end
626
-
627
- [:wd, :pack].each do |t|
628
- h = Hash.new
629
- config[t].each { |e| h[e[0]] = e[1] }
630
- config[t] = h
631
- end
632
-
633
- case typ
634
- when :local
635
- @repos_local = config
636
- when :remote
637
- @repos_remote = config
638
- end
639
- end
640
-
641
- def del_file(file, typ, opts = {})
642
- file = case typ
643
- when :pack
644
- File.join(DIR_PACK, file)
645
- when :wd
646
- file
647
- end
648
- FileUtils.rm_f File.join(@rsync_work_dir, file)
649
- end
650
-
651
- def md5(path)
652
- unless File.directory? path
653
- path[/(.*)[\/|\\](.*)/]
654
- folder, file = $1, $2
655
- Dir.chdir(folder) do
656
- r = %x[md5sum #{esc(file)}]
657
- @logger.debug r
658
- r[/\A\w*/]
659
- end
660
- end
661
- end
662
-
663
- def zip7(file)
664
- out = %x[7z x #{esc(file)} -y]
665
- @logger.debug out
666
- out
667
- end
668
-
669
- def gzip(file)
670
- @logger.debug "Gzipping #{file}"
671
- out = %x[gzip -f --best --rsyncable --keep #{esc(file)}]
672
- @logger.debug out
673
- end
674
-
675
- def unpack_file(file, path)
676
- Dir.chdir(path) do |dir|
677
- zip7(file)
678
- # TODO: Evaluate if this is actually wanted / useful at all..
679
- =begin
680
- if file[/\.tar\.?/]
681
- file[/(.*)\/(.*)/]
682
- fil = $2
683
- fil = file unless fil
684
- f2 = fil.gsub('.gz', '')
685
- zip7(f2)
686
- FileUtils.rm_f f2
687
- end
688
- =end
689
- end
690
- end
691
-
692
- def unpack(opts = {})
693
- items = if opts[:path]
694
- [pack_path(opts[:path])]
695
- else
696
- Dir[pack_path('**/*')]
697
- end
698
-
699
- items.each do |file|
700
- unless File.directory? file
701
- relative = file.clone
702
- relative.gsub!(@rsync_work_dir, '')
703
- relative.gsub!(/\A[\\|\/]\.rsync[\\|\/]\.pack[\\|\/]/, '')
704
- fil = relative
705
- folder = "."
706
- folder, fil = $1, $2 if relative[/(.*)\/(.*)/]
707
- #puts "Relative: #{relative}, Folder: #{folder}, File: #{fil} (Origin: #{file})"
708
-
709
- path = File.join(@rsync_work_dir, folder)
710
- FileUtils.mkdir_p path
711
- unpack_file(file, path)
712
- end
713
- end
714
- end
715
-
716
- def command_lines(cmd, opts = [], chdir = true, redirect = '')
717
- command(cmd, opts, chdir).split("\n")
718
- end
719
-
720
- def command(cmd, opts = [], chdir = true, redirect = '', &block)
721
- path = @rsync_work_dir || @rsync_dir || @path
722
-
723
- opts << "--stats" if @stats
724
- opts << "--timeout=60"
725
-
726
- opts = [opts].flatten.map {|s| s }.join(' ') # escape()
727
- rsync_cmd = "rsync #{cmd} #{opts} #{redirect} 2>&1"
728
-
729
- while rsync_cmd[WINDRIVE] do
730
- drive = rsync_cmd[WINDRIVE]
731
- #if ENV['six-app-root']
732
- # rsync_cmd.gsub!(drive, "\"#{ENV['six-app-root']}") # /cygdrive/#{$1}
733
- #else
734
- rsync_cmd.gsub!(drive, "\"/cygdrive/#{$1}")
735
- #end
736
- end
737
-
738
- if @logger
739
- @logger.debug(rsync_cmd)
740
- end
741
-
742
- out = nil
743
- if chdir && (Dir.getwd != path)
744
- Dir.chdir(path) { out = run_command(rsync_cmd, &block) }
745
- else
746
- out = run_command(rsync_cmd, &block)
747
- end
748
-
749
- #@logger.debug(out)
750
-
751
- out
752
- end
753
-
754
- def run_command(rsync_cmd, &block)
755
- # TODO: Make this switchable? Verbosity ?
756
- # Or actually parse this live for own stats?
757
- #puts rsync_cmd
758
- s = nil
759
- out = ''
760
- #$stdout.sync = true # Seems to fix C:/Packaging/six-updater/NEW - Copy/ruby/lib/ruby/gems/1.9.1/gems/log4r-1.0.5/lib/log4r/outputter/iooutputter.rb:43:in `flush': Broken pipe (Errno::EPIPE)
761
-
762
- # Simpler method but on windows the !? exitstatus is not working properly..
763
- # Does nicely display error output in logwindow though
764
- io = IO.popen(rsync_cmd)
765
- io.sync = true
766
- io.each do |buffer|
767
- process_msg buffer
768
- out << buffer
769
- end
770
- status = 0
771
- if out.to_s[/rsync error: .* \(code ([0-9]*)\)/]
772
- status = $1.to_s.to_i
773
- end
774
- #puts "Status: #{status} Exitstatus: #{$?.exitstatus}"
775
- if status > 0
776
- if status == 1 && out == ''
777
- return ''
778
- end
779
- if out.to_s =~ /max connections \((.*)\) reached/
780
- @logger.warn "Server reached maximum connections."
781
- end
782
- raise Rsync::RsyncExecuteError.new(rsync_cmd + ':' + out.to_s)
783
- end
784
-
785
- =begin
786
- #`#{rsync_cmd}`.chomp
787
- status = Open3.popen3(rsync_cmd) { |io_in, io_out, io_err, waitth|
788
- io_out.sync = true
789
- io_err.sync = true
790
-
791
- io_out.each do |buffer|
792
- process_msg buffer
793
- out << buffer
794
- end
795
-
796
- #while !io_out.eof?
797
- # buffer = io_out.readline
798
- # # print buf#.gsub("\r", '')
799
- # process_msg buffer
800
- # out << buffer
801
- #end
802
- error = io_err.gets
803
- if error
804
- @logger.debug "Error: " + error.chomp
805
- # exit
806
- end
807
- # puts "Result: " + io_out.gets
808
- s = waitth.value
809
- }
810
- # FIXME: This doesn't work with the new popen or is there a way?
811
- if s.exitstatus > 0
812
- @logger.debug "Exitstatus: #{s.exitstatus}"
813
- if (s.exitstatus == 1 && out.size == 0)# || s.exitstatus == 5
814
- return ''
815
- end
816
- if out.to_s =~ /max connections \((.*)\) reached/
817
- @logger.warn "Server reached maximum connections."
818
- end
819
- raise Rsync::RsyncExecuteError.new(rsync_cmd + ':' + out.to_s)
820
- end
821
- =end
822
- status
823
- end
824
-
825
- def process_msg(msg)
826
- if msg[/[k|m|g]?B\/s/i]
827
- msg.gsub!("\n", '')
828
- print "#{msg}\r" if @verbose
829
- else
830
- @logger.debug msg
831
- print msg if @verbose
832
- end
833
- msg
834
-
835
- =begin
836
- m = nil
837
- if msg[/\r/]
838
- # TODO; must still be written even if there is no next message :P
839
-
840
- if @write
841
- print msg
842
- end
843
- m = msg.gsub("\r", '')
844
- #@previous = m
845
- else
846
- m = msg
847
- #if @previous
848
- # @logger.debug @previous
849
- # @previous = nil
850
- #end
851
- #unless @previous
852
- # @logger.debug m
853
- #end
854
- @logger.debug m
855
- puts m if @write
856
- end
857
- m
858
- =end
859
- end
860
- end
861
- end
862
- end
863
- end
1
+ # encoding: UTF-8
2
+
3
+ module Six
4
+ module Repositories
5
+ module Rsync
6
+ # TODO: Configure somewhere!
7
+ KEY = "C:/users/sb/documents/keys/id_rsa.ppk"
8
+ # TODO: Linux
9
+ RSH = "-r --rsh=\"'#{File.join(BASE_PATH, "tools", "bin", "cygnative.exe")}' plink.exe -i #{KEY}\""
10
+
11
+ DIR_RSYNC = '.rsync'
12
+ DIR_PACK = File.join(DIR_RSYNC, '.pack')
13
+ REGEX_FOLDER = /(.*)[\\|\/](.*)/
14
+ class RsyncExecuteError < StandardError; end
15
+ class RsyncError < StandardError; end
16
+
17
+ class Lib
18
+ attr_accessor :verbose
19
+ PROTECTED = false
20
+ WINDRIVE = /\"(\w)\:/
21
+ DEFAULT_CONFIG = {:hosts => [], :exclude => []}.to_yaml
22
+ DEFAULT_TIMEOUT = 60
23
+ PARAMS = if PROTECTED
24
+ "--dry-run --times -O --no-whole-file -r --delete --progress -h --exclude=.rsync"
25
+ else
26
+ "--times -O --no-whole-file -r --delete --progress -h --exclude=.rsync"
27
+ end
28
+
29
+ def initialize(base = nil, logger = nil)
30
+ @rsync_dir = nil
31
+ @rsync_work_dir = nil
32
+ @path = nil
33
+ @stats = false
34
+ @verbose = true
35
+ @logger = logger
36
+
37
+ @repos_local = {:pack => Hash.new, :wd => Hash.new, :version => 0}
38
+ @repos_remote = {:pack => Hash.new, :wd => Hash.new, :version => 0}
39
+
40
+ if base.is_a?(Rsync::Base)
41
+ @rsync_dir = base.repo.path
42
+ @rsync_work_dir = base.dir.path if base.dir
43
+ elsif base.is_a?(Hash)
44
+ @rsync_dir = base[:repository]
45
+ @rsync_work_dir = base[:working_directory]
46
+ end
47
+
48
+ etc = File.join(TOOLS_PATH, 'etc')
49
+ FileUtils.mkdir_p etc
50
+ fstab = File.join(etc, 'fstab')
51
+ str = ""
52
+ str = File.open(fstab) {|file| file.read} if File.exists?(fstab)
53
+ unless str[/cygdrive/]
54
+ str += "\nnone /cygdrive cygdrive user,noacl,posix=0 0 0\n"
55
+ File.open(fstab, 'w') {|file| file.puts str}
56
+ end
57
+ end
58
+
59
+ def status
60
+ @logger.info "Showing changes on #{@rsync_work_dir}"
61
+ @config = load_config
62
+ unless @config
63
+ @logger.error "Not an Rsync repository!"
64
+ return
65
+ end
66
+
67
+ load_repos(:local)
68
+ load_repos(:remote)
69
+
70
+ @logger.info "Calculating Checksums..."
71
+ @repos_local[:wd] = calc_sums(:wd)
72
+ # Added or Changed files
73
+ ar = Dir[File.join(@rsync_work_dir, '/**/*')]
74
+
75
+ change = false
76
+ i = 0
77
+ @repos_local[:wd].each_pair do |key, value|
78
+ i += 1
79
+ if value != @repos_remote[:wd][key]
80
+ change = true
81
+ @logger.info "Modified: #{i}/#{@repos_local[:wd].size}: #{key}"
82
+ end
83
+ end
84
+
85
+ # Deleted files
86
+ @logger.info "Checking for deleted files..."
87
+
88
+ i = 0
89
+ @repos_remote[:wd].each_pair do |key, value|
90
+ i += 1
91
+ if @repos_local[:wd][key].nil?
92
+ @logger.info "Removed: #{packed}"
93
+ end
94
+ end
95
+ end
96
+
97
+ def init
98
+ @logger.info "Processing: #{rsync_path}"
99
+ if File.exists? rsync_path
100
+ @logger.error "Seems to already be an Rsync repository, Aborting!"
101
+ raise RsyncError
102
+ end
103
+ if File.exists? @rsync_work_dir
104
+ @logger.error "Seems to already be a folder, Aborting!"
105
+ raise RsyncError
106
+ end
107
+ FileUtils.mkdir_p pack_path
108
+ save_config(config)
109
+ save_repos(:local)
110
+ save_repos(:remote)
111
+ end
112
+
113
+ def clone(repository, name, opts = {})
114
+ @path = opts[:path] || '.'
115
+ @rsync_work_dir = opts[:path] ? File.join(@path, name) : name
116
+
117
+ # TODO: Solve logger mess completely.
118
+ @logger = opts[:log] if opts[:log]
119
+
120
+ case repository
121
+ when Array
122
+ config[:hosts] += repository
123
+ when String
124
+ config[:hosts] << repository
125
+ end
126
+
127
+ begin
128
+ init
129
+
130
+ # TODO: Eval move to update?
131
+ arr_opts = []
132
+ arr_opts << "-I" if opts[:force]
133
+ begin
134
+ update('', arr_opts)
135
+ rescue RsyncError
136
+ @logger.error "Unable to sucessfully update, aborting..."
137
+ @logger.debug "#{$!}"
138
+ FileUtils.rm_rf @rsync_work_dir if File.exists?(@rsync_work_dir)
139
+ end
140
+ rescue RsyncError
141
+ @logger.error "Unable to initialize"
142
+ @logger.debug "#{$!}"
143
+ end
144
+
145
+ opts[:bare] ? {:repository => @rsync_work_dir} : {:working_directory => @rsync_work_dir}
146
+ end
147
+
148
+ def update(cmd, x_opts = [], opts = {})
149
+ @logger.debug "Checking for updates..."
150
+ @config = load_config
151
+ unless @config
152
+ @logger.error "Not an Rsync repository!"
153
+ raise RsyncError
154
+ end
155
+
156
+ unless config[:hosts].size > 0
157
+ @logger.error "No hosts configured!"
158
+ raise RsyncError
159
+ end
160
+
161
+ load_repos(:local)
162
+ load_repos(:remote)
163
+
164
+ hosts = config[:hosts].clone
165
+ host = hosts.sample
166
+
167
+ if opts[:force]
168
+ done = false
169
+ b = false
170
+ verbose = @verbose
171
+ @verbose = false
172
+ while hosts.size > 0 && !done do
173
+ # FIXME: Nasty
174
+ host = hosts.sample if b
175
+ b = true
176
+ hosts -= [host]
177
+ @logger.info "Trying #{host}"
178
+
179
+ begin
180
+ arr_opts = []
181
+ arr_opts << PARAMS
182
+ arr_opts += x_opts
183
+ if host[/\A(\w)*\@/]
184
+ arr_opts << RSH #"-e ssh"
185
+ end
186
+ arr_opts << esc(File.join(host, '.pack/.'))
187
+ arr_opts << esc(pack_path)
188
+ command(cmd, arr_opts)
189
+ calc
190
+ save_repos
191
+ done = true
192
+ rescue
193
+ @logger.debug "#{$!}"
194
+ end
195
+ end
196
+ @verbose = verbose
197
+ raise RsyncError if !done
198
+ else
199
+ #reset(:hard => true)
200
+ calc
201
+ save_repos
202
+
203
+ # fetch latest sums and only update when changed
204
+ compare_sums(true, host)
205
+ end
206
+ end
207
+
208
+ # TODO: Allow local-self healing, AND remote healing. reset and fetch?
209
+ def reset(opts = {})
210
+ @logger.info "Resetting!"
211
+ if opts[:hard]
212
+ @config = load_config
213
+ calc
214
+ save_repos
215
+ compare_sums(false)
216
+ end
217
+ end
218
+
219
+ # TODO: WIP
220
+ def add(file)
221
+ @logger.error "Please use commit instead!"
222
+ return
223
+ @logger.info "Adding #{file}"
224
+ if (file == ".")
225
+ load_repos(:remote)
226
+ @logger.info "Calculating Checksums..."
227
+ ar = Dir[File.join(@rsync_work_dir, '/**/*')]
228
+
229
+ change = false
230
+ i = 0
231
+ ar.each do |file|
232
+ i += 1
233
+ unless file[/\.gz\Z/]
234
+ relative = file.clone
235
+ relative.gsub!(@rsync_work_dir, '')
236
+ relative.gsub!(/\A[\\|\/]/, '')
237
+
238
+ checksum = md5(file)
239
+ if checksum != @repos_remote[:wd][relative]
240
+ change = true
241
+ @logger.info "Packing #{i}/#{ar.size}: #{file}"
242
+ gzip(file)
243
+ @repos_remote[:wd][relative] = checksum
244
+ @repos_remote[:pack]["#{relative}.gz"] = md5("#{file}.gz")
245
+ FileUtils.mv("#{file}.gz", pack_path("#{relative}.gz"))
246
+ end
247
+ end
248
+ end
249
+ save_repos if change
250
+ else
251
+
252
+ end
253
+ end
254
+
255
+ def commit
256
+ @logger.info "Committing changes on #{@rsync_work_dir}"
257
+ @config = load_config
258
+ unless @config
259
+ @logger.error "Not an Rsync repository!"
260
+ return
261
+ end
262
+
263
+ unless config[:hosts].size > 0
264
+ @logger.error "No hosts configured!"
265
+ return
266
+ end
267
+
268
+ load_repos(:local)
269
+ load_repos(:remote)
270
+
271
+ @logger.info "Calculating Checksums..."
272
+ @repos_local[:wd] = calc_sums(:wd)
273
+ # Added or Changed files
274
+ ar = Dir[File.join(@rsync_work_dir, '/**/*')]
275
+
276
+ change = false
277
+ i = 0
278
+ @repos_local[:wd].each_pair do |key, value|
279
+ i += 1
280
+ if value != @repos_remote[:wd][key]
281
+ change = true
282
+ @logger.info "Packing #{i}/#{@repos_local[:wd].size}: #{key}"
283
+ file = File.join(@rsync_work_dir, key)
284
+ file[REGEX_FOLDER]
285
+ folder = $1
286
+ folder.gsub!(@rsync_work_dir, '')
287
+ gzip(file)
288
+ @repos_local[:pack]["#{key}.gz"] = md5("#{file}.gz")
289
+ FileUtils.mkdir_p pack_path(folder) if folder
290
+ FileUtils.mv("#{file}.gz", pack_path("#{key}.gz"))
291
+ end
292
+ end
293
+
294
+ # Deleted files
295
+ @logger.info "Checking for deleted files..."
296
+
297
+ i = 0
298
+ @repos_remote[:wd].each_pair do |key, value|
299
+ i += 1
300
+ if @repos_local[:wd][key].nil?
301
+ packed = "#{key}.gz"
302
+ change = true
303
+ file = pack_path(packed)
304
+ file[REGEX_FOLDER]
305
+ folder = $2
306
+
307
+ @logger.info "Removing: #{packed}"
308
+ @repos_local[:wd].delete key
309
+ @repos_local[:pack].delete packed
310
+ FileUtils.rm_f(file) if File.exists?(file)
311
+ end
312
+ end
313
+
314
+ if change
315
+ @logger.info "Changes found!"
316
+ save_repos(:local)
317
+
318
+ host = config[:hosts].sample
319
+
320
+ verfile_srv = File.join(".pack", ".repository.yml")
321
+ verbose = @verbose
322
+ @verbose = false
323
+ begin
324
+ fetch_file(verfile_srv, host)
325
+ rescue
326
+ # FIXME: Should never assume that :)
327
+ @logger.warn "Unable to retrieve version file from server, repository probably doesnt exist!"
328
+ @logger.debug "#{$!}"
329
+ # raise RsyncExecuteError
330
+ end
331
+ @verbose = verbose
332
+
333
+ load_repos(:remote)
334
+
335
+ if @repos_local[:version] < @repos_remote[:version] # && !force
336
+ @logger.warn "WARNING, version on server is NEWER, aborting!"
337
+ raise RsyncError
338
+ end
339
+ @repos_local[:version] += 1
340
+ @repos_remote[:version] = @repos_local[:version]
341
+ @repos_remote[:pack] = @repos_local[:pack].clone
342
+ @repos_remote[:wd] = @repos_local[:wd].clone
343
+ save_repos(:remote)
344
+ save_repos(:local)
345
+ push(host)
346
+ else
347
+ @logger.info "No changes found!"
348
+ end
349
+ end
350
+
351
+ def push(host = nil)
352
+ @logger.info "Pushing..."
353
+ @config = load_config
354
+ host = config[:hosts].sample unless host
355
+ # TODO: UNCLUSTERFUCK
356
+ arr_opts = []
357
+ arr_opts << PARAMS
358
+
359
+ # Upload .pack changes
360
+ if host[/\A(\w)*\@/]
361
+ arr_opts << RSH
362
+ end
363
+ arr_opts << esc(pack_path('.'))
364
+ arr_opts << esc(File.join(host, '.pack'))
365
+
366
+ command('', arr_opts)
367
+ end
368
+
369
+ def compare_set(typ, host, online = true)
370
+ load_repos(:local)
371
+ load_repos(:remote)
372
+
373
+ #if local[typ][:md5] == remote[typ][:md5]
374
+ # @logger.info "#{typ} Match!"
375
+ #else
376
+ # @logger.info "#{typ} NOT match, updating!"
377
+
378
+ mismatch = []
379
+ @repos_remote[typ].each_pair do |key, value|
380
+ if value == @repos_local[typ][key]
381
+ #@logger.info "Match! #{key}"
382
+ else
383
+ @logger.debug "Mismatch! #{key}"
384
+ mismatch << key
385
+ end
386
+ end
387
+
388
+ if mismatch.size > 0
389
+ case typ
390
+ when :pack
391
+ # direct unpack of gz into working folder
392
+ # Update file
393
+ if online
394
+ hosts = config[:hosts].clone
395
+ done = false
396
+
397
+ ## Pack
398
+ if online
399
+ b = false
400
+ while hosts.size > 0 && !done do
401
+ # FIXME: Nasty
402
+ if b
403
+ host = hosts.sample
404
+ @logger.info "Trying #{host}"
405
+ end
406
+ slist = nil
407
+ b = true
408
+ hosts -= [host]
409
+ begin
410
+ # TODO: Progress bar
411
+ if mismatch.size > (@repos_remote[typ].size / 2)
412
+ @logger.info "Many files mismatched (#{mismatch.size}), running full update on .pack folder"
413
+ arr_opts = []
414
+ arr_opts << PARAMS
415
+ if host[/\A(\w)*\@/]
416
+ arr_opts << RSH
417
+ end
418
+
419
+ arr_opts << esc(File.join(host, '.pack/.'))
420
+ arr_opts << esc(pack_path)
421
+ command('', arr_opts)
422
+ else
423
+ c = mismatch.size
424
+ @logger.info "Fetching #{mismatch.size} files... Please wait"
425
+ slist = File.join(TEMP_PATH, ".six-updater_#{rand 9999}-list")
426
+ slist.gsub!("\\", "/")
427
+ File.open(slist, 'w') do |f|
428
+ mismatch.each { |e| f.puts e }
429
+ end
430
+ arr_opts = []
431
+ arr_opts << PARAMS
432
+ arr_opts << RSH if host[/\A(\w)*\@/]
433
+
434
+ cyg_slist = "\"#{slist}\""
435
+
436
+ while cyg_slist[WINDRIVE] do
437
+ drive = cyg_slist[WINDRIVE]
438
+ cyg_slist.gsub!(drive, "\"/cygdrive/#{$1}")
439
+ end
440
+ arr_opts << "--files-from=#{cyg_slist}"
441
+
442
+ arr_opts << esc(File.join(host, '.pack/.'))
443
+ arr_opts << esc(pack_path)
444
+
445
+ command('', arr_opts)
446
+ end
447
+ done = true
448
+ rescue
449
+ @logger.warn "Failure"
450
+ @logger.debug "#{$!}"
451
+ ensure
452
+ FileUtils.rm_f slist if slist
453
+ end
454
+ end
455
+ @logger.warn "There was a problem during updating, please retry!" unless done
456
+ end
457
+ end
458
+ when :wd
459
+ mismatch.each_with_index do |e, index|
460
+ # TODO: Nicer progress bar...
461
+ @logger.info "Unpacking #{index + 1}/#{mismatch.size}: #{e}"
462
+ unpack(:path => "#{e}.gz")
463
+ end
464
+ end
465
+ end
466
+
467
+ @repos_local[typ].each_pair do |key, value|
468
+ del_file(key, typ) unless config[:exclude].include?(key) || !@repos_remote[typ][key].nil?
469
+ end
470
+
471
+ @repos_local[typ] = calc_sums(typ)
472
+ @repos_local[:version] = @repos_remote[:version]
473
+ save_repos
474
+ end
475
+
476
+ def compare_sums(online = true, host = config[:hosts].sample)
477
+ hosts = config[:hosts].clone
478
+ done = false
479
+
480
+ ## Pack
481
+ if online
482
+ b = false
483
+ verbose = @verbose
484
+ @verbose = false
485
+ while hosts.size > 0 && !done do
486
+ # FIXME: Nasty
487
+ host = hosts.sample if b
488
+ b = true
489
+ hosts -= [host]
490
+ @logger.info "Trying #{host}"
491
+
492
+ begin
493
+ FileUtils.cp(pack_path(".repository.yml"), rsync_path(".repository-pack.yml"))
494
+ fetch_file(".pack/.repository.yml", host)
495
+
496
+ load_repos(:remote)
497
+ load_repos(:local)
498
+
499
+ if @repos_local[:version] > @repos_remote[:version] # && !force
500
+ @logger.warn "WARNING, version on server is OLDER, aborting!"
501
+ FileUtils.cp(rsync_path(".repository-pack.yml"), pack_path(".repository.yml"))
502
+ raise RsyncError
503
+ end
504
+ done = true
505
+ rescue
506
+ @logger.debug "#{$!}"
507
+ ensure
508
+ FileUtils.rm(rsync_path(".repository-pack.yml"))
509
+ end
510
+ end
511
+ @verbose = verbose
512
+ end
513
+ if done && online
514
+ # TODO: Don't do actions when not online
515
+ @logger.info "Verifying Packed files..."
516
+ compare_set(:pack, host)
517
+ @logger.info "Verifying Unpacked files..."
518
+ compare_set(:wd, host)
519
+ save_repos
520
+ end
521
+ end
522
+
523
+ private
524
+ def esc(val); "\"#{val}\""; end
525
+ def escape(s); "\"" + s.to_s.gsub('\"', '\"\\\"\"') + "\""; end
526
+
527
+ def config
528
+ cfg = @config ||= YAML::load(DEFAULT_CONFIG)
529
+ cfg[:exclude] = [] unless cfg[:exclude]
530
+ cfg[:hosts] = [] unless cfg[:hosts]
531
+ cfg
532
+ end
533
+
534
+ def rsync_path(path = '')
535
+ p = File.join(@rsync_work_dir, DIR_RSYNC)
536
+ p = File.join(p, path) unless path.size == 0
537
+ p
538
+ end
539
+
540
+ def pack_path(path = '')
541
+ p = File.join(@rsync_work_dir, DIR_PACK)
542
+ p = File.join(p, path) unless path.size == 0
543
+ p
544
+ end
545
+
546
+ def fetch_file(path, host)
547
+ path[/(.*)\/(.*)/]
548
+ folder, file = $1, $2
549
+ folder = "." unless folder
550
+ file = path unless file
551
+ # Only fetch a specific file
552
+ @logger.debug "Fetching #{path} from #{host}"
553
+ arr_opts = []
554
+ arr_opts << PARAMS
555
+ if host[/\A(\w)*\@/]
556
+ arr_opts << RSH
557
+ end
558
+ arr_opts << esc(File.join(host, path))
559
+ arr_opts << esc(rsync_path(folder))
560
+
561
+ command('', arr_opts)
562
+ end
563
+
564
+ def calc
565
+ @logger.info "Calculating checksums"
566
+ [:pack, :wd].each { |t| @repos_local[t] = calc_sums(t) }
567
+ end
568
+
569
+ def calc_sums(typ)
570
+ @logger.debug "Calculating checksums of #{typ} files"
571
+ ar = []
572
+ reg = case typ
573
+ when :pack
574
+ ar = Dir[pack_path('**/*')]
575
+ /\A[\\|\/]\.rsync[\\|\/]\.pack[\\|\/]/
576
+ when :wd
577
+ ar = Dir[File.join(@rsync_work_dir, '/**/*')]
578
+ /\A[\\|\/]/
579
+ end
580
+ h = Hash.new
581
+ ar.each do |file|
582
+ relative = file.clone
583
+ relative.gsub!(@rsync_work_dir, '')
584
+ relative.gsub!(reg, '')
585
+
586
+ sum = md5(file)
587
+ h[relative] = sum if sum && !config[:exclude].include?(relative)
588
+ end
589
+ h
590
+ end
591
+
592
+ def load_config; load_yaml(File.join(rsync_path, 'config.yml')); end
593
+
594
+ def load_yaml(file)
595
+ if File.exists?(file)
596
+ YAML::load_file(file)
597
+ else
598
+ nil
599
+ end
600
+ end
601
+
602
+ def save_default_config
603
+ FileUtils.mkdir_p rsync_path
604
+ save_config(config)
605
+ end
606
+
607
+ def save_config(config = YAML::load(DEFAULT_CONFIG))
608
+ File.open(File.join(rsync_path, 'config.yml'), 'w') { |file| file.puts config.to_yaml }
609
+ end
610
+
611
+ def save_repos(typ = :local)
612
+ file, config = nil, nil
613
+ case typ
614
+ when :local
615
+ file = rsync_path('.repository.yml')
616
+ config = @repos_local.clone
617
+ when :remote
618
+ file = pack_path('.repository.yml')
619
+ config = @repos_remote.clone
620
+ end
621
+ config[:pack] = config[:pack].sort
622
+ config[:wd] = config[:wd].sort
623
+ File.open(file, 'w') { |file| file.puts config.to_yaml }
624
+ end
625
+
626
+ def load_repos(typ)
627
+ config = Hash.new
628
+ case typ
629
+ when :local
630
+ File.open(rsync_path('.repository.yml')) { |file| config = YAML::load(file) }
631
+ when :remote
632
+ File.open(pack_path('.repository.yml')) { |file| config = YAML::load(file) }
633
+ end
634
+
635
+ [:wd, :pack].each do |t|
636
+ h = Hash.new
637
+ config[t].each { |e| h[e[0]] = e[1] }
638
+ config[t] = h
639
+ end
640
+
641
+ case typ
642
+ when :local
643
+ @repos_local = config
644
+ when :remote
645
+ @repos_remote = config
646
+ end
647
+ end
648
+
649
+ def del_file(file, typ, opts = {})
650
+ file = case typ
651
+ when :pack
652
+ File.join(DIR_PACK, file)
653
+ when :wd
654
+ file
655
+ end
656
+ if File.exists?(file)
657
+ FileUtils.rm_f File.join(@rsync_work_dir, file)
658
+ @logger.info "Removed: #{file}"
659
+ end
660
+ end
661
+
662
+ def md5(path)
663
+ unless File.directory? path
664
+ path[/(.*)[\/|\\](.*)/]
665
+ folder, file = $1, $2
666
+ Dir.chdir(folder) do
667
+ r = %x[md5sum #{esc(file)}]
668
+ @logger.debug r
669
+ r[/\A\w*/]
670
+ end
671
+ end
672
+ end
673
+
674
+ def zip7(file)
675
+ out = %x[7z x #{esc(file)} -y]
676
+ @logger.debug out
677
+ out
678
+ end
679
+
680
+ def gzip(file)
681
+ @logger.debug "Gzipping #{file}"
682
+ out = %x[gzip -f --best --rsyncable --keep #{esc(file)}]
683
+ @logger.debug out
684
+ end
685
+
686
+ def unpack_file(file, path)
687
+ Dir.chdir(path) do |dir|
688
+ zip7(file)
689
+ # TODO: Evaluate if this is actually wanted / useful at all..
690
+ =begin
691
+ if file[/\.tar\.?/]
692
+ file[/(.*)\/(.*)/]
693
+ fil = $2
694
+ fil = file unless fil
695
+ f2 = fil.gsub('.gz', '')
696
+ zip7(f2)
697
+ FileUtils.rm_f f2
698
+ end
699
+ =end
700
+ end
701
+ end
702
+
703
+ def unpack(opts = {})
704
+ items = if opts[:path]
705
+ [pack_path(opts[:path])]
706
+ else
707
+ Dir[pack_path('**/*')]
708
+ end
709
+
710
+ items.each do |file|
711
+ unless File.directory? file
712
+ relative = file.clone
713
+ relative.gsub!(@rsync_work_dir, '')
714
+ relative.gsub!(/\A[\\|\/]\.rsync[\\|\/]\.pack[\\|\/]/, '')
715
+ fil = relative
716
+ folder = "."
717
+ folder, fil = $1, $2 if relative[/(.*)\/(.*)/]
718
+ #puts "Relative: #{relative}, Folder: #{folder}, File: #{fil} (Origin: #{file})"
719
+
720
+ path = File.join(@rsync_work_dir, folder)
721
+ FileUtils.mkdir_p path
722
+ unpack_file(file, path)
723
+ end
724
+ end
725
+ end
726
+
727
+ def command_lines(cmd, opts = [], chdir = true, redirect = '')
728
+ command(cmd, opts, chdir).split("\n")
729
+ end
730
+
731
+ def command(cmd, opts = [], chdir = true, redirect = '', &block)
732
+ path = @rsync_work_dir || @rsync_dir || @path
733
+
734
+ opts << "--stats" if @stats
735
+ opts << "--timeout=#{DEFAULT_TIMEOUT}"
736
+
737
+ opts = [opts].flatten.map {|s| s }.join(' ') # escape()
738
+ rsync_cmd = "rsync #{cmd} #{opts} #{redirect} 2>&1"
739
+
740
+ while rsync_cmd[WINDRIVE] do
741
+ drive = rsync_cmd[WINDRIVE]
742
+ rsync_cmd.gsub!(drive, "\"/cygdrive/#{$1}")
743
+ end
744
+
745
+ if @logger
746
+ @logger.debug(rsync_cmd)
747
+ end
748
+
749
+ out = nil
750
+ if chdir && (Dir.getwd != path)
751
+ Dir.chdir(path) { out = run_command(rsync_cmd, &block) }
752
+ else
753
+ out = run_command(rsync_cmd, &block)
754
+ end
755
+
756
+ out
757
+ end
758
+
759
+ def run_command(rsync_cmd, &block)
760
+ # TODO: Make this switchable? Verbosity ?
761
+ # Or actually parse this live for own stats?
762
+ s = nil
763
+ out = ''
764
+
765
+ # Simpler method but on windows the !? exitstatus is not working properly..
766
+ # Does nicely display error output in logwindow though
767
+ io = IO.popen(rsync_cmd)
768
+ io.sync = true
769
+ io.each do |buffer|
770
+ process_msg buffer
771
+ out << buffer
772
+ end
773
+
774
+ out[/rsync error: .* \(code ([0-9]*)\)/]
775
+ status = $1 ? $1 : 0
776
+
777
+ if status > 0
778
+ return '' if status == 1 && out == ''
779
+ case out
780
+ when /max connections \((.*)\) reached/
781
+ @logger.warn "Server reached maximum connections."
782
+ end
783
+ raise Rsync::RsyncExecuteError.new(rsync_cmd + ':' + out)
784
+ end
785
+
786
+ status
787
+ end
788
+
789
+ def process_msg(msg)
790
+ if msg[/[k|m|g]?B\/s/i]
791
+ msg.gsub!("\n", '')
792
+ print "#{msg}\r" if @verbose
793
+ else
794
+ @logger.debug msg
795
+ print msg if @verbose
796
+ end
797
+ msg
798
+ end
799
+
800
+ end
801
+ end
802
+ end
803
+ end