buildr 1.1.3 → 1.2.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/CHANGELOG +48 -0
- data/README +1 -1
- data/Rakefile +204 -0
- data/bin/buildr +7 -0
- data/lib/buildr.rb +155 -16
- data/lib/buildr/cobertura.rb +26 -19
- data/lib/buildr/hibernate.rb +8 -6
- data/lib/buildr/javacc.rb +1 -0
- data/lib/buildr/jdepend.rb +31 -4
- data/lib/buildr/jetty.rb +26 -28
- data/lib/buildr/openjpa.rb +8 -6
- data/lib/buildr/xmlbeans.rb +9 -4
- data/lib/core/build.rb +40 -50
- data/lib/core/checks.rb +358 -0
- data/lib/core/common.rb +161 -62
- data/lib/core/generate.rb +65 -0
- data/lib/core/help.rb +72 -0
- data/lib/core/project.rb +32 -37
- data/lib/core/rake_ext.rb +12 -66
- data/lib/core/transports.rb +388 -363
- data/lib/java/ant.rb +33 -36
- data/lib/java/artifact.rb +172 -160
- data/lib/java/compile.rb +13 -21
- data/lib/java/eclipse.rb +5 -5
- data/lib/java/idea.ipr.template +284 -0
- data/lib/java/idea.rb +107 -72
- data/lib/java/java.rb +42 -18
- data/lib/java/packaging.rb +242 -124
- data/lib/java/test.rb +532 -135
- data/lib/tasks/zip.rb +72 -23
- metadata +24 -10
@@ -0,0 +1,65 @@
|
|
1
|
+
module Buildr
|
2
|
+
module Generate #:nodoc:
|
3
|
+
|
4
|
+
task "generate" do
|
5
|
+
script = Generate.from_directory(true).join("\n")
|
6
|
+
buildfile = File.expand_path("buildfile")
|
7
|
+
File.open(buildfile, "w") { |file| file.write script }
|
8
|
+
puts "Created #{buildfile}"
|
9
|
+
end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
def from_directory(root = false)
|
14
|
+
name = File.basename(Dir.pwd)
|
15
|
+
if root
|
16
|
+
header = <<-EOF
|
17
|
+
# Generated by Buildr #{Buildr::VERSION}, change to your liking
|
18
|
+
|
19
|
+
# Version number for this release
|
20
|
+
VERSION_NUMBER = "1.0.0"
|
21
|
+
# Version number for the next release
|
22
|
+
NEXT_VERSION = "1.0.1"
|
23
|
+
# Group identifier for your projects
|
24
|
+
GROUP = "#{name}"
|
25
|
+
COPYRIGHT = ""
|
26
|
+
|
27
|
+
# Specify Maven 2.0 remote repositories here, like this:
|
28
|
+
repositories.remote << "http://www.ibiblio.org/maven2/"
|
29
|
+
|
30
|
+
desc "The #{name.capitalize} project"
|
31
|
+
define "#{name}" do
|
32
|
+
|
33
|
+
project.version = VERSION_NUMBER
|
34
|
+
project.group = GROUP
|
35
|
+
manifest["Implementation-Vendor"] = COPYRIGHT
|
36
|
+
EOF
|
37
|
+
script = header.split("\n")
|
38
|
+
else
|
39
|
+
script = [ %{define "#{name}" do} ]
|
40
|
+
end
|
41
|
+
script << " compile.with # Add classpath dependencies" if File.exist?("src/main/java")
|
42
|
+
script << " resources" if File.exist?("src/main/resources")
|
43
|
+
script << " test.compile.with # Add classpath dependencies" if File.exist?("src/test/java")
|
44
|
+
script << " test.resources" if File.exist?("src/test/resources")
|
45
|
+
if File.exist?("src/main/webapp")
|
46
|
+
script << " package(:war)"
|
47
|
+
elsif File.exist?("src/main/java")
|
48
|
+
script << " package(:jar)"
|
49
|
+
end
|
50
|
+
dirs = FileList["*"].exclude("src", "target", "report").
|
51
|
+
select { |file| File.directory?(file) && File.exist?(File.join(file, "src")) }
|
52
|
+
unless dirs.empty?
|
53
|
+
script << ""
|
54
|
+
dirs.sort.each do |dir|
|
55
|
+
Dir.chdir(dir) { script << from_directory.flatten.map { |line| " " + line } << "" }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
script << "end"
|
59
|
+
script.flatten
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
data/lib/core/help.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require "core/common"
|
2
|
+
require "core/project"
|
3
|
+
|
4
|
+
|
5
|
+
task "help" do
|
6
|
+
# Greeater.
|
7
|
+
Rake.application.usage
|
8
|
+
puts
|
9
|
+
|
10
|
+
# Show only the top-level projects.
|
11
|
+
projects.reject(&:parent).tap do |top_level|
|
12
|
+
unless top_level.empty?
|
13
|
+
puts "Top-level projects (buildr help:projects for full list):"
|
14
|
+
width = [top_level.map(&:name).map(&:size), 20].flatten.max
|
15
|
+
top_level.each do |project|
|
16
|
+
puts project.comment.blank? ? project.name : (" %-#{width}s # %s" % [project.name, project.comment])
|
17
|
+
end
|
18
|
+
puts
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Show all the top-level tasks, excluding projects.
|
23
|
+
puts "Common tasks:"
|
24
|
+
task("help:tasks").invoke
|
25
|
+
puts
|
26
|
+
puts "For help on command line options:"
|
27
|
+
puts " buildr --help"
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
module Buildr
|
32
|
+
|
33
|
+
# :call-seq:
|
34
|
+
# help() { ... }
|
35
|
+
#
|
36
|
+
# Use this to enhance the help task, e.g. to print some important information about your build,
|
37
|
+
# configuration options, build instructions, etc.
|
38
|
+
def help(&block)
|
39
|
+
Rake.application["help"].enhance &block
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
namespace "help" do
|
46
|
+
|
47
|
+
desc "List all projects defined by this buildfile"
|
48
|
+
task "projects" do
|
49
|
+
width = projects.map(&:name).map(&:size).max
|
50
|
+
projects.each do |project|
|
51
|
+
puts project.comment.blank? ? " #{project.name}" : (" %-#{width}s # %s" % [project.name, project.comment])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "List all tasks available from this buildfile"
|
56
|
+
task "tasks" do
|
57
|
+
Rake.application.tasks.select(&:comment).reject { |task| Project === task }.tap do |tasks|
|
58
|
+
width = [tasks.map(&:name).map(&:size), 20].flatten.max
|
59
|
+
tasks.each do |task|
|
60
|
+
printf " %-#{width}s # %s\n", task.name, task.comment
|
61
|
+
end
|
62
|
+
puts
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
task "projects" do
|
70
|
+
warn_deprecated "Please run help:projects instead."
|
71
|
+
task("help:projects").invoke
|
72
|
+
end
|
data/lib/core/project.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "core/rake_ext"
|
2
|
+
require "core/common"
|
2
3
|
|
3
4
|
module Buildr
|
4
5
|
|
@@ -65,14 +66,14 @@ module Buildr
|
|
65
66
|
# recursive, compiling foo will also compile foo:bar.
|
66
67
|
#
|
67
68
|
# If you run:
|
68
|
-
#
|
69
|
+
# buildr compile
|
69
70
|
# from the command line, it will execute the compile task of the current project.
|
70
71
|
#
|
71
|
-
# Projects and sub-projects follow a directory heirarchy. The
|
72
|
+
# Projects and sub-projects follow a directory heirarchy. The Buildfile is assumed to
|
72
73
|
# reside in the same directory as the top-level project, and each sub-project is
|
73
74
|
# contained in a sub-directory in the same name. For example:
|
74
75
|
# /home/foo
|
75
|
-
# |__
|
76
|
+
# |__ Buildfile
|
76
77
|
# |__ src/main/java
|
77
78
|
# |__ foo
|
78
79
|
# |__ src/main/java
|
@@ -185,12 +186,13 @@ module Buildr
|
|
185
186
|
options = names.pop if Hash === names.last
|
186
187
|
rake_check_options options, :scope if options
|
187
188
|
@projects ||= {}
|
189
|
+
names = names.flatten
|
188
190
|
if options && options[:scope]
|
189
191
|
# We assume parent project is evaluated.
|
190
192
|
if names.empty?
|
191
|
-
parent = @projects[options[:scope]] or raise "No such project #{options[:scope]}"
|
192
|
-
@projects.values.select { |project| project.parent == parent }.
|
193
|
-
|
193
|
+
parent = @projects[options[:scope].to_s] or raise "No such project #{options[:scope]}"
|
194
|
+
@projects.values.select { |project| project.parent == parent }.each { |project| project.invoke }.
|
195
|
+
map { |project| [project] + projects(:scope=>project) }.flatten.sort_by(&:name)
|
194
196
|
else
|
195
197
|
names.uniq.map { |name| project(name, :scope=>options[:scope]) }
|
196
198
|
end
|
@@ -224,27 +226,22 @@ module Buildr
|
|
224
226
|
# current directory.
|
225
227
|
#
|
226
228
|
# Complicated? Try this:
|
227
|
-
#
|
229
|
+
# buildr build
|
228
230
|
# is the same as:
|
229
|
-
#
|
231
|
+
# buildr foo:build
|
230
232
|
# But:
|
231
233
|
# cd bar
|
232
|
-
#
|
234
|
+
# buildr build
|
233
235
|
# is the same as:
|
234
|
-
#
|
236
|
+
# buildr foo:bar:build
|
235
237
|
#
|
236
238
|
# The optional block is called with the project name when the task executes
|
237
239
|
# and returns a message that, for example "Building project #{name}".
|
238
240
|
def local_task(args, &block)
|
239
241
|
task args do |task|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
else
|
244
|
-
projects.each do |project|
|
245
|
-
puts block.call(project.name) if block && verbose
|
246
|
-
task("#{project.name}:#{task.name}").invoke
|
247
|
-
end
|
242
|
+
local_projects do |project|
|
243
|
+
puts block.call(project.name) if block && verbose
|
244
|
+
task("#{project.name}:#{task.name}").invoke
|
248
245
|
end
|
249
246
|
end
|
250
247
|
end
|
@@ -271,11 +268,17 @@ module Buildr
|
|
271
268
|
task_name
|
272
269
|
end
|
273
270
|
|
274
|
-
def local_projects(dir =
|
275
|
-
dir = File.expand_path(dir)
|
271
|
+
def local_projects(dir = nil, &block) #:nodoc:
|
272
|
+
dir = File.expand_path(dir || Rake.application.original_dir)
|
276
273
|
projects = Project.projects.select { |project| project.base_dir == dir }
|
277
274
|
if projects.empty? && dir != Dir.pwd && File.dirname(dir) != dir
|
278
|
-
local_projects(File.dirname(dir))
|
275
|
+
local_projects(File.dirname(dir), &block)
|
276
|
+
elsif block
|
277
|
+
if projects.empty?
|
278
|
+
warn "No projects defined for directory #{Rake.application.original_dir}" if verbose
|
279
|
+
else
|
280
|
+
projects.each { |project| block[project] }
|
281
|
+
end
|
279
282
|
else
|
280
283
|
projects
|
281
284
|
end
|
@@ -312,13 +315,13 @@ module Buildr
|
|
312
315
|
#
|
313
316
|
# Returns the project's base directory.
|
314
317
|
#
|
315
|
-
# The
|
316
|
-
# base directory is the one in which we find the
|
318
|
+
# The Buildfile defines top-level project, so it's logical that the top-level project's
|
319
|
+
# base directory is the one in which we find the Buildfile. And each sub-project has
|
317
320
|
# a base directory that is one level down, with the same name as the sub-project.
|
318
321
|
#
|
319
322
|
# For example:
|
320
323
|
# /home/foo/ <-- base_directory of project "foo"
|
321
|
-
# /home/foo/
|
324
|
+
# /home/foo/Buildfile <-- builds "foo"
|
322
325
|
# /home/foo/bar <-- sub-project "foo:bar"
|
323
326
|
def base_dir()
|
324
327
|
if @base_dir.nil?
|
@@ -327,7 +330,7 @@ module Buildr
|
|
327
330
|
# using the same name as the project.
|
328
331
|
@base_dir = File.join(@parent.base_dir, name.split(":").last)
|
329
332
|
else
|
330
|
-
# For top-level project, a good default is the directory where we found the
|
333
|
+
# For top-level project, a good default is the directory where we found the Buildfile.
|
331
334
|
@base_dir = Dir.pwd
|
332
335
|
end
|
333
336
|
end
|
@@ -357,7 +360,7 @@ module Buildr
|
|
357
360
|
# Essentially, joins all the supplied names and expands the path relative to #base_dir.
|
358
361
|
# Symbol arguments are converted to paths by calling the attribute accessor on the project.
|
359
362
|
#
|
360
|
-
# Keep in mind that all tasks are defined and executed relative to the
|
363
|
+
# Keep in mind that all tasks are defined and executed relative to the Buildfile directory,
|
361
364
|
# so you want to use #path_to to get the actual path within the project as a matter of practice.
|
362
365
|
#
|
363
366
|
# For example:
|
@@ -499,7 +502,7 @@ module Buildr
|
|
499
502
|
task_name, deps = Rake.application.resolve_args(args)
|
500
503
|
deps = [deps] unless deps.respond_to?(:to_ary)
|
501
504
|
task = Buildr.options.parallel ? multitask(task_name) : task(task_name)
|
502
|
-
|
505
|
+
parent.task(task_name).enhance [task] if parent
|
503
506
|
task.enhance deps, &block
|
504
507
|
end
|
505
508
|
|
@@ -531,7 +534,7 @@ module Buildr
|
|
531
534
|
# You pass a block that is executed in the context of the project definition.
|
532
535
|
# This block is used to define the project and tasks that are part of the project.
|
533
536
|
# Do not perform any work inside the project itself, as it will execute each time
|
534
|
-
# the
|
537
|
+
# the Buildfile is loaded. Instead, use it to create and extend tasks that are
|
535
538
|
# related to the project.
|
536
539
|
#
|
537
540
|
# For example:
|
@@ -546,7 +549,7 @@ module Buildr
|
|
546
549
|
# => "1.0"
|
547
550
|
# puts project("foo:bar").compile.classpath.map(&:to_spec)
|
548
551
|
# => "org.apache.axis2:axis2:jar:1.1"
|
549
|
-
# %
|
552
|
+
# % buildr build
|
550
553
|
# => Compiling 14 source files in foo:bar
|
551
554
|
def define(name, properties = nil, &block) #:yields:project
|
552
555
|
Project.define(name, properties, &block)
|
@@ -614,14 +617,6 @@ module Buildr
|
|
614
617
|
Project.projects *args
|
615
618
|
end
|
616
619
|
|
617
|
-
desc "List all projects defined by this Rakefile"
|
618
|
-
task "projects" do
|
619
|
-
wide = projects.map(&:name).map(&:size).max
|
620
|
-
projects.each do |project|
|
621
|
-
puts project.comment.blank? ? project.name : ("%-#{wide}s #%s" % [project.name, project.comment])
|
622
|
-
end
|
623
|
-
end
|
624
|
-
|
625
620
|
# Forces all the projects to be evaluated before executing any other task.
|
626
621
|
# If we don't do that, we don't get to have tasks available when running Rake.
|
627
622
|
task("buildr:projects") { projects }
|
data/lib/core/rake_ext.rb
CHANGED
@@ -1,14 +1,10 @@
|
|
1
|
-
module Rake #:nodoc
|
2
|
-
class Task
|
1
|
+
module Rake #:nodoc
|
2
|
+
class Task #:nodoc:
|
3
3
|
|
4
|
-
def invoke
|
5
|
-
if stack.include?(name)
|
6
|
-
fail "Circular dependency " + (stack + [name]).join("=>")
|
7
|
-
end
|
4
|
+
def invoke()
|
5
|
+
fail "Circular dependency " + (stack + [name]).join("=>") if stack.include?(name)
|
8
6
|
@lock.synchronize do
|
9
|
-
if application.options.trace
|
10
|
-
puts "** Invoke #{name} #{format_trace_flags}"
|
11
|
-
end
|
7
|
+
puts "** Invoke #{name} #{format_trace_flags}" if application.options.trace
|
12
8
|
return if @already_invoked
|
13
9
|
begin
|
14
10
|
stack.push name
|
@@ -21,10 +17,14 @@ module Rake #:nodoc:
|
|
21
17
|
end
|
22
18
|
end
|
23
19
|
|
24
|
-
def invoke_prerequisites()
|
20
|
+
def invoke_prerequisites()
|
25
21
|
prerequisites.each { |n| application[n, @scope].invoke }
|
26
22
|
end
|
27
23
|
|
24
|
+
def inspect()
|
25
|
+
"#{self.class}: #{name}"
|
26
|
+
end
|
27
|
+
|
28
28
|
protected
|
29
29
|
|
30
30
|
def stack()
|
@@ -33,8 +33,8 @@ module Rake #:nodoc:
|
|
33
33
|
|
34
34
|
end
|
35
35
|
|
36
|
-
class MultiTask
|
37
|
-
def invoke_prerequisites
|
36
|
+
class MultiTask #:nodoc:
|
37
|
+
def invoke_prerequisites()
|
38
38
|
threads = @prerequisites.collect do |p|
|
39
39
|
copy = stack.dup
|
40
40
|
Thread.new(p) { |r| stack.replace copy ; application[r].invoke }
|
@@ -61,58 +61,4 @@ module Rake #:nodoc:
|
|
61
61
|
|
62
62
|
end
|
63
63
|
|
64
|
-
class CheckTask < Rake::Task
|
65
|
-
|
66
|
-
def execute()
|
67
|
-
@warnings = []
|
68
|
-
super
|
69
|
-
report if verbose
|
70
|
-
end
|
71
|
-
|
72
|
-
def note(*msg)
|
73
|
-
@warnings += msg
|
74
|
-
end
|
75
|
-
|
76
|
-
def report()
|
77
|
-
if @warnings.empty?
|
78
|
-
puts HighLine.new.color("No warnings", :green)
|
79
|
-
else
|
80
|
-
warn "These are possible problems with your Rakefile"
|
81
|
-
@warnings.each { |msg| warn " #{msg}" }
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
end
|
86
|
-
|
87
|
-
|
88
|
-
desc "Check your Rakefile for common errors"
|
89
|
-
CheckTask.define_task "check"
|
90
|
-
|
91
|
-
# Check for circular dependencies
|
92
|
-
task "check" do
|
93
|
-
# Keep track of tasks we already checked, to avoid death circles.
|
94
|
-
checked = {}
|
95
|
-
# The stack keeps track of all the tasks we visit, so we can display the tasks
|
96
|
-
# involved in the circular dependency.
|
97
|
-
expand = lambda do |stack, task|
|
98
|
-
# Already been here, no need to check again, but make sure we're not seeing
|
99
|
-
# the same task twice due to a circular dependency.
|
100
|
-
fail "Circular " + (stack + [task]).join("=>") if stack.include?(task)
|
101
|
-
unless checked[task]
|
102
|
-
checked[task] = true
|
103
|
-
# Variable task may be a Task, but may also be a task name. In the later
|
104
|
-
# case, we need to resolve it into a Task. But it may also be a filename,
|
105
|
-
# pointing to a file that may exist, just not during the check, so we need
|
106
|
-
# this check to avoid dying on "Don't know how to build ..."
|
107
|
-
if real_task = Rake.application.lookup(task, [])
|
108
|
-
one_deeper = stack + [task.to_s]
|
109
|
-
real_task.prerequisites.each { |prereq| expand[one_deeper, prereq.to_s] }
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
Rake.application.tasks.each do |task|
|
114
|
-
expand[ [], task.to_s ]
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
64
|
end
|
data/lib/core/transports.rb
CHANGED
@@ -8,6 +8,8 @@ require "digest/md5"
|
|
8
8
|
require "digest/sha1"
|
9
9
|
require "facet/progressbar"
|
10
10
|
require "highline"
|
11
|
+
require "tempfile"
|
12
|
+
require "uri/sftp"
|
11
13
|
|
12
14
|
|
13
15
|
# Monkeypatching: SFTP never defines the mkdir method on its session or the underlying
|
@@ -27,294 +29,264 @@ module Net #:nodoc:all
|
|
27
29
|
end
|
28
30
|
end
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
return false if /close/i =~ res['proxy-connection'].to_s
|
39
|
-
return true if /keep-alive/i =~ res['proxy-connection'].to_s
|
40
|
-
(@curr_http_version == '1.1')
|
41
|
-
end
|
32
|
+
|
33
|
+
# Not quite open-uri, but similar. Provides read and write methods for the resource represented by the URI.
|
34
|
+
# Currently supports reads for URI::HTTP and writes for URI::SFTP. Also provides convenience methods for
|
35
|
+
# downloads and uploads.
|
36
|
+
module URI
|
37
|
+
|
38
|
+
# Raised when trying to read/download a resource that doesn't exist.
|
39
|
+
class NotFoundError < RuntimeError
|
42
40
|
end
|
43
|
-
end
|
44
41
|
|
42
|
+
class << self
|
43
|
+
|
44
|
+
# :call-seq:
|
45
|
+
# read(uri, options?) => content
|
46
|
+
# read(uri, options?) { |chunk| ... }
|
47
|
+
#
|
48
|
+
# Reads from the resource behind this URI. The first form returns the content of the resource,
|
49
|
+
# the second form yields to the block with each chunk of content (usually more than one).
|
50
|
+
#
|
51
|
+
# For example:
|
52
|
+
# File.open "image.jpg", "w" do |file|
|
53
|
+
# URI.read("http://example.com/image.jpg") { |chunk| file.write chunk }
|
54
|
+
# end
|
55
|
+
# Shorter version:
|
56
|
+
# File.open("image.jpg", "w") { |file| file.write URI.read("http://example.com/image.jpg") }
|
57
|
+
#
|
58
|
+
# Supported options:
|
59
|
+
# * :proxy -- Collection of proxy settings, accessed by scheme.
|
60
|
+
# * :modified -- Only download if file modified since this timestamp. Returns nil if not modified.
|
61
|
+
# * :progress -- Show the progress bar while reading.
|
62
|
+
def read(uri, options = nil, &block)
|
63
|
+
uri = URI.parse(uri.to_s) unless URI === uri
|
64
|
+
uri.read options, &block
|
65
|
+
end
|
45
66
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
#
|
72
|
-
|
67
|
+
# :call-seq:
|
68
|
+
# download(uri, target, options?)
|
69
|
+
#
|
70
|
+
# Downloads the resource to the target.
|
71
|
+
#
|
72
|
+
# The target may be a file name (string or task), in which case the file is created from the resource.
|
73
|
+
# The target may also be any object that responds to +write+, e.g. File, StringIO, Pipe.
|
74
|
+
#
|
75
|
+
# Use the progress bar when running in verbose mode.
|
76
|
+
def download(uri, target, options = nil)
|
77
|
+
uri = URI.parse(uri.to_s) unless URI === uri
|
78
|
+
uri.download target, options
|
79
|
+
end
|
80
|
+
|
81
|
+
# :call-seq:
|
82
|
+
# write(uri, content, options?)
|
83
|
+
# write(uri, options?) { |bytes| .. }
|
84
|
+
#
|
85
|
+
# Writes to the resource behind the URI. The first form writes the content from a string or an object
|
86
|
+
# that responds to +read+ and optionally +size+. The second form writes the content by yielding to the
|
87
|
+
# block. Each yield should return up to the specified number of bytes, the last yield returns nil.
|
88
|
+
#
|
89
|
+
# For example:
|
90
|
+
# File.open "killer-app.jar", "rb" do |file|
|
91
|
+
# write("sftp://localhost/jars/killer-app.jar") { |chunk| file.read(chunk) }
|
92
|
+
# end
|
93
|
+
# Or:
|
94
|
+
# write "sftp://localhost/jars/killer-app.jar", File.read("killer-app.jar")
|
95
|
+
#
|
96
|
+
# Supported options:
|
97
|
+
# * :proxy -- Collection of proxy settings, accessed by scheme.
|
98
|
+
# * :progress -- Show the progress bar while reading.
|
99
|
+
def write(uri, *args, &block)
|
100
|
+
uri = URI.parse(uri.to_s) unless URI === uri
|
101
|
+
uri.write *args, &block
|
102
|
+
end
|
103
|
+
|
104
|
+
# :call-seq:
|
105
|
+
# upload(uri, source, options?)
|
106
|
+
#
|
107
|
+
# Uploads from source to the resource.
|
108
|
+
#
|
109
|
+
# The source may be a file name (string or task), in which case the file is uploaded to the resource.
|
110
|
+
# The source may also be any object that responds to +read+ (and optionally +size+), e.g. File, StringIO, Pipe.
|
111
|
+
#
|
112
|
+
# Use the progress bar when running in verbose mode.
|
113
|
+
def upload(uri, source, options = nil)
|
114
|
+
uri = URI.parse(uri.to_s) unless URI === uri
|
115
|
+
uri.upload source, options
|
73
116
|
end
|
74
117
|
|
75
|
-
|
118
|
+
end
|
76
119
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
120
|
+
class Generic
|
121
|
+
|
122
|
+
# :call-seq:
|
123
|
+
# read(options?) => content
|
124
|
+
# read(options?) { |chunk| ... }
|
125
|
+
#
|
126
|
+
# Reads from the resource behind this URI. The first form returns the content of the resource,
|
127
|
+
# the second form yields to the block with each chunk of content (usually more than one).
|
128
|
+
#
|
129
|
+
# For options, see URI::read.
|
130
|
+
def read(options = nil, &block)
|
131
|
+
fail "This protocol doesn't support reading (yet, how about helping by implementing it?)"
|
132
|
+
end
|
86
133
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
134
|
+
# :call-seq:
|
135
|
+
# download(target, options?)
|
136
|
+
#
|
137
|
+
# Downloads the resource to the target.
|
138
|
+
#
|
139
|
+
# The target may be a file name (string or task), in which case the file is created from the resource.
|
140
|
+
# The target may also be any object that responds to +write+, e.g. File, StringIO, Pipe.
|
141
|
+
#
|
142
|
+
# Use the progress bar when running in verbose mode.
|
143
|
+
def download(target, options = nil)
|
144
|
+
case target
|
145
|
+
when Rake::Task
|
146
|
+
download target.name, options
|
147
|
+
when String
|
148
|
+
# If download breaks we end up with a partial file which is
|
149
|
+
# worse than not having a file at all, so download to temporary
|
150
|
+
# file and then move over.
|
151
|
+
modified = File.stat(target).mtime if File.exist?(target)
|
152
|
+
temp = nil
|
153
|
+
Tempfile.open File.basename(target) do |temp|
|
154
|
+
temp.binmode
|
155
|
+
read({:progress=>verbose}.merge(options || {}).merge(:modified=>modified)) { |chunk| temp.write chunk }
|
97
156
|
end
|
157
|
+
mkpath File.dirname(target)
|
158
|
+
File.move temp.path, target
|
159
|
+
when File
|
160
|
+
read({:progress=>verbose}.merge(options || {}).merge(:modified=>target.mtime)) { |chunk| target.write chunk }
|
161
|
+
target.flush
|
162
|
+
else
|
163
|
+
raise ArgumentError, "Expecting a target that is either a file name (string, task) or object that responds to write (file, pipe)." unless target.respond_to?(:write)
|
164
|
+
read({:progress=>verbose}.merge(options || {})) { |chunk| target.write chunk }
|
165
|
+
target.flush
|
98
166
|
end
|
99
|
-
|
167
|
+
end
|
168
|
+
|
169
|
+
# :call-seq:
|
170
|
+
# write(content, options?)
|
171
|
+
# write(options?) { |bytes| .. }
|
172
|
+
#
|
173
|
+
# Writes to the resource behind the URI. The first form writes the content from a string or an object
|
174
|
+
# that responds to +read+ and optionally +size+. The second form writes the content by yielding to the
|
175
|
+
# block. Each yield should return up to the specified number of bytes, the last yield returns nil.
|
176
|
+
#
|
177
|
+
# For options, see URI::write.
|
178
|
+
def write(*args, &block)
|
179
|
+
fail "This protocol doesn't support writing (yet, how about helping by implementing it?)"
|
100
180
|
end
|
101
181
|
|
102
|
-
#
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
182
|
+
# :call-seq:
|
183
|
+
# upload(source, options?)
|
184
|
+
#
|
185
|
+
# Uploads from source to the resource.
|
186
|
+
#
|
187
|
+
# The source may be a file name (string or task), in which case the file is uploaded to the resource.
|
188
|
+
# If the source is a directory, uploads all files inside the directory (including nested directories).
|
189
|
+
# The source may also be any object that responds to +read+ (and optionally +size+), e.g. File, StringIO, Pipe.
|
190
|
+
#
|
191
|
+
# Use the progress bar when running in verbose mode.
|
192
|
+
def upload(source, options = nil)
|
193
|
+
source = source.name if Rake::Task === source
|
194
|
+
options ||= {}
|
195
|
+
if String === source
|
196
|
+
raise NotFoundError, "No source file/directory to upload." unless File.exist?(source)
|
197
|
+
if File.directory?(source)
|
198
|
+
Dir.glob("#{source}/**/*").reject { |file| File.directory?(file) }.each do |file|
|
199
|
+
path = self.path + file.sub(source, "")
|
200
|
+
(self + path).upload file, {:digests=>[]}.merge(options)
|
118
201
|
end
|
202
|
+
else
|
203
|
+
File.open(source, "rb") { |input| upload input, options }
|
119
204
|
end
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
attr_reader :options
|
128
|
-
|
129
|
-
# Initialize the transport with the specified URL and options.
|
130
|
-
def initialize(url, options)
|
131
|
-
@uri = URI.parse(url.to_s)
|
132
|
-
@base_path = @uri.path || "/"
|
133
|
-
@base_path += "/" unless @base_path[-1] == ?/
|
134
|
-
@options = options || {}
|
135
|
-
end
|
136
|
-
|
137
|
-
# Downloads a file from the specified path, relative to the server URI.
|
138
|
-
# Downloads to either the target file, or by calling the block with each
|
139
|
-
# chunk of the file. Returns the file's modified timestamp, or now.
|
140
|
-
#
|
141
|
-
# For example:
|
142
|
-
# Transports.perform("http://server/libs") do |http|
|
143
|
-
# http.download("my_project/test.jar", "test.jar")
|
144
|
-
# http.download("my_project/readme") { |text| $stdout.write text }
|
145
|
-
# end
|
146
|
-
def download(path, target, &block)
|
147
|
-
fail "Upload not implemented for this transport"
|
148
|
-
end
|
149
|
-
|
150
|
-
# Uploads a file (source) to the server at the specified path,
|
151
|
-
# relative to the server URI.
|
152
|
-
#
|
153
|
-
# For example:
|
154
|
-
# Transports.perform("sftp://server/libs") do |sftp|
|
155
|
-
# sftp.mkpath "my_project"
|
156
|
-
# sftp.upload("target/test.jar", "my_project/test.jar")
|
157
|
-
# end
|
158
|
-
def upload(source, path)
|
159
|
-
fail "Upload not implemented for this transport"
|
160
|
-
end
|
161
|
-
|
162
|
-
# Creates a path on the server relative to the server URI.
|
163
|
-
# See #upload for example.
|
164
|
-
def mkpath(path)
|
165
|
-
fail "Upload not implemented for this transport"
|
166
|
-
end
|
167
|
-
|
168
|
-
protected
|
169
|
-
|
170
|
-
# :call-seq:
|
171
|
-
# with_progress_bar(file_name, length) { |progress| ... }
|
172
|
-
#
|
173
|
-
# Displays a progress bar while executing the block. The first
|
174
|
-
# argument provides a filename to display, the second argument
|
175
|
-
# its size in bytes.
|
176
|
-
#
|
177
|
-
# The block is yielded with a progress object that implements
|
178
|
-
# a single method. Call << for each block of bytes down/uploaded.
|
179
|
-
def with_progress_bar(file_name, length)
|
180
|
-
if verbose && $stdout.isatty
|
181
|
-
progress_bar = Console::ProgressBar.new(file_name, length)
|
182
|
-
# Extend the progress bar so we can display count/total.
|
183
|
-
class << progress_bar
|
184
|
-
def total()
|
185
|
-
convert_bytes(@total)
|
186
|
-
end
|
187
|
-
end
|
188
|
-
# Squeeze the filename into 30 characters.
|
189
|
-
if file_name.size > 30
|
190
|
-
base, ext = file_name.split(".")
|
191
|
-
ext ||= ""
|
192
|
-
truncated = "#{base[0..26-ext.size]}...#{ext}"
|
193
|
-
else
|
194
|
-
truncated = file_name
|
205
|
+
elsif source.respond_to?(:read)
|
206
|
+
digests = (options[:digests] || [:md5, :sha1]).
|
207
|
+
inject({}) { |hash, name| hash[name] = Digest.const_get(name.to_s.upcase).new ; hash }
|
208
|
+
size = source.size rescue nil
|
209
|
+
write (options).merge(:progress=>verbose && size, :size=>size) do |bytes|
|
210
|
+
source.read(bytes).tap do |chunk|
|
211
|
+
digests.values.each { |digest| digest << chunk } if chunk
|
195
212
|
end
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
213
|
+
end
|
214
|
+
digests.each do |key, digest|
|
215
|
+
self.merge("#{self.path}.#{key}").write "#{digest.hexdigest} #{File.basename(path)}",
|
216
|
+
(options).merge(:progress=>false)
|
217
|
+
end
|
218
|
+
else
|
219
|
+
raise ArgumentError, "Expecting source to be a file name (string, task) or any object that responds to read (file, pipe)."
|
220
|
+
end
|
221
|
+
end
|
201
222
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
223
|
+
protected
|
224
|
+
|
225
|
+
# :call-seq:
|
226
|
+
# with_progress_bar(enable, file_name, size) { |progress| ... }
|
227
|
+
#
|
228
|
+
# Displays a progress bar while executing the block. The first argument must be true for the
|
229
|
+
# progress bar to show (TTY output also required), as a convenient for selectively using the
|
230
|
+
# progress bar from a single block.
|
231
|
+
#
|
232
|
+
# The second argument provides a filename to display, the third its size in bytes.
|
233
|
+
#
|
234
|
+
# The block is yielded with a progress object that implements a single method.
|
235
|
+
# Call << for each block of bytes down/uploaded.
|
236
|
+
def with_progress_bar(enable, file_name, size) #:nodoc:
|
237
|
+
if enable && $stdout.isatty
|
238
|
+
progress_bar = Console::ProgressBar.new(file_name, size)
|
239
|
+
# Extend the progress bar so we can display count/total.
|
240
|
+
class << progress_bar
|
241
|
+
def total()
|
242
|
+
convert_bytes(@total)
|
211
243
|
end
|
244
|
+
end
|
245
|
+
# Squeeze the filename into 30 characters.
|
246
|
+
if file_name.size > 30
|
247
|
+
base, ext = file_name.split(".")
|
248
|
+
truncated = "#{base[0..26-ext.to_s.size]}...#{ext}"
|
212
249
|
else
|
213
|
-
|
250
|
+
truncated = file_name
|
251
|
+
end
|
252
|
+
progress_bar.format = "#{truncated}: %3d%% %s %s/%s %s"
|
253
|
+
progress_bar.format = "%3d%% %s %s/%s %s"
|
254
|
+
progress_bar.format_arguments = [:percentage, :bar, :bytes, :total, :stat]
|
255
|
+
progress_bar.bar_mark = "."
|
256
|
+
|
257
|
+
begin
|
214
258
|
class << progress_bar
|
215
259
|
def <<(bytes)
|
260
|
+
inc bytes.respond_to?(:size) ? bytes.size : bytes
|
216
261
|
end
|
217
262
|
end
|
218
263
|
yield progress_bar
|
264
|
+
ensure
|
265
|
+
progress_bar.finish
|
219
266
|
end
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
#
|
225
|
-
# Use the Digester to create digests for files you are downloading or
|
226
|
-
# uploading, and either verify their signatures (download) or create
|
227
|
-
# signatures (upload).
|
228
|
-
#
|
229
|
-
# The method takes one argument with the list of digest algorithms to
|
230
|
-
# support. Leave if empty and it will default to MD5 and SHA1.
|
231
|
-
#
|
232
|
-
# The method then yields the block passing it a Digester. The Digester
|
233
|
-
# supports two methods. Use << to pass data that you are down/uploading.
|
234
|
-
# Once all data is transmitted, use each to iterate over the digests.
|
235
|
-
# The each method calls the block with the digest type (e.g. "md5")
|
236
|
-
# and the hexadecimal digest value.
|
237
|
-
#
|
238
|
-
# For example:
|
239
|
-
# with_digests do |digester|
|
240
|
-
# download url do |block|
|
241
|
-
# digester << block
|
242
|
-
# end
|
243
|
-
# digester.each do |type, hexdigest|
|
244
|
-
# signature = download "#{url}.#{type}"
|
245
|
-
# fail "Mismatch" unless signature == hexdigest
|
246
|
-
# end
|
247
|
-
# end
|
248
|
-
def with_digests(types = nil)
|
249
|
-
digester = Digester.new(types)
|
250
|
-
yield digester
|
251
|
-
digester.to_hash
|
252
|
-
end
|
253
|
-
|
254
|
-
class Digester #:nodoc:
|
255
|
-
|
256
|
-
def initialize(types)
|
257
|
-
# Digests disabled for now, we have a keep-alive problem with Net::HTTP.
|
258
|
-
#types ||= [ "md5", "sha1" ]
|
259
|
-
types ||= []
|
260
|
-
@digests = types.inject({}) do |hash, type|
|
261
|
-
hash[type.to_s.downcase] = Digest.const_get(type.to_s.upcase).new
|
262
|
-
hash
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
|
-
# Add bytes for digestion.
|
267
|
-
def <<(bytes)
|
268
|
-
@digests.each { |type, digest| digest << bytes }
|
269
|
-
end
|
270
|
-
|
271
|
-
# Iterate over all the digests calling the block with two arguments:
|
272
|
-
# the digest type (e.g. "md5") and the hexadecimal digest value.
|
273
|
-
def each()
|
274
|
-
@digests.each { |type, digest| yield type, digest.hexdigest }
|
275
|
-
end
|
276
|
-
|
277
|
-
# Returns a hash that maps each digest type to its hexadecimal digest value.
|
278
|
-
def to_hash()
|
279
|
-
@digests.keys.inject({}) do |hash, type|
|
280
|
-
hash[type] = @digests[type].hexdigest
|
281
|
-
hash
|
267
|
+
else
|
268
|
+
progress_bar = Object.new
|
269
|
+
class << progress_bar
|
270
|
+
def <<(bytes)
|
282
271
|
end
|
283
272
|
end
|
284
|
-
|
273
|
+
yield progress_bar
|
285
274
|
end
|
286
|
-
|
287
275
|
end
|
288
276
|
|
277
|
+
end
|
289
278
|
|
290
|
-
|
279
|
+
class HTTP #:nodoc:
|
291
280
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
rake_check_options options, :proxy, :digests
|
296
|
-
proxy = options[:proxy]
|
297
|
-
end
|
281
|
+
# See URI::Generic#read
|
282
|
+
def read(options = nil, &block)
|
283
|
+
options ||= {}
|
298
284
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
@http = Net::HTTP.start(@uri.host, @uri.port, proxy.host, proxy.port, proxy.user, proxy.password)
|
305
|
-
else
|
306
|
-
@http = Net::HTTP.start(@uri.host, @uri.port)
|
307
|
-
end
|
308
|
-
end
|
309
|
-
|
310
|
-
def download(path, target = nil, &block)
|
311
|
-
puts "Requesting #{@uri}/#{path} " if Rake.application.options.trace
|
312
|
-
if target && File.exist?(target)
|
313
|
-
last_modified = File.stat(target).mtime.utc
|
314
|
-
headers = { "If-Modified-Since" => CGI.rfc1123_date(last_modified) }
|
315
|
-
end
|
316
|
-
path = path[1..-1] if path[0..0] == '/'
|
317
|
-
@http.request_get(@base_path + path, headers) do |response|
|
285
|
+
headers = { "If-Modified-Since" => CGI.rfc1123_date(options[:modified].utc) } if options[:modified]
|
286
|
+
result = nil
|
287
|
+
request = lambda do |http|
|
288
|
+
puts "Requesting #{self}" if Rake.application.options.trace
|
289
|
+
http.request_get(path, headers) do |response|
|
318
290
|
case response
|
319
291
|
when Net::HTTPNotModified
|
320
292
|
# No modification, nothing to do.
|
@@ -323,155 +295,208 @@ module Buildr
|
|
323
295
|
when Net::HTTPRedirection
|
324
296
|
# Try to download from the new URI, handle relative redirects.
|
325
297
|
puts "Redirected to #{response['Location']}" if Rake.application.options.trace
|
326
|
-
|
298
|
+
result = (self + URI.parse(response["location"])).read(options, &block)
|
327
299
|
|
328
300
|
when Net::HTTPOK
|
329
|
-
puts "Downloading #{
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
# Read the body of the page and write it out.
|
336
|
-
response.read_body do |chunk|
|
337
|
-
write[chunk]
|
338
|
-
digester << chunk
|
339
|
-
progress << chunk
|
340
|
-
end
|
341
|
-
# Check server digests before approving the download.
|
342
|
-
digester.each do |type, hexdigest|
|
343
|
-
@http.request_get("#{@base_path}#{path}.#{type.to_s.downcase}") do |response|
|
344
|
-
if Net::HTTPOK === response
|
345
|
-
puts "Checking signature from #{@uri}/#{path}" if Rake.application.options.trace
|
346
|
-
fail "Checksum failure for #{@uri}/#{path}: #{type.to_s.upcase} digest on server did not match downloaded file" unless
|
347
|
-
response.read_body.split.first == hexdigest
|
348
|
-
end
|
349
|
-
end
|
350
|
-
end
|
301
|
+
puts "Downloading #{self}" if verbose
|
302
|
+
with_progress_bar options[:progress], path.split("/").last, response.content_length do |progress|
|
303
|
+
if block
|
304
|
+
response.read_body do |chunk|
|
305
|
+
block.call chunk
|
306
|
+
progress << chunk
|
351
307
|
end
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
temp = Tempfile.open(File.basename(target))
|
358
|
-
temp.binmode
|
359
|
-
download[ proc { |chunk| temp.write chunk } ]
|
360
|
-
temp.close
|
361
|
-
File.move temp.path, target
|
362
|
-
else
|
363
|
-
download[ block ]
|
308
|
+
else
|
309
|
+
result = ""
|
310
|
+
response.read_body do |chunk|
|
311
|
+
result << chunk
|
312
|
+
progress << chunk
|
364
313
|
end
|
365
|
-
|
366
314
|
end
|
367
315
|
end
|
316
|
+
|
368
317
|
when Net::HTTPNotFound
|
369
|
-
raise
|
318
|
+
raise NotFoundError, "Looking for #{self} and all I got was a 404!"
|
370
319
|
else
|
371
|
-
|
320
|
+
raise RuntimeError, "Failed to download #{self}: #{response.message}"
|
372
321
|
end
|
373
322
|
end
|
374
|
-
last_modified
|
375
323
|
end
|
376
324
|
|
377
|
-
|
378
|
-
|
379
|
-
|
325
|
+
proxy = options[:proxy] && options[:proxy].http
|
326
|
+
if proxy
|
327
|
+
proxy = URI.parse(proxy) if String === proxy
|
328
|
+
Net::HTTP.start(host, port, proxy.host, proxy.port, proxy.user, proxy.password) { |http| request[http] }
|
329
|
+
else
|
330
|
+
Net::HTTP.start(host, port) { |http| request[http] }
|
380
331
|
end
|
381
|
-
|
332
|
+
result
|
382
333
|
end
|
383
334
|
|
384
|
-
|
385
|
-
HTTPS = HTTP #:nodoc:
|
386
|
-
|
335
|
+
end
|
387
336
|
|
388
|
-
|
337
|
+
class SFTP #:nodoc:
|
389
338
|
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
339
|
+
class << self
|
340
|
+
# Caching of passwords, so we only need to ask once.
|
341
|
+
def passwords()
|
342
|
+
@passwords ||= {}
|
394
343
|
end
|
344
|
+
end
|
395
345
|
|
396
|
-
|
346
|
+
# See URI::Generic#write
|
347
|
+
def write(*args, &block)
|
348
|
+
options = args.pop if Hash === args.last
|
349
|
+
options ||= {}
|
350
|
+
if String === args.first
|
351
|
+
ios = StringIO.new(args.first, "r")
|
352
|
+
write(options.merge(:size=>args.first.size)) { |bytes| ios.read(bytes) }
|
353
|
+
elsif args.first.respond_to?(:read)
|
354
|
+
size = args.first.size rescue nil
|
355
|
+
write({:size=>size}.merge(options)) { |bytes| args.first.read(bytes) }
|
356
|
+
elsif args.empty? && block
|
397
357
|
|
398
|
-
def initialize(url, options)
|
399
|
-
super
|
400
|
-
rake_check_options options, :digests, :permissions, :port, :uri, :username, :password
|
401
|
-
@permissions = options.delete :permissions
|
402
358
|
# SSH options are based on the username/password from the URI.
|
403
|
-
ssh_options = { :port
|
404
|
-
ssh_options[:password] ||= SFTP.passwords[
|
359
|
+
ssh_options = { :port=>port, :username=>user }.merge(options[:ssh_options] || {})
|
360
|
+
ssh_options[:password] ||= SFTP.passwords[host]
|
405
361
|
begin
|
406
|
-
puts "Connecting to #{
|
407
|
-
session = Net::SSH.start(
|
408
|
-
SFTP.passwords[
|
362
|
+
puts "Connecting to #{host}" if Rake.application.options.trace
|
363
|
+
session = Net::SSH.start(host, ssh_options)
|
364
|
+
SFTP.passwords[host] = ssh_options[:password]
|
409
365
|
rescue Net::SSH::AuthenticationFailed=>ex
|
410
366
|
# Only if running with console, prompt for password.
|
411
367
|
if !ssh_options[:password] && $stdout.isatty
|
412
|
-
password = HighLine.new.ask("Password for #{
|
368
|
+
password = HighLine.new.ask("Password for #{host}:") { |q| q.echo = "*" }
|
413
369
|
ssh_options[:password] = password
|
414
370
|
retry
|
415
371
|
end
|
416
372
|
raise
|
417
373
|
end
|
418
|
-
@sftp = session.sftp.connect
|
419
|
-
puts "connected" if Rake.application.options.trace
|
420
|
-
end
|
421
374
|
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
puts "Uploading signature to #{digest_file}" if Rake.application.options.trace
|
446
|
-
@sftp.open_handle(digest_file, "w") do |handle|
|
447
|
-
@sftp.write(handle, "#{hexdigest} #{path}")
|
448
|
-
end
|
449
|
-
@sftp.setstat(digest_file, :permissions => @permissions) if @permissions
|
375
|
+
session.sftp.connect do |sftp|
|
376
|
+
puts "connected" if Rake.application.options.trace
|
377
|
+
|
378
|
+
# To create a path, we need to create all its parent. We use realpath to determine if
|
379
|
+
# the path already exists, otherwise mkdir fails.
|
380
|
+
puts "Creating path #{@base_path}" if Rake.application.options.trace
|
381
|
+
path.split("/").inject("") do |base, part|
|
382
|
+
combined = base + part
|
383
|
+
sftp.realpath combined rescue sftp.mkdir combined, {}
|
384
|
+
"#{combined}/"
|
385
|
+
end
|
386
|
+
|
387
|
+
with_progress_bar options[:progress] && options[:size], path.split("/"), options[:size] || 0 do |progress|
|
388
|
+
puts "Uploading to #{path}" if Rake.application.options.trace
|
389
|
+
sftp.open_handle(path, "w") do |handle|
|
390
|
+
# Writing in chunks gives us the benefit of a progress bar,
|
391
|
+
# but also require that we maintain a position in the file,
|
392
|
+
# since write() with two arguments always writes at position 0.
|
393
|
+
pos = 0
|
394
|
+
while chunk = yield(32 * 4096)
|
395
|
+
sftp.write(handle, chunk, pos)
|
396
|
+
pos += chunk.size
|
397
|
+
progress << chunk
|
450
398
|
end
|
399
|
+
sftp.setstat(target_path, :permissions => options[:permissions]) if options[:permissions]
|
451
400
|
end
|
452
|
-
|
453
401
|
end
|
454
402
|
end
|
403
|
+
else
|
404
|
+
raise ArgumentError, "Either give me the content, or pass me a block, otherwise what would I upload?"
|
455
405
|
end
|
406
|
+
end
|
407
|
+
|
408
|
+
end
|
456
409
|
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
410
|
+
|
411
|
+
# File URL. Keep in mind that file URLs take the form of <code>file://host/path</code>, although the host
|
412
|
+
# is not used, so typically all you will see are three backslashes. This methods accept common variants,
|
413
|
+
# like <code>file:/path</code> but always returns a valid URL.
|
414
|
+
class FILE < Generic
|
415
|
+
|
416
|
+
COMPONENT = [ :host, :path ].freeze
|
417
|
+
|
418
|
+
def initialize(*args)
|
419
|
+
super
|
420
|
+
# file:something (opaque) becomes file:///something
|
421
|
+
if path.nil?
|
422
|
+
set_path "/#{opaque}"
|
423
|
+
unless opaque.nil?
|
424
|
+
set_opaque nil
|
425
|
+
warn "#{caller[2]}: We'll accept this URL, but just so you know, it needs three slashes, as in: #{to_s}"
|
466
426
|
end
|
467
427
|
end
|
428
|
+
# Sadly, file://something really means file://something/ (something being server)
|
429
|
+
set_path "/" if path.empty?
|
430
|
+
|
431
|
+
# On windows, file://c:/something is not a valid URL, but people do it anyway, so if we see a drive-as-host,
|
432
|
+
# we'll just be nice enough to fix it. (URI actually strips the colon here)
|
433
|
+
if host =~ /^[a-zA-Z]$/
|
434
|
+
set_path "/#{host}:#{path}"
|
435
|
+
set_host nil
|
436
|
+
end
|
437
|
+
end
|
468
438
|
|
469
|
-
|
470
|
-
|
471
|
-
|
439
|
+
# See URI::Generic#read
|
440
|
+
def read(options = nil, &block)
|
441
|
+
options ||= {}
|
442
|
+
raise ArgumentError, "Either you're attempting to read a file from another host (which we don't support), or you used two slashes by mistake, where you should have file:///<path>." unless host.blank?
|
443
|
+
|
444
|
+
path = real_path
|
445
|
+
# TODO: complain about clunky URLs
|
446
|
+
raise NotFoundError, "Looking for #{self} and can't find it." unless File.exists?(path)
|
447
|
+
raise NotFoundError, "Looking for the file #{self}, and it happens to be a directory." if File.directory?(path)
|
448
|
+
File.open path, "rb" do |input|
|
449
|
+
with_progress_bar options[:progress], path.split("/").last, input.stat.size do |progress|
|
450
|
+
block ? block.call(input.read) : input.read
|
451
|
+
end
|
472
452
|
end
|
453
|
+
end
|
454
|
+
|
455
|
+
# See URI::Generic#write
|
456
|
+
def write(*args, &block)
|
457
|
+
options = args.pop if Hash === args.last
|
458
|
+
options ||= {}
|
459
|
+
raise ArgumentError, "Either you're attempting to write a file to another host (which we don't support), or you used two slashes by mistake, where you should have file:///<path>." unless host.blank?
|
460
|
+
|
461
|
+
if String === args.first
|
462
|
+
ios = StringIO.new(args.first, "r")
|
463
|
+
write(options.merge(:size=>args.first.size)) { |bytes| ios.read(bytes) }
|
464
|
+
elsif args.first.respond_to?(:read)
|
465
|
+
size = args.first.size rescue nil
|
466
|
+
write({:size=>size}.merge(options)) { |bytes| args.first.read(bytes) }
|
467
|
+
elsif args.empty? && block
|
468
|
+
temp = nil
|
469
|
+
Tempfile.open File.basename(path) do |temp|
|
470
|
+
temp.binmode
|
471
|
+
with_progress_bar options[:progress] && options[:size], path.split("/"), options[:size] || 0 do |progress|
|
472
|
+
while chunk = yield(32 * 4096)
|
473
|
+
temp.write chunk
|
474
|
+
progress << chunk
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
478
|
+
real_path.tap do |path|
|
479
|
+
mkpath File.dirname(path)
|
480
|
+
File.move temp.path, path
|
481
|
+
end
|
482
|
+
else
|
483
|
+
raise ArgumentError, "Either give me the content, or pass me a block, otherwise what would I upload?"
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
def to_s()
|
488
|
+
"file://#{host}#{path}"
|
489
|
+
end
|
473
490
|
|
491
|
+
# The URL path always starts with a backslash. On most operating systems (Linux, Darwin, BSD) it points
|
492
|
+
# to the absolute path on the file system. But on Windows, it comes before the drive letter, creating an
|
493
|
+
# unusable path, so real_path fixes that. Ugly but necessary hack.
|
494
|
+
def real_path() #:nodoc:
|
495
|
+
RUBY_PLATFORM =~ /win32/ && path =~ /^\/[a-zA-Z]:\// ? path[1..-1] : path
|
474
496
|
end
|
475
497
|
|
498
|
+
@@schemes["FILE"] = FILE
|
499
|
+
|
476
500
|
end
|
501
|
+
|
477
502
|
end
|