Index: test/unit/lib/buffered_io_test.rb =================================================================== --- test/unit/lib/buffered_io_test.rb (revision 0) +++ test/unit/lib/buffered_io_test.rb (revision 0) @@ -0,0 +1,50 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../../test_helper' + +class BufferedIOTest < Test::Unit::TestCase + + CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + def self.rand_string(length=8) + s='' + length.times{ s << CHARS[rand(CHARS.length)] } + s + end + + TEST_STRING=rand_string(1000) + + def test_stop_caching + io = BufferedIO.new(StringIO.new(TEST_STRING)) + io.cache = true + t = io.read(5) + assert_equal 5, io.internal_buffer.size + io.cache = false + t += io.read + assert_equal TEST_STRING, t + assert_equal 5, io.internal_buffer.size + end + + def test_is_binary_data + io = BufferedIO.new(StringIO.new(TEST_STRING)) + io2 = BufferedIO.new(StringIO.new(TEST_STRING)) + io2.cache = true + [io, io2].each do |io| + assert_equal false, io.is_binary_data? + t = io.read + assert_equal TEST_STRING, t + end + end + +end Index: app/models/repository.rb =================================================================== --- app/models/repository.rb (revision 2054) +++ app/models/repository.rb (working copy) @@ -67,8 +67,8 @@ scm.properties(path, identifier) end - def cat(path, identifier=nil) - scm.cat(path, identifier) + def cat(path, identifier=nil, &block) + scm.cat(path, identifier, &block) end def diff(path, rev, rev_to) Index: app/controllers/repositories_controller.rb =================================================================== --- app/controllers/repositories_controller.rb (revision 2054) +++ app/controllers/repositories_controller.rb (working copy) @@ -110,19 +110,21 @@ def entry @entry = @repository.entry(@path, @rev) show_error_not_found and return unless @entry - + # If the entry is a dir, show the browser browse and return if @entry.is_dir? - - @content = @repository.cat(@path, @rev) - show_error_not_found and return unless @content - if 'raw' == params[:format] || @content.is_binary_data? - # Force the download if it's a binary file - send_data @content, :filename => @path.split('/').last - else - # Prevent empty lines when displaying a file with Windows style eol - @content.gsub!("\r\n", "\n") - end + + @repository.cat(@path, @rev) do |content| + show_error_not_found and return unless content + if 'raw' == params[:format] || content.is_binary_data? + # Force the download if it's a binary file + content.size = @entry.size + send_data content, :filename => @path.split('/').last + else + # Prevent empty lines when displaying a file with Windows style eol + @content = content.read.gsub("\r\n", "\n") + end + end end def annotate Index: lib/redmine/scm/adapters/subversion_adapter.rb =================================================================== --- lib/redmine/scm/adapters/subversion_adapter.rb (revision 2054) +++ lib/redmine/scm/adapters/subversion_adapter.rb (working copy) @@ -190,17 +190,21 @@ diff end - def cat(path, identifier=nil) + def cat(path, identifier=nil, &block) identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" cmd = "#{SVN_BIN} cat #{target(URI.escape(path))}@#{identifier}" cmd << credentials_string cat = nil - shellout(cmd) do |io| + if block_given? + shellout(cmd) do |io| + io.binmode + yield BufferedIO.new(io) + end + else + io = shellout(cmd) io.binmode - cat = io.read + return BufferedIO.new(io) end - return nil if $? && $?.exitstatus != 0 - cat end def annotate(path, identifier=nil) Index: lib/redmine/scm/adapters/bazaar_adapter.rb =================================================================== --- lib/redmine/scm/adapters/bazaar_adapter.rb (revision 2054) +++ lib/redmine/scm/adapters/bazaar_adapter.rb (working copy) @@ -151,17 +151,21 @@ diff end - def cat(path, identifier=nil) + def cat(path, identifier=nil, &block) cmd = "#{BZR_BIN} cat" cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0 cmd << " #{target(path)}" cat = nil - shellout(cmd) do |io| + if block_given? + shellout(cmd) do |io| + io.binmode + yield BufferedIO.new(io) + end + else + io = shellout(cmd) io.binmode - cat = io.read + return BufferedIO.new(io) end - return nil if $? && $?.exitstatus != 0 - cat end def annotate(path, identifier=nil) Index: lib/redmine/scm/adapters/abstract_adapter.rb =================================================================== --- lib/redmine/scm/adapters/abstract_adapter.rb (revision 2054) +++ lib/redmine/scm/adapters/abstract_adapter.rb (working copy) @@ -113,7 +113,7 @@ return nil end - def cat(path, identifier=nil) + def cat(path, identifier=nil, &block) return nil end @@ -172,9 +172,15 @@ def self.shellout(cmd, &block) logger.debug "Shelling out: #{cmd}" if logger && logger.debug? begin - IO.popen(cmd, "r+") do |io| + if block_given? + IO.popen(cmd, "r+") do |io| + io.close_write + block.call(io) + end + else + io = IO.popen(cmd, "r+") io.close_write - block.call(io) if block_given? + return io end rescue Errno::ENOENT => e msg = strip_credential(e.message) Index: lib/redmine/scm/adapters/git_adapter.rb =================================================================== --- lib/redmine/scm/adapters/git_adapter.rb (revision 2054) +++ lib/redmine/scm/adapters/git_adapter.rb (working copy) @@ -249,18 +249,22 @@ blame end - def cat(path, identifier=nil) + def cat(path, identifier=nil, &block) if identifier.nil? identifier = 'HEAD' end cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote(identifier + ':' + path)}" cat = nil - shellout(cmd) do |io| + if block_given? + shellout(cmd) do |io| + io.binmode + yield BufferedIO.new(io) + end + else + io = shellout(cmd) io.binmode - cat = io.read + return BufferedIO.new(io) end - return nil if $? && $?.exitstatus != 0 - cat end end end Index: lib/redmine/scm/adapters/mercurial_adapter.rb =================================================================== --- lib/redmine/scm/adapters/mercurial_adapter.rb (revision 2054) +++ lib/redmine/scm/adapters/mercurial_adapter.rb (working copy) @@ -169,17 +169,21 @@ diff end - def cat(path, identifier=nil) + def cat(path, identifier=nil, &block) cmd = "#{HG_BIN} -R #{target('')} cat" cmd << " -r " + (identifier ? identifier.to_s : "tip") cmd << " #{target(path)}" cat = nil - shellout(cmd) do |io| + if block_given? + shellout(cmd) do |io| + io.binmode + yield BufferedIO.new(io) + end + else + io = shellout(cmd) io.binmode - cat = io.read + return BufferedIO.new(io) end - return nil if $? && $?.exitstatus != 0 - cat end def annotate(path, identifier=nil) Index: lib/redmine/scm/adapters/filesystem_adapter.rb =================================================================== --- lib/redmine/scm/adapters/filesystem_adapter.rb (revision 2054) +++ lib/redmine/scm/adapters/filesystem_adapter.rb (working copy) @@ -71,8 +71,17 @@ entries.sort_by_name end - def cat(path, identifier=nil) - File.new(target(path), "rb").read + def cat(path, identifier=nil, &block) + if block_given? + File.open(target(path), "rb") do |io| + io.binmode + yield BufferedIO.new(io) + end + else + io = File.new(target(path), "rb") + io.binmode + return BufferedIO.new(io) + end end private Index: lib/redmine/scm/adapters/cvs_adapter.rb =================================================================== --- lib/redmine/scm/adapters/cvs_adapter.rb (revision 2054) +++ lib/redmine/scm/adapters/cvs_adapter.rb (working copy) @@ -241,7 +241,7 @@ diff end - def cat(path, identifier=nil) + def cat(path, identifier=nil, &block) identifier = (identifier) ? identifier : "HEAD" logger.debug " cat path:'#{path}',identifier #{identifier}" path_with_project="#{url}#{with_leading_slash(path)}" @@ -249,11 +249,16 @@ cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier cmd << " -p #{shell_quote path_with_project}" cat = nil - shellout(cmd) do |io| - cat = io.read + if block_given? + shellout(cmd) do |io| + io.binmode + yield BufferedIO.new(io) + end + else + io = shellout(cmd) + io.binmode + return BufferedIO.new(io) end - return nil if $? && $?.exitstatus != 0 - cat end def annotate(path, identifier=nil) Index: lib/redmine/scm/adapters/darcs_adapter.rb =================================================================== --- lib/redmine/scm/adapters/darcs_adapter.rb (revision 2054) +++ lib/redmine/scm/adapters/darcs_adapter.rb (working copy) @@ -134,17 +134,21 @@ diff end - def cat(path, identifier=nil) + def cat(path, identifier=nil, &block) cmd = "#{DARCS_BIN} show content --repodir #{@url}" cmd << " --match \"hash #{identifier}\"" if identifier cmd << " #{shell_quote path}" cat = nil - shellout(cmd) do |io| + if block_given? + shellout(cmd) do |io| + io.binmode + yield BufferedIO.new(io) + end + else + io = shellout(cmd) io.binmode - cat = io.read + return BufferedIO.new(io) end - return nil if $? && $?.exitstatus != 0 - cat end private Index: lib/buffered_io.rb =================================================================== --- lib/buffered_io.rb (revision 0) +++ lib/buffered_io.rb (revision 0) @@ -0,0 +1,133 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +class BufferedIO + + attr_accessor :cache, :size + + def initialize(io) + @source = io + @cache = false + @synced = true + end + + def cache=(b) + if b and synced? + @cache = true + else + @cache = false + end + end + + def internal_buffer + @internal_buffer || @internal_buffer = StringIO.new + end + + def is_binary_data? + if synced? + position = internal_buffer.pos + internal_buffer.rewind + s = read_and_cache(4096) + internal_buffer.pos = position + s.is_binary_data? + else + raise Exception.new("Cannot do this test on an uncached buffer") + end + end + + def synced? + @synced + end + + def read(x=nil) + if cache + read_and_cache(x) + else + read_and_dont_cache(x) + end + end + + def pos=(x) + if x > internal_buffer.size + if sinced? and cache + _append @source.read(x - internal_buffer.pos) + internal_buffer.pos = x + else + @source.pos = x + end + else + internal_buffer.pos = x + end + end + + ["pos", "rewind", "tell"].each do |m| + define_method(m) do + if synced? + internal_buffer.send(m) + else + @source.send(m) + end + end + end + + private + + def _append(s) + unless s.nil? + internal_buffer << s + internal_buffer.pos -= s.size + end + end + + def read_source(x) + @synced = false + @source.read(x) + end + + # Read the buffer by filling readed data in the cache. + def read_and_cache(x=nil) + if synced? + to_read = x ? to_read = x + internal_buffer.pos - internal_buffer.size : nil + _append(@source.read(to_read)) if to_read.nil? or to_read > 0 + internal_buffer.read(x) + else + # Data is missing + raise "Cannot cache a partially cached stream" + end + end + + # Read without filling the cache and ram by the way. + # Not cached data will not be readable anymore if the + # buffer cannot rewind. + def read_and_dont_cache(x=nil) + if synced? + # Source and cache buffer are synced + if internal_buffer.pos == internal_buffer.size + # There is nothing to read in the cache + read_source(x) + else # internal_buffer.pos < @source.pos + if x.nil? or (internal_buffer.pos + x) > internal_buffer.size + # There is a first part to read in the cache + # and another in the source + internal_buffer.read + read_source(x) + else + # All is in the cache + internal_buffer.read(x) + end + end + else + @internal_buffer = nil + read_source(x) + end + end +end \ No newline at end of file