monster_remote 0.0.1 → 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/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
|