dandelion 0.2.0 → 0.2.1
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/README.md +24 -3
- data/dandelion.gemspec +5 -2
- data/lib/dandelion/backend.rb +54 -0
- data/lib/dandelion/backend/ftp.rb +80 -0
- data/lib/dandelion/backend/s3.rb +52 -0
- data/lib/dandelion/backend/sftp.rb +90 -0
- data/lib/dandelion/cli.rb +25 -35
- data/lib/dandelion/deployment.rb +20 -14
- data/lib/dandelion/version.rb +1 -1
- data/test/test_diff_deployment.rb +9 -10
- data/test/test_ftp.rb +47 -0
- data/test/test_sftp.rb +47 -0
- metadata +72 -65
- data/lib/dandelion/service.rb +0 -168
data/README.md
CHANGED
@@ -15,11 +15,11 @@ Alternatively, you can build the gem yourself:
|
|
15
15
|
|
16
16
|
Config
|
17
17
|
------
|
18
|
-
Configuration options are specified in a YAML file (Dandelion looks for
|
19
|
-
|
18
|
+
Configuration options are specified in a YAML file (Dandelion looks for
|
19
|
+
`dandelion.yml` by default). Example:
|
20
20
|
|
21
21
|
# Required
|
22
|
-
scheme: sftp
|
22
|
+
scheme: sftp
|
23
23
|
host: example.com
|
24
24
|
username: user
|
25
25
|
password: pass
|
@@ -29,7 +29,28 @@ named `dandelion.yml` by default):
|
|
29
29
|
exclude:
|
30
30
|
- .gitignore
|
31
31
|
- dandelion.yml
|
32
|
+
|
33
|
+
Schemes
|
34
|
+
-------
|
35
|
+
There is support for multiple backend file transfer schemes. The configuration
|
36
|
+
must specify one of these schemes and the set of additional parameters required
|
37
|
+
by the given scheme.
|
38
|
+
|
39
|
+
**SFTP**: `scheme: sftp`
|
40
|
+
|
41
|
+
Required: `host`, `username`, `password`
|
42
|
+
Optional: `path`, `exclude`
|
32
43
|
|
44
|
+
**FTP**: `scheme: ftp`
|
45
|
+
|
46
|
+
Required: `host`, `username`, `password`
|
47
|
+
Optional: `path`, `exclude`
|
48
|
+
|
49
|
+
**Amazon S3**: `scheme: s3`
|
50
|
+
|
51
|
+
Required: `access_key_id`, `secret_access_key`, `bucket_name`
|
52
|
+
Optional: `path`, `exclude`
|
53
|
+
|
33
54
|
Usage
|
34
55
|
-----
|
35
56
|
From the root directory of a Git repository, run:
|
data/dandelion.gemspec
CHANGED
@@ -10,10 +10,13 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.email = ['scottbnel@gmail.com']
|
11
11
|
s.homepage = 'http://github.com/scttnlsn/dandelion'
|
12
12
|
s.summary = "dandelion-#{s.version}"
|
13
|
-
s.description = 'Git repository deployment
|
13
|
+
s.description = 'Incremental Git repository deployment'
|
14
14
|
|
15
|
-
s.add_dependency 'net-sftp', '>= 2.0.5'
|
16
15
|
s.add_dependency 'grit', '>= 2.4.1'
|
16
|
+
|
17
|
+
s.add_development_dependency 'mocha', '>= 0.9.12'
|
18
|
+
s.add_development_dependency 'net-sftp', '>= 2.0.5'
|
19
|
+
s.add_development_dependency 'aws-s3', '>= 0.6.0'
|
17
20
|
|
18
21
|
s.files = `git ls-files`.split("\n")
|
19
22
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module Dandelion
|
4
|
+
module Backend
|
5
|
+
class MissingFileError < StandardError; end
|
6
|
+
class UnsupportedSchemeError < StandardError; end
|
7
|
+
|
8
|
+
class MissingDependencyError < StandardError
|
9
|
+
attr_reader :gems
|
10
|
+
|
11
|
+
def initialize(gems)
|
12
|
+
@gems = gems
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Backend
|
17
|
+
class << self
|
18
|
+
@@backends = {}
|
19
|
+
|
20
|
+
def create(config)
|
21
|
+
Dir.glob(File.join(File.dirname(__FILE__), 'backend', '*.rb')) { |file| require file }
|
22
|
+
raise UnsupportedSchemeError unless @@backends.include? config['scheme']
|
23
|
+
begin
|
24
|
+
@@backends[config['scheme']].new(config)
|
25
|
+
rescue LoadError
|
26
|
+
raise MissingDependencyError.new(@@backends[config['scheme']].gem_list)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def scheme(scheme)
|
31
|
+
@@backends[scheme] = self
|
32
|
+
end
|
33
|
+
|
34
|
+
def gems(*gems)
|
35
|
+
@gems = gems
|
36
|
+
end
|
37
|
+
|
38
|
+
def gem_list
|
39
|
+
@gems
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def temp(file, data)
|
46
|
+
tmp = Tempfile.new(file.gsub('/', '.'))
|
47
|
+
tmp << data
|
48
|
+
tmp.flush
|
49
|
+
yield(tmp.path)
|
50
|
+
tmp.close
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'dandelion/backend'
|
2
|
+
|
3
|
+
module Dandelion
|
4
|
+
module Backend
|
5
|
+
class FTP < Backend
|
6
|
+
scheme 'ftp'
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
require 'net/ftp'
|
10
|
+
@config = config
|
11
|
+
@ftp = Net::FTP.open(@config['host'], @config['username'], @config['password'])
|
12
|
+
@ftp.passive = true
|
13
|
+
@ftp.chdir(@config['path']) if @config['path']
|
14
|
+
end
|
15
|
+
|
16
|
+
def read(file)
|
17
|
+
begin
|
18
|
+
# Implementation of FTP#getbinaryfile differs between 1.8
|
19
|
+
# and 1.9 so we call FTP#retrbinary directly
|
20
|
+
content = ''
|
21
|
+
@ftp.retrbinary("RETR #{file}", 4096) do |data|
|
22
|
+
content += data
|
23
|
+
end
|
24
|
+
content
|
25
|
+
rescue Net::FTPPermError => e
|
26
|
+
raise MissingFileError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def write(file, data)
|
31
|
+
temp(file, data) do |temp|
|
32
|
+
begin
|
33
|
+
@ftp.putbinaryfile temp, file
|
34
|
+
rescue Net::FTPPermError => e
|
35
|
+
mkdir_p File.dirname(file)
|
36
|
+
@ftp.putbinaryfile temp, file
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete(file)
|
42
|
+
begin
|
43
|
+
@ftp.delete file
|
44
|
+
cleanup File.dirname(file)
|
45
|
+
rescue Net::FTPPermError => e
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
"ftp://#{@config['username']}@#{@config['host']}/#{@config['path']}"
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def cleanup(dir)
|
56
|
+
unless dir == File.dirname(dir)
|
57
|
+
if empty? dir
|
58
|
+
@ftp.rmdir dir
|
59
|
+
cleanup File.dirname(dir)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def empty?(dir)
|
65
|
+
return @ftp.nlst(dir).empty?
|
66
|
+
end
|
67
|
+
|
68
|
+
def mkdir_p(dir)
|
69
|
+
unless dir == File.dirname(dir)
|
70
|
+
begin
|
71
|
+
@ftp.mkdir dir
|
72
|
+
rescue Net::FTPPermError => e
|
73
|
+
mkdir_p File.dirname(dir)
|
74
|
+
@ftp.mkdir dir
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'dandelion/backend'
|
2
|
+
|
3
|
+
module Dandelion
|
4
|
+
module Backend
|
5
|
+
class S3 < Backend
|
6
|
+
scheme 's3'
|
7
|
+
gems 'aws-s3'
|
8
|
+
|
9
|
+
def initialize(config)
|
10
|
+
require 'aws/s3'
|
11
|
+
@access_key_id = config['access_key_id']
|
12
|
+
@secret_access_key = config['secret_access_key']
|
13
|
+
@bucket_name = config['bucket_name']
|
14
|
+
@path = config['path']
|
15
|
+
end
|
16
|
+
|
17
|
+
def read(file)
|
18
|
+
s3connect!
|
19
|
+
raise MissingFileError unless AWS::S3::S3Object.exists? path(file), @bucket_name
|
20
|
+
AWS::S3::S3Object.value path(file), @bucket_name
|
21
|
+
end
|
22
|
+
|
23
|
+
def write(file, data)
|
24
|
+
s3connect!
|
25
|
+
AWS::S3::S3Object.store path(file), data, @bucket_name
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete(file)
|
29
|
+
s3connect!
|
30
|
+
AWS::S3::S3Object.delete path(file), @bucket_name
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
"s3://#{@access_key_id}@#{@bucket_name}/#{@path}"
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def s3connect!
|
40
|
+
AWS::S3::Base.establish_connection!(:access_key_id => @access_key_id, :secret_access_key => @secret_access_key, :use_ssl => true) unless AWS::S3::Base.connected?
|
41
|
+
end
|
42
|
+
|
43
|
+
def path(file)
|
44
|
+
if @path and !@path.empty?
|
45
|
+
"#{@path}/#{file}"
|
46
|
+
else
|
47
|
+
file
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'dandelion/backend'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module Dandelion
|
5
|
+
module Backend
|
6
|
+
class SFTP < Backend
|
7
|
+
scheme 'sftp'
|
8
|
+
gems 'net-sftp'
|
9
|
+
|
10
|
+
def initialize(config)
|
11
|
+
require 'net/sftp'
|
12
|
+
@config = config
|
13
|
+
@sftp = Net::SFTP.start(@config['host'], @config['username'], :password => @config['password'])
|
14
|
+
end
|
15
|
+
|
16
|
+
def read(file)
|
17
|
+
begin
|
18
|
+
@sftp.file.open(path(file), 'r') do |f|
|
19
|
+
f.gets
|
20
|
+
end
|
21
|
+
rescue Net::SFTP::StatusException => e
|
22
|
+
raise unless e.code == 2
|
23
|
+
raise MissingFileError
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def write(file, data)
|
28
|
+
temp(file, data) do |temp|
|
29
|
+
begin
|
30
|
+
@sftp.upload! temp, path(file)
|
31
|
+
rescue Net::SFTP::StatusException => e
|
32
|
+
raise unless e.code == 2
|
33
|
+
mkdir_p File.dirname(path(file))
|
34
|
+
@sftp.upload! temp, path(file)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def delete(file)
|
40
|
+
begin
|
41
|
+
@sftp.remove! path(file)
|
42
|
+
cleanup File.dirname(path(file))
|
43
|
+
rescue Net::SFTP::StatusException => e
|
44
|
+
raise unless e.code == 2
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
"sftp://#{@config['username']}@#{@config['host']}/#{@config['path']}"
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def cleanpath(path)
|
55
|
+
Pathname.new(path).cleanpath.to_s if path
|
56
|
+
end
|
57
|
+
|
58
|
+
def cleanup(dir)
|
59
|
+
unless cleanpath(dir) == cleanpath(@config['path']) or dir == File.dirname(dir)
|
60
|
+
if empty? dir
|
61
|
+
@sftp.rmdir! dir
|
62
|
+
cleanup File.dirname(dir)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def empty?(dir)
|
68
|
+
@sftp.dir.entries(dir).delete_if { |file| file.name == '.' or file.name == '..' }.empty?
|
69
|
+
end
|
70
|
+
|
71
|
+
def mkdir_p(dir)
|
72
|
+
begin
|
73
|
+
@sftp.mkdir! dir
|
74
|
+
rescue Net::SFTP::StatusException => e
|
75
|
+
raise unless e.code == 2
|
76
|
+
mkdir_p File.dirname(dir)
|
77
|
+
retry
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def path(file)
|
82
|
+
if @config['path'] and !@config['path'].empty?
|
83
|
+
File.join @config['path'], file
|
84
|
+
else
|
85
|
+
file
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/dandelion/cli.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'dandelion'
|
2
|
+
require 'dandelion/backend'
|
2
3
|
require 'dandelion/deployment'
|
3
4
|
require 'dandelion/git'
|
4
|
-
require 'dandelion/service'
|
5
5
|
require 'dandelion/version'
|
6
6
|
require 'optparse'
|
7
7
|
require 'yaml'
|
@@ -120,51 +120,41 @@ module Dandelion
|
|
120
120
|
end
|
121
121
|
|
122
122
|
def execute
|
123
|
-
|
124
|
-
|
125
|
-
log.info("
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
end
|
123
|
+
begin
|
124
|
+
backend = Backend::Backend.create(@config)
|
125
|
+
log.info("Connecting to: #{backend}")
|
126
|
+
rescue Backend::MissingDependencyError => e
|
127
|
+
log.fatal("The '#{@config['scheme']}' scheme requires additional gems:")
|
128
|
+
log.fatal(' ' + e.gems.join("\n ") + "\n")
|
129
|
+
log.fatal("Please install the gems: gem install #{e.gems.join(' ')}")
|
130
|
+
exit
|
131
|
+
rescue Backend::UnsupportedSchemeError
|
132
|
+
log.fatal("Unsupported scheme: #{@config['scheme']}")
|
133
|
+
exit
|
135
134
|
end
|
136
|
-
|
137
|
-
|
138
|
-
private
|
139
|
-
|
140
|
-
def deployment(service)
|
135
|
+
|
141
136
|
begin
|
142
|
-
deployment = Deployment::
|
143
|
-
rescue Deployment::RemoteRevisionError
|
144
|
-
deployment = Deployment::FullDeployment.new(@repo, service, @config['exclude'])
|
137
|
+
deployment = Deployment::Deployment.create(@repo, backend, @config['exclude'])
|
145
138
|
rescue Git::DiffError
|
146
139
|
log.fatal('Error: could not generate diff')
|
147
140
|
log.fatal('Try merging remote changes before running dandelion again')
|
148
141
|
exit
|
149
142
|
end
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
end
|
156
|
-
|
157
|
-
def service
|
158
|
-
if @config['scheme'] == 'sftp'
|
159
|
-
Service::SFTP.new(@config['host'], @config['username'], @config['password'], @config['path'])
|
160
|
-
elsif @config['scheme'] == 'ftp'
|
161
|
-
Service::FTP.new(@config['host'], @config['username'], @config['password'], @config['path'])
|
162
|
-
else
|
163
|
-
log.fatal("Unsupported scheme: #{@config['scheme']}")
|
143
|
+
|
144
|
+
log.info("Remote revision: #{deployment.remote_revision || '---'}")
|
145
|
+
log.info("Local revision: #{deployment.local_revision}")
|
146
|
+
|
147
|
+
if @command == 'status'
|
164
148
|
exit
|
149
|
+
elsif @command == 'deploy'
|
150
|
+
validate_deployment deployment
|
151
|
+
deployment.deploy
|
152
|
+
log.info("Deployment complete")
|
165
153
|
end
|
166
154
|
end
|
167
155
|
|
156
|
+
private
|
157
|
+
|
168
158
|
def validate_deployment(deployment)
|
169
159
|
begin
|
170
160
|
@repo.remote_list.each do |remote|
|
data/lib/dandelion/deployment.rb
CHANGED
@@ -6,9 +6,19 @@ module Dandelion
|
|
6
6
|
class FastForwardError < StandardError; end
|
7
7
|
|
8
8
|
class Deployment
|
9
|
-
|
9
|
+
class << self
|
10
|
+
def create(repo, backend, exclude)
|
11
|
+
begin
|
12
|
+
DiffDeployment.new(repo, backend, exclude)
|
13
|
+
rescue RemoteRevisionError
|
14
|
+
FullDeployment.new(repo, backend, exclude)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(repo, backend, exclude = nil, revision = 'HEAD')
|
10
20
|
@repo = repo
|
11
|
-
@
|
21
|
+
@backend = backend
|
12
22
|
@exclude = exclude || []
|
13
23
|
@tree = Git::Tree.new(@repo, revision)
|
14
24
|
end
|
@@ -21,12 +31,8 @@ module Dandelion
|
|
21
31
|
nil
|
22
32
|
end
|
23
33
|
|
24
|
-
def remote_uri
|
25
|
-
@service.uri
|
26
|
-
end
|
27
|
-
|
28
34
|
def write_revision
|
29
|
-
@
|
35
|
+
@backend.write('.revision', local_revision)
|
30
36
|
end
|
31
37
|
|
32
38
|
def validate_state(remote = nil)
|
@@ -50,8 +56,8 @@ module Dandelion
|
|
50
56
|
end
|
51
57
|
|
52
58
|
class DiffDeployment < Deployment
|
53
|
-
def initialize(repo,
|
54
|
-
super(repo,
|
59
|
+
def initialize(repo, backend, exclude = nil, revision = 'HEAD')
|
60
|
+
super(repo, backend, exclude, revision)
|
55
61
|
@diff = Git::Diff.new(@repo, read_remote_revision, revision)
|
56
62
|
end
|
57
63
|
|
@@ -77,7 +83,7 @@ module Dandelion
|
|
77
83
|
log.info("Skipping file: #{file}")
|
78
84
|
else
|
79
85
|
log.info("Uploading file: #{file}")
|
80
|
-
@
|
86
|
+
@backend.write(file, @tree.show(file))
|
81
87
|
end
|
82
88
|
end
|
83
89
|
end
|
@@ -88,7 +94,7 @@ module Dandelion
|
|
88
94
|
log.info("Skipping file: #{file}")
|
89
95
|
else
|
90
96
|
log.info("Deleting file: #{file}")
|
91
|
-
@
|
97
|
+
@backend.delete(file)
|
92
98
|
end
|
93
99
|
end
|
94
100
|
end
|
@@ -105,8 +111,8 @@ module Dandelion
|
|
105
111
|
|
106
112
|
def read_remote_revision
|
107
113
|
begin
|
108
|
-
@
|
109
|
-
rescue
|
114
|
+
@backend.read('.revision').chomp
|
115
|
+
rescue Backend::MissingFileError
|
110
116
|
raise RemoteRevisionError
|
111
117
|
end
|
112
118
|
end
|
@@ -119,7 +125,7 @@ module Dandelion
|
|
119
125
|
log.info("Skipping file: #{file}")
|
120
126
|
else
|
121
127
|
log.info("Uploading file: #{file}")
|
122
|
-
@
|
128
|
+
@backend.write(file, @tree.show(file))
|
123
129
|
end
|
124
130
|
end
|
125
131
|
write_revision
|
data/lib/dandelion/version.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'dandelion'
|
2
2
|
require 'dandelion/deployment'
|
3
|
-
require 'dandelion/service'
|
4
3
|
require 'test/unit'
|
5
4
|
|
6
5
|
def fixture(name)
|
@@ -57,7 +56,7 @@ class MockRepo
|
|
57
56
|
end
|
58
57
|
end
|
59
58
|
|
60
|
-
class
|
59
|
+
class MockBackend
|
61
60
|
attr_reader :reads, :writes, :deletes
|
62
61
|
|
63
62
|
def initialize(remote_revision)
|
@@ -87,8 +86,8 @@ class TestDiffDeployment < Test::Unit::TestCase
|
|
87
86
|
@head_revision = '0ca605e9f0f1d42ce8193ac36db11ec3cc9efc08'
|
88
87
|
@remote_revision = 'ff1f1d4bd0c99e1c9cca047c46b2194accf89504'
|
89
88
|
@repo = MockRepo.new
|
90
|
-
@
|
91
|
-
@diff_deployment = Dandelion::Deployment::DiffDeployment.new(@repo, @
|
89
|
+
@backend = MockBackend.new(@remote_revision)
|
90
|
+
@diff_deployment = Dandelion::Deployment::DiffDeployment.new(@repo, @backend, [], @head_revision)
|
92
91
|
end
|
93
92
|
|
94
93
|
def test_diff_deployment_local_revision
|
@@ -101,7 +100,7 @@ class TestDiffDeployment < Test::Unit::TestCase
|
|
101
100
|
|
102
101
|
def test_diff_deployment_write_revision
|
103
102
|
@diff_deployment.write_revision
|
104
|
-
assert_equal @head_revision, @
|
103
|
+
assert_equal @head_revision, @backend.writes['.revision']
|
105
104
|
end
|
106
105
|
|
107
106
|
def test_diff_deployment_revisions_match
|
@@ -114,10 +113,10 @@ class TestDiffDeployment < Test::Unit::TestCase
|
|
114
113
|
|
115
114
|
def test_diff_deployment_deploy
|
116
115
|
@diff_deployment.deploy
|
117
|
-
assert_equal 3, @
|
118
|
-
assert_equal 'bar', @
|
119
|
-
assert_equal 'bar', @
|
120
|
-
assert_equal @head_revision, @
|
121
|
-
assert_equal ['foobar'], @
|
116
|
+
assert_equal 3, @backend.writes.length
|
117
|
+
assert_equal 'bar', @backend.writes['foo']
|
118
|
+
assert_equal 'bar', @backend.writes['baz/foo']
|
119
|
+
assert_equal @head_revision, @backend.writes['.revision']
|
120
|
+
assert_equal ['foobar'], @backend.deletes
|
122
121
|
end
|
123
122
|
end
|
data/test/test_ftp.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'dandelion/backend/ftp'
|
2
|
+
require 'mocha'
|
3
|
+
require 'net/ftp'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
class TestFTP < Test::Unit::TestCase
|
7
|
+
def setup
|
8
|
+
@ftp = mock()
|
9
|
+
Net::FTP.stubs(:open).returns(@ftp)
|
10
|
+
@ftp.expects(:passive=).with(true).once
|
11
|
+
@ftp.expects(:chdir).with('foo').once
|
12
|
+
@backend = Dandelion::Backend::FTP.new('path' => 'foo')
|
13
|
+
class << @backend
|
14
|
+
def temp(file, data)
|
15
|
+
yield(:temp)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_read
|
21
|
+
@ftp.expects(:retrbinary).with('RETR bar', 4096).once
|
22
|
+
@ftp.expects(:retrbinary).with('RETR bar/baz', 4096).once
|
23
|
+
@ftp.expects(:retrbinary).with('RETR bar/baz/qux', 4096).once
|
24
|
+
@backend.read('bar')
|
25
|
+
@backend.read('bar/baz')
|
26
|
+
@backend.read('bar/baz/qux')
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_write
|
30
|
+
@ftp.expects(:putbinaryfile).with(:temp, 'bar').once
|
31
|
+
@ftp.expects(:putbinaryfile).with(:temp, 'bar/baz').once
|
32
|
+
@backend.write('bar', 'baz')
|
33
|
+
@backend.write('bar/baz', 'qux')
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_delete
|
37
|
+
@ftp.expects(:delete).with('bar').once
|
38
|
+
@ftp.expects(:delete).with('bar/baz').once
|
39
|
+
@ftp.expects(:delete).with('bar/baz/qux').once
|
40
|
+
@ftp.expects(:rmdir).with('bar').twice
|
41
|
+
@ftp.expects(:rmdir).with('bar/baz').once
|
42
|
+
@backend.stubs(:empty?).returns(true)
|
43
|
+
@backend.delete('bar')
|
44
|
+
@backend.delete('bar/baz')
|
45
|
+
@backend.delete('bar/baz/qux')
|
46
|
+
end
|
47
|
+
end
|
data/test/test_sftp.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'dandelion/backend/sftp'
|
2
|
+
require 'mocha'
|
3
|
+
require 'net/sftp'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
class TestSFTP < Test::Unit::TestCase
|
7
|
+
def setup
|
8
|
+
@sftp = mock()
|
9
|
+
Net::SFTP.stubs(:start).returns(@sftp)
|
10
|
+
@backend = Dandelion::Backend::SFTP.new('path' => 'foo')
|
11
|
+
class << @backend
|
12
|
+
def temp(file, data)
|
13
|
+
yield(:temp)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_read
|
19
|
+
file = mock()
|
20
|
+
@sftp.stubs(:file).returns(file)
|
21
|
+
file.expects(:open).with('foo/bar', 'r').once
|
22
|
+
file.expects(:open).with('foo/bar/baz', 'r').once
|
23
|
+
file.expects(:open).with('foo/bar/baz/qux', 'r').once
|
24
|
+
@backend.read('bar')
|
25
|
+
@backend.read('bar/baz')
|
26
|
+
@backend.read('bar/baz/qux')
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_write
|
30
|
+
@sftp.expects(:upload!).with(:temp, 'foo/bar').once
|
31
|
+
@sftp.expects(:upload!).with(:temp, 'foo/bar/baz').once
|
32
|
+
@backend.write('bar', 'baz')
|
33
|
+
@backend.write('bar/baz', 'qux')
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_delete
|
37
|
+
@sftp.expects(:remove!).with('foo/bar').once
|
38
|
+
@sftp.expects(:remove!).with('foo/bar/baz').once
|
39
|
+
@sftp.expects(:remove!).with('foo/bar/baz/qux').once
|
40
|
+
@sftp.expects(:rmdir!).with('foo/bar').twice
|
41
|
+
@sftp.expects(:rmdir!).with('foo/bar/baz').once
|
42
|
+
@backend.stubs(:empty?).returns(true)
|
43
|
+
@backend.delete('bar')
|
44
|
+
@backend.delete('bar/baz')
|
45
|
+
@backend.delete('bar/baz/qux')
|
46
|
+
end
|
47
|
+
end
|
metadata
CHANGED
@@ -1,62 +1,69 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: dandelion
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
|
6
|
-
- 0
|
7
|
-
- 2
|
8
|
-
- 0
|
9
|
-
version: 0.2.0
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
prerelease:
|
10
6
|
platform: ruby
|
11
|
-
authors:
|
7
|
+
authors:
|
12
8
|
- Scott Nelson
|
13
9
|
autorequire:
|
14
10
|
bindir: bin
|
15
11
|
cert_chain: []
|
16
|
-
|
17
|
-
date: 2011-04-17 00:00:00 -04:00
|
12
|
+
date: 2011-05-19 00:00:00.000000000 -04:00
|
18
13
|
default_executable:
|
19
|
-
dependencies:
|
20
|
-
- !ruby/object:Gem::Dependency
|
21
|
-
name:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: grit
|
17
|
+
requirement: &2161286340 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 2.4.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *2161286340
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: mocha
|
28
|
+
requirement: &2161285840 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.9.12
|
34
|
+
type: :development
|
22
35
|
prerelease: false
|
23
|
-
|
36
|
+
version_requirements: *2161285840
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: net-sftp
|
39
|
+
requirement: &2161285380 !ruby/object:Gem::Requirement
|
24
40
|
none: false
|
25
|
-
requirements:
|
26
|
-
- -
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
segments:
|
29
|
-
- 2
|
30
|
-
- 0
|
31
|
-
- 5
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
32
44
|
version: 2.0.5
|
33
|
-
type: :
|
34
|
-
version_requirements: *id001
|
35
|
-
- !ruby/object:Gem::Dependency
|
36
|
-
name: grit
|
45
|
+
type: :development
|
37
46
|
prerelease: false
|
38
|
-
|
47
|
+
version_requirements: *2161285380
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: aws-s3
|
50
|
+
requirement: &2161284920 !ruby/object:Gem::Requirement
|
39
51
|
none: false
|
40
|
-
requirements:
|
41
|
-
- -
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
version_requirements: *id002
|
50
|
-
description: Git repository deployment via FTP/SFTP
|
51
|
-
email:
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 0.6.0
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *2161284920
|
59
|
+
description: Incremental Git repository deployment
|
60
|
+
email:
|
52
61
|
- scottbnel@gmail.com
|
53
|
-
executables:
|
62
|
+
executables:
|
54
63
|
- dandelion
|
55
64
|
extensions: []
|
56
|
-
|
57
65
|
extra_rdoc_files: []
|
58
|
-
|
59
|
-
files:
|
66
|
+
files:
|
60
67
|
- .gitignore
|
61
68
|
- Gemfile
|
62
69
|
- README.md
|
@@ -64,14 +71,18 @@ files:
|
|
64
71
|
- bin/dandelion
|
65
72
|
- dandelion.gemspec
|
66
73
|
- lib/dandelion.rb
|
74
|
+
- lib/dandelion/backend.rb
|
75
|
+
- lib/dandelion/backend/ftp.rb
|
76
|
+
- lib/dandelion/backend/s3.rb
|
77
|
+
- lib/dandelion/backend/sftp.rb
|
67
78
|
- lib/dandelion/cli.rb
|
68
79
|
- lib/dandelion/deployment.rb
|
69
80
|
- lib/dandelion/git.rb
|
70
|
-
- lib/dandelion/service.rb
|
71
81
|
- lib/dandelion/version.rb
|
72
82
|
- test/fixtures/diff
|
73
83
|
- test/fixtures/ls_tree
|
74
84
|
- test/test_diff_deployment.rb
|
85
|
+
- test/test_ftp.rb
|
75
86
|
- test/test_git.git/HEAD
|
76
87
|
- test/test_git.git/config
|
77
88
|
- test/test_git.git/description
|
@@ -99,42 +110,37 @@ files:
|
|
99
110
|
- test/test_git.git/objects/ff/1f1d4bd0c99e1c9cca047c46b2194accf89504
|
100
111
|
- test/test_git.git/refs/heads/master
|
101
112
|
- test/test_git.rb
|
113
|
+
- test/test_sftp.rb
|
102
114
|
has_rdoc: true
|
103
115
|
homepage: http://github.com/scttnlsn/dandelion
|
104
116
|
licenses: []
|
105
|
-
|
106
117
|
post_install_message:
|
107
118
|
rdoc_options: []
|
108
|
-
|
109
|
-
require_paths:
|
119
|
+
require_paths:
|
110
120
|
- lib
|
111
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
122
|
none: false
|
113
|
-
requirements:
|
114
|
-
- -
|
115
|
-
- !ruby/object:Gem::Version
|
116
|
-
|
117
|
-
|
118
|
-
version: "0"
|
119
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ! '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
128
|
none: false
|
121
|
-
requirements:
|
122
|
-
- -
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
|
125
|
-
- 0
|
126
|
-
version: "0"
|
129
|
+
requirements:
|
130
|
+
- - ! '>='
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
127
133
|
requirements: []
|
128
|
-
|
129
134
|
rubyforge_project:
|
130
|
-
rubygems_version: 1.
|
135
|
+
rubygems_version: 1.6.2
|
131
136
|
signing_key:
|
132
137
|
specification_version: 3
|
133
|
-
summary: dandelion-0.2.
|
134
|
-
test_files:
|
138
|
+
summary: dandelion-0.2.1
|
139
|
+
test_files:
|
135
140
|
- test/fixtures/diff
|
136
141
|
- test/fixtures/ls_tree
|
137
142
|
- test/test_diff_deployment.rb
|
143
|
+
- test/test_ftp.rb
|
138
144
|
- test/test_git.git/HEAD
|
139
145
|
- test/test_git.git/config
|
140
146
|
- test/test_git.git/description
|
@@ -162,3 +168,4 @@ test_files:
|
|
162
168
|
- test/test_git.git/objects/ff/1f1d4bd0c99e1c9cca047c46b2194accf89504
|
163
169
|
- test/test_git.git/refs/heads/master
|
164
170
|
- test/test_git.rb
|
171
|
+
- test/test_sftp.rb
|
data/lib/dandelion/service.rb
DELETED
@@ -1,168 +0,0 @@
|
|
1
|
-
require 'net/ftp'
|
2
|
-
require 'net/sftp'
|
3
|
-
require 'tempfile'
|
4
|
-
|
5
|
-
module Dandelion
|
6
|
-
module Service
|
7
|
-
class MissingFileError < StandardError; end
|
8
|
-
|
9
|
-
class Service
|
10
|
-
def initialize(host, username, path)
|
11
|
-
@host = host
|
12
|
-
@username = username
|
13
|
-
@path = path
|
14
|
-
end
|
15
|
-
|
16
|
-
def uri
|
17
|
-
"#{@scheme}://#{@username}@#{@host}/#{@path}"
|
18
|
-
end
|
19
|
-
|
20
|
-
protected
|
21
|
-
|
22
|
-
def temp(file, data)
|
23
|
-
tmp = Tempfile.new(file.gsub('/', '.'))
|
24
|
-
tmp << data
|
25
|
-
tmp.flush
|
26
|
-
yield(tmp.path)
|
27
|
-
tmp.close
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
class FTP < Service
|
32
|
-
def initialize(host, username, password, path)
|
33
|
-
super(host, username, path)
|
34
|
-
@scheme = 'ftp'
|
35
|
-
@ftp = Net::FTP.open(host, username, password)
|
36
|
-
@ftp.passive = true
|
37
|
-
@ftp.chdir(path)
|
38
|
-
end
|
39
|
-
|
40
|
-
def read(file)
|
41
|
-
begin
|
42
|
-
# Implementation of FTP#getbinaryfile differs between 1.8
|
43
|
-
# and 1.9 so we call FTP#retrbinary directly
|
44
|
-
content = ''
|
45
|
-
@ftp.retrbinary("RETR #{file}", 4096) do |data|
|
46
|
-
content += data
|
47
|
-
end
|
48
|
-
content
|
49
|
-
rescue Net::FTPPermError => e
|
50
|
-
raise MissingFileError
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def write(file, data)
|
55
|
-
# Creates directory only if necessary
|
56
|
-
mkdir_p(File.dirname(file))
|
57
|
-
|
58
|
-
temp(file, data) do |temp|
|
59
|
-
@ftp.putbinaryfile(temp, file)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def delete(file)
|
64
|
-
begin
|
65
|
-
@ftp.delete(file)
|
66
|
-
cleanup(File.dirname(file))
|
67
|
-
rescue Net::FTPPermError => e
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
private
|
72
|
-
|
73
|
-
def cleanup(dir)
|
74
|
-
unless File.identical?(dir, @path)
|
75
|
-
if empty?(dir)
|
76
|
-
@ftp.rmdir(dir)
|
77
|
-
cleanup(File.dirname(dir))
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def empty?(dir)
|
83
|
-
return @ftp.nlst(dir).empty?
|
84
|
-
end
|
85
|
-
|
86
|
-
def mkdir_p(dir)
|
87
|
-
return if dir == "."
|
88
|
-
parent_dir = File.dirname(dir)
|
89
|
-
file_names = @ftp.nlst(parent_dir)
|
90
|
-
unless file_names.include? dir
|
91
|
-
mkdir_p(parent_dir)
|
92
|
-
@ftp.mkdir(dir)
|
93
|
-
end
|
94
|
-
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
class SFTP < Service
|
99
|
-
def initialize(host, username, password, path)
|
100
|
-
super(host, username, path)
|
101
|
-
@scheme = 'sftp'
|
102
|
-
@sftp = Net::SFTP.start(host, username, :password => password)
|
103
|
-
end
|
104
|
-
|
105
|
-
def read(file)
|
106
|
-
begin
|
107
|
-
@sftp.file.open(File.join(@path, file), 'r') do |f|
|
108
|
-
f.gets
|
109
|
-
end
|
110
|
-
rescue Net::SFTP::StatusException => e
|
111
|
-
raise unless e.code == 2
|
112
|
-
raise MissingFileError
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def write(file, data)
|
117
|
-
path = File.join(@path, file)
|
118
|
-
begin
|
119
|
-
dir = File.dirname(path)
|
120
|
-
@sftp.stat!(dir)
|
121
|
-
rescue Net::SFTP::StatusException => e
|
122
|
-
raise unless e.code == 2
|
123
|
-
mkdir_p(dir)
|
124
|
-
end
|
125
|
-
temp(file, data) do |temp|
|
126
|
-
@sftp.upload!(temp, path)
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
def delete(file)
|
131
|
-
begin
|
132
|
-
path = File.join(@path, file)
|
133
|
-
@sftp.remove!(path)
|
134
|
-
cleanup(File.dirname(path))
|
135
|
-
rescue Net::SFTP::StatusException => e
|
136
|
-
raise unless e.code == 2
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
private
|
141
|
-
|
142
|
-
def cleanup(dir)
|
143
|
-
unless File.identical?(dir, @path)
|
144
|
-
if empty?(dir)
|
145
|
-
@sftp.rmdir!(dir)
|
146
|
-
cleanup(File.dirname(dir))
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
def empty?(dir)
|
152
|
-
@sftp.dir.entries(dir).map do |entry|
|
153
|
-
entry.name unless entry.name == '.' or entry.name == '..'
|
154
|
-
end.compact.empty?
|
155
|
-
end
|
156
|
-
|
157
|
-
def mkdir_p(dir)
|
158
|
-
begin
|
159
|
-
@sftp.mkdir!(dir)
|
160
|
-
rescue Net::SFTP::StatusException => e
|
161
|
-
raise unless e.code == 2
|
162
|
-
mkdir_p(File.dirname(dir))
|
163
|
-
mkdir_p(dir)
|
164
|
-
end
|
165
|
-
end
|
166
|
-
end
|
167
|
-
end
|
168
|
-
end
|