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