ksync 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (7) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README +31 -0
  3. data/Rakefile.rb +8 -0
  4. data/bin/ksync +30 -0
  5. data/lib/ksync.rb +159 -0
  6. data/test/test_ksync.rb +109 -0
  7. metadata +78 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Kirk Adoniadis (kiriakos.adoniadis@gmail.com)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,31 @@
1
+ This gem is a simple folder synchroniser between a changing source folder and a
2
+ 'backup' destination folder (presumed to be unchanged). It uses a list of files hash which is a
3
+ hash containing the filepath as the key and file information [FileSize, FileMTime] as value.
4
+
5
+ In order to compare the source and destination folders:
6
+ a. it always creates a list of files hash for the source
7
+ b. it uses an existing list of files hash for the destination or creates one if non existing
8
+ c. It compares the 2 lists of files hash and copies/deletes from destination accordingly.
9
+ d. It compares files based on size and then on last modification date. It may optionally use
10
+ hash calculation (inactive by default because it is slow)
11
+
12
+ NOTE: If any files in the destination folder have changed and the files hash exists in the
13
+ destination and the -u option (use hash) is not specified, then these changes will NOT be detected.
14
+ This is a fast backup solution and presumes that the destination never changes.
15
+
16
+ The gem comes with a frontend, 'ksync' which can be used directly to invoke the object
17
+
18
+ Examples of usage:
19
+
20
+ To backup folder c:/dev to c:/dev_backup with default options :
21
+ > ksync c:/dev c:/dev_backup
22
+ The above, but inside rour ruby code :
23
+ > require 'ksync'
24
+ > KSync.new({:src => 'c:/dev', :dst => 'c:/dev_backup'}).do_sync
25
+ the method do_sync will return false if there were no changes, true otherwise
26
+
27
+ To backup folder c:/dev to c:/dev_backup forcing hash calculation (hash calculation will only be used if the files have
28
+ the same size and modification date):
29
+ > ksync --use_hash c:/dev c:/dev_backup
30
+ or
31
+ > ksync -u c:/dev c:/dev_backup
data/Rakefile.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'test'
5
+ end
6
+
7
+ desc "Run tests"
8
+ task :default => :test
data/bin/ksync ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env jruby
2
+
3
+ require 'optparse'
4
+ require 'ksync'
5
+
6
+ def message(o)
7
+ puts o
8
+ exit
9
+ end
10
+
11
+ options = {}
12
+ options[:real_copy] = true
13
+ options[:use_hash] = false
14
+ options[:verbose] = 0
15
+ o = OptionParser.new
16
+ o.banner = "Usage: #{$0} [options] source_folder destination_folder"
17
+ o.on('-d', '--dry_run', 'dry run (default : do the real copy - no dry run)') { |s| options[:real_copy] = false }
18
+ o.on('-v', '--verbosity=value', 'The level of verbosity (1..3) (default = 0 : very silent)') { |s| options[:verbose] = s.to_i }
19
+ o.on('-u', '--use_hash', 'use hash calculation (default : dont use hash)') { |s| options[:use_hash] = true }
20
+ o.on('-h', '--help') { message(o) }
21
+
22
+ o.parse!
23
+
24
+ if !ARGV[0] || !ARGV[1]
25
+ message(o)
26
+ else
27
+ options[:src] = ARGV[0]
28
+ options[:dst] = ARGV[1]
29
+ KSync.new(options).do_sync
30
+ end
data/lib/ksync.rb ADDED
@@ -0,0 +1,159 @@
1
+ require 'fileutils'
2
+ require 'digest'
3
+ require 'find'
4
+
5
+ class KSync
6
+
7
+ SSIZE_I=0
8
+ MTIME_I=1
9
+
10
+ attr_accessor :all_h, :ll
11
+ def initialize(init_opt)
12
+ @all_h={}
13
+ @changes = false
14
+ init_opt[:real_copy] = init_opt[:real_copy] == nil ? true : init_opt[:real_copy]
15
+ init_opt[:use_hash] = init_opt[:use_hash] == nil ? false : init_opt[:use_hash]
16
+ init_opt[:verbose] = init_opt[:verbose] == nil ? 0 : init_opt[:verbose]
17
+ return nil if !init_opt[:src] | !init_opt[:dst]
18
+ @options = init_opt
19
+ @ll = [init_opt[:src], init_opt[:dst]]
20
+ create_hash
21
+ end
22
+
23
+ def do_sync
24
+ src=0
25
+ dst=1
26
+ # browse source hash to detect new and changed files
27
+ return false unless File.exists?(ll.first)
28
+ @all_h[ll[src]].each do |k,v|
29
+ src_fp = File.join(@ll[src],k)
30
+ dst_fp = File.join(@ll[dst],k)
31
+ if !@all_h[@ll[dst]][k]
32
+ puts "N: #{k}" if @options[:verbose] > 0
33
+ do_copy(src_fp, dst_fp, v) if @options[:real_copy]
34
+ @changes = true
35
+ else
36
+ if !File.directory?(File.join(@ll[src],k))
37
+ if !same?(src_fp, dst_fp, v, all_h[@ll[dst]][k])
38
+ puts "C: #{k}" if @options[:verbose] > 0
39
+ do_copy(src_fp, dst_fp, v) if @options[:real_copy]
40
+ @changes = true
41
+ end
42
+ end
43
+ end
44
+ end
45
+ # browse destination hash to detect obsolete files
46
+ @all_h[ll[dst]].each do |k,v|
47
+ if !all_h[ll[src]][k]
48
+ dst_fp = File.join(ll[dst],k)
49
+ puts "D: #{dst_fp}" if @options[:verbose] > 0
50
+ do_delete dst_fp if @options[:real_copy]
51
+ @changes = true
52
+ end
53
+ end
54
+ #puts "there were changes" if @changes
55
+ #puts "nothing changed" if !@changes
56
+ return @changes
57
+ end
58
+
59
+ def get_hash(in_file)
60
+ if !File.exists?(in_file)
61
+ puts "ERR: inexistent file : #{in_file}"
62
+ return nil
63
+ end
64
+ sha1 = Digest::SHA1.new
65
+ begin
66
+ File.open(in_file) do |file|
67
+ buffer = ''
68
+ # Read the file 512 bytes at a time
69
+ while not file.eof
70
+ file.read(512, buffer)
71
+ sha1.update(buffer)
72
+ end
73
+ end
74
+ rescue
75
+ puts "ERR: while calculating hash for : #{in_file}: (#{$!})"
76
+ return nil
77
+ end
78
+ return sha1.to_s
79
+ end
80
+
81
+ def do_copy(src_fp, dst_fp, src)
82
+ begin
83
+ if File.directory?(src_fp)
84
+ file_op = "mkdir"
85
+ FileUtils.mkdir_p dst_fp
86
+ else
87
+ file_op = "cp"
88
+ FileUtils.copy_file src_fp, dst_fp
89
+ end
90
+ rescue
91
+ puts "ERR: while copying (#{file_op}) [#{src_fp} to #{dst_fp}] : (#{$!})"
92
+ end
93
+ end
94
+
95
+ def do_delete (dst_fp)
96
+ begin
97
+ if File.exists?(dst_fp)
98
+ if File.directory?(dst_fp)
99
+ file_op = 'remove_dir'
100
+ FileUtils.remove_dir(dst_fp)
101
+ else
102
+ file_op = 'rm'
103
+ FileUtils.rm dst_fp
104
+ end
105
+ else
106
+ puts "ERR: inexistent file/folder : #{dst_fp}"
107
+ end
108
+ rescue
109
+ puts "ERR: (#{file_op}) for #{dst_fp} : (#{$!})"
110
+ end
111
+ end
112
+
113
+ # if hash => also check hash if sizes are same
114
+ def same?(src_fp, dst_fp, src, dst)
115
+ size_ok = false
116
+ date_ok = false
117
+ if src[SSIZE_I] == dst[SSIZE_I] # sizes are the same
118
+ size_ok = true
119
+ if dst[MTIME_I] - dst[MTIME_I] >= 0
120
+ date_ok = true
121
+ else
122
+ puts "DATE diff for #{src_fp} vs #{dst_fp}" if @options[:verbose] > 1
123
+ end
124
+ else
125
+ puts "SIZE diff for #{src_fp} vs #{dst_fp}" if @options[:verbose] > 1
126
+ end
127
+ return false if !size_ok
128
+ return false if !date_ok
129
+ return true if !@options[:use_hash]
130
+ h1 = get_hash(src_fp)
131
+ h2 = get_hash(dst_fp)
132
+ if h1 != h2
133
+ puts "SHA1 diff for #{src_fp} vs #{dst_fp}" if @options[:verbose] > 1
134
+ return false
135
+ end
136
+ return true
137
+ end
138
+ def create_hash
139
+ if !File.exists?(@ll.first)
140
+ puts "source folder #{@ll.first} does not exist ! "
141
+ return
142
+ end
143
+ @ll.each do |prefix|
144
+ @all_h[prefix] = {}
145
+ next if prefix == @ll.last && @options[:real_copy] == false
146
+ puts "creating hash for #{prefix}" if @options[:verbose] > 1
147
+ cnt = 0
148
+ FileUtils.mkdir_p prefix if !File.exists?(prefix)
149
+ Find.find(prefix).each do |f|
150
+ next if prefix == f
151
+ ff = f.unpack('U*').pack('U*') # to get rid of funny encoding related errors
152
+ @all_h[prefix][ff.split(prefix)[1]] = [File.size(ff), File.mtime(ff)]
153
+ puts "(#{ff})" if @options[:verbose] > 2
154
+ puts cnt if cnt % 100 == 0 && @options[:verbose] > 2
155
+ cnt += 1
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,109 @@
1
+ gem 'minitest', '>= 4.4.0' # use the gem version, not the 1.9 bundled version of minitest
2
+ require 'minitest/autorun'
3
+
4
+ require 'ksync'
5
+ require 'fileutils'
6
+
7
+ require 'turn'
8
+
9
+ Turn.config.format = :outline
10
+
11
+ class KSyncTest < MiniTest::Unit::TestCase
12
+ def setup
13
+ begin
14
+ @src_path = File.join(File.dirname(__FILE__), "src/folder1/folder2")
15
+ @dst_path = File.join(File.dirname(__FILE__), "src")
16
+ create_tree("src")
17
+ @opts = {}
18
+ @opts = {:src => File.join(File.dirname(__FILE__), "src"), :dst => File.join(File.dirname(__FILE__), "dst")}
19
+ rescue
20
+ puts "error trying to do the setup :#{$!}"
21
+ end
22
+ end
23
+ def teardown
24
+ FileUtils.rm_rf @opts[:src]
25
+ FileUtils.rm_rf @opts[:dst]
26
+ end
27
+
28
+ # support methods
29
+ def create_tree(base_path)
30
+ l1 = File.join(File.dirname(__FILE__), base_path)
31
+ l2 = File.join(File.dirname(__FILE__), "#{base_path}/folder1")
32
+ l3 = File.join(File.dirname(__FILE__), "#{base_path}/folder1/folder2")
33
+ FileUtils.mkdir_p File.join(File.dirname(__FILE__),File.join(base_path, "/folder1/folder2"))
34
+ # create some files and write some content
35
+ [l1,l2,l3].each do |folder|
36
+ (1..10).each do |f|
37
+ create_fill_file(File.join(folder, "File%02d" % f))
38
+ end
39
+ end
40
+ end
41
+
42
+ def create_fill_file(filename)
43
+ File.open(filename, "w") do |fh|
44
+ (1..10).each do
45
+ fh.write ('a'..'z').to_a.shuffle[0,8].join + "\n"
46
+ end
47
+ end
48
+ end
49
+ # only compares folders based on file existence not file size or other
50
+ def compare_folders(src,dst)
51
+ Find.find(src).each do |f|
52
+ dst_file_folder = f.gsub(src,dst)
53
+ #puts "=> #{dst_file_folder}"
54
+ if !File.exists?(dst_file_folder)
55
+ puts "non existent file #{File.join(dst,f.split('/')[1..-1])}"
56
+ return false
57
+ end
58
+ end
59
+ return true
60
+ end
61
+
62
+ # tests
63
+ def test_new
64
+ assert_equal(File.exists?(@opts[:dst]), false, "initial conditions not correct")
65
+ KSync.new(@opts).do_sync
66
+ assert_equal(compare_folders(@opts[:src], @opts[:dst]), true)
67
+ end
68
+
69
+ def test_nothing_changed
70
+ create_tree("dst")
71
+ assert_equal(compare_folders(@opts[:src], @opts[:dst]), true)
72
+ assert_equal(KSync.new(@opts).do_sync, false)
73
+ end
74
+
75
+ def test_more_files_in_source
76
+ create_fill_file(File.join(@opts[:src],"NEWFILE.txt"))
77
+ assert_equal(KSync.new(@opts).do_sync, true)
78
+ assert_equal(compare_folders(@opts[:src], @opts[:dst]), true)
79
+ end
80
+
81
+ def test_less_files_in_source
82
+ FileUtils.rm(File.join(@opts[:src],"File01"))
83
+ assert_equal(KSync.new(@opts).do_sync, true)
84
+ assert_equal(compare_folders(@opts[:src], @opts[:dst]), true)
85
+ end
86
+
87
+ def test_source_file_changed_using_hash
88
+ create_fill_file(File.join(@opts[:src],"File01"))
89
+ @opts[:use_hash] = true
90
+ assert_equal(KSync.new(@opts).do_sync, true)
91
+ assert_equal(compare_folders(@opts[:src], @opts[:dst]), true)
92
+ end
93
+
94
+ def test_dont_create_src_folder
95
+ @opts2 = {:src => File.join(File.dirname(__FILE__), "src_non"), :dst => File.join(File.dirname(__FILE__), "dst_non")}
96
+ assert_equal(File.exists?(@opts2[:src]), false, "initial conditions not correct")
97
+ @opts2[:verbose] = 3
98
+ assert_equal(KSync.new(@opts2).do_sync, false)
99
+ end
100
+
101
+ def test_dry_run
102
+ assert_equal(File.exists?(@opts[:dst]), false, "initial conditions not correct")
103
+ @opts[:real_copy] = false
104
+ @opts[:verbose] = 3
105
+ KSync.new(@opts).do_sync
106
+ assert_equal(File.exists?(@opts[:dst]), false)
107
+ end
108
+
109
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ksync
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kirk Adoniadis
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-07 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: turn
16
+ version_requirements: &2056 !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ! '>='
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ none: false
22
+ requirement: *2056
23
+ prerelease: false
24
+ type: :runtime
25
+ - !ruby/object:Gem::Dependency
26
+ name: minitest
27
+ version_requirements: &2074 !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ! '>='
30
+ - !ruby/object:Gem::Version
31
+ version: 4.4.0
32
+ none: false
33
+ requirement: *2074
34
+ prerelease: false
35
+ type: :runtime
36
+ description: ksync is a simple script which is used to sync between 2 folders, the destination folder being used as a repository
37
+ email: kiriakos.adoniadis@gmail.com
38
+ executables:
39
+ - ksync
40
+ extensions: []
41
+ extra_rdoc_files:
42
+ - README
43
+ - MIT-LICENSE
44
+ files:
45
+ - Rakefile.rb
46
+ - lib/ksync.rb
47
+ - bin/ksync
48
+ - README
49
+ - MIT-LICENSE
50
+ - test/test_ksync.rb
51
+ homepage: http://rubygems.org/gems/ksync
52
+ licenses:
53
+ - MIT
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: 1.9.2
63
+ none: false
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ none: false
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 1.8.9
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: ! 'Ksync: A simple file backup/syncing class'
76
+ test_files:
77
+ - test/test_ksync.rb
78
+ ...