1 # Copyright 2010 Google Inc.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 require 'google/api_client/reference'
15 require 'faraday/multipart'
20 # Uploadable media support. Holds an IO stream & content type.
22 # @see Faraday::UploadIO
24 # media = Google::APIClient::UploadIO.new('mymovie.m4v', 'video/mp4')
25 class UploadIO < Faraday::Multipart::FilePart
27 # @return [Fixnum] Size of chunks to upload. Default is nil, meaning upload the entire file in a single request
28 attr_accessor :chunk_size
31 # Get the length of the stream
34 # Length of stream, in bytes
36 io.respond_to?(:length) ? io.length : File.size(local_path)
41 # Wraps an input stream and limits data to a given range
44 # chunk = Google::APIClient::RangedIO.new(io, 0, 1000)
47 # Bind an input stream to a specific range.
51 # @param [Fixnum] offset
52 # Starting offset of the range
53 # @param [Fixnum] length
55 def initialize(io, offset, length)
64 def read(amount = nil, buf = nil)
73 size = [@length - @pos, amount].min
78 result = @io.read(size)
79 result.force_encoding("BINARY") if result.respond_to?(:force_encoding)
80 buffer << result if result
107 @io.pos = @offset + pos
112 # Resumable uploader.
114 class ResumableUpload < Request
115 # @return [Fixnum] Max bytes to send in a single request
116 attr_accessor :chunk_size
119 # Creates a new uploader.
121 # @param [Hash] options
123 def initialize(options={})
125 self.uri = options[:uri]
126 self.http_method = :put
127 @offset = options[:offset] || 0
133 # Sends all remaining chunks to the server
135 # @deprecated Pass the instance to {Google::APIClient#execute} instead
137 # @param [Google::APIClient] api_client
138 # API Client instance to use for sending
139 def send_all(api_client)
142 result = send_chunk(api_client)
143 break unless result.status == 308
150 # Sends the next chunk to the server
152 # @deprecated Pass the instance to {Google::APIClient#execute} instead
154 # @param [Google::APIClient] api_client
155 # API Client instance to use for sending
156 def send_chunk(api_client)
157 return api_client.execute(self)
161 # Check if upload is complete
163 # @return [TrueClass, FalseClass]
164 # Whether or not the upload complete successfully
170 # Check if the upload URL expired (upload not completed in alotted time.)
171 # Expired uploads must be restarted from the beginning
173 # @return [TrueClass, FalseClass]
174 # Whether or not the upload has expired and can not be resumed
180 # Check if upload is resumable. That is, neither complete nor expired
182 # @return [TrueClass, FalseClass] True if upload can be resumed
184 return !(self.complete? or self.expired?)
188 # Convert to an HTTP request. Returns components in order of method, URI,
189 # request headers, and body
193 # @return [Array<(Symbol, Addressable::URI, Hash, [#read,#to_str])>]
196 raise Google::APIClient::ClientError, "Upload already complete"
198 self.headers.update({
199 'Content-Length' => "0",
200 'Content-Range' => "bytes */#{media.length}" })
202 start_offset = @offset
203 remaining = self.media.length - start_offset
204 chunk_size = self.media.chunk_size || self.chunk_size || self.media.length
205 content_length = [remaining, chunk_size].min
206 chunk = RangedIO.new(self.media.io, start_offset, content_length)
207 end_offset = start_offset + content_length - 1
208 self.headers.update({
209 'Content-Length' => "#{content_length}",
210 'Content-Type' => self.media.content_type,
211 'Content-Range' => "bytes #{start_offset}-#{end_offset}/#{media.length}" })
218 # Check the result from the server, updating the offset and/or location
223 # @param [Faraday::Response] response
226 # @return [Google::APIClient::Result]
227 # Processed API response
228 def process_http_response(response)
233 range = response.headers['range']
235 @offset = range.scan(/\d+/).collect{|x| Integer(x)}.last + 1
237 if response.headers['location']
238 self.uri = response.headers['location']
243 # Invalidate the offset to mark it needs to be queried on the
247 return Google::APIClient::Result.new(self, response)
251 # Hashified verison of the API request
255 super.merge(:offset => @offset)