21128: Merge commit 'adbbc9e3c7a36d39b30f403555ee5889e32adcc0' into 21128-toolbar...
[arvados.git] / sdk / ruby-google-api-client / lib / google / api_client / media.rb
1 # Copyright 2010 Google Inc.
2 #
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
6 #
7 #      http://www.apache.org/licenses/LICENSE-2.0
8 #
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'
16
17 module Google
18   class APIClient
19     ##
20     # Uploadable media support.  Holds an IO stream & content type.
21     #
22     # @see Faraday::UploadIO
23     # @example
24     #   media = Google::APIClient::UploadIO.new('mymovie.m4v', 'video/mp4')
25     class UploadIO < Faraday::Multipart::FilePart
26       
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
29             
30       ##
31       # Get the length of the stream
32       #
33       # @return [Fixnum]
34       #   Length of stream, in bytes
35       def length
36         io.respond_to?(:length) ? io.length : File.size(local_path)
37       end
38     end
39     
40     ##
41     # Wraps an input stream and limits data to a given range
42     #
43     # @example
44     #   chunk = Google::APIClient::RangedIO.new(io, 0, 1000)
45     class RangedIO 
46       ##
47       # Bind an input stream to a specific range.
48       #
49       # @param [IO] io
50       #   Source input stream
51       # @param [Fixnum] offset
52       #   Starting offset of the range
53       # @param [Fixnum] length
54       #   Length of range
55       def initialize(io, offset, length)
56         @io = io
57         @offset = offset
58         @length = length
59         self.rewind
60       end
61       
62       ##
63       # @see IO#read
64       def read(amount = nil, buf = nil)
65         buffer = buf || ''
66         if amount.nil?
67           size = @length - @pos
68           done = ''
69         elsif amount == 0
70           size = 0
71           done = ''
72         else 
73           size = [@length - @pos, amount].min
74           done = nil
75         end
76
77         if size > 0
78           result = @io.read(size)
79           result.force_encoding("BINARY") if result.respond_to?(:force_encoding)
80           buffer << result if result
81           @pos = @pos + size
82         end
83
84         if buffer.length > 0
85           buffer
86         else
87           done
88         end
89       end
90
91       ##
92       # @see IO#rewind
93       def rewind
94         self.pos = 0
95       end
96
97       ##
98       # @see IO#pos
99       def pos
100         @pos
101       end
102
103       ##
104       # @see IO#pos=
105       def pos=(pos)
106         @pos = pos
107         @io.pos = @offset + pos
108       end
109     end
110     
111     ##
112     # Resumable uploader.
113     #
114     class ResumableUpload < Request
115       # @return [Fixnum] Max bytes to send in a single request
116       attr_accessor :chunk_size
117   
118       ##
119       # Creates a new uploader.
120       #
121       # @param [Hash] options
122       #   Request options
123       def initialize(options={})
124         super options
125         self.uri = options[:uri]
126         self.http_method = :put
127         @offset = options[:offset] || 0
128         @complete = false
129         @expired = false
130       end
131       
132       ##
133       # Sends all remaining chunks to the server
134       #
135       # @deprecated Pass the instance to {Google::APIClient#execute} instead
136       #
137       # @param [Google::APIClient] api_client
138       #   API Client instance to use for sending
139       def send_all(api_client)
140         result = nil
141         until complete?
142           result = send_chunk(api_client)
143           break unless result.status == 308
144         end
145         return result
146       end
147       
148       
149       ##
150       # Sends the next chunk to the server
151       #
152       # @deprecated Pass the instance to {Google::APIClient#execute} instead
153       #
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)
158       end
159
160       ##
161       # Check if upload is complete
162       #
163       # @return [TrueClass, FalseClass]
164       #   Whether or not the upload complete successfully
165       def complete?
166         return @complete
167       end
168
169       ##
170       # Check if the upload URL expired (upload not completed in alotted time.)
171       # Expired uploads must be restarted from the beginning
172       #
173       # @return [TrueClass, FalseClass]
174       #   Whether or not the upload has expired and can not be resumed
175       def expired?
176         return @expired
177       end
178       
179       ##
180       # Check if upload is resumable. That is, neither complete nor expired
181       #
182       # @return [TrueClass, FalseClass] True if upload can be resumed
183       def resumable?
184         return !(self.complete? or self.expired?)
185       end
186       
187       ##
188       # Convert to an HTTP request. Returns components in order of method, URI,
189       # request headers, and body
190       #
191       # @api private
192       #
193       # @return [Array<(Symbol, Addressable::URI, Hash, [#read,#to_str])>]
194       def to_http_request
195         if @complete
196           raise Google::APIClient::ClientError, "Upload already complete"
197         elsif @offset.nil?
198           self.headers.update({ 
199             'Content-Length' => "0", 
200             'Content-Range' => "bytes */#{media.length}" })
201         else
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}" })
212           self.body = chunk
213         end
214         super
215       end
216       
217       ##
218       # Check the result from the server, updating the offset and/or location
219       # if available.
220       #
221       # @api private
222       #
223       # @param [Faraday::Response] response
224       #   HTTP response
225       #
226       # @return [Google::APIClient::Result]
227       #   Processed API response
228       def process_http_response(response)
229         case response.status
230         when 200...299
231           @complete = true
232         when 308
233           range = response.headers['range']
234           if range
235             @offset = range.scan(/\d+/).collect{|x| Integer(x)}.last + 1
236           end
237           if response.headers['location']
238             self.uri = response.headers['location']
239           end
240         when 400...499
241           @expired = true
242         when 500...599
243           # Invalidate the offset to mark it needs to be queried on the
244           # next request
245           @offset = nil
246         end
247         return Google::APIClient::Result.new(self, response)
248       end
249       
250       ##
251       # Hashified verison of the API request
252       #
253       # @return [Hash]
254       def to_hash
255         super.merge(:offset => @offset)
256       end
257       
258     end
259   end
260 end