dir_sync 0.1.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/.gemtest +0 -0
- data/HISTORY.rdoc +4 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +29 -0
- data/Rakefile +12 -0
- data/bin/dir_sync +13 -0
- data/bin/drain +18 -0
- data/lib/change_log_file_system.rb +15 -0
- data/lib/change_resolver.rb +74 -0
- data/lib/historical_traverser.rb +46 -0
- data/lib/synchroniser.rb +15 -0
- data/lib/traverser.rb +60 -0
- data/spec/change_resolver_spec.rb +165 -0
- data/spec/synchroniser_spec.rb +31 -0
- data/spec/traverser_spec.rb +58 -0
- metadata +124 -0
data/.gemtest
ADDED
File without changes
|
data/HISTORY.rdoc
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Mark Ryall
|
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.rdoc
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
= dir_sync
|
2
|
+
|
3
|
+
Produce a script to synchronise two directories. Kind of like unison but a bit simpler.
|
4
|
+
|
5
|
+
= Usage
|
6
|
+
|
7
|
+
dir_sync name folder1 folder2 ...
|
8
|
+
|
9
|
+
This will output all require mkdir and cp commands that would be required to sync the
|
10
|
+
folders. It will also produce the index file (~/.dir_sync/NAME) which will be used
|
11
|
+
in subsequent synchronisations to detect deletions. You can then review the commands
|
12
|
+
before executing them (in case they seem likely to do something undesirable). Note that
|
13
|
+
files that differ only in modification time will be overwritten with the most recent one.
|
14
|
+
|
15
|
+
drain script
|
16
|
+
|
17
|
+
This simply runs each line in the specified script. As each line is executed, a new script is
|
18
|
+
produced. The purpose of this is to allow the script to be interrupted and able to resume.
|
19
|
+
|
20
|
+
Together, these scripts can be used as follows:
|
21
|
+
|
22
|
+
dir_sync photos /Volumes/BACKUP/sync/photos /Users/Shared/Photos | tee sync.sh
|
23
|
+
drain sync.sh
|
24
|
+
|
25
|
+
= Future Plans
|
26
|
+
|
27
|
+
* actually execute file commands perhaps with optional confirmation
|
28
|
+
* consider introducing checksums/filesize comparisons to avoid copying identical files that
|
29
|
+
differ only in modification time.
|
data/Rakefile
ADDED
data/bin/dir_sync
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << File.dirname(__FILE__)+'/../lib'
|
4
|
+
|
5
|
+
require 'synchroniser'
|
6
|
+
|
7
|
+
if ARGV.size < 3
|
8
|
+
puts "usage: #{__FILE__} name directory1 directory2 ..."
|
9
|
+
puts " set DEBUG for verbose output"
|
10
|
+
exit 1
|
11
|
+
end
|
12
|
+
|
13
|
+
Synchroniser.iterate *ARGV
|
data/bin/drain
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
if ARGV.empty?
|
4
|
+
puts "Runs a set of single line commands one by one (so that you can interrupt and the script will resume)"
|
5
|
+
puts "usage: #{__FILE__} script"
|
6
|
+
exit 1
|
7
|
+
end
|
8
|
+
|
9
|
+
file = ARGV.shift
|
10
|
+
lines = File.readlines(file).map {|s| s.chomp.strip }.select {|s| !s.empty? and !s.start_with?('#')}
|
11
|
+
|
12
|
+
loop do
|
13
|
+
break if lines.empty?
|
14
|
+
lines.shift.tap {|s| puts "> #{s}" }.tap {|s| puts `#{s}` }
|
15
|
+
File.open("#{file}.tmp", 'w') {|f| f.puts lines.join "\n" }
|
16
|
+
`mv #{file}.tmp #{file}`
|
17
|
+
end
|
18
|
+
`rm #{file}`
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class ChangeLogFileSystem
|
2
|
+
def initialize io
|
3
|
+
@io = io
|
4
|
+
end
|
5
|
+
|
6
|
+
def cp from, to
|
7
|
+
to_dir = File.dirname to
|
8
|
+
@io.puts "mkdir -p \"#{to_dir}\"" unless File.exist? to_dir
|
9
|
+
@io.puts "cp -p \"#{from}\" \"#{to}\""
|
10
|
+
end
|
11
|
+
|
12
|
+
def rm path
|
13
|
+
@io.puts "rm \"#{path}\""
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
class ChangeResolver
|
2
|
+
def debug message
|
3
|
+
puts "# #{message}" if ENV['DEBUG']
|
4
|
+
end
|
5
|
+
|
6
|
+
def initialize history, *traversers
|
7
|
+
@history, @traversers = history, traversers
|
8
|
+
end
|
9
|
+
|
10
|
+
def iterate
|
11
|
+
report_traversers
|
12
|
+
first = candidates.first
|
13
|
+
return false unless first
|
14
|
+
debug "first is #{first.description}"
|
15
|
+
send dispatch(first), first
|
16
|
+
advance_matching_traversers first.name, @history, *non_empty_traversers
|
17
|
+
end
|
18
|
+
|
19
|
+
def candidate
|
20
|
+
candidates.first
|
21
|
+
end
|
22
|
+
private
|
23
|
+
def dispatch traverser
|
24
|
+
return :rm if traverser.ignored?
|
25
|
+
if traverser.equivalent? @history
|
26
|
+
return all_equivalent_traversers?(traverser) ? :ignore : :rm
|
27
|
+
end
|
28
|
+
:cp
|
29
|
+
end
|
30
|
+
|
31
|
+
def rm traverser
|
32
|
+
traverser.rm
|
33
|
+
end
|
34
|
+
|
35
|
+
def cp traverser
|
36
|
+
@history.report traverser
|
37
|
+
traverser.cp *@traversers
|
38
|
+
end
|
39
|
+
|
40
|
+
def ignore traverser
|
41
|
+
end
|
42
|
+
|
43
|
+
def report_traversers
|
44
|
+
report @history
|
45
|
+
@traversers.each { |traverser| report traverser }
|
46
|
+
end
|
47
|
+
|
48
|
+
def report traverser
|
49
|
+
debug "#{traverser.base}: #{traverser.description}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def advance_matching_traversers name, *traversers
|
53
|
+
traversers.select{|t| t.name == name}.each &:advance
|
54
|
+
end
|
55
|
+
|
56
|
+
def all_equivalent_traversers? traverser
|
57
|
+
@traversers.all? {|t| traverser.equivalent? t }
|
58
|
+
end
|
59
|
+
|
60
|
+
def non_empty_traversers
|
61
|
+
@traversers.select {|t| !t.empty? }
|
62
|
+
end
|
63
|
+
|
64
|
+
def candidates
|
65
|
+
non_empty_traversers.sort do |left,right|
|
66
|
+
combine left.name <=> right.name, right.ts <=> left.ts
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def combine *exps
|
71
|
+
exps.each { |exp| return exp unless exp == 0 }
|
72
|
+
0
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class HistoricalTraverser
|
2
|
+
attr_reader :name, :ts, :base, :description
|
3
|
+
REGEXP = /:(\d+)$/
|
4
|
+
|
5
|
+
def initialize name
|
6
|
+
home = File.expand_path '~'
|
7
|
+
Pathname.new("#{home}/.dir_sync").mkpath
|
8
|
+
@path_old = "#{home}/.dir_sync/#{name}"
|
9
|
+
@path_new = "#{@path_old}.new"
|
10
|
+
@new = File.open @path_new, 'w'
|
11
|
+
@base = '<history>'
|
12
|
+
if File.exist? @path_old
|
13
|
+
@fiber = Fiber.new do
|
14
|
+
File.open(@path_old) do |file|
|
15
|
+
file.each {|line| Fiber.yield line.chomp}
|
16
|
+
end
|
17
|
+
Fiber.yield nil
|
18
|
+
end
|
19
|
+
@description = :nothing
|
20
|
+
advance
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def advance
|
25
|
+
@description = @fiber.resume if @description
|
26
|
+
if @description
|
27
|
+
match = REGEXP.match @description
|
28
|
+
raise "unable to parse line \"#{@description}\"" unless match
|
29
|
+
@name = match.pre_match
|
30
|
+
@ts = match[1].to_i
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def close
|
35
|
+
@new.close
|
36
|
+
`mv #{@path_new} #{@path_old}`
|
37
|
+
end
|
38
|
+
|
39
|
+
def report traverser
|
40
|
+
@new.puts traverser.description
|
41
|
+
end
|
42
|
+
|
43
|
+
def empty?
|
44
|
+
@description.nil?
|
45
|
+
end
|
46
|
+
end
|
data/lib/synchroniser.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'traverser'
|
2
|
+
require 'historical_traverser'
|
3
|
+
require 'change_resolver'
|
4
|
+
require 'change_log_file_system'
|
5
|
+
|
6
|
+
module Synchroniser
|
7
|
+
def self.iterate name, *paths
|
8
|
+
file_system = ChangeLogFileSystem.new $stdout
|
9
|
+
traversers = paths.map {|path| Traverser.new path, file_system }
|
10
|
+
history = HistoricalTraverser.new name
|
11
|
+
resolver = ChangeResolver.new history, *traversers
|
12
|
+
loop { break unless resolver.iterate }
|
13
|
+
history.close
|
14
|
+
end
|
15
|
+
end
|
data/lib/traverser.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
class Traverser
|
4
|
+
attr_reader :base
|
5
|
+
|
6
|
+
TOLERANCE=5
|
7
|
+
IGNORE_PATTERNS = [
|
8
|
+
/\.DS_Store$/,
|
9
|
+
/\/\._/
|
10
|
+
]
|
11
|
+
|
12
|
+
def initialize path, file_system
|
13
|
+
@file_system = file_system
|
14
|
+
@base = Pathname.new path
|
15
|
+
@base.mkpath
|
16
|
+
@fiber = Fiber.new do
|
17
|
+
@base.find { |child| Fiber.yield child if child.file? }
|
18
|
+
Fiber.yield nil
|
19
|
+
end
|
20
|
+
@current = @fiber.resume
|
21
|
+
end
|
22
|
+
|
23
|
+
def description
|
24
|
+
empty? ? 'empty' : "#{name}:#{ts}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def advance
|
28
|
+
@current = @fiber.resume if @current
|
29
|
+
end
|
30
|
+
|
31
|
+
def name
|
32
|
+
@current.relative_path_from(@base).to_s if @current
|
33
|
+
end
|
34
|
+
|
35
|
+
def ts
|
36
|
+
@current.mtime.to_i if @current
|
37
|
+
end
|
38
|
+
|
39
|
+
def empty?
|
40
|
+
@current.nil?
|
41
|
+
end
|
42
|
+
|
43
|
+
def ignored?
|
44
|
+
IGNORE_PATTERNS.any? {|pattern| pattern.match name }
|
45
|
+
end
|
46
|
+
|
47
|
+
def cp *traversers
|
48
|
+
traversers.each do |t|
|
49
|
+
@file_system.cp @current.to_s, "#{t.base}/#{name}" unless equivalent? t
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def rm
|
54
|
+
@file_system.rm @current.to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
def equivalent? traverser
|
58
|
+
name == traverser.name and (ts - traverser.ts).abs <= TOLERANCE
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
$: << File.dirname(__FILE__)+'/../lib'
|
2
|
+
|
3
|
+
require 'change_resolver'
|
4
|
+
|
5
|
+
describe ChangeResolver do
|
6
|
+
let(:history) { stub 'history', name: nil, base: nil, description: nil, report: nil, advance: nil }
|
7
|
+
let(:traversers) { [] }
|
8
|
+
let(:resolver) { ChangeResolver.new history, *traversers }
|
9
|
+
|
10
|
+
def stub_history hash
|
11
|
+
hash.each do |meth,ret|
|
12
|
+
history.stub!(meth).and_return ret
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def stub_traversers hashes
|
17
|
+
hashes.each_with_index do |traverser_stubs, index|
|
18
|
+
stubs = {
|
19
|
+
ts: 0,
|
20
|
+
cp: nil,
|
21
|
+
advance: nil,
|
22
|
+
empty?: false,
|
23
|
+
ignored?: false,
|
24
|
+
base: nil,
|
25
|
+
description: nil,
|
26
|
+
equivalent?: false
|
27
|
+
}.merge traverser_stubs
|
28
|
+
traversers << stub("traverser#{index}").tap do |traverser|
|
29
|
+
stubs.each do |meth,ret|
|
30
|
+
traverser.stub!(meth).and_return ret
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def candidate
|
37
|
+
resolver.candidate
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#iterate' do
|
41
|
+
it 'should return true if any traverser is not empty' do
|
42
|
+
stub_traversers [
|
43
|
+
{ name: 'a'},
|
44
|
+
{ name: 'a'}
|
45
|
+
]
|
46
|
+
resolver.iterate.should be_true
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should return false if all traversers are empty' do
|
50
|
+
stub_traversers [
|
51
|
+
{ name: 'a'},
|
52
|
+
{ name: 'b'}
|
53
|
+
]
|
54
|
+
traversers[0].should_receive(:empty?).and_return true
|
55
|
+
traversers[1].should_receive(:empty?).and_return true
|
56
|
+
resolver.iterate.should be_false
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should copy all other traversers to candidate' do
|
60
|
+
stub_traversers [
|
61
|
+
{ name: 'a'},
|
62
|
+
{ name: 'b'},
|
63
|
+
{ name: 'c'},
|
64
|
+
]
|
65
|
+
traversers[0].should_receive(:cp).with *traversers
|
66
|
+
resolver.iterate
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should remove files that are ignored' do
|
70
|
+
stub_traversers [
|
71
|
+
{ name: 'a', ignored?: true},
|
72
|
+
{ name: 'b'},
|
73
|
+
{ name: 'c'},
|
74
|
+
]
|
75
|
+
traversers[0].should_receive :rm
|
76
|
+
resolver.iterate
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'should ignore files that appear in history and have not been removed from any traverser' do
|
80
|
+
stub_history name: 'a'
|
81
|
+
stub_traversers [
|
82
|
+
{ name: 'a'},
|
83
|
+
{ name: 'a'}
|
84
|
+
]
|
85
|
+
traversers[0].should_receive(:equivalent?).with(history).and_return true
|
86
|
+
traversers[0].should_receive(:equivalent?).with(traversers[0]).and_return true
|
87
|
+
traversers[0].should_receive(:equivalent?).with(traversers[1]).and_return true
|
88
|
+
traversers[0].should_not_receive :rm
|
89
|
+
resolver.iterate
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should remove files that appear in history but have been removed from a traverser' do
|
93
|
+
stub_history name: 'a'
|
94
|
+
stub_traversers [
|
95
|
+
{ name: 'a'},
|
96
|
+
{ name: 'b'},
|
97
|
+
{ name: 'a'}
|
98
|
+
]
|
99
|
+
traversers[0].should_receive(:equivalent?).with(history).and_return true
|
100
|
+
traversers[0].should_receive(:equivalent?).with(traversers[0]).and_return true
|
101
|
+
traversers[0].should_receive(:equivalent?).with(traversers[1]).and_return false
|
102
|
+
traversers[0].should_receive :rm
|
103
|
+
resolver.iterate
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'should advance history when it has candidate name' do
|
107
|
+
stub_history name: 'a'
|
108
|
+
stub_traversers [{ name: 'a'}, { name: 'b'}]
|
109
|
+
history.should_receive :advance
|
110
|
+
resolver.iterate
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should advance traversers with the candidate name' do
|
114
|
+
stub_history name: 'b'
|
115
|
+
stub_traversers [
|
116
|
+
{ name: 'a'},
|
117
|
+
{ name: 'a'},
|
118
|
+
{ name: 'b'},
|
119
|
+
]
|
120
|
+
history.should_not_receive :advance
|
121
|
+
traversers[0].should_receive :advance
|
122
|
+
traversers[1].should_receive :advance
|
123
|
+
traversers[2].should_not_receive :advance
|
124
|
+
resolver.iterate
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe '#candidate' do
|
129
|
+
it 'should determine the next candidate according to the name collation order' do
|
130
|
+
stub_traversers [
|
131
|
+
{ name: 'a'},
|
132
|
+
{ name: 'b'},
|
133
|
+
{ name: 'c'},
|
134
|
+
]
|
135
|
+
candidate.should == traversers[0]
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should determine the next candidate according to the name collation order' do
|
139
|
+
stub_traversers [
|
140
|
+
{ name: 'c'},
|
141
|
+
{ name: 'b'},
|
142
|
+
{ name: 'a'},
|
143
|
+
]
|
144
|
+
candidate.should == traversers[2]
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'should determine the next candidate for the same name by most recent timestamp' do
|
148
|
+
stub_traversers [
|
149
|
+
{ name: 'a', ts: 10},
|
150
|
+
{ name: 'a', ts: 20},
|
151
|
+
{ name: 'b'},
|
152
|
+
]
|
153
|
+
candidate.should == traversers[1]
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'should determine the next candidate for the same name by most recent timestamp' do
|
157
|
+
stub_traversers [
|
158
|
+
{ name: 'a', ts: 20},
|
159
|
+
{ name: 'a', ts: 10},
|
160
|
+
{ name: 'b'},
|
161
|
+
]
|
162
|
+
candidate.should == traversers[0]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
$: << File.dirname(__FILE__)+'/../lib'
|
2
|
+
|
3
|
+
require 'synchroniser'
|
4
|
+
|
5
|
+
describe Synchroniser do
|
6
|
+
let(:file_system) { stub 'file_system' }
|
7
|
+
let(:resolver) { stub 'resolver'}
|
8
|
+
let(:history) { stub 'history', close: nil }
|
9
|
+
let(:traverser_a) { stub 'traverser_a' }
|
10
|
+
let(:traverser_b) { stub 'traverser_b' }
|
11
|
+
|
12
|
+
before do
|
13
|
+
ChangeLogFileSystem.should_receive(:new).with($stdout).and_return file_system
|
14
|
+
HistoricalTraverser.should_receive(:new).with('test').and_return history
|
15
|
+
Traverser.should_receive(:new).with('a', file_system).and_return traverser_a
|
16
|
+
Traverser.should_receive(:new).with('b', file_system).and_return traverser_b
|
17
|
+
ChangeResolver.should_receive(:new).with(history, traverser_a, traverser_b).and_return resolver
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should call iterate once resolved if it returns false' do
|
21
|
+
resolver.should_receive(:iterate).and_return false
|
22
|
+
Synchroniser.iterate 'test', 'a', 'b'
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should repeatedly call iterate on resolved until it returns false' do
|
26
|
+
resolver.should_receive(:iterate).and_return true
|
27
|
+
resolver.should_receive(:iterate).and_return true
|
28
|
+
resolver.should_receive(:iterate).and_return false
|
29
|
+
Synchroniser.iterate 'test', 'a', 'b'
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
$: << File.dirname(__FILE__)+'/../lib'
|
2
|
+
|
3
|
+
require 'traverser'
|
4
|
+
|
5
|
+
describe Traverser do
|
6
|
+
let(:pathname) { stub 'pathname', mkpath: nil, find: nil}
|
7
|
+
let(:file_system) { stub 'file_system'}
|
8
|
+
let(:traverser) { Traverser.new 'a', file_system }
|
9
|
+
|
10
|
+
before do
|
11
|
+
Pathname.should_receive(:new).with('a').and_return pathname
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should create path' do
|
15
|
+
pathname.should_receive :mkpath
|
16
|
+
traverser
|
17
|
+
end
|
18
|
+
|
19
|
+
def with_file name, mtime=0
|
20
|
+
child_pathname = stub 'current_pathname', file?: true, mtime: mtime, to_s: "a/#{name}"
|
21
|
+
child_pathname.stub!(:relative_path_from).with(pathname).and_return name
|
22
|
+
pathname.stub!(:find).and_yield child_pathname
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should skip copying to itself' do
|
26
|
+
with_file '1.txt'
|
27
|
+
file_system.should_not_receive :cp
|
28
|
+
traverser.cp traverser
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should copy to traversers with a different file name' do
|
32
|
+
with_file '1.txt'
|
33
|
+
other_traverser = stub 'other_traverser', base: 'b', name: '2.txt'
|
34
|
+
file_system.should_receive(:cp).with 'a/1.txt', 'b/1.txt'
|
35
|
+
traverser.cp other_traverser
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should skip copying to traversers with the same file name and timestamp' do
|
39
|
+
with_file '1.txt', 10
|
40
|
+
other_traverser = stub 'other_traverser', base: 'b', name: '1.txt', ts: 10
|
41
|
+
file_system.should_not_receive :cp
|
42
|
+
traverser.cp other_traverser
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should skip copying to traversers with the same file name and timestamp is within tolerance' do
|
46
|
+
with_file '1.txt', 10
|
47
|
+
other_traverser = stub 'other_traverser', base: 'b', name: '1.txt', ts: 15
|
48
|
+
file_system.should_not_receive :cp
|
49
|
+
traverser.cp other_traverser
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should skip copying to traversers with the same file name and timestamp is outside tolerance' do
|
53
|
+
with_file '1.txt', 10
|
54
|
+
other_traverser = stub 'other_traverser', base: 'b', name: '1.txt', ts: 16
|
55
|
+
file_system.should_receive(:cp).with 'a/1.txt', 'b/1.txt'
|
56
|
+
traverser.cp other_traverser
|
57
|
+
end
|
58
|
+
end
|
metadata
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dir_sync
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mark Ryall
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-27 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: &70177781836300 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70177781836300
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rspec
|
27
|
+
requirement: &70177781835000 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70177781835000
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: guard
|
38
|
+
requirement: &70177781834120 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70177781834120
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: guard-rspec
|
49
|
+
requirement: &70177781832600 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70177781832600
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: aruba
|
60
|
+
requirement: &70177781831880 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70177781831880
|
69
|
+
description: ! 'Bidirectional directory synchronisation for any number of directories
|
70
|
+
|
71
|
+
'
|
72
|
+
email: mark@ryall.name
|
73
|
+
executables:
|
74
|
+
- dir_sync
|
75
|
+
- drain
|
76
|
+
extensions: []
|
77
|
+
extra_rdoc_files: []
|
78
|
+
files:
|
79
|
+
- lib/change_log_file_system.rb
|
80
|
+
- lib/change_resolver.rb
|
81
|
+
- lib/historical_traverser.rb
|
82
|
+
- lib/synchroniser.rb
|
83
|
+
- lib/traverser.rb
|
84
|
+
- spec/change_resolver_spec.rb
|
85
|
+
- spec/synchroniser_spec.rb
|
86
|
+
- spec/traverser_spec.rb
|
87
|
+
- bin/dir_sync
|
88
|
+
- bin/drain
|
89
|
+
- README.rdoc
|
90
|
+
- MIT-LICENSE
|
91
|
+
- HISTORY.rdoc
|
92
|
+
- Rakefile
|
93
|
+
- .gemtest
|
94
|
+
homepage: http://github.com/markryall/dir_sync
|
95
|
+
licenses: []
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options: []
|
98
|
+
require_paths:
|
99
|
+
- lib
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ! '>='
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
segments:
|
107
|
+
- 0
|
108
|
+
hash: -2227443201844382087
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
none: false
|
111
|
+
requirements:
|
112
|
+
- - ! '>='
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
segments:
|
116
|
+
- 0
|
117
|
+
hash: -2227443201844382087
|
118
|
+
requirements: []
|
119
|
+
rubyforge_project:
|
120
|
+
rubygems_version: 1.8.10
|
121
|
+
signing_key:
|
122
|
+
specification_version: 3
|
123
|
+
summary: directory synchroniser
|
124
|
+
test_files: []
|