270 lines
10 KiB
Ruby
270 lines
10 KiB
Ruby
# Copyright 2015 Google Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
require 'google/apis/core/multipart'
|
|
require 'google/apis/core/http_command'
|
|
require 'google/apis/core/api_command'
|
|
require 'google/apis/errors'
|
|
require 'addressable/uri'
|
|
require 'tempfile'
|
|
require 'mime-types'
|
|
|
|
module Google
|
|
module Apis
|
|
module Core
|
|
# Base upload command. Not intended to be used directly
|
|
# @private
|
|
class BaseUploadCommand < ApiCommand
|
|
UPLOAD_PROTOCOL_HEADER = 'X-Goog-Upload-Protocol'
|
|
UPLOAD_CONTENT_TYPE_HEADER = 'X-Goog-Upload-Header-Content-Type'
|
|
UPLOAD_CONTENT_LENGTH = 'X-Goog-Upload-Header-Content-Length'
|
|
CONTENT_TYPE_HEADER = 'Content-Type'
|
|
|
|
# File name or IO containing the content to upload
|
|
# @return [String, File, #read]
|
|
attr_accessor :upload_source
|
|
|
|
# Content type of the upload material
|
|
# @return [String]
|
|
attr_accessor :upload_content_type
|
|
|
|
# Content, as UploadIO
|
|
# @return [Google::Apis::Core::UploadIO]
|
|
attr_accessor :upload_io
|
|
|
|
# Ensure the content is readable and wrapped in an IO instance.
|
|
#
|
|
# @return [void]
|
|
# @raise [Google::Apis::ClientError] if upload source is invalid
|
|
def prepare!
|
|
super
|
|
if streamable?(upload_source)
|
|
self.upload_io = upload_source
|
|
@close_io_on_finish = false
|
|
elsif self.upload_source.is_a?(String)
|
|
self.upload_io = File.new(upload_source, 'r')
|
|
if self.upload_content_type.nil?
|
|
type = MIME::Types.of(upload_source)
|
|
self.upload_content_type = type.first.content_type unless type.nil? || type.empty?
|
|
end
|
|
@close_io_on_finish = true
|
|
else
|
|
fail Google::Apis::ClientError, 'Invalid upload source'
|
|
end
|
|
if self.upload_content_type.nil? || self.upload_content_type.empty?
|
|
self.upload_content_type = 'application/octet-stream'
|
|
end
|
|
end
|
|
|
|
# Close IO stream when command done. Only closes the stream if it was opened by the command.
|
|
def release!
|
|
upload_io.close if @close_io_on_finish
|
|
end
|
|
|
|
private
|
|
|
|
def streamable?(upload_source)
|
|
upload_source.is_a?(IO) || upload_source.is_a?(StringIO) || upload_source.is_a?(Tempfile)
|
|
end
|
|
end
|
|
|
|
# Implementation of the raw upload protocol
|
|
class RawUploadCommand < BaseUploadCommand
|
|
RAW_PROTOCOL = 'raw'
|
|
|
|
# Ensure the content is readable and wrapped in an {{Google::Apis::Core::UploadIO}} instance.
|
|
#
|
|
# @return [void]
|
|
# @raise [Google::Apis::ClientError] if upload source is invalid
|
|
def prepare!
|
|
super
|
|
self.body = upload_io
|
|
header[UPLOAD_PROTOCOL_HEADER] = RAW_PROTOCOL
|
|
header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type
|
|
end
|
|
end
|
|
|
|
# Implementation of the multipart upload protocol
|
|
class MultipartUploadCommand < BaseUploadCommand
|
|
MULTIPART_PROTOCOL = 'multipart'
|
|
MULTIPART_RELATED = 'multipart/related'
|
|
|
|
# Encode the multipart request
|
|
#
|
|
# @return [void]
|
|
# @raise [Google::Apis::ClientError] if upload source is invalid
|
|
def prepare!
|
|
super
|
|
multipart = Multipart.new
|
|
multipart.add_json(body)
|
|
multipart.add_upload(upload_io, content_type: upload_content_type)
|
|
self.body = multipart.assemble
|
|
header['Content-Type'] = multipart.content_type
|
|
header[UPLOAD_PROTOCOL_HEADER] = MULTIPART_PROTOCOL
|
|
end
|
|
end
|
|
|
|
# Implementation of the resumable upload protocol
|
|
class ResumableUploadCommand < BaseUploadCommand
|
|
UPLOAD_COMMAND_HEADER = 'X-Goog-Upload-Command'
|
|
UPLOAD_OFFSET_HEADER = 'X-Goog-Upload-Offset'
|
|
BYTES_RECEIVED_HEADER = 'X-Goog-Upload-Size-Received'
|
|
UPLOAD_URL_HEADER = 'X-Goog-Upload-URL'
|
|
UPLOAD_STATUS_HEADER = 'X-Goog-Upload-Status'
|
|
STATUS_ACTIVE = 'active'
|
|
STATUS_FINAL = 'final'
|
|
STATUS_CANCELLED = 'cancelled'
|
|
RESUMABLE = 'resumable'
|
|
START_COMMAND = 'start'
|
|
QUERY_COMMAND = 'query'
|
|
UPLOAD_COMMAND = 'upload, finalize'
|
|
|
|
# Reset upload to initial state.
|
|
#
|
|
# @return [void]
|
|
# @raise [Google::Apis::ClientError] if upload source is invalid
|
|
def prepare!
|
|
@state = :start
|
|
@upload_url = nil
|
|
@offset = 0
|
|
super
|
|
end
|
|
|
|
# Check the to see if the upload is complete or needs to be resumed.
|
|
#
|
|
# @param [Fixnum] status
|
|
# HTTP status code of response
|
|
# @param [HTTP::Message::Headers] header
|
|
# Response headers
|
|
# @param [String, #read] body
|
|
# Response body
|
|
# @return [Object]
|
|
# Response object
|
|
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
|
|
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
|
|
# @raise [Google::Apis::AuthorizationError] Authorization is required
|
|
def process_response(status, header, body)
|
|
@offset = Integer(header[BYTES_RECEIVED_HEADER].first) unless header[BYTES_RECEIVED_HEADER].empty?
|
|
@upload_url = header[UPLOAD_URL_HEADER].first unless header[UPLOAD_URL_HEADER].empty?
|
|
upload_status = header[UPLOAD_STATUS_HEADER].first
|
|
logger.debug { sprintf('Upload status %s', upload_status) }
|
|
if upload_status == STATUS_ACTIVE
|
|
@state = :active
|
|
elsif upload_status == STATUS_FINAL
|
|
@state = :final
|
|
elsif upload_status == STATUS_CANCELLED
|
|
@state = :cancelled
|
|
fail Google::Apis::ClientError, body
|
|
end
|
|
super(status, header, body)
|
|
end
|
|
|
|
def send_start_command(client)
|
|
logger.debug { sprintf('Sending upload start command to %s', url) }
|
|
|
|
request_header = header.dup
|
|
apply_request_options(request_header)
|
|
request_header[UPLOAD_PROTOCOL_HEADER] = RESUMABLE
|
|
request_header[UPLOAD_COMMAND_HEADER] = START_COMMAND
|
|
request_header[UPLOAD_CONTENT_LENGTH] = upload_io.size.to_s
|
|
request_header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type
|
|
|
|
client.request(method.to_s.upcase,
|
|
url.to_s, query: nil,
|
|
body: body,
|
|
header: request_header,
|
|
follow_redirect: true)
|
|
rescue => e
|
|
raise Google::Apis::ServerError, e.message
|
|
end
|
|
|
|
# Query for the status of an incomplete upload
|
|
#
|
|
# @param [HTTPClient] client
|
|
# HTTP client
|
|
# @return [HTTP::Message]
|
|
# @raise [Google::Apis::ServerError] Unable to send the request
|
|
def send_query_command(client)
|
|
logger.debug { sprintf('Sending upload query command to %s', @upload_url) }
|
|
|
|
request_header = header.dup
|
|
apply_request_options(request_header)
|
|
request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
|
|
|
|
client.post(@upload_url, body: '', header: request_header, follow_redirect: true)
|
|
end
|
|
|
|
|
|
# Send the actual content
|
|
#
|
|
# @param [HTTPClient] client
|
|
# HTTP client
|
|
# @return [HTTP::Message]
|
|
# @raise [Google::Apis::ServerError] Unable to send the request
|
|
def send_upload_command(client)
|
|
logger.debug { sprintf('Sending upload command to %s', @upload_url) }
|
|
|
|
content = upload_io
|
|
content.pos = @offset
|
|
|
|
request_header = header.dup
|
|
apply_request_options(request_header)
|
|
request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
|
|
request_header[UPLOAD_COMMAND_HEADER] = UPLOAD_COMMAND
|
|
request_header[UPLOAD_OFFSET_HEADER] = @offset.to_s
|
|
request_header[CONTENT_TYPE_HEADER] = upload_content_type
|
|
|
|
client.post(@upload_url, body: content, header: request_header, follow_redirect: true)
|
|
end
|
|
|
|
# Execute the upload request once. This will typically perform two HTTP requests -- one to initiate or query
|
|
# for the status of the upload, the second to send the (remaining) content.
|
|
#
|
|
# @private
|
|
# @param [HTTPClient] client
|
|
# HTTP client
|
|
# @yield [result, err] Result or error if block supplied
|
|
# @return [Object]
|
|
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
|
|
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
|
|
# @raise [Google::Apis::AuthorizationError] Authorization is required
|
|
def execute_once(client, &block)
|
|
case @state
|
|
when :start
|
|
response = send_start_command(client)
|
|
result = process_response(response.status_code, response.header, response.body)
|
|
when :active
|
|
response = send_query_command(client)
|
|
result = process_response(response.status_code, response.header, response.body)
|
|
when :cancelled, :final
|
|
error(@last_error, rethrow: true, &block)
|
|
end
|
|
if @state == :active
|
|
response = send_upload_command(client)
|
|
result = process_response(response.status_code, response.header, response.body)
|
|
end
|
|
|
|
success(result, &block) if @state == :final
|
|
rescue => e
|
|
# Some APIs like Youtube generate non-retriable 401 errors and mark
|
|
# the upload as finalized. Save the error just in case we get
|
|
# retried.
|
|
@last_error = e
|
|
error(e, rethrow: true, &block)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|