ksync 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README +31 -0
- data/Rakefile.rb +8 -0
- data/bin/ksync +30 -0
- data/lib/ksync.rb +159 -0
- data/test/test_ksync.rb +109 -0
- 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
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
|
data/test/test_ksync.rb
ADDED
@@ -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
|
+
...
|