ksync 0.5.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.
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
+ ...