448 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			448 lines
		
	
	
		
			16 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 'addressable/uri'
 | |
| require 'addressable/template'
 | |
| require 'google/apis/version'
 | |
| require 'google/apis/core/api_command'
 | |
| require 'google/apis/core/batch'
 | |
| require 'google/apis/core/upload'
 | |
| require 'google/apis/core/download'
 | |
| require 'google/apis/options'
 | |
| require 'googleauth'
 | |
| require 'httpclient'
 | |
| 
 | |
| module Google
 | |
|   module Apis
 | |
|     module Core
 | |
|       # Helper class for enumerating over a result set requiring multiple fetches
 | |
|       class PagedResults
 | |
|         include Enumerable
 | |
| 
 | |
|         attr_reader :last_result
 | |
| 
 | |
|         # @param [BaseService] service
 | |
|         #   Current service instance
 | |
|         # @param [Fixnum] max
 | |
|         #   Maximum number of items to iterate over. Nil if no limit
 | |
|         # @param [Boolean] cache
 | |
|         #  True (default) if results should be cached so multiple iterations can be used.
 | |
|         # @param [Symbol] items
 | |
|         #   Name of the field in the result containing the items. Defaults to :items
 | |
|         def initialize(service, max: nil, items: :items, cache: true, response_page_token: :next_page_token, &block)
 | |
|           @service = service
 | |
|           @block = block
 | |
|           @max = max
 | |
|           @items_field = items
 | |
|           @response_page_token_field = response_page_token
 | |
|           if cache
 | |
|             @result_cache = Hash.new do |h, k|
 | |
|               h[k] = @block.call(k, @service)
 | |
|             end
 | |
|             @fetch_proc = Proc.new { |token| @result_cache[token] }
 | |
|           else
 | |
|             @fetch_proc = Proc.new { |token| @block.call(token, @service) }
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         # Iterates over result set, fetching additional pages as needed
 | |
|         def each
 | |
|           page_token = nil
 | |
|           item_count = 0
 | |
|           loop do
 | |
|             @last_result = @fetch_proc.call(page_token)
 | |
|             items = @last_result.send(@items_field)
 | |
|             if items.kind_of?(Array)
 | |
|               for item in items
 | |
|                 item_count = item_count + 1
 | |
|                 break if @max && item_count > @max
 | |
|                 yield item
 | |
|               end
 | |
|             elsif items.kind_of?(Hash)
 | |
|               items.each do |key, val|
 | |
|                 item_count = item_count + 1
 | |
|                 break if @max && item_count > @max
 | |
|                 yield key, val
 | |
|               end
 | |
|             elsif items
 | |
|               # yield singular non-nil items (for genomics API)
 | |
|               yield items
 | |
|             end
 | |
|             break if @max && item_count >= @max
 | |
|             next_page_token = @last_result.send(@response_page_token_field)
 | |
|             break if next_page_token.nil? || next_page_token == page_token
 | |
|             page_token = next_page_token
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       # Base service for all APIs. Not to be used directly.
 | |
|       #
 | |
|       class BaseService
 | |
|         include Logging
 | |
| 
 | |
|         # Root URL (host/port) for the API
 | |
|         # @return [Addressable::URI]
 | |
|         attr_accessor :root_url
 | |
| 
 | |
|         # Additional path prefix for all API methods
 | |
|         # @return [Addressable::URI]
 | |
|         attr_accessor :base_path
 | |
| 
 | |
|         # Alternate path prefix for media uploads
 | |
|         # @return [Addressable::URI]
 | |
|         attr_accessor :upload_path
 | |
| 
 | |
|         # Alternate path prefix for all batch methods
 | |
|         # @return [Addressable::URI]
 | |
|         attr_accessor :batch_path
 | |
| 
 | |
|         # HTTP client
 | |
|         # @return [HTTPClient]
 | |
|         attr_accessor :client
 | |
| 
 | |
|         # General settings
 | |
|         # @return [Google::Apis::ClientOptions]
 | |
|         attr_accessor :client_options
 | |
| 
 | |
|         # Default options for all requests
 | |
|         # @return [Google::Apis::RequestOptions]
 | |
|         attr_accessor :request_options
 | |
| 
 | |
|         # @param [String,Addressable::URI] root_url
 | |
|         #   Root URL for the API
 | |
|         # @param [String,Addressable::URI] base_path
 | |
|         #   Additional path prefix for all API methods
 | |
|         # @api private
 | |
|         def initialize(root_url, base_path)
 | |
|           self.root_url = root_url
 | |
|           self.base_path = base_path
 | |
|           self.upload_path = "upload/#{base_path}"
 | |
|           self.batch_path = 'batch'
 | |
|           self.client_options = Google::Apis::ClientOptions.default.dup
 | |
|           self.request_options = Google::Apis::RequestOptions.default.dup
 | |
|         end
 | |
| 
 | |
|         # @!attribute [rw] authorization
 | |
|         # @return [Signet::OAuth2::Client]
 | |
|         #  OAuth2 credentials
 | |
|         def authorization=(authorization)
 | |
|           request_options.authorization = authorization
 | |
|         end
 | |
| 
 | |
|         def authorization
 | |
|           request_options.authorization
 | |
|         end
 | |
| 
 | |
|         # TODO: with(options) method
 | |
| 
 | |
|         # Perform a batch request. Calls made within the block are sent in a single network
 | |
|         # request to the server.
 | |
|         #
 | |
|         # @example
 | |
|         #   service.batch do |s|
 | |
|         #     s.get_item(id1) do |res, err|
 | |
|         #       # process response for 1st call
 | |
|         #     end
 | |
|         #     # ...
 | |
|         #     s.get_item(idN) do |res, err|
 | |
|         #       # process response for Nth call
 | |
|         #     end
 | |
|         #   end
 | |
|         #
 | |
|         # @param [Hash, Google::Apis::RequestOptions] options
 | |
|         #  Request-specific options
 | |
|         # @yield [self]
 | |
|         # @return [void]
 | |
|         def batch(options = nil)
 | |
|           batch_command = BatchCommand.new(:post, Addressable::URI.parse(root_url + batch_path))
 | |
|           batch_command.options = request_options.merge(options)
 | |
|           apply_command_defaults(batch_command)
 | |
|           begin
 | |
|             start_batch(batch_command)
 | |
|             yield self
 | |
|           ensure
 | |
|             end_batch
 | |
|           end
 | |
|           batch_command.execute(client)
 | |
|         end
 | |
| 
 | |
|         # Perform a batch upload request. Calls made within the block are sent in a single network
 | |
|         # request to the server. Batch uploads are useful for uploading multiple small files. For larger
 | |
|         # files, use single requests which use a resumable upload protocol.
 | |
|         #
 | |
|         # @example
 | |
|         #   service.batch do |s|
 | |
|         #     s.insert_item(upload_source: 'file1.txt') do |res, err|
 | |
|         #       # process response for 1st call
 | |
|         #     end
 | |
|         #     # ...
 | |
|         #     s.insert_item(upload_source: 'fileN.txt') do |res, err|
 | |
|         #       # process response for Nth call
 | |
|         #     end
 | |
|         #   end
 | |
|         #
 | |
|         # @param [Hash, Google::Apis::RequestOptions] options
 | |
|         #  Request-specific options
 | |
|         # @yield [self]
 | |
|         # @return [void]
 | |
|         def batch_upload(options = nil)
 | |
|           batch_command = BatchUploadCommand.new(:put, Addressable::URI.parse(root_url + upload_path))
 | |
|           batch_command.options = request_options.merge(options)
 | |
|           apply_command_defaults(batch_command)
 | |
|           begin
 | |
|             start_batch(batch_command)
 | |
|             yield self
 | |
|           ensure
 | |
|             end_batch
 | |
|           end
 | |
|           batch_command.execute(client)
 | |
|         end
 | |
| 
 | |
|         # Get the current HTTP client
 | |
|         # @return [HTTPClient]
 | |
|         def client
 | |
|           @client ||= new_client
 | |
|         end
 | |
| 
 | |
| 
 | |
|         # Simple escape hatch for making API requests directly to a given
 | |
|         # URL. This is not intended to be used as a generic HTTP client
 | |
|         # and should be used only in cases where no service method exists
 | |
|         # (e.g. fetching an export link for a Google Drive file.)
 | |
|         #
 | |
|         # @param [Symbol] method
 | |
|         #   HTTP method as symbol (e.g. :get, :post, :put, ...)
 | |
|         # @param [String] url
 | |
|         #   URL to call
 | |
|         # @param [Hash<String,String>] params
 | |
|         #   Optional hash of query parameters
 | |
|         # @param [#read] body
 | |
|         #   Optional body for POST/PUT
 | |
|         # @param [IO, String] download_dest
 | |
|         #   IO stream or filename to receive content download
 | |
|         # @param [Google::Apis::RequestOptions] options
 | |
|         #   Request-specific options
 | |
|         #
 | |
|         # @yield [result, err] Result & error if block supplied
 | |
|         # @yieldparam result [String] HTTP response body
 | |
|         # @yieldparam err [StandardError] error object if request failed
 | |
|         #
 | |
|         # @return [String] HTTP response body
 | |
|         def http(method, url, params: nil, body: nil, download_dest: nil, options: nil, &block)
 | |
|           if download_dest
 | |
|             command = DownloadCommand.new(method, url, body: body)
 | |
|           else
 | |
|             command = HttpCommand.new(method, url, body: body)
 | |
|           end
 | |
|           command.options = request_options.merge(options)
 | |
|           apply_command_defaults(command)
 | |
|           command.query.merge(Hash(params))
 | |
|           execute_or_queue_command(command, &block)
 | |
|         end
 | |
| 
 | |
|         # Executes a given query with paging, automatically retrieving
 | |
|         # additional pages as necessary. Requires a block that returns the
 | |
|         # result set of a page. The current page token is supplied as an argument
 | |
|         # to the block.
 | |
|         #
 | |
|         # Note: The returned enumerable also contains a `last_result` field
 | |
|         # containing the full result of the last query executed.
 | |
|         #
 | |
|         # @param [Fixnum] max
 | |
|         #   Maximum number of items to iterate over. Defaults to nil -- no upper bound.
 | |
|         # @param [Symbol] items
 | |
|         #   Name of the field in the result containing the items. Defaults to :items
 | |
|         # @param [Boolean] cache
 | |
|         #  True (default) if results should be cached so multiple iterations can be used.
 | |
|         # @return [Enumerble]
 | |
|         # @yield [token, service]
 | |
|         #   Current page token & service instance
 | |
|         # @yieldparam [String] token
 | |
|         #   Current page token to be used in the query
 | |
|         # @yieldparam [service]
 | |
|         #   Current service instance
 | |
|         # @since 0.9.4
 | |
|         #
 | |
|         # @example Retrieve all files,
 | |
|         #   file_list = service.fetch_all { |token, s| s.list_files(page_token: token) }
 | |
|         #   file_list.each { |f| ... }
 | |
|         def fetch_all(max: nil, items: :items, cache: true, response_page_token: :next_page_token, &block)
 | |
|           fail "fetch_all may not be used inside a batch" if batch?
 | |
|           return PagedResults.new(self, max: max, items: items, cache: cache, response_page_token: response_page_token, &block)
 | |
|         end
 | |
| 
 | |
|         protected
 | |
| 
 | |
|         # Create a new upload command.
 | |
|         #
 | |
|         # @param [symbol] method
 | |
|         #   HTTP method for uploading (typically :put or :post)
 | |
|         # @param [String] path
 | |
|         #  Additional path to upload endpoint, appended to API base path
 | |
|         # @param [Hash, Google::Apis::RequestOptions] options
 | |
|         #  Request-specific options
 | |
|         # @return [Google::Apis::Core::UploadCommand]
 | |
|         def make_upload_command(method, path, options)
 | |
|           template = Addressable::Template.new(root_url + upload_path + path)
 | |
|           if batch?
 | |
|             command = MultipartUploadCommand.new(method, template)
 | |
|           else
 | |
|             command = ResumableUploadCommand.new(method, template)
 | |
|           end
 | |
|           command.options = request_options.merge(options)
 | |
|           apply_command_defaults(command)
 | |
|           command
 | |
|         end
 | |
| 
 | |
|         # Create a new download command.
 | |
|         #
 | |
|         # @param [symbol] method
 | |
|         #   HTTP method for uploading (typically :get)
 | |
|         # @param [String] path
 | |
|         #  Additional path to download endpoint, appended to API base path
 | |
|         # @param [Hash, Google::Apis::RequestOptions] options
 | |
|         #  Request-specific options
 | |
|         # @return [Google::Apis::Core::DownloadCommand]
 | |
|         def make_download_command(method, path, options)
 | |
|           template = Addressable::Template.new(root_url + base_path + path)
 | |
|           command = DownloadCommand.new(method, template)
 | |
|           command.options = request_options.merge(options)
 | |
|           command.query['alt'] = 'media'
 | |
|           apply_command_defaults(command)
 | |
|           command
 | |
|         end
 | |
| 
 | |
|         # Create a new command.
 | |
|         #
 | |
|         # @param [symbol] method
 | |
|         #   HTTP method (:get, :post, :delete, etc...)
 | |
|         # @param [String] path
 | |
|         #  Additional path, appended to API base path
 | |
|         # @param [Hash, Google::Apis::RequestOptions] options
 | |
|         #  Request-specific options
 | |
|         # @return [Google::Apis::Core::DownloadCommand]
 | |
|         def make_simple_command(method, path, options)
 | |
|           full_path =
 | |
|             if path.start_with? "/"
 | |
|               path[1..-1]
 | |
|             else
 | |
|               base_path + path
 | |
|             end
 | |
|           template = Addressable::Template.new(root_url + full_path)
 | |
|           command = ApiCommand.new(method, template)
 | |
|           command.options = request_options.merge(options)
 | |
|           apply_command_defaults(command)
 | |
|           command
 | |
|         end
 | |
| 
 | |
|         # Execute the request. If a batch is in progress, the request is added to the batch instead.
 | |
|         #
 | |
|         # @param [Google::Apis::Core::HttpCommand] command
 | |
|         #   Command to execute
 | |
|         # @return [Object] response object if command executed and no callback supplied
 | |
|         # @yield [result, err] Result & error if block supplied
 | |
|         # @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_or_queue_command(command, &callback)
 | |
|           batch_command = current_batch
 | |
|           if batch_command
 | |
|             fail "Can not combine services in a batch" if Thread.current[:google_api_batch_service] != self
 | |
|             batch_command.add(command, &callback)
 | |
|             nil
 | |
|           else
 | |
|             command.execute(client, &callback)
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         # Update commands with service-specific options. To be implemented by subclasses
 | |
|         # @param [Google::Apis::Core::HttpCommand] _command
 | |
|         def apply_command_defaults(_command)
 | |
|         end
 | |
| 
 | |
|         private
 | |
| 
 | |
|         # Get the current batch context
 | |
|         #
 | |
|         # @return [Google:Apis::Core::BatchRequest]
 | |
|         def current_batch
 | |
|           Thread.current[:google_api_batch]
 | |
|         end
 | |
| 
 | |
|         # Check if a batch is in progress
 | |
|         # @return [Boolean]
 | |
|         def batch?
 | |
|           !current_batch.nil?
 | |
|         end
 | |
| 
 | |
|         # Start a new thread-local batch context
 | |
|         # @param [Google::Apis::Core::BatchCommand] cmd
 | |
|         def start_batch(cmd)
 | |
|           fail "Batch already in progress" if batch?
 | |
|           Thread.current[:google_api_batch] = cmd
 | |
|           Thread.current[:google_api_batch_service] = self
 | |
|         end
 | |
| 
 | |
|         # Clear thread-local batch context
 | |
|         def end_batch
 | |
|           Thread.current[:google_api_batch] = nil
 | |
|           Thread.current[:google_api_batch_service] = nil
 | |
|         end
 | |
| 
 | |
|         # Create a new HTTP client
 | |
|         # @return [HTTPClient]
 | |
|         def new_client
 | |
|           client = ::HTTPClient.new
 | |
| 
 | |
|           if client_options.transparent_gzip_decompression
 | |
|             client.transparent_gzip_decompression = client_options.transparent_gzip_decompression
 | |
|           end
 | |
|           
 | |
|           client.proxy = client_options.proxy_url if client_options.proxy_url
 | |
| 
 | |
|           if client_options.open_timeout_sec
 | |
|             client.connect_timeout = client_options.open_timeout_sec
 | |
|           end
 | |
| 
 | |
|           if client_options.read_timeout_sec
 | |
|             client.receive_timeout = client_options.read_timeout_sec
 | |
|           end
 | |
| 
 | |
|           if client_options.send_timeout_sec
 | |
|             client.send_timeout = client_options.send_timeout_sec
 | |
|           end
 | |
| 
 | |
|           client.follow_redirect_count = 5
 | |
|           client.default_header = { 'User-Agent' => user_agent }
 | |
| 
 | |
|           client.debug_dev = logger if client_options.log_http_requests
 | |
|           client
 | |
|         end
 | |
| 
 | |
| 
 | |
|         # Build the user agent header
 | |
|         # @return [String]
 | |
|         def user_agent
 | |
|           sprintf('%s/%s google-api-ruby-client/%s %s (gzip)',
 | |
|                   client_options.application_name,
 | |
|                   client_options.application_version,
 | |
|                   Google::Apis::VERSION,
 | |
|                   Google::Apis::OS_VERSION)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| end
 |