monster_remote 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +5 -0
- data/LICENSE +0 -1
- data/README.md +141 -0
- data/Rakefile +12 -0
- data/bin/monster_remote +16 -0
- data/lib/monster/remote/cli.rb +104 -0
- data/lib/monster/remote/configuration.rb +53 -0
- data/lib/monster/remote/filters/filter.rb +24 -0
- data/lib/monster/remote/{content_name_based_filter.rb → filters/name_based_filter.rb} +9 -12
- data/lib/monster/remote/sync.rb +62 -30
- data/lib/monster/remote/wrappers/net_ftp.rb +107 -51
- data/lib/monster/remote.rb +14 -2
- data/lib/monster_remote.rb +8 -0
- data/monster_remote.gemspec +29 -0
- data/spec/monster/remote/cli_spec.rb +237 -0
- data/spec/monster/remote/configuration_spec.rb +124 -0
- data/spec/monster/remote/filters/filter_spec.rb +16 -0
- data/spec/monster/remote/filters/name_based_filter_spec.rb +57 -0
- data/spec/monster/remote/sync_spec.rb +108 -0
- data/spec/monster/remote/wrappers/net_ftp_spec.rb +220 -0
- data/spec/spec_helper.rb +55 -0
- metadata +38 -16
- data/lib/monster/remote/version.rb +0 -5
data/Gemfile
ADDED
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -0,0 +1,141 @@
|
|
1
|
+
Monster Remote (gem: monster_remote)
|
2
|
+
====================================
|
3
|
+
|
4
|
+
#What is this?
|
5
|
+
A gem to help you publish your [jekyll](http://jekyllrb.com) blog via
|
6
|
+
ftp (or any other "plugable" remote machine connection provider).
|
7
|
+
|
8
|
+
#How is this?
|
9
|
+
After install the gem
|
10
|
+
|
11
|
+
gem install monster_remote
|
12
|
+
|
13
|
+
A new executable is gonna be at your service: `monster_remote`.
|
14
|
+
|
15
|
+
##Syncing
|
16
|
+
Enter the jekyll blog root path and type:
|
17
|
+
|
18
|
+
monster_remote [--ftp] -h your_server_address -u your_user_here
|
19
|
+
|
20
|
+
The --ftp option is default, you could create you own connection
|
21
|
+
provider if you need it.
|
22
|
+
|
23
|
+
You will be prompted for your password. To reduce the size of the
|
24
|
+
command by configuring some of these informations on your jekyll
|
25
|
+
configuration file (or in a .monster.yml file inside your jekyll site):
|
26
|
+
|
27
|
+
monster:
|
28
|
+
remote:
|
29
|
+
host: ftp.some_host.com
|
30
|
+
user: omg_my_user
|
31
|
+
pass: true
|
32
|
+
local_dir: _site
|
33
|
+
remote_dir:
|
34
|
+
verbose: false
|
35
|
+
|
36
|
+
Monster will rely on this configurations if you execute it without options.
|
37
|
+
But the command line options overrides the configuration file info.
|
38
|
+
|
39
|
+
##Filtering specific files
|
40
|
+
A filter is an object which `respond_to? :filter`, you can stack
|
41
|
+
filters within the synchronization execution. The code to do that
|
42
|
+
has to stay on a `monster_config.rb` or a `config.rb`, create
|
43
|
+
this file on the root directory of your jekyll site:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
# monster_config.rb
|
47
|
+
|
48
|
+
Monster::Remote::add_filter(my_filter)
|
49
|
+
```
|
50
|
+
|
51
|
+
`monster_remote` is shipped with a "name_based_filter", if you want to
|
52
|
+
reject specific files or directories based on the name, you could do
|
53
|
+
something like these:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
# monster_config.rb
|
57
|
+
|
58
|
+
my_filter = Monster::Remote::Filters::NameBasedFilter.new
|
59
|
+
my_filter.reject /^.*rc/
|
60
|
+
my_filter.reject /^not_allowed_dir\//
|
61
|
+
|
62
|
+
Monster::Remote::add_filter(my_filter)
|
63
|
+
```
|
64
|
+
|
65
|
+
Note: the param to `#reject` can be a valid regex, so you could define
|
66
|
+
some fine grained rules here. The above example will reject any file
|
67
|
+
starting with a "." and ending with "rc", wich is pretty much any
|
68
|
+
"classic" configuration file that you have on your directory. Neither
|
69
|
+
"not_allowed_dir" gonna be synced. You could provide an array if you prefer:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
my_filter.reject [/^.*rc/, /ˆnot_allowed_dir\//]`
|
73
|
+
```
|
74
|
+
|
75
|
+
Or you could use a string:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
my_filter.reject ".my_secret_file"
|
79
|
+
```
|
80
|
+
|
81
|
+
If you need execute more specific or complex logic, you could use a "raw
|
82
|
+
filter". Just provides a block with the logic you need, an array with
|
83
|
+
the dir structure will be passed as argument and a filtered array should
|
84
|
+
be returned. Just files and dirs on the result array will be synced:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
my_custom_filter = Monster::Remote::Filters::Filter.new
|
88
|
+
my_custom_filter.reject lambda { |entries|
|
89
|
+
# do whatever you need here, for example:
|
90
|
+
entries.reject do |entry|
|
91
|
+
entry =~ /^.*rc/ || entry =~ /^not_allowed_dir\//
|
92
|
+
end
|
93
|
+
}
|
94
|
+
|
95
|
+
Monster::Remote::add_filter(my_custom_filter)
|
96
|
+
```
|
97
|
+
|
98
|
+
Since a filter is an object which `responds_to? :filter` you could
|
99
|
+
implement your filter in a class. The `#filter` will receive an array
|
100
|
+
with the list of files and dirs in given directory, you need to return a
|
101
|
+
new array with only the allowed files and dirs:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
class MyOMGFilter
|
105
|
+
def filter(entries)
|
106
|
+
// your logic here
|
107
|
+
return new_filtered_array
|
108
|
+
end
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
#Protocol Wrappers
|
113
|
+
A wrapper is an object with the following methods:
|
114
|
+
|
115
|
+
* `::new(driver=Net::FTP)`
|
116
|
+
- the class from the real connection objects will be generated
|
117
|
+
* `#open(host=nil, user=nil, password=nil, port=nil)`
|
118
|
+
- called when the synchrony starts
|
119
|
+
- if a block is given, it will be yielded and two arguments will be
|
120
|
+
passed to the block:
|
121
|
+
- an object that `responds_to? :create_dir`, and `responds_to?
|
122
|
+
:copy_file`. This object has an internal reference to the real
|
123
|
+
connection
|
124
|
+
- `#create_dir(dir)` - creates a remote dir
|
125
|
+
- `#remove_dir(dir)` - removes a remote dir
|
126
|
+
- `#copy_file(from, to)` - copies a file to the remote location
|
127
|
+
- `#remove_file(file)` - removes a remote file
|
128
|
+
- the real connection object (you can ignore the second argument if you want)
|
129
|
+
- this method close the real connection before returns
|
130
|
+
|
131
|
+
Internally a wrapper will use a real "thing" (like the default `Net::FTP`)
|
132
|
+
to replicate the local dir structure to the remote dir.
|
133
|
+
|
134
|
+
#Programatically Syncing
|
135
|
+
The `Monster::Remote::Sync` class has a very well defined interface, and
|
136
|
+
you could use this to write your own sync logic if you need:
|
137
|
+
|
138
|
+
* `::new(wrapper, local_dir=nil, remote_dir=nil, verbose=nil)`
|
139
|
+
* `start(user = nil, password = nil, host = "localhost", port = 21)`
|
140
|
+
- calls the wrapper.open method passing a block with instructions to
|
141
|
+
replicate the local configured dir
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
lib = File.expand_path('../lib/', __FILE__)
|
2
|
+
$:.unshift lib unless $:.include?(lib)
|
3
|
+
|
4
|
+
task :default => [:specs]
|
5
|
+
task :specs => [:"specs:all"]
|
6
|
+
namespace :specs do
|
7
|
+
require 'rspec/core/rake_task'
|
8
|
+
RSpec::Core::RakeTask.new "all" do |t|
|
9
|
+
t.pattern = "spec/**/*_spec.rb"
|
10
|
+
t.rspec_opts = ['--color', '--format documentation', '--require spec_helper']
|
11
|
+
end
|
12
|
+
end
|
data/bin/monster_remote
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
lib = File.expand_path("../../lib", __FILE__)
|
3
|
+
$:.unshift lib unless $:.include?(lib)
|
4
|
+
|
5
|
+
require 'monster_remote'
|
6
|
+
|
7
|
+
begin
|
8
|
+
Monster::Remote::CLI.new.run
|
9
|
+
rescue Interrupt => e
|
10
|
+
exit 1
|
11
|
+
rescue SystemExit => e
|
12
|
+
exit e.status
|
13
|
+
rescue Exception => e
|
14
|
+
raise e
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Monster
|
4
|
+
module Remote
|
5
|
+
|
6
|
+
class CLI
|
7
|
+
|
8
|
+
def initialize(syncer=Sync, out=STDOUT, input=STDIN)
|
9
|
+
@syncer = syncer
|
10
|
+
@out = out
|
11
|
+
@in = input
|
12
|
+
end
|
13
|
+
|
14
|
+
def run(args=ARGV)
|
15
|
+
options = parse_options(args)
|
16
|
+
|
17
|
+
show_version if options[:show_version]
|
18
|
+
password = nil
|
19
|
+
|
20
|
+
config = Configuration.new
|
21
|
+
if options[:password] || config.password_required?
|
22
|
+
password = wait_for_password
|
23
|
+
end
|
24
|
+
|
25
|
+
connection_wrapper = options[:wrapper] || Monster::Remote::Wrappers::NetFTP
|
26
|
+
local_dir = options[:local_dir] || config.local_dir || Dir.pwd
|
27
|
+
remote_dir = options[:remote_dir] || config.remote_dir || File.basename(local_dir)
|
28
|
+
out = (options[:verbose] || config.verbose?) ? STDOUT : nil
|
29
|
+
host = options[:host] || config.host || "localhost"
|
30
|
+
port = options[:port] || config.port || 21
|
31
|
+
user = options[:user] || config.user || nil
|
32
|
+
|
33
|
+
sync = @syncer.new(connection_wrapper, local_dir, remote_dir, out)
|
34
|
+
sync.start(user, password, host, port)
|
35
|
+
end
|
36
|
+
|
37
|
+
def show_version
|
38
|
+
@out.puts Monster::Remote::VERSION
|
39
|
+
exit(0)
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait_for_password
|
43
|
+
@out.print "password:"
|
44
|
+
|
45
|
+
system("stty -echo")
|
46
|
+
|
47
|
+
password = @in.gets.strip
|
48
|
+
|
49
|
+
system("stty echo")
|
50
|
+
system("echo \"\"")
|
51
|
+
|
52
|
+
password
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def parse_options(args, options={})
|
57
|
+
parser = OptionParser.new do |opts|
|
58
|
+
opts.banner = "monster_remote v#{Monster::Remote::VERSION}"
|
59
|
+
opts.banner << " :: Remote sync your jekyll site :: Usage: monster_remote [options]"
|
60
|
+
|
61
|
+
opts.on "-v", "--version", "Show version" do
|
62
|
+
options[:show_version] = true
|
63
|
+
end
|
64
|
+
|
65
|
+
opts.on "-p", "--password", "Password for connection" do
|
66
|
+
options[:password] = true
|
67
|
+
end
|
68
|
+
|
69
|
+
opts.on "-u", "--user USER", "User for connection" do |user|
|
70
|
+
options[:user] = user
|
71
|
+
end
|
72
|
+
|
73
|
+
opts.on "--ftp", "Transfer with NetFTP wrapper" do
|
74
|
+
options[:wrapper] = Monster::Remote::Wrappers::NetFTP
|
75
|
+
end
|
76
|
+
|
77
|
+
opts.on "--verbose", "Verbose mode" do
|
78
|
+
options[:verbose] = true
|
79
|
+
end
|
80
|
+
|
81
|
+
opts.on "-l", "--local-dir DIR_PATH", "Local dir to replicate" do |dir|
|
82
|
+
options[:local_dir] = dir
|
83
|
+
end
|
84
|
+
|
85
|
+
opts.on "-r", "--remote-dir DIR_PATH", "Remote root dir" do |dir|
|
86
|
+
options[:remote_dir] = dir
|
87
|
+
end
|
88
|
+
|
89
|
+
opts.on "-H", "--host HOST", "Server host" do |host|
|
90
|
+
options[:host] = host
|
91
|
+
end
|
92
|
+
|
93
|
+
opts.on "-P", "--port SERVER_PORT", "Server port" do |port|
|
94
|
+
options[:port] = port
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
parser.parse!(args)
|
99
|
+
options
|
100
|
+
end
|
101
|
+
|
102
|
+
end # CLI
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Monster
|
4
|
+
module Remote
|
5
|
+
|
6
|
+
class Configuration
|
7
|
+
|
8
|
+
def initialize(configuration_file = "_config.yml")
|
9
|
+
unless File.exists?(configuration_file)
|
10
|
+
configuration_file = ".monster.yml"
|
11
|
+
end
|
12
|
+
|
13
|
+
@configs = {}
|
14
|
+
|
15
|
+
begin
|
16
|
+
file_info = YAML::load( File.open(configuration_file) )
|
17
|
+
@configs = file_info && file_info["monster"] && file_info["monster"]["remote"]
|
18
|
+
rescue Errno::ENOENT => e
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def password_required?
|
23
|
+
read_config("pass")
|
24
|
+
end
|
25
|
+
|
26
|
+
def verbose?
|
27
|
+
read_config("verbose")
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def expected
|
32
|
+
[:host, :port, :user, :local_dir, :remote_dir, :verbose]
|
33
|
+
end
|
34
|
+
|
35
|
+
def respond_to?(method)
|
36
|
+
expected.include? method || super
|
37
|
+
end
|
38
|
+
|
39
|
+
def method_missing(method, *args)
|
40
|
+
if expected.include? method
|
41
|
+
return read_config(method.to_s)
|
42
|
+
end
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
def read_config(name)
|
47
|
+
@configs && @configs[name]
|
48
|
+
end
|
49
|
+
|
50
|
+
end# Configuration
|
51
|
+
|
52
|
+
end# Remote
|
53
|
+
end# Monster
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Monster
|
2
|
+
module Remote
|
3
|
+
|
4
|
+
class Filter
|
5
|
+
|
6
|
+
def reject(reject_logic)
|
7
|
+
@rejecting ||= []
|
8
|
+
@rejecting << reject_logic
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def filter(entries)
|
13
|
+
return [] if entries.nil?
|
14
|
+
allowed = entries
|
15
|
+
@rejecting.each do |logic|
|
16
|
+
allowed = logic.call(allowed)
|
17
|
+
end
|
18
|
+
allowed
|
19
|
+
end
|
20
|
+
|
21
|
+
end # Filter
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -1,27 +1,24 @@
|
|
1
|
+
require 'monster/remote/filters/filter'
|
2
|
+
|
1
3
|
module Monster
|
2
4
|
module Remote
|
3
5
|
|
4
|
-
class
|
6
|
+
class NameBasedFilter < Filter
|
5
7
|
|
6
8
|
def reject(reject_logic)
|
7
|
-
@rejecting ||= []
|
8
|
-
rejections = []
|
9
9
|
if reject_logic.respond_to? :call
|
10
|
-
|
10
|
+
super(reject_logic)
|
11
11
|
else
|
12
|
-
|
12
|
+
become_block(reject_logic).each do |rejection|
|
13
|
+
super(rejection)
|
14
|
+
end
|
13
15
|
end
|
14
|
-
@rejecting += rejections
|
15
16
|
self
|
16
17
|
end
|
17
18
|
|
18
19
|
def filter(directory)
|
19
|
-
|
20
|
-
|
21
|
-
@rejecting.each do |logic|
|
22
|
-
allowed = logic.call(allowed)
|
23
|
-
end
|
24
|
-
allowed
|
20
|
+
dir_structure = become_array(directory)
|
21
|
+
super(dir_structure)
|
25
22
|
end
|
26
23
|
|
27
24
|
private
|
data/lib/monster/remote/sync.rb
CHANGED
@@ -3,55 +3,87 @@ module Monster
|
|
3
3
|
|
4
4
|
class Sync
|
5
5
|
|
6
|
-
|
7
|
-
|
6
|
+
attr_writer :verbose, :host, :port
|
7
|
+
|
8
|
+
def initialize(wrapper, local_dir=nil, remote_dir=nil, verbose=nil)
|
9
|
+
@wrapper = wrapper
|
10
|
+
local_dir && @local_dir = local_dir
|
11
|
+
remote_dir && @remote_dir = remote_dir
|
12
|
+
verbose && @verbose = verbose
|
8
13
|
end
|
9
14
|
|
10
|
-
def start
|
11
|
-
@
|
12
|
-
|
15
|
+
def start(user = nil, password = nil, host = "localhost", port = 21)
|
16
|
+
@host = host
|
17
|
+
@port = port
|
18
|
+
@user = user || ""
|
19
|
+
@password = password || ""
|
20
|
+
|
21
|
+
@wrapper || raise(MissingProtocolWrapperError)
|
22
|
+
local_dir || raise(MissingLocalDirError)
|
23
|
+
remote_dir || raise(MissingRemoteDirError)
|
24
|
+
|
25
|
+
out("syncing from: #{local_dir} to: #{remote_dir}")
|
26
|
+
|
27
|
+
open(@wrapper) do |wrapper|
|
28
|
+
out("connection openned, using: #{wrapper}")
|
29
|
+
copy_to_remote(wrapper, local_dir)
|
13
30
|
end
|
14
31
|
end
|
15
32
|
|
16
|
-
|
17
|
-
|
33
|
+
private
|
34
|
+
def create_dir(wrapper, local_dir_path, remote_dir_path, entry_path)
|
35
|
+
out("creating dir #{remote_dir_path}")
|
36
|
+
wrapper.create_dir(remote_dir_path)
|
37
|
+
out("diggin into #{local_dir_path}")
|
38
|
+
Dir.entries(local_dir_path).each do |dir|
|
39
|
+
copy_to_remote(wrapper, dir, entry_path)
|
40
|
+
end
|
18
41
|
end
|
19
42
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
43
|
+
def copy_file(wrapper, local_file, remote_file)
|
44
|
+
out("coping file to #{remote_file}")
|
45
|
+
wrapper.copy_file(local_file, remote_file)
|
23
46
|
end
|
24
47
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
|
48
|
+
def copy_to_remote(wrapper, entry, path=nil)
|
49
|
+
is_dot_dir = entry =~ /^\.$|^\.\.$/
|
50
|
+
out("ignoring dir #{entry}")
|
51
|
+
return if is_dot_dir
|
52
|
+
|
53
|
+
entry_path = path ? File.join(path, entry) : ""
|
54
|
+
local_path = File.join(local_dir, entry_path).gsub(/\.*\/$/, "")
|
55
|
+
remote_path = File.join(remote_dir, entry_path).gsub(/\.*\/$/, "")
|
29
56
|
|
30
|
-
|
31
|
-
|
32
|
-
|
57
|
+
if File.directory?(local_path)
|
58
|
+
out("copying #{local_path}")
|
59
|
+
create_dir(wrapper, local_path, remote_path, entry_path)
|
60
|
+
else
|
61
|
+
copy_file(wrapper, local_path, remote_path)
|
62
|
+
end
|
33
63
|
end
|
34
64
|
|
35
|
-
def
|
36
|
-
@
|
37
|
-
self
|
65
|
+
def out(msg)
|
66
|
+
@verbose && @verbose.puts(msg)
|
38
67
|
end
|
39
68
|
|
40
|
-
def
|
41
|
-
@
|
42
|
-
self
|
69
|
+
def local_dir
|
70
|
+
@local_dir
|
43
71
|
end
|
44
72
|
|
45
|
-
def
|
46
|
-
@
|
47
|
-
self
|
73
|
+
def remote_dir
|
74
|
+
@remote_dir
|
48
75
|
end
|
49
76
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
77
|
+
def open(wrapper, &block)
|
78
|
+
begin
|
79
|
+
out("trying to connect using wrapper")
|
80
|
+
wrapper.new.open(@host, @user, @password, @port, &block)
|
81
|
+
rescue Exception => e
|
82
|
+
out("connection failed, #{e.message}")
|
83
|
+
raise e
|
84
|
+
end
|
53
85
|
end
|
54
|
-
end # Sync
|
55
86
|
|
87
|
+
end# Sync
|
56
88
|
end
|
57
89
|
end
|