Add method for checking if upload can be resumed
[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       # Get the length of the stream
27       #
28       # @return [Fixnum]
29       #   Length of stream, in bytes
30       def length
31         io.respond_to?(:length) ? io.length : File.size(local_path)
32       end
33     end
34     
35     ##
36     # Resumable uploader.
37     #
38     class ResumableUpload < Request
39       # @return [Fixnum] Max bytes to send in a single request
40       attr_accessor :chunk_size
41   
42       ##
43       # Creates a new uploader.
44       #
45       # @param [Hash] options
46       #   Request options
47       def initialize(options={})
48         super options
49         self.uri = options[:uri]
50         self.http_method = :put
51         @offset = options[:offset] || 0
52         @complete = false
53         @expired = false
54       end
55       
56       ##
57       # Sends all remaining chunks to the server
58       #
59       # @deprecated Pass the instance to {Google::APIClient#execute} instead
60       #
61       # @param [Google::APIClient] api_client
62       #   API Client instance to use for sending
63       def send_all(api_client)
64         result = nil
65         until complete?
66           result = send_chunk(api_client)
67           break unless result.status == 308
68         end
69         return result
70       end
71       
72       
73       ##
74       # Sends the next chunk to the server
75       #
76       # @deprecated Pass the instance to {Google::APIClient#execute} instead
77       #
78       # @param [Google::APIClient] api_client
79       #   API Client instance to use for sending
80       def send_chunk(api_client)
81         return api_client.execute(self)
82       end
83
84       ##
85       # Check if upload is complete
86       #
87       # @return [TrueClass, FalseClass]
88       #   Whether or not the upload complete successfully
89       def complete?
90         return @complete
91       end
92
93       ##
94       # Check if the upload URL expired (upload not completed in alotted time.)
95       # Expired uploads must be restarted from the beginning
96       #
97       # @return [TrueClass, FalseClass]
98       #   Whether or not the upload has expired and can not be resumed
99       def expired?
100         return @expired
101       end
102       
103       ##
104       # Check if upload is resumable. That is, neither complete nor expired
105       #
106       # @return [TrueClass, FalseClass] True if upload can be resumed
107       def resumable?
108         return !(self.complete? or self.expired?)
109       end
110       
111       ##
112       # Convert to an HTTP request. Returns components in order of method, URI,
113       # request headers, and body
114       #
115       # @api private
116       #
117       # @return [Array<(Symbol, Addressable::URI, Hash, [#read,#to_str])>]
118       def to_http_request
119         if @complete
120           raise Google::APIClient::ClientError, "Upload already complete"
121         elsif @offset.nil?
122           self.headers.update({ 
123             'Content-Length' => "0", 
124             'Content-Range' => "bytes */#{media.length}" })
125         else
126           start_offset = @offset
127           self.media.io.pos = start_offset
128           chunk = self.media.io.read(chunk_size)
129           content_length = chunk.bytesize
130           end_offset = start_offset + content_length - 1
131           
132           self.headers.update({
133             'Content-Length' => "#{content_length}",
134             'Content-Type' => self.media.content_type, 
135             'Content-Range' => "bytes #{start_offset}-#{end_offset}/#{media.length}" })
136           self.body = chunk
137         end
138         super
139       end
140       
141       ##
142       # Check the result from the server, updating the offset and/or location
143       # if available.
144       #
145       # @api private
146       #
147       # @param [Faraday::Response] response
148       #   HTTP response
149       #
150       # @return [Google::APIClient::Result]
151       #   Processed API response
152       def process_http_response(response)
153         case response.status
154         when 200...299
155           @complete = true
156         when 308
157           range = response.headers['range']
158           if range
159             @offset = range.scan(/\d+/).collect{|x| Integer(x)}.last + 1
160           end
161           if response.headers['location']
162             self.uri = response.headers['location']
163           end
164         when 400...499
165           @expired = true
166         when 500...599
167           # Invalidate the offset to mark it needs to be queried on the
168           # next request
169           @offset = nil
170         end
171         return Google::APIClient::Result.new(self, response)
172       end
173       
174       ##
175       # Hashified verison of the API request
176       #
177       # @return [Hash]
178       def to_hash
179         super.merge(:offset => @offset)
180       end
181       
182     end
183   end
184 end