remotesync 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/bin/rrinit +184 -0
  3. data/bin/rrpull +75 -0
  4. data/bin/rrpush +35 -0
  5. data/lib/commons.rb +284 -0
  6. metadata +70 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 13cc5deafe2ae0fa3348ac64b6212feef2b8df773b4005a49d134652e86a99d4
4
+ data.tar.gz: 9cb6df03332a34df4320776273ec54b4327b1f49cae294e599ef9decc24063a3
5
+ SHA512:
6
+ metadata.gz: 892ac784c4fd58041b470056d862d1b0c8e57a31f889d4857cbedd27ad2af05b766dc9d3ab39b0de6798a174f23d683f7a90ec05c9271edc86ffc4ce0047c3d7
7
+ data.tar.gz: f65bf5a6b664b96078fda80e56b0cab90f9f61acc28331a1c235e015e62f66d7984de0716df9a1f551e36fc92d1e59202f33315226baafb9e699a89b562132d6
data/bin/rrinit ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require_relative "../lib/commons"
4
+
5
+ args = CmdParser.setup(INIT_COMMAND, :hostname, :path) do |opts|
6
+ CmdParser.autoset_param "-pPORT", "--port=PORT", "Port of the remote host", :port, [:propagate, :delete, :unlock, :status]
7
+ CmdParser.autoset_param "-uUSER", "--user=USER", "User of the remote host", :user, [:propagate, :delete, :unlock, :status]
8
+ CmdParser.autoset_param "-nNETNS", "--netns=NETNS", "Network namespace", :netns, [:propagate, :delete, :unlock, :status]
9
+ CmdParser.autoset_param "-oOWNER", "--owner=OWNER", "Ownership of the files (defaults to the current user)", :owner, [:propagate, :delete, :unlock, :status]
10
+
11
+ CmdParser.autoset_flag "-r", "--recursive", "Ownership of the files (defaults to the current user)", :recursive, [:propagate]
12
+ CmdParser.autoset_flag "-f", "--force", "Force the overwrite of existing data", :force
13
+ CmdParser.autoset_flag "-R", "--pull-only", "Allows only the pull from the remote", :pullonly, [:pushonly]
14
+ CmdParser.autoset_flag "-W", "--push-only", "Allows only the push to the remote", :pushonly, [:pullonly]
15
+
16
+ CmdParser.autoset_flag "-D", "--delete", "Delete all the sync files (asks for confirmation)", :delete, [:propagate, :unlock, :status]
17
+ CmdParser.autoset_flag "-p", "--propagate", "Propagate the root file to all the sub-directories", :propagate, [:delete, :unlock, :status]
18
+ CmdParser.autoset_flag "-u", "--unlock", "Unlock all the remote information files", :unlock, [:delete, :propagate, :status]
19
+ CmdParser.autoset_flag "-s", "--status", "Prints the status of the folder", :status, [:delete, :propagate, :unlock]
20
+ end
21
+
22
+ if CmdParser.opt :delete
23
+ unless FileTest.exists? RemoteInfo.filename(".")
24
+ puts "This folder is not initialized yet. " + "Deletion aborted.".red
25
+ exit
26
+ end
27
+
28
+ sub = CmdParser.opt(:recursive) ? " and its subdirectories" : ""
29
+ print "Are you sure you want to delete all the sync information in this directory#{sub}? [y/N] "
30
+ confirm = gets.chomp.downcase
31
+ if confirm != "y"
32
+ puts "Aborted.".red
33
+ exit
34
+ end
35
+
36
+ print "Deleting all the remote files... "
37
+ puts "" if CmdParser.opt :verbose
38
+ FS.each_folder(".", CmdParser.opt(:recursive)) do |current|
39
+ vputs "Deleting remote information file for #{current}"
40
+ OS.remove_file RemoteInfo.filename(current)
41
+ end
42
+
43
+ puts "Done!".green
44
+ exit
45
+
46
+ elsif CmdParser.opt :propagate
47
+ print "Propagating the remote information to all the subfolders... "
48
+ unless FileTest.exists? RemoteInfo.filename(".")
49
+ puts "\n" + "Error".red + ": Initialize this directory first. Aborting propagation."
50
+ exit
51
+ end
52
+
53
+ rfile = RemoteInfo.load
54
+
55
+ CmdParser.set :port, rfile.port
56
+ CmdParser.set :user, rfile.user
57
+ CmdParser.set :netns, rfile.netns
58
+ CmdParser.set :owner, rfile.owner
59
+ CmdParser.set :recursive, true
60
+ args[:hostname] = rfile.host
61
+ args[:path] = rfile.path
62
+
63
+ vputs "\nPropagation information:"
64
+ vputs "\tHost: #{rfile.host}"
65
+ vputs "\tPath: #{rfile.path}"
66
+ vputs "\tPort: #{rfile.port}" if rfile.port
67
+ vputs "\tUser: #{rfile.user}" if rfile.user
68
+ vputs "\tNetwork namespace: #{rfile.netns}" if rfile.netns
69
+ vputs "\tOwner: #{rfile.owner}" if rfile.owner
70
+ #Does propagation through the normal procedure
71
+
72
+ elsif CmdParser.opt :unlock
73
+ print "Unlocking all the remote files... "
74
+ RemoteInfo.unlock_all current
75
+ puts "Done!".green
76
+ exit
77
+
78
+ elsif CmdParser.opt :status
79
+ unless FileTest.exists? RemoteInfo.filename(".")
80
+ puts "This folder is not initialized yet."
81
+ exit
82
+ end
83
+
84
+ rinfo = RemoteInfo.load "."
85
+ userstring = rinfo.user ? rinfo.user.to_s + "@" : ""
86
+ portstring = rinfo.port ? ":"+rinfo.port.to_s : ""
87
+
88
+ mode = []
89
+ if rinfo.can_push?
90
+ mode.push "Push".green
91
+ else
92
+ mode.push "Push".red
93
+ end
94
+
95
+ if rinfo.can_pull?
96
+ mode.push "Pull".green
97
+ else
98
+ mode.push "Pull".red
99
+ end
100
+ modestring = mode.join("|")
101
+
102
+ puts "Root remote information:"
103
+ puts "\tHost: #{userstring}#{rinfo.host}#{portstring}"
104
+ puts "\tPath: #{rinfo.path}"
105
+ puts "\tNetwork namespace: #{rinfo.netns}" if rinfo.netns
106
+ puts "\tOwner: #{rinfo.owner}" if rinfo.owner
107
+ puts "\tMode: #{modestring}"
108
+
109
+ puts "Local folders:"
110
+ ignore = []
111
+ FS.each_folder(".", true) do |current|
112
+ next if current == "."
113
+
114
+ skip = false
115
+ ignore.each do |ig|
116
+ skip = true if current.start_with? ig
117
+ end
118
+ next if skip
119
+
120
+ valid = File.join(rinfo.path, current)
121
+
122
+ status = ""
123
+ if FileTest.exists? RemoteInfo.filename(current)
124
+ cinfo = RemoteInfo.load current
125
+
126
+ if cinfo.path != valid || cinfo.host != rinfo.host
127
+ status = "DIFFERENT REMOTE".yellow + "(" + ScpUtils.remote(nil, cinfo) + ")"
128
+ ignore.push current
129
+ else
130
+ status = "OK".green
131
+ end
132
+ else
133
+ status = "LOCAL ONLY".red
134
+ ignore.push current
135
+ end
136
+ puts "\t- #{current}: #{status}"
137
+ end
138
+ exit
139
+
140
+ end
141
+
142
+
143
+ err = false
144
+ unless args[:hostname]
145
+ puts "Error".red + ": You have to specify the remote hostname."
146
+ err = true
147
+ end
148
+
149
+ unless args[:path]
150
+ puts "Error".red + ": You have to specify the remote path."
151
+ err = true
152
+ end
153
+
154
+ if err
155
+ puts "Try #{INIT_COMMAND} -h to check how to use this command"
156
+ exit
157
+ end
158
+
159
+ unless CmdParser.opt :propagate
160
+ sub = CmdParser.opt(:recursive) ? " and its subdirectories" : ""
161
+ print "Initializing the directory#{sub}... "
162
+ end
163
+
164
+ FS.each_folder(".", CmdParser.opt(:recursive)) do |current|
165
+ if CmdParser.opt(:force) || !FileTest.exists?(RemoteInfo.filename(current))
166
+ rfile = RemoteInfo.new
167
+ rfile.port = CmdParser.opt :port
168
+ rfile.user = CmdParser.opt :user
169
+ rfile.netns = CmdParser.opt :netns
170
+ rfile.owner = CmdParser.opt :owner
171
+ rfile.operations = "w" if CmdParser.opt :pushonly
172
+ rfile.operations = "r" if CmdParser.opt :pullonly
173
+
174
+ rfile.host = args[:hostname]
175
+ rfile.path = (current != "." ? File.join(args[:path], current) : args[:path])
176
+
177
+ vputs "Writing remote information for #{current}..."
178
+ rfile.save_info current
179
+ else
180
+ vputs "Skipping #{current} (already exists)..."
181
+ end
182
+ end
183
+
184
+ puts "Done!".green
data/bin/rrpull ADDED
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require_relative "../lib/commons"
4
+
5
+ args = CmdParser.setup(PULL_COMMAND) do |opts|
6
+ CmdParser.autoset_param "-fFILE", "--file=FILE", "File to push", :file, [:directory]
7
+ CmdParser.autoset_param "-dDIR", "--directory=DIR", "Directory to push", :directory, [:file]
8
+
9
+ CmdParser.autoset_param "-oOWNER", "--owner=OWNER", "Ownership of the files (defaults to the current user)", :owner
10
+ end
11
+
12
+ unless FileTest.exists? RemoteInfo.filename(".")
13
+ puts "Error".red + ": please, use #{INIT_COMMAND} before #{PULL_COMMAND}"
14
+ exit
15
+ end
16
+
17
+ remote_info = RemoteInfo.load
18
+
19
+ unless remote_info.can_pull?
20
+ puts "Error".red + ": pull forbidden (push-only directory). Aborted."
21
+ exit
22
+ end
23
+
24
+ isdir = !CmdParser.opt(:file)
25
+
26
+ OS.run "mkdir -p \"#{CmdParser.opt(:directory)}\"" if CmdParser.opt(:directory)
27
+
28
+ #Lock all the remote information files
29
+ RemoteInfo.lock_all(CmdParser.opt(:directory) || ".", isdir) do
30
+ vputs "Locked all the remote information files in the path"
31
+ local = ScpUtils.local CmdParser.opt(:file), CmdParser.opt(:directory)
32
+ remote = ScpUtils.remote local, remote_info
33
+ remote = File.join(remote, ".")
34
+
35
+ command = ScpUtils.scp remote, local, isdir, remote_info.port
36
+
37
+ new_owner = CmdParser.opt(:owner) || remote_info.owner || OS.username
38
+ result = nil
39
+ if remote_info.netns
40
+ puts "Requiring root privileges to run in network namespace..."
41
+ result = OS.run "sudo ip netns exec #{remote_info.netns} #{command}"
42
+ new_owner = OS.username unless new_owner
43
+ else
44
+ result = OS.run command
45
+ end
46
+
47
+ unless result
48
+ puts "An error occurred while executing the copy command. Exiting"
49
+ RemoteInfo.unlock_all
50
+ exit
51
+ end
52
+
53
+ result[1].split("\n").each do |errline|
54
+ if errline.downcase.include?("permission denied") && !errline.downcase.include?(RemoteInfo::FILENAME)
55
+ puts "Error:" + errline
56
+ end
57
+ end
58
+
59
+ if new_owner
60
+ puts "Requiring root privileges to change ownership..."
61
+ OS.run "sudo chown #{new_owner} \"#{local}\" #{isdir ? "-R" : ""}"
62
+ end
63
+ vputs "Unlocked all the remote information files in the path"
64
+ end
65
+
66
+ rinit_script = File.join File.expand_path(File.dirname(__FILE__)), INIT_COMMAND
67
+ #Propagates the creation of remote file to all the new subfolders
68
+ vputs "Calling #{INIT_COMMAND} to propagate the information..."
69
+ result = OS.run "ruby \"#{rinit_script}\" -p"
70
+ puts result[0]
71
+ if result[1] != ""
72
+ puts "Error".red + ":" + result[1]
73
+ end
74
+
75
+ puts "The local version is up to date.".green
data/bin/rrpush ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require_relative "../lib/commons"
4
+
5
+ args = CmdParser.setup(PUSH_COMMAND) do |opts|
6
+ CmdParser.autoset_param "-fFILE", "--file=FILE", "File to push", :file, [:directory]
7
+ CmdParser.autoset_param "-dDIR", "--directory=DIR", "Directory to push", :directory, [:file]
8
+ end
9
+
10
+ unless FileTest.exists? RemoteInfo.filename(".")
11
+ puts "Error".red + ": please, use #{INIT_COMMAND} before #{PUSH_COMMAND}"
12
+ exit
13
+ end
14
+
15
+ remote_info = RemoteInfo.load
16
+
17
+ unless remote_info.can_push?
18
+ puts "Error".red + ": push forbidden (pull-only directory). Aborted."
19
+ exit
20
+ end
21
+
22
+ isdir = !CmdParser.opt(:file)
23
+
24
+ local = ScpUtils.local CmdParser.opt(:file), CmdParser.opt(:directory)
25
+ remote = ScpUtils.remote local, remote_info
26
+
27
+ command = ScpUtils.scp local, remote, isdir, remote_info.port
28
+ if remote_info.netns
29
+ puts "Requiring root privileges to run in network namespace..."
30
+ OS.run "sudo ip netns exec #{remote_info.netns} #{command}"
31
+ else
32
+ OS.run command
33
+ end
34
+
35
+ puts "The remote version is up to date.".green
data/lib/commons.rb ADDED
@@ -0,0 +1,284 @@
1
+ require "optparse"
2
+ require "open3"
3
+ require "colorize"
4
+
5
+ INIT_COMMAND = "rrinit"
6
+ PUSH_COMMAND = "rrpush"
7
+ PULL_COMMAND = "rrpull"
8
+
9
+ class RemoteInfo
10
+ FILENAME = ".reminfo"
11
+
12
+ attr_accessor :host
13
+ attr_accessor :port
14
+ attr_accessor :user
15
+ attr_accessor :path
16
+ attr_accessor :netns
17
+ attr_accessor :owner
18
+ attr_accessor :operations
19
+
20
+ def initialize
21
+ @netns = nil
22
+ @owner = nil
23
+
24
+ @port = nil
25
+ @user = nil
26
+
27
+ @operations = "rw"
28
+ end
29
+
30
+ def self.load(folder=".")
31
+ rinfo = RemoteInfo.new
32
+ rinfo.read_info(folder)
33
+ return rinfo
34
+ end
35
+
36
+ def self.filename(folder)
37
+ return File.join(folder, FILENAME)
38
+ end
39
+
40
+ def can_pull?
41
+ return @operations.include? "r"
42
+ end
43
+
44
+ def can_push?
45
+ return @operations.include? "w"
46
+ end
47
+
48
+ def read_info(folder)
49
+ info = File.read(File.join(folder, FILENAME))
50
+
51
+ rows = info.split "\n"
52
+ rows.each do |r|
53
+ key, value = *r.split("\t")
54
+
55
+ key = key.strip
56
+ value = value.strip
57
+
58
+ case key
59
+ when "host"
60
+ @host = value
61
+ when "port"
62
+ @port = value
63
+ when "user"
64
+ @user = value
65
+ when "path"
66
+ @path = value
67
+ when "netns"
68
+ @netns = value
69
+ when "owner"
70
+ @owner = value
71
+ when "operations"
72
+ @operations = value
73
+ end
74
+ end
75
+ end
76
+
77
+ def save_info(to=".")
78
+ content = ""
79
+ content += "host\t#@host\n"
80
+ content += "port\t#@port\n" if @port
81
+ content += "user\t#@user\n" if @user
82
+ content += "path\t#@path\n"
83
+ content += "netns\t#@netns\n" if @netns
84
+ content += "owner\t#@owner\n" if @owner
85
+ content += "operations\t#@operations\n"
86
+
87
+ dest = to ? File.join(to, FILENAME) : FILENAME
88
+
89
+ File.write dest, content
90
+ end
91
+
92
+ def self.unlock_all(local)
93
+ self.lock_all(local, true) {}
94
+ end
95
+
96
+ def self.lock_all(local, prevent_write=true)
97
+ if (prevent_write)
98
+ FS.each_remote_info(local) do |rinfo|
99
+ OS.run "chmod -w \"#{rinfo}\""
100
+ end
101
+ end
102
+
103
+ yield
104
+
105
+ if (prevent_write)
106
+ FS.each_remote_info(local) do |rinfo|
107
+ OS.run "chmod +w \"#{rinfo}\""
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ class FS
114
+ def self.each_folder(dir, recursive=true)
115
+ queue = ["."]
116
+ visited = []
117
+ while queue.size > 0
118
+ current = queue.shift
119
+
120
+ yield current
121
+
122
+ if recursive
123
+ Dir.entries(current).each do |entry|
124
+ next if [".",".."].include? entry
125
+ to_queue = (current == "." ? entry : File.join(current, entry))
126
+
127
+ queue.push to_queue if FileTest.directory? to_queue
128
+ end
129
+ end
130
+
131
+ queue -= visited
132
+ visited.push current
133
+ end
134
+ end
135
+
136
+ def self.each_remote_info(dir)
137
+ self.each_folder(dir) do |folder|
138
+ filename = File.join(folder, RemoteInfo::FILENAME)
139
+ yield filename if FileTest.exists? filename
140
+ end
141
+ end
142
+ end
143
+
144
+ class ScpUtils
145
+ def self.remote(local, file)
146
+ user = file.user
147
+ port = file.port
148
+ host = file.host
149
+ path = file.path
150
+
151
+ userstring = user ? user + "@" : ""
152
+
153
+ remotepath = path
154
+ remotepath = File.join(remotepath, local) if local && local != "."
155
+
156
+ result = "#{userstring}#{host}:#{remotepath}"
157
+ end
158
+
159
+ def self.local(*candidates)
160
+ candidates.each do |c|
161
+ return c if c
162
+ end
163
+
164
+ return "."
165
+ end
166
+
167
+ def self.scp(from, to, dir, port=nil)
168
+ dirstring = dir ? "-r" : ""
169
+ portstring = port ? "-P " + port : ""
170
+
171
+ return "scp #{portstring} #{dirstring} \"#{from}\" \"#{to}\""
172
+ end
173
+ end
174
+
175
+ class CmdParser
176
+ @@data = {}
177
+ @@bindings = {}
178
+
179
+ def self.opt(key)
180
+ return @@data[key]
181
+ end
182
+
183
+ def self.set(key, value)
184
+ @@data[key] = value
185
+ end
186
+
187
+ def self.setup(cmd, *args)
188
+ parser = OptionParser.new do |opts|
189
+ opts.banner = "Usage: #{cmd} [options] #{args.join " "}"
190
+
191
+ @@opts = opts
192
+ yield opts
193
+ @@opts = nil
194
+
195
+ opts.on("-v", "--verbose", "Writes a lot of information") do
196
+ CmdParser.set :verbose, true
197
+ end
198
+
199
+ opts.on("-h", "--help", "Prints this help") do
200
+ puts opts
201
+ exit
202
+ end
203
+ end
204
+
205
+ parser.parse! ARGV
206
+
207
+ arguments = {}
208
+ for i in 0...args.size
209
+ arguments[args[i]] = ARGV[i]
210
+ end
211
+
212
+ return arguments
213
+ end
214
+
215
+ def self.autoset_flag(short, long, description, flag, incompatibles=[])
216
+ @@bindings[flag] = short
217
+ @@opts.on(short, long, description) do
218
+ incompatibles.each do |incompatible|
219
+ CmdParser.assert_not_set incompatible, short, @@bindings[incompatible]
220
+ end
221
+
222
+ CmdParser.set flag, true
223
+ end
224
+ end
225
+
226
+ def self.autoset_param(short, long, description, flag, incompatibles=[])
227
+ @@bindings[flag] = short
228
+ @@opts.on(short, long, description) do |v|
229
+ incompatibles.each do |incompatible|
230
+ CmdParser.assert_not_set incompatible, short, @@bindings[incompatible]
231
+ end
232
+
233
+ CmdParser.set flag, v
234
+ end
235
+ end
236
+
237
+ def self.assert
238
+ error_message = yield
239
+ if error_message
240
+ puts error_message
241
+ exit
242
+ end
243
+ end
244
+
245
+ def self.assert_not_set(forbidden_key, actual_param, forbidden_param, message="incompatible commands")
246
+ if CmdParser.opt(forbidden_key) != nil
247
+ puts "Do not use #{forbidden_param} with #{actual_param} (#{message}). Aborting."
248
+ exit
249
+ end
250
+ end
251
+ end
252
+
253
+ class ConsoleRunner
254
+ def remove_file(file)
255
+ File.unlink file
256
+ end
257
+
258
+ def username
259
+ return `echo $USER`.chomp
260
+ end
261
+
262
+ def run(cmd)
263
+ vputs "Executing command: #{cmd}"
264
+ stdout, stderr, status = Open3.capture3(cmd)
265
+ return [stdout, stderr, status]
266
+ end
267
+ end
268
+
269
+ class SimulatorRunner < ConsoleRunner
270
+ def remove_file(file)
271
+ puts "SIMULATE: removed #{file}"
272
+ end
273
+
274
+ def run(cmd)
275
+ puts "SIMULATE: #{cmd}"
276
+ end
277
+ end
278
+
279
+ def vputs *args
280
+ puts *args if CmdParser.opt :verbose
281
+ end
282
+
283
+ OS = ConsoleRunner.new
284
+ # OS = SimulatorRunner.new
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remotesync
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Simone Scalabrino
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-01-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: colorize
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.8'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.8.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.8'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.8.1
33
+ description: Sync remote folders using ssh. Supports network namespaces in Linux.
34
+ email: s.scalabrino9@gmail.com
35
+ executables:
36
+ - rrinit
37
+ - rrpull
38
+ - rrpush
39
+ extensions: []
40
+ extra_rdoc_files: []
41
+ files:
42
+ - bin/rrinit
43
+ - bin/rrpull
44
+ - bin/rrpush
45
+ - lib/commons.rb
46
+ homepage: https://github.com/intersimone999/remotesync
47
+ licenses:
48
+ - GPL-3.0
49
+ metadata: {}
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubyforge_project:
66
+ rubygems_version: 2.7.3
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: Sync remote folders using ssh
70
+ test_files: []