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 ADDED
@@ -0,0 +1,5 @@
1
+ source :rubygems
2
+
3
+ group :test do
4
+ gem "rspec"
5
+ end
data/LICENSE CHANGED
@@ -11,4 +11,3 @@ distributed under the License is distributed on an "AS IS" BASIS,
11
11
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
-
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
@@ -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 ContentNameBasedFilter
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
- rejections << reject_logic
10
+ super(reject_logic)
11
11
  else
12
- rejections = become_block(reject_logic)
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
- return [] if directory.nil?
20
- allowed = become_array(directory)
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
@@ -3,55 +3,87 @@ module Monster
3
3
 
4
4
  class Sync
5
5
 
6
- def self.with
7
- Sync.new
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
- @provider.open(@host, @port || 21, @user, @pass) do |con|
12
- con.copy_dir(@local_dir || "./_site", @remote_dir || ".")
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
- def add_filter(filter)
17
- @provider.add_filter(filter)
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 local_dir(local_dir)
21
- @local_dir = local_dir
22
- self
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 remote_dir(remote_dir)
26
- @remote_dir = remote_dir
27
- self
28
- end
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
- def remote_connection_provider(provider)
31
- @provider = provider
32
- self
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 host(host)
36
- @host = host
37
- self
65
+ def out(msg)
66
+ @verbose && @verbose.puts(msg)
38
67
  end
39
68
 
40
- def port(port)
41
- @port = port
42
- self
69
+ def local_dir
70
+ @local_dir
43
71
  end
44
72
 
45
- def user(user)
46
- @user = user
47
- self
73
+ def remote_dir
74
+ @remote_dir
48
75
  end
49
76
 
50
- def pass(pass)
51
- @pass = pass
52
- self
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