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