lib/google_apps/transport.rb in google_apps-0.4.9 vs lib/google_apps/transport.rb in google_apps-0.4.9.1

- old
+ new

@@ -1,15 +1,15 @@ -require 'net/http' require 'cgi' require 'openssl' require 'rexml/document' module GoogleApps class Transport attr_reader :request, :response, :domain, :feeds - attr_accessor :auth, :user, :group, :nickname, :export + attr_accessor :auth, :user, :group, :nickname, :export, :group, :requester, :migration + SUCCESS_CODES = [200, 201, 202] BOUNDARY = "=AaB03xDFHT8xgg" PAGE_SIZE = { user: 100, group: 200 } @@ -21,10 +21,12 @@ @migration = targets[:migration] || "https://apps-apis.google.com/a/feeds/migration/2.0/#{domain}" @group = targets[:group] || "https://apps-apis.google.com/a/feeds/group/2.0/#{domain}" @nickname = targets[:nickname] || "https://apps-apis.google.com/a/feeds/#{domain}/nickname/2.0" @export = targets[:export] || "https://apps-apis.google.com/a/feeds/compliance/audit/mail/export/#{domain}" @domain = domain + @requester = AppsRequest || targets[:requester] + @doc_handler = DocumentHandler.new format: (targets[:format] || :atom) @token = nil @response = nil @request = nil @feeds = [] end @@ -37,90 +39,111 @@ # authenticate 'username@domain', 'password' # # authenticate returns the HTTP response received # from Google def authenticate(account, pass) - uri = URI(@auth) - @request = Net::HTTP::Post.new(uri.path) - @request.body = auth_body(account, pass) - set_headers :auth + add(@auth, nil, auth_body(account, pass), :auth) - @response = request uri - set_auth_token @response end + # request_export performs the GoogleApps API call to # generate a mailbox export. It takes the username # and an GoogleApps::Atom::Export instance as # arguments # # request_export 'username', document # - # request_export returns the HTTP response received - # from Google. + # request_export returns the request ID on success or + # the HTTP response object on failure. def request_export(username, document) - add(@export + "/#{username}", document) + result = add(@export + "/#{username}", :export_response, document) + + get_values(result, 'apps:property', ['name', 'requestId'], 'value')[0].to_i + #success_response? ? get_values('apps:property', ['name', 'requestId'], 'value')[0].to_i : @response end + # export_status checks the status of a mailbox export # request. It takes the username and the request_id # as arguments # # export_status 'username', 847576 # - # export_status will return the status of the HTTP - # response from Google + # export_status will return the body of the HTTP response + # from Google def export_status(username, req_id) - get(@export + "/#{username}", req_id) + get(@export + "/#{username}", :export_status, req_id) end - def fetch_export(username, req_id, filename) # :nodoc: - # TODO: Shouldn't rely on export_status being run first. Self, this is lazy and stupid. + + # export_ready? checks the export_status response for the + # presence of an apps:property element with a fileUrl name + # attribute. + # + # export_ready? 'lholcomb2', 82834 + # + # export_ready? returns true if there is a fileUrl present + # in the response and false if there is no fileUrl present + # in the response. + def export_ready?(username, req_id) export_status(username, req_id) - doc = REXML::Document.new(@response.body) - urls = [] - doc.elements.each('entry/apps:property') do |property| - urls << property.attributes['value'] if property.attributes['name'].match 'fileUrl' - end - urls.each do |url| - download(url, filename + "#{urls.index(url)}") + !(export_file_urls.empty?) + end + + + # fetch_export downloads the mailbox export from Google. + # It takes a username, request id and a filename as + # arguments. If the export consists of more than one file + # the file name will have numbers appended to indicate the + # piece of the export. + # + # fetch_export 'lholcomb2', 838382, 'lholcomb2' + # + # fetch_export reutrns nil in the event that the export is + # not yet ready. + def fetch_export(username, req_id, filename) + if export_ready?(username, req_id) + download_export(filename).each_with_index { |url, index| url.gsub!(/.*/, "#{filename}#{index}")} + else + nil end end + # download makes a get request of the provided url # and writes the body to the provided filename. # # download 'url', 'save_file' def download(url, filename) - uri = URI(url) - @request = Net::HTTP::Get.new uri.path - set_headers :user + @request = @requester.new :get, URI(url), headers(:other) File.open(filename, "w") do |file| - file.puts request(uri).body + file.puts @request.send_request.body end end + # get is a generic target for method_missing. It is # intended to handle the general case of retrieving a # record from the Google Apps Domain. It takes an API # endpoint and an id as arguments. # # get 'endpoint', 'username' # # get returns the HTTP response received from Google. - def get(endpoint, id = nil) - # TODO: Need to handle <link rel='next' for pagination if wanting all users + def get(endpoint, type, id = nil) id ? uri = URI(endpoint + build_id(id)) : uri = URI(endpoint) - @request = Net::HTTP::Get.new(uri.request_uri) - set_headers :user + @request = @requester.new :get, uri, headers(:other) - @response = request uri + @response = @request.send_request + + process_response(type) end # get_users retrieves as many users as specified from the # domain. If no starting point is given it will grab all the @@ -151,25 +174,18 @@ # # get_all 'users', start: 'lholcomb2', limit: 300 # # get_all returns the HTTP response received from Google. def get_all(type, options = {}) - @feeds, current_page = [], 0 - type = type.to_s - type.gsub!(/\w*s$/) { |match| match[0..-2] } + @feeds, page = [], 0 + type = normalize_type type options[:limit] ? limit = options[:limit] : limit = 1000000 - options[:start] ? get(instance_variable_get("@#{type}") + "?#{start_query(type)}=#{options[:start]}") : get(instance_variable_get("@#{type}")) + options[:start] ? get(instance_variable_get("@#{type}") + "?#{start_query(type)}=#{options[:start]}", :feed) : get(instance_variable_get("@#{type}"), :feed) - add_feed - current_page += 1 + fetch_feed(page, limit) - while (@feeds.last.next_page) and (current_page * PAGE_SIZE[:user] < limit) - get_next_page - current_page += 1 - end - @response end # add_member_to adds a member to a group in the domain. @@ -178,11 +194,11 @@ # # add_member_to 'test', document # # add_member_to returns the response received from Google. def add_member_to(group_id, document) - add(@group + "/#{group_id}/member", document) + add(@group + "/#{group_id}/member", nil, document) end # delete_member_from removes a member from a group in the # domain. It takes a group_id and member_id as arguments. @@ -212,17 +228,19 @@ # and a GoogleApps::Atom document as arguments. # # add 'endpoint', document # # add returns the HTTP response received from Google. - def add(endpoint, document) + def add(endpoint, type, document, header_type = nil) + header_type = :others unless header_type uri = URI(endpoint) - @request = Net::HTTP::Post.new(uri.path) - @request.body = document.to_s - set_headers :user + @request = @requester.new :post, uri, headers(header_type) + @request.add_body document.to_s - @response = request uri + @response = @request.send_request + + process_response type end # update is a generic target for method_missing. It is # intended to handle the general case of updating an # item that already exists in your GoogleApps Domain. @@ -230,18 +248,18 @@ # as arguments. # # update 'endpoint', document # # update returns the HTTP response received from Google - def update(endpoint, target, document) - # TODO: Username needs to come from somewhere for uri + def update(endpoint, type, target, document) uri = URI(endpoint + "/#{target}") - @request = Net::HTTP::Put.new(uri.path) - @request.body = document.to_s - set_headers :user + @request = @requester.new :put, uri, headers(:other) + @request.add_body document.to_s - @response = request uri + @response = @request.send_request + + process_response type end # delete is a generic target for method_missing. It is # intended to handle the general case of deleting an # item from your GoogleApps Domain. delete takes an @@ -250,14 +268,13 @@ # delete 'endpoint', 'id' # # delete returns the HTTP response received from Google. def delete(endpoint, id) uri = URI(endpoint + "/#{id}") - @request = Net::HTTP::Delete.new(uri.path) - set_headers :user + @request = @requester.new :delete, uri, headers(:other) - @response = request uri + @response = @request.send_request end # migration performs mail migration from a local # mail environment to GoogleApps. migrate takes a # username a GoogleApps::Atom::Properties dcoument @@ -265,32 +282,30 @@ # # migrate 'user', properties, message # # migrate returns the HTTP response received from Google. def migrate(username, properties, message) - uri = URI(@migration + "/#{username}/mail") - @request = Net::HTTP::Post.new(uri.path) - @request.body = multi_part(properties.to_s, message) - set_headers :migrate + @request = @requester.new(:post, URI(@migration + "/#{username}/mail"), headers(:migration)) + @request.add_body multi_part(properties.to_s, message) - @response = request uri + @request.send_request end - # TODO: This should perform the instance_variable_get and pass the value to the appropriate method. + def method_missing(name, *args) super unless name.match /([a-z]*)_([a-z]*)/ case $1 when "new", "add" - self.send(:add, instance_variable_get("@#{$2}"), *args) + self.send(:add, instance_variable_get("@#{$2}"), $2, *args) when "delete" self.send(:delete, instance_variable_get("@#{$2}"), *args) when "update" - self.send(:update, instance_variable_get("@#{$2}"), *args) + self.send(:update, instance_variable_get("@#{$2}"), $2, *args) when "get" - self.send(:get, instance_variable_get("@#{$2}"), *args) + self.send(:get, instance_variable_get("@#{$2}"), $2, *args) else super end end @@ -316,24 +331,75 @@ def build_id(id) id =~ /^\?/ ? id : "/#{id}" end + # export_file_urls searches @response for any apps:property elements with a + # fileUrl name attribute and returns an array of the values. + def export_file_urls + Atom::XML::Document.string(@response.body).find('//apps:property').inject([]) do |urls, prop| + urls << prop.attributes['value'] if prop.attributes['name'].match 'fileUrl' + urls + end + end + + + def download_export(filename) + export_file_urls.each_with_index do |url, index| + download(url, filename + "#{index}") + end + end + + + # process_response takes the HTTPResponse and either returns a + # document of the specified type or in the event of an error it + # returns the HTTPResponse. + def process_response(doc_type = nil) + case doc_type + when nil + success_response? ? true : raise("Error: #{response.code}, #{response.message}") + else + success_response? ? @doc_handler.create_doc(@response.body, doc_type) : raise("Error: #{response.code}, #{response.message}") + end + end + + + # error_response? checks to see if Google Responded with a success + # code. + def success_response? + SUCCESS_CODES.include?(@response.code.to_i) + end + + # Grab the auth token from the response body def set_auth_token @response.body.split("\n").grep(/auth=(.*)/i) @token = $1 end + # get_next_page retrieves the next page in the response. def get_next_page get @feeds.last.next_page add_feed end + # fetch_feed retrieves the remaining pages in the request. + # It takes a page and a limit as arguments. + def fetch_feed(page, limit) + add_feed + page += 1 + + while (@feeds.last.next_page) and (page * PAGE_SIZE[:user] < limit) + get_next_page + page += 1 + end + end + + # start_query builds the value for the starting point # query string used for retrieving batches of objects # from Google. def start_query(type) case type @@ -343,34 +409,41 @@ "startGroup" end end + def normalize_type(type) + type.to_s.gsub!(/\w*s$/) { |match| match[0..-2] } + end + + # add_feed adds a feed to the @feeds array. def add_feed @feeds << GoogleApps::Atom.feed(@response.body) end - - def request(uri) - # TODO: Clashes with @request reader - Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http| - http.request(@request) + # get_values returns an array of all the value attributes + # on elements matching the given key_attrib pair on the + # specified element type. + def get_values(document, element, key_attrib, value = 'value') + document.find('//' + element).inject([]) do |values, element| + values << element.attributes[value] if element.attributes[key_attrib[0]].match key_attrib[1] + values end end - def set_headers(request_type) - case request_type + + def headers(category) + case category when :auth - @request['content-type'] = "application/x-www-form-urlencoded" - when :migrate - @request['content-type'] = "multipart/related; boundary=\"#{BOUNDARY}\"" - @request['authorization'] = "GoogleLogin auth=#{@token}" + [['content-type', 'application/x-www-form-urlencoded']] + when :migration + [['content-type', "multipart/related; boundary=\"#{BOUNDARY}\""], ['authorization', "GoogleLogin auth=#{@token}"]] else - @request['content-type'] = "application/atom+xml" - @request['authorization'] = "GoogleLogin auth=#{@token}" + [['content-type', 'application/atom+xml'], ['authorization', "GoogleLogin auth=#{@token}"]] end end + def multi_part(properties, message) post_body = [] post_body << "--#{BOUNDARY}\n" post_body << "Content-Type: application/atom+xml\n\n" \ No newline at end of file