Merge branch 'master' of https://code.google.com/p/google-api-ruby-client
[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
15 module Google
16   class APIClient
17     ##
18     # Uploadable media support.  Holds an IO stream & content type.
19     #
20     # @see Faraday::UploadIO
21     # @example
22     #   media = Google::APIClient::UploadIO.new('mymovie.m4v', 'video/mp4')
23     class UploadIO < Faraday::UploadIO      
24       ##
25       # Get the length of the stream
26       # @return [Integer]
27       #   Length of stream, in bytes
28       def length
29         io.respond_to?(:length) ? io.length : File.size(local_path)
30       end
31     end
32     
33     ##
34     # Resumable uploader.
35     #
36     class ResumableUpload
37       attr_reader :result
38       attr_accessor :client
39       attr_accessor :chunk_size
40       attr_accessor :media
41       attr_accessor :location
42   
43       ##
44       # Creates a new uploader.
45       #
46       # @param [Google::APIClient::Result] result
47       #   Result of the initial request that started the upload
48       # @param [Google::APIClient::UploadIO] media
49       #   Media to upload
50       # @param [String] location
51       #  URL to upload to    
52       def initialize(result, media, location)
53         self.media = media
54         self.location = location
55         self.chunk_size = 256 * 1024
56         
57         @api_method = result.reference.api_method
58         @result = result
59         @offset = 0
60         @complete = false
61       end
62       
63       ##
64       # Sends all remaining chunks to the server
65       #
66       # @param [Google::APIClient] api_client
67       #   API Client instance to use for sending
68       def send_all(api_client)
69         until complete?
70           send_chunk(api_client)
71           break unless result.status == 308
72         end
73         return result
74       end
75       
76       
77       ##
78       # Sends the next chunk to the server
79       #
80       # @param [Google::APIClient] api_client
81       #   API Client instance to use for sending
82       def send_chunk(api_client)
83         if @offset.nil?
84           return resync_range(api_client)
85         end
86
87         start_offset = @offset
88         self.media.io.pos = start_offset
89         chunk = self.media.io.read(chunk_size)
90         content_length = chunk.bytesize
91
92         end_offset = start_offset + content_length - 1
93         @result = api_client.execute(
94           :uri => self.location,
95           :http_method => :put,
96           :headers => {
97             'Content-Length' => "#{content_length}",
98             'Content-Type' => self.media.content_type, 
99             'Content-Range' => "bytes #{start_offset}-#{end_offset}/#{media.length}" },
100           :body => chunk)
101         return process_result(@result)
102       end
103
104       ##
105       # Check if upload is complete
106       #
107       # @return [TrueClass, FalseClass]
108       #   Whether or not the upload complete successfully
109       def complete?
110         return @complete
111       end
112
113       ##
114       # Check if the upload URL expired (upload not completed in alotted time.)
115       # Expired uploads must be restarted from the beginning
116       #
117       # @return [TrueClass, FalseClass]
118       #   Whether or not the upload has expired and can not be resumed
119       def expired?
120         return @result.status == 404 || @result.status == 410
121       end
122       
123       ##
124       # Get the last saved range from the server in case an error occurred 
125       # and the offset is not known.
126       #
127       # @param [Google::APIClient] api_client
128       #   API Client instance to use for sending
129       def resync_range(api_client)
130         r = api_client.execute(
131           :uri => self.location,
132           :http_method => :put,
133           :headers => { 
134             'Content-Length' => "0", 
135             'Content-Range' => "bytes */#{media.length}" })
136         return process_result(r)
137       end
138       
139       ##
140       # Check the result from the server, updating the offset and/or location
141       # if available.
142       #
143       # @param [Google::APIClient::Result] r
144       #  Result of a chunk upload or range query
145       def process_result(result)
146         case result.status
147         when 200...299
148           @complete = true
149           if @api_method
150             # Inject the original API method so data is parsed correctly
151             result.reference.api_method = @api_method
152           end
153           return result
154         when 308
155           range = result.headers['range']
156           if range
157             @offset = range.scan(/\d+/).collect{|x| Integer(x)}.last + 1
158           end
159           if result.headers['location']
160             self.location = result.headers['location']
161           end
162         when 500...599
163           # Invalidate the offset to mark it needs to be queried on the
164           # next request
165           @offset = nil
166         end
167         return nil
168       end
169       
170     end
171   end
172 end