diff --git a/lib/google/api_client/media.rb b/lib/google/api_client/media.rb index 45726cded..5066bcebd 100644 --- a/lib/google/api_client/media.rb +++ b/lib/google/api_client/media.rb @@ -21,7 +21,11 @@ module Google # @see Faraday::UploadIO # @example # media = Google::APIClient::UploadIO.new('mymovie.m4v', 'video/mp4') - class UploadIO < Faraday::UploadIO + class UploadIO < Faraday::UploadIO + + # @return [Fixnum] Size of chunks to upload. Default is nil, meaning upload the entire file in a single request + attr_accessor :chunk_size + ## # Get the length of the stream # @@ -32,6 +36,77 @@ module Google end end + ## + # Wraps an input stream and limits data to a given range + # + # @example + # chunk = Google::APIClient::RangedIO.new(io, 0, 1000) + class RangedIO + ## + # Bind an input stream to a specific range. + # + # @param [IO] io + # Source input stream + # @param [Fixnum] offset + # Starting offset of the range + # @param [Fixnum] length + # Length of range + def initialize(io, offset, length) + @io = io + @offset = offset + @length = length + self.rewind + end + + ## + # @see IO#read + def read(amount = nil, buf = nil) + buffer = buf || '' + if amount.nil? + size = @length - @pos + done = '' + elsif amount == 0 + size = 0 + done = '' + else + size = [@length - @pos, amount].min + done = nil + end + + if size > 0 + result = @io.read(size) + result.force_encoding("BINARY") if result.respond_to?(:force_encoding) + buffer << result if result + @pos = @pos + size + end + + if buffer.length > 0 + buffer + else + done + end + end + + ## + # @see IO#rewind + def rewind + self.pos = 0 + end + + ## + # @see IO#pos + def pos + @pos + end + + ## + # @see IO#pos= + def pos=(pos) + @pos = pos + @io.pos = @offset + pos + end + end + ## # Resumable uploader. # @@ -124,11 +199,11 @@ module Google 'Content-Range' => "bytes */#{media.length}" }) else start_offset = @offset - self.media.io.pos = start_offset - chunk = self.media.io.read(chunk_size) - content_length = chunk.bytesize + remaining = self.media.length - start_offset + chunk_size = self.media.chunk_size || self.chunk_size || self.media.length + content_length = [remaining, chunk_size].min + chunk = RangedIO.new(self.media.io, start_offset, content_length) end_offset = start_offset + content_length - 1 - self.headers.update({ 'Content-Length' => "#{content_length}", 'Content-Type' => self.media.content_type, diff --git a/lib/google/api_client/request.rb b/lib/google/api_client/request.rb index 71cfa0b48..5dc4d91a6 100644 --- a/lib/google/api_client/request.rb +++ b/lib/google/api_client/request.rb @@ -165,7 +165,7 @@ module Google # Resumamble slightly different than other upload protocols in that it requires at least # 2 requests. if result.status == 200 && self.upload_type == 'resumable' - upload = result.resumable_upload + upload = result.resumable_upload unless upload.complete? logger.debug { "#{self.class} Sending upload body" } result = upload.send(connection) diff --git a/spec/google/api_client/media_spec.rb b/spec/google/api_client/media_spec.rb index ed70ec309..0123443cf 100644 --- a/spec/google/api_client/media_spec.rb +++ b/spec/google/api_client/media_spec.rb @@ -57,6 +57,54 @@ describe Google::APIClient::UploadIO do end end +describe Google::APIClient::RangedIO do + before do + @source = StringIO.new("1234567890abcdef") + @io = Google::APIClient::RangedIO.new(@source, 1, 5) + end + + it 'should return the correct range when read entirely' do + @io.read.should == "23456" + end + + it 'should maintain position' do + @io.read(1).should == '2' + @io.read(2).should == '34' + @io.read(2).should == '56' + end + + it 'should allow rewinds' do + @io.read(2).should == '23' + @io.rewind() + @io.read(2).should == '23' + end + + it 'should allow setting position' do + @io.pos = 3 + @io.read.should == '56' + end + + it 'should not allow position to be set beyond range' do + @io.pos = 10 + @io.read.should == '' + end + + it 'should return empty string when read amount is zero' do + @io.read(0).should == '' + end + + it 'should return empty string at EOF if amount is nil' do + @io.read + @io.read.should == '' + end + + it 'should return nil at EOF if amount is positive int' do + @io.read + @io.read(1).should == nil + end + +end + describe Google::APIClient::ResumableUpload do CLIENT = Google::APIClient.new(:application_name => 'API Client Tests') unless defined?(CLIENT)