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