buildr 1.1.3 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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
- # rake compile
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 Rakefile is assumed to
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
- # |__ Rakefile
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
- each { |project| project.invoke }.sort_by(&:name)
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
- # rake build
229
+ # buildr build
228
230
  # is the same as:
229
- # rake foo:build
231
+ # buildr foo:build
230
232
  # But:
231
233
  # cd bar
232
- # rake build
234
+ # buildr build
233
235
  # is the same as:
234
- # rake foo:bar:build
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
- projects = local_projects
241
- if projects.empty?
242
- warn "No projects defined for directory #{Rake.application.original_dir}" if verbose
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 = Rake.application.original_dir) #:nodoc:
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 Rakefile defines top-level project, so it's logical that the top-level project's
316
- # base directory is the one in which we find the Rakefile. And each sub-project has
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/Rakefile <-- builds "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 Rakefile.
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 Rakefile directory,
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
- Rake.application.lookup(task_name, parent.name.split(":")).enhance [task] if parent
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 Rakefile is loaded. Instead, use it to create and extend tasks that are
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
- # % rake build
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 }
@@ -1,14 +1,10 @@
1
- module Rake #:nodoc:
2
- class Task
1
+ module Rake #:nodoc
2
+ class Task #:nodoc:
3
3
 
4
- def invoke #:nodoc:
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() #:nodoc:
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
@@ -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
- # Monkeypatching Net::HTTP to solve keep_alive bug, see http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/10818
31
- module Net
32
- class HTTP
33
- def keep_alive?(req, res)
34
- return false if /close/i =~ req['connection'].to_s
35
- return false if @seems_1_0_server
36
- return false if /close/i =~ res['connection'].to_s
37
- return true if /keep-alive/i =~ res['connection'].to_s
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
- module Buildr
47
-
48
- # Transports are used for downloading artifacts from remote repositories, uploading
49
- # artifacts to deployment repositories, and anything else you need to move around.
50
- #
51
- # The HTTP transport is used for all URLs with the scheme http or https. You can only
52
- # use the HTTP transport to download artifacts.
53
- #
54
- # The HTTP transport supports the following options:
55
- # * :proxy -- Proxy server to use. A hash with the values host, port, user and password.
56
- # You can also pass a URL (string or URI object).
57
- #
58
- # The SFTP transport is used for all URLs with the schema sftp. You can only use the
59
- # SFTP transport to upload artifacts.
60
- #
61
- # The SFTP transport supports the following options:
62
- # * :username -- The username.
63
- # * :password -- A password. If unspecified, you will be prompted to enter a password.
64
- # * :permissions -- Permissions to set on the uploaded file.
65
- # You can also pass the username/password in the URL.
66
- #
67
- # The SFTP transport will automatically create MD5 and SHA1 digest files for each file
68
- # it uploads.
69
- module Transports
70
-
71
- # Indicates the requested resource was not found.
72
- class NotFound < Exception
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
- class << self
118
+ end
76
119
 
77
- # :call-seq:
78
- # perform(url, options?) { |transport| ... }
79
- #
80
- # Perform one or more operations using an open connection to the
81
- # specified URL. For examples, see Transport#download and Transport#upload.
82
- def perform(url, options = nil, &block)
83
- uri = URI.parse(url.to_s)
84
- const_get(uri.scheme.upcase).perform(uri, options, &block)
85
- end
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
- # :call-seq:
88
- # download(url, target, options?)
89
- #
90
- # Convenience method for downloading a single file from the specified
91
- # URL to the target file.
92
- def download(url, target, options = nil, &block)
93
- uri = URI.parse(url.to_s)
94
- path, uri.path = uri.path, ""
95
- const_get(uri.scheme.upcase).perform(uri, options) do |transport|
96
- transport.download(path, target, &block)
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
- # Extend this class if you are implementing a new transport.
103
- class Transport
104
-
105
- class << self
106
-
107
- # :call-seq:
108
- # perform(url, options?) { |transport| ... }
109
- #
110
- # Perform one or more operations using an open connection to the
111
- # specified URL. For examples, see #download and #upload.
112
- def perform(url, options = nil)
113
- instance = new(url, options)
114
- begin
115
- yield instance
116
- ensure
117
- instance.close
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
- end
121
-
122
- # The server URI.
123
- attr_reader :uri
124
- # The base path on the server, always ending with a slash.
125
- attr_reader :base_path
126
- # Options passed during construction.
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
- progress_bar.format = "#{truncated}: %3d%% %s %s/%s %s"
197
- progress_bar.format = "%3d%% %s %s/%s %s"
198
- progress_bar.format_arguments = [:percentage, :bar, :bytes, :total, :stat]
199
- progress_bar.bar_mark = "."
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
- begin
203
- class << progress_bar
204
- def <<(bytes)
205
- inc bytes.respond_to?(:size) ? bytes.size : bytes
206
- end
207
- end
208
- yield progress_bar
209
- ensure
210
- progress_bar.finish
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
- progress_bar = Object.new
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
- end
221
-
222
- # :call-seq:
223
- # with_digests(types?) { |digester| ... } => hash
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
- class HTTP < Transport #:nodoc:
279
+ class HTTP #:nodoc:
291
280
 
292
- def initialize(url, options)
293
- super
294
- if options
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
- case proxy
300
- when Hash
301
- @http = Net::HTTP.start(@uri.host, @uri.port, proxy[:host], proxy[:port], proxy[:user], proxy[:password])
302
- when URI, String
303
- proxy = URI.parse(proxy.to_s)
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
- last_modified = Transports.download(@uri + URI.parse(response["location"]), target, @options, &block)
298
+ result = (self + URI.parse(response["location"])).read(options, &block)
327
299
 
328
300
  when Net::HTTPOK
329
- puts "Downloading #{@uri}/#{path}" if verbose
330
- last_modified = Time.parse(response["Last-Modified"] || "")
331
- with_progress_bar path.split("/").last, response.content_length do |progress|
332
- with_digests(@options[:digests]) do |digester|
333
-
334
- download = proc do |write|
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
- if target
354
- # If download breaks we end up with a partial file which is
355
- # worse than not having a file at all, so download to temporary
356
- # file and then move over.
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 NotFound, "Looking for #{@uri}/#{path} and all I got was a 404!"
318
+ raise NotFoundError, "Looking for #{self} and all I got was a 404!"
370
319
  else
371
- fail "Failed to download #{@uri}/#{path}: #{response.message}"
320
+ raise RuntimeError, "Failed to download #{self}: #{response.message}"
372
321
  end
373
322
  end
374
- last_modified
375
323
  end
376
324
 
377
- def close()
378
- @http.finish
379
- @http = nil
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
- # Use the HTTP transport for HTTPS connections.
385
- HTTPS = HTTP #:nodoc:
386
-
335
+ end
387
336
 
388
- class SFTP < Transport #:nodoc:
337
+ class SFTP #:nodoc:
389
338
 
390
- class << self
391
- def passwords()
392
- @passwords ||= {}
393
- end
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
- attr_reader :sftp
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=>@uri.port, :username=>@uri.user }.merge(options || {})
404
- ssh_options[:password] ||= SFTP.passwords[@uri.host]
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 #{@uri.host}" if Rake.application.options.trace
407
- session = Net::SSH.start(@uri.host, ssh_options)
408
- SFTP.passwords[@uri.host] = ssh_options[:password]
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 #{@uri.host}:") { |q| q.echo = "*" }
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
- def upload(source, path)
423
- File.open(source) do |file|
424
- with_progress_bar path.split("/").last, File.size(source) do |progress|
425
- with_digests(@options[:digests]) do |digester|
426
- target_path = "#{@base_path}#{path}"
427
- puts "Uploading to #{target_path}" if Rake.application.options.trace
428
- @sftp.open_handle(target_path, "w") do |handle|
429
- # Writing in chunks gives us the benefit of a progress bar,
430
- # but also require that we maintain a position in the file,
431
- # since write() with two arguments always writes at position 0.
432
- pos = 0
433
- while chunk = file.read(32 * 4096)
434
- @sftp.write(handle, chunk, pos)
435
- pos += chunk.size
436
- digester << chunk
437
- progress << chunk
438
- end
439
- end
440
- @sftp.setstat(target_path, :permissions => @permissions) if @permissions
441
-
442
- # Upload all the digests.
443
- digester.each do |type, hexdigest|
444
- digest_file = "#{@base_path}#{path}.#{type}"
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
- def mkpath(path)
458
- # To create a path, we need to create all its parent.
459
- # We use realpath to determine if the path already exists,
460
- # otherwise mkdir fails.
461
- puts "Creating path #{@base_path}" if Rake.application.options.trace
462
- path.split("/").inject(@base_path) do |base, part|
463
- combined = base + part
464
- @sftp.realpath combined rescue @sftp.mkdir combined, {}
465
- "#{combined}/"
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
- def close()
470
- @sftp.close
471
- @sftp = nil
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