Continue internal shuffling...
[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       # @return [Integer]
28       #   Length of stream, in bytes
29       def length
30         io.respond_to?(:length) ? io.length : File.size(local_path)
31       end
32     end
33     
34     ##
35     # Resumable uploader.
36     #
37     class ResumableUpload < Request
38       attr_accessor :chunk_size
39   
40       ##
41       # Creates a new uploader.
42       #
43       # @param [Google::APIClient::Result] result
44       #   Result of the initial request that started the upload
45       # @param [Google::APIClient::UploadIO] media
46       #   Media to upload
47       # @param [String] location
48       #  URL to upload to    
49       def initialize(options={})
50         super options
51         self.uri = options[:uri]
52         self.http_method = :put
53         @offset = options[:offset] || 0
54         @complete = false
55         @expired = false
56       end
57       
58       ##
59       # Sends all remaining chunks to the server
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       # @param [Google::APIClient] api_client
77       #   API Client instance to use for sending
78       def send_chunk(api_client)
79         return api_client.execute(self)
80       end
81
82       ##
83       # Check if upload is complete
84       #
85       # @return [TrueClass, FalseClass]
86       #   Whether or not the upload complete successfully
87       def complete?
88         return @complete
89       end
90
91       ##
92       # Check if the upload URL expired (upload not completed in alotted time.)
93       # Expired uploads must be restarted from the beginning
94       #
95       # @return [TrueClass, FalseClass]
96       #   Whether or not the upload has expired and can not be resumed
97       def expired?
98         return @expired
99       end
100       
101       def to_http_request
102         if @complete
103           raise Google::APIClient::ClientError, "Upload already complete"
104         elsif @offset.nil?
105           self.headers.update({ 
106             'Content-Length' => "0", 
107             'Content-Range' => "bytes */#{media.length}" })
108         else
109           start_offset = @offset
110           self.media.io.pos = start_offset
111           chunk = self.media.io.read(chunk_size)
112           content_length = chunk.bytesize
113           end_offset = start_offset + content_length - 1
114           
115           self.headers.update({
116             'Content-Length' => "#{content_length}",
117             'Content-Type' => self.media.content_type, 
118             'Content-Range' => "bytes #{start_offset}-#{end_offset}/#{media.length}" })
119           self.body = chunk
120         end
121         super
122       end
123       
124       def to_hash
125         super.merge(:offset => @offset)
126       end
127       
128       ##
129       # Check the result from the server, updating the offset and/or location
130       # if available.
131       #
132       # @param [Faraday::Response] r
133       #  Result of a chunk upload or range query
134       def process_http_response(response)
135         case response.status
136         when 200...299
137           @complete = true
138         when 308
139           range = response.headers['range']
140           if range
141             @offset = range.scan(/\d+/).collect{|x| Integer(x)}.last + 1
142           end
143           if response.headers['location']
144             self.uri = response.headers['location']
145           end
146         when 400...499
147           @expired = true
148         when 500...599
149           # Invalidate the offset to mark it needs to be queried on the
150           # next request
151           @offset = nil
152         end
153         return Google::APIClient::Result.new(self, response)
154       end      
155     end
156   end
157 end