koala 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. data/.gitignore +3 -1
  2. data/.rspec +1 -0
  3. data/.travis.yml +4 -0
  4. data/.yardopts +3 -0
  5. data/CHANGELOG +28 -0
  6. data/Gemfile +14 -0
  7. data/Guardfile +6 -0
  8. data/koala.gemspec +3 -3
  9. data/lib/koala/api/batch_operation.rb +83 -0
  10. data/lib/koala/api/graph_api.rb +476 -0
  11. data/lib/koala/{graph_batch_api.rb → api/graph_batch_api.rb} +22 -17
  12. data/lib/koala/api/graph_collection.rb +107 -0
  13. data/lib/koala/api/legacy.rb +26 -0
  14. data/lib/koala/{rest_api.rb → api/rest_api.rb} +34 -13
  15. data/lib/koala/api.rb +93 -0
  16. data/lib/koala/http_service/multipart_request.rb +41 -0
  17. data/lib/koala/http_service/response.rb +18 -0
  18. data/lib/koala/http_service/uploadable_io.rb +187 -0
  19. data/lib/koala/http_service.rb +69 -20
  20. data/lib/koala/oauth.rb +170 -36
  21. data/lib/koala/realtime_updates.rb +89 -51
  22. data/lib/koala/test_users.rb +122 -32
  23. data/lib/koala/utils.rb +11 -4
  24. data/lib/koala/version.rb +1 -1
  25. data/lib/koala.rb +16 -96
  26. data/readme.md +9 -9
  27. data/spec/cases/api_spec.rb +19 -12
  28. data/spec/cases/error_spec.rb +10 -0
  29. data/spec/cases/graph_api_batch_spec.rb +100 -58
  30. data/spec/cases/graph_collection_spec.rb +23 -7
  31. data/spec/cases/http_service_spec.rb +5 -26
  32. data/spec/cases/koala_spec.rb +22 -4
  33. data/spec/cases/legacy_spec.rb +115 -0
  34. data/spec/cases/multipart_request_spec.rb +7 -7
  35. data/spec/cases/oauth_spec.rb +134 -48
  36. data/spec/cases/realtime_updates_spec.rb +154 -47
  37. data/spec/cases/test_users_spec.rb +276 -219
  38. data/spec/cases/uploadable_io_spec.rb +1 -1
  39. data/spec/cases/utils_spec.rb +29 -5
  40. data/spec/fixtures/mock_facebook_responses.yml +41 -30
  41. data/spec/spec_helper.rb +3 -0
  42. data/spec/support/custom_matchers.rb +28 -0
  43. data/spec/support/graph_api_shared_examples.rb +192 -14
  44. data/spec/support/koala_test.rb +10 -1
  45. data/spec/support/mock_http_service.rb +2 -2
  46. data/spec/support/rest_api_shared_examples.rb +5 -165
  47. metadata +75 -99
  48. data/lib/koala/batch_operation.rb +0 -74
  49. data/lib/koala/graph_api.rb +0 -270
  50. data/lib/koala/graph_collection.rb +0 -59
  51. data/lib/koala/multipart_request.rb +0 -35
  52. data/lib/koala/uploadable_io.rb +0 -181
  53. data/spec/cases/graph_and_rest_api_spec.rb +0 -22
  54. data/spec/cases/graph_api_spec.rb +0 -22
  55. data/spec/cases/rest_api_spec.rb +0 -22
@@ -1,22 +1,17 @@
1
+ require 'koala/api'
2
+ require 'koala/api/batch_operation'
3
+
1
4
  module Koala
2
5
  module Facebook
3
- module GraphBatchAPIMethods
4
-
5
- def self.included(base)
6
- base.class_eval do
7
- attr_reader :original_api
8
-
9
- def initialize(access_token, api)
10
- super(access_token)
11
- @original_api = api
12
- end
13
-
14
- alias_method :graph_call_outside_batch, :graph_call
15
- alias_method :graph_call, :graph_call_in_batch
6
+ # @private
7
+ class GraphBatchAPI < API
8
+ # inside a batch call we can do anything a regular Graph API can do
9
+ include GraphAPIMethods
16
10
 
17
- alias_method :check_graph_api_response, :check_response
18
- alias_method :check_response, :check_graph_batch_api_response
19
- end
11
+ attr_reader :original_api
12
+ def initialize(access_token, api)
13
+ super(access_token)
14
+ @original_api = api
20
15
  end
21
16
 
22
17
  def batch_calls
@@ -38,12 +33,22 @@ module Koala
38
33
 
39
34
  def check_graph_batch_api_response(response)
40
35
  if response.is_a?(Hash) && response["error"] && !response["error"].is_a?(Hash)
41
- APIError.new("type" => "Error #{response["error"]}", "message" => response["error_description"])
36
+ # old error format -- see http://developers.facebook.com/blog/post/596/
37
+ APIError.new({"type" => "Error #{response["error"]}", "message" => response["error_description"]}.merge(response))
42
38
  else
43
39
  check_graph_api_response(response)
44
40
  end
45
41
  end
46
42
 
43
+ # redefine the graph_call and check_response methods
44
+ # so we can use this API inside the batch block just like any regular Graph API
45
+ alias_method :graph_call_outside_batch, :graph_call
46
+ alias_method :graph_call, :graph_call_in_batch
47
+
48
+ alias_method :check_graph_api_response, :check_response
49
+ alias_method :check_response, :check_graph_batch_api_response
50
+
51
+ # execute the queued batch calls
47
52
  def execute(http_options = {})
48
53
  return [] unless batch_calls.length > 0
49
54
  # Turn the call args collected into what facebook expects
@@ -0,0 +1,107 @@
1
+ module Koala
2
+ module Facebook
3
+ class API
4
+ # A light wrapper for collections returned from the Graph API.
5
+ # It extends Array to allow you to page backward and forward through
6
+ # result sets, and providing easy access to paging information.
7
+ class GraphCollection < Array
8
+
9
+ # The raw paging information from Facebook (next/previous URLs).
10
+ attr_reader :paging
11
+ # @return [Koala::Facebook::GraphAPI] the api used to make requests.
12
+ attr_reader :api
13
+ # The entire raw response from Facebook.
14
+ attr_reader :raw_response
15
+
16
+ # Initialize the array of results and store various additional paging-related information.
17
+ #
18
+ # @param response the response from Facebook (a hash whose "data" key is an array)
19
+ # @param api the Graph {Koala::Facebook::API API} instance to use to make calls
20
+ # (usually the API that made the original call).
21
+ #
22
+ # @return [Koala::Facebook::GraphCollection] an initialized GraphCollection
23
+ # whose paging, raw_response, and api attributes are populated.
24
+ def initialize(response, api)
25
+ super response["data"]
26
+ @paging = response["paging"]
27
+ @raw_response = response
28
+ @api = api
29
+ end
30
+
31
+ # @private
32
+ # Turn the response into a GraphCollection if they're pageable;
33
+ # if not, return the original response.
34
+ # The Ads API (uniquely so far) returns a hash rather than an array when queried
35
+ # with get_connections.
36
+ def self.evaluate(response, api)
37
+ response.is_a?(Hash) && response["data"].is_a?(Array) ? self.new(response, api) : response
38
+ end
39
+
40
+ # Retrieve the next page of results.
41
+ #
42
+ # @return a GraphCollection array of additional results (an empty array if there are no more results)
43
+ def next_page
44
+ base, args = next_page_params
45
+ base ? @api.get_page([base, args]) : nil
46
+ end
47
+
48
+ # Retrieve the previous page of results.
49
+ #
50
+ # @return a GraphCollection array of additional results (an empty array if there are no earlier results)
51
+ def previous_page
52
+ base, args = previous_page_params
53
+ base ? @api.get_page([base, args]) : nil
54
+ end
55
+
56
+ # Arguments that can be sent to {Koala::Facebook::API#graph_call} to retrieve the next page of results.
57
+ #
58
+ # @example
59
+ # @api.graph_call(*collection.next_page_params)
60
+ #
61
+ # @return an array of arguments, or nil if there are no more pages
62
+ def next_page_params
63
+ @paging && @paging["next"] ? parse_page_url(@paging["next"]) : nil
64
+ end
65
+
66
+ # Arguments that can be sent to {Koala::Facebook::API#graph_call} to retrieve the previous page of results.
67
+ #
68
+ # @example
69
+ # @api.graph_call(*collection.previous_page_params)
70
+ #
71
+ # @return an array of arguments, or nil if there are no previous pages
72
+ def previous_page_params
73
+ @paging && @paging["previous"] ? parse_page_url(@paging["previous"]) : nil
74
+ end
75
+
76
+ # @private
77
+ def parse_page_url(url)
78
+ GraphCollection.parse_page_url(url)
79
+ end
80
+
81
+ # Parse the previous and next page URLs Facebook provides in pageable results.
82
+ # You'll mainly need to use this when using a non-Rails framework (one without url_for);
83
+ # to store paging information between page loads, pass the URL (from GraphCollection#paging)
84
+ # and use parse_page_url to turn it into parameters useful for {Koala::Facebook::API#get_page}.
85
+ #
86
+ # @param url the paging URL to turn into graph_call parameters
87
+ #
88
+ # @return an array of parameters that can be provided via graph_call(*parsed_params)
89
+ def self.parse_page_url(url)
90
+ match = url.match(/.com\/(.*)\?(.*)/)
91
+ base = match[1]
92
+ args = match[2]
93
+ params = CGI.parse(args)
94
+ new_params = {}
95
+ params.each_pair do |key,value|
96
+ new_params[key] = value.join ","
97
+ end
98
+ [base,new_params]
99
+ end
100
+ end
101
+ end
102
+
103
+ # @private
104
+ # legacy support for when GraphCollection lived directly under Koala::Facebook
105
+ GraphCollection = API::GraphCollection
106
+ end
107
+ end
@@ -0,0 +1,26 @@
1
+ require 'koala/api'
2
+ module Koala
3
+ module Facebook
4
+ # Legacy support for old pre-1.2 APIs
5
+
6
+ # A wrapper for the old APIs deprecated in 1.2.0, which triggers a deprecation warning when used.
7
+ # Otherwise, this class functions identically to API.
8
+ # @see API
9
+ # @private
10
+ class OldAPI < API
11
+ def initialize(*args)
12
+ Koala::Utils.deprecate("#{self.class.name} is deprecated and will be removed in a future version; please use the API class instead.")
13
+ super
14
+ end
15
+ end
16
+
17
+ # @private
18
+ class GraphAPI < OldAPI; end
19
+
20
+ # @private
21
+ class RestAPI < OldAPI; end
22
+
23
+ # @private
24
+ class GraphAndRestAPI < OldAPI; end
25
+ end
26
+ end
@@ -2,27 +2,47 @@ module Koala
2
2
  module Facebook
3
3
  REST_SERVER = "api.facebook.com"
4
4
 
5
+ # Methods used to interact with Facebook's legacy REST API.
6
+ # Where possible, you should use the newer, faster Graph API to interact with Facebook;
7
+ # in the future, the REST API will be deprecated.
8
+ # For now, though, there are a few methods that can't be done through the Graph API.
9
+ #
10
+ # When using the REST API, Koala will use Facebook's faster read-only servers
11
+ # whenever the call allows.
12
+ #
13
+ # See https://github.com/arsduo/koala/wiki/REST-API for a general introduction to Koala
14
+ # and the Rest API.
5
15
  module RestAPIMethods
6
- def fql_query(fql, args = {}, options = {})
7
- rest_call('fql.query', args.merge(:query => fql), options)
8
- end
9
-
10
- def fql_multiquery(queries = {}, args = {}, options = {})
11
- if results = rest_call('fql.multiquery', args.merge(:queries => MultiJson.encode(queries)), options)
12
- # simplify the multiquery result format
13
- results.inject({}) {|outcome, data| outcome[data["name"]] = data["fql_result_set"]; outcome}
14
- end
15
- end
16
-
16
+ # Set a Facebook application's properties.
17
+ #
18
+ # @param properties a hash of properties you want to update with their new values.
19
+ # @param (see #rest_call)
20
+ # @param options (see #rest_call)
21
+ #
22
+ # @return true if successful, false if not. (This call currently doesn't give useful feedback on failure.)
17
23
  def set_app_properties(properties, args = {}, options = {})
18
24
  raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "setAppProperties requires an access token"}) unless @access_token
19
25
  rest_call("admin.setAppProperties", args.merge(:properties => MultiJson.encode(properties)), options, "post")
20
26
  end
21
27
 
22
- def rest_call(fb_method, args = {}, options = {}, method = "get")
28
+ # Make a call to the REST API.
29
+ #
30
+ # @note The order of the last two arguments is non-standard (for historical reasons). Sorry.
31
+ #
32
+ # @param fb_method the API call you want to make
33
+ # @param args (see Koala::Facebook::GraphAPIMethods#graph_call)
34
+ # @param options (see Koala::Facebook::GraphAPIMethods#graph_call)
35
+ # @param verb (see Koala::Facebook::GraphAPIMethods#graph_call)
36
+ #
37
+ # @raise [Koala::Facebook::APIError] if Facebook returns an error
38
+ #
39
+ # @return the result from Facebook
40
+ def rest_call(fb_method, args = {}, options = {}, verb = "get")
41
+ Koala::Utils.deprecate("The REST API is now deprecated; please use the equivalent Graph API methods instead. See http://developers.facebook.com/blog/post/616/.")
42
+
23
43
  options = options.merge!(:rest_api => true, :read_only => READ_ONLY_METHODS.include?(fb_method.to_s))
24
44
 
25
- api("method/#{fb_method}", args.merge('format' => 'json'), method, options) do |response|
45
+ api("method/#{fb_method}", args.merge('format' => 'json'), verb, options) do |response|
26
46
  # check for REST API-specific errors
27
47
  if response.is_a?(Hash) && response["error_code"]
28
48
  raise APIError.new("type" => response["error_code"], "message" => response["error_msg"])
@@ -30,6 +50,7 @@ module Koala
30
50
  end
31
51
  end
32
52
 
53
+ # @private
33
54
  # read-only methods for which we can use API-read
34
55
  # taken directly from the FB PHP library (https://github.com/facebook/php-sdk/blob/master/src/facebook.php)
35
56
  READ_ONLY_METHODS = [
data/lib/koala/api.rb ADDED
@@ -0,0 +1,93 @@
1
+ # graph_batch_api and legacy are required at the bottom, since they depend on API being defined
2
+ require 'koala/api/graph_api'
3
+ require 'koala/api/rest_api'
4
+
5
+ module Koala
6
+ module Facebook
7
+ class API
8
+ # Creates a new API client.
9
+ # @param [String] access_token access token
10
+ # @note If no access token is provided, you can only access some public information.
11
+ # @return [Koala::Facebook::API] the API client
12
+ def initialize(access_token = nil)
13
+ @access_token = access_token
14
+ end
15
+
16
+ attr_reader :access_token
17
+
18
+ include GraphAPIMethods
19
+ include RestAPIMethods
20
+
21
+ # Makes a request to the appropriate Facebook API.
22
+ # @note You'll rarely need to call this method directly.
23
+ #
24
+ # @see GraphAPIMethods#graph_call
25
+ # @see RestAPIMethods#rest_call
26
+ #
27
+ # @param path the server path for this request (leading / is prepended if not present)
28
+ # @param args arguments to be sent to Facebook
29
+ # @param verb the HTTP method to use
30
+ # @param options request-related options for Koala and Faraday.
31
+ # See https://github.com/arsduo/koala/wiki/HTTP-Services for additional options.
32
+ # @option options [Symbol] :http_component which part of the response (headers, body, or status) to return
33
+ # @option options [Boolean] :beta use Facebook's beta tier
34
+ # @option options [Boolean] :use_ssl force SSL for this request, even if it's tokenless.
35
+ # (All API requests with access tokens use SSL.)
36
+ # @param error_checking_block a block to evaluate the response status for additional JSON-encoded errors
37
+ #
38
+ # @yield The response body for evaluation
39
+ #
40
+ # @raise [Koala::Facebook::APIError] if Facebook returns an error (response status >= 500)
41
+ #
42
+ # @return the body of the response from Facebook (unless another http_component is requested)
43
+ def api(path, args = {}, verb = "get", options = {}, &error_checking_block)
44
+ # Fetches the given path in the Graph API.
45
+ args["access_token"] = @access_token || @app_access_token if @access_token || @app_access_token
46
+
47
+ # add a leading /
48
+ path = "/#{path}" unless path =~ /^\//
49
+
50
+ # make the request via the provided service
51
+ result = Koala.make_request(path, args, verb, options)
52
+
53
+ # Check for any 500 errors before parsing the body
54
+ # since we're not guaranteed that the body is valid JSON
55
+ # in the case of a server error
56
+ raise APIError.new({"type" => "HTTP #{result.status.to_s}", "message" => "Response body: #{result.body}"}) if result.status >= 500
57
+
58
+ # parse the body as JSON and run it through the error checker (if provided)
59
+ # Note: Facebook sometimes sends results like "true" and "false", which aren't strictly objects
60
+ # and cause MultiJson.decode to fail -- so we account for that by wrapping the result in []
61
+ body = MultiJson.decode("[#{result.body.to_s}]")[0]
62
+ yield body if error_checking_block
63
+
64
+ # if we want a component other than the body (e.g. redirect header for images), return that
65
+ if component = options[:http_component]
66
+ component == :response ? result : result.send(options[:http_component])
67
+ else
68
+ body
69
+ end
70
+ end
71
+ end
72
+
73
+ class APIError < StandardError
74
+ attr_accessor :fb_error_type, :raw_response
75
+
76
+ # Creates a new APIError.
77
+ #
78
+ # Assigns the error type (as reported by Facebook) to #fb_error_type
79
+ # and the raw error response available to #raw_response.
80
+ #
81
+ # @param details error details containing "type" and "message" keys.
82
+ def initialize(details = {})
83
+ self.raw_response = details
84
+ self.fb_error_type = details["type"]
85
+ super("#{fb_error_type}: #{details["message"]}")
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ require 'koala/api/graph_batch_api'
92
+ # legacy support for old pre-1.2 API interfaces
93
+ require 'koala/api/legacy'
@@ -0,0 +1,41 @@
1
+ require 'faraday'
2
+
3
+ module Koala
4
+ module HTTPService
5
+ class MultipartRequest < Faraday::Request::Multipart
6
+ # Facebook expects nested parameters to be passed in a certain way
7
+ # Based on our testing (https://github.com/arsduo/koala/issues/125),
8
+ # Faraday needs two changes to make that work:
9
+ # 1) [] need to be escaped (e.g. params[foo]=bar ==> params%5Bfoo%5D=bar)
10
+ # 2) such messages need to be multipart-encoded
11
+
12
+ self.mime_type = 'multipart/form-data'.freeze
13
+
14
+ def process_request?(env)
15
+ # if the request values contain any hashes or arrays, multipart it
16
+ super || !!(env[:body].respond_to?(:values) && env[:body].values.find {|v| v.is_a?(Hash) || v.is_a?(Array)})
17
+ end
18
+
19
+
20
+ def process_params(params, prefix = nil, pieces = nil, &block)
21
+ params.inject(pieces || []) do |all, (key, value)|
22
+ key = "#{prefix}%5B#{key}%5D" if prefix
23
+
24
+ case value
25
+ when Array
26
+ values = value.inject([]) { |a,v| a << [nil, v] }
27
+ process_params(values, key, all, &block)
28
+ when Hash
29
+ process_params(value, key, all, &block)
30
+ else
31
+ all << block.call(key, value)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ # @private
39
+ # legacy support for when MultipartRequest lived directly under Koala
40
+ MultipartRequest = HTTPService::MultipartRequest
41
+ end
@@ -0,0 +1,18 @@
1
+ module Koala
2
+ module HTTPService
3
+ class Response
4
+ attr_reader :status, :body, :headers
5
+
6
+ # Creates a new Response object, which standardizes the response received by Facebook for use within Koala.
7
+ def initialize(status, body, headers)
8
+ @status = status
9
+ @body = body
10
+ @headers = headers
11
+ end
12
+ end
13
+ end
14
+
15
+ # @private
16
+ # legacy support for when Response lived directly under Koala
17
+ Response = HTTPService::Response
18
+ end
@@ -0,0 +1,187 @@
1
+ require "net/http/post/multipart"
2
+
3
+ module Koala
4
+ module HTTPService
5
+ class UploadableIO
6
+ attr_reader :io_or_path, :content_type, :filename
7
+
8
+ def initialize(io_or_path_or_mixed, content_type = nil, filename = nil)
9
+ # see if we got the right inputs
10
+ parse_init_mixed_param io_or_path_or_mixed, content_type
11
+
12
+ # filename is used in the Ads API
13
+ # if it's provided, take precedence over the detected filename
14
+ # otherwise, fall back to a dummy name
15
+ @filename = filename || @filename || "koala-io-file.dum"
16
+
17
+ raise KoalaError.new("Invalid arguments to initialize an UploadableIO") unless @io_or_path
18
+ raise KoalaError.new("Unable to determine MIME type for UploadableIO") if !@content_type
19
+ end
20
+
21
+ def to_upload_io
22
+ UploadIO.new(@io_or_path, @content_type, @filename)
23
+ end
24
+
25
+ def to_file
26
+ @io_or_path.is_a?(String) ? File.open(@io_or_path) : @io_or_path
27
+ end
28
+
29
+ def self.binary_content?(content)
30
+ content.is_a?(UploadableIO) || DETECTION_STRATEGIES.detect {|method| send(method, content)}
31
+ end
32
+
33
+ private
34
+ DETECTION_STRATEGIES = [
35
+ :sinatra_param?,
36
+ :rails_3_param?,
37
+ :file_param?
38
+ ]
39
+
40
+ PARSE_STRATEGIES = [
41
+ :parse_rails_3_param,
42
+ :parse_sinatra_param,
43
+ :parse_file_object,
44
+ :parse_string_path,
45
+ :parse_io
46
+ ]
47
+
48
+ def parse_init_mixed_param(mixed, content_type = nil)
49
+ PARSE_STRATEGIES.each do |method|
50
+ send(method, mixed, content_type)
51
+ return if @io_or_path && @content_type
52
+ end
53
+ end
54
+
55
+ # Expects a parameter of type ActionDispatch::Http::UploadedFile
56
+ def self.rails_3_param?(uploaded_file)
57
+ uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
58
+ end
59
+
60
+ def parse_rails_3_param(uploaded_file, content_type = nil)
61
+ if UploadableIO.rails_3_param?(uploaded_file)
62
+ @io_or_path = uploaded_file.tempfile.path
63
+ @content_type = content_type || uploaded_file.content_type
64
+ @filename = uploaded_file.original_filename
65
+ end
66
+ end
67
+
68
+ # Expects a Sinatra hash of file info
69
+ def self.sinatra_param?(file_hash)
70
+ file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
71
+ end
72
+
73
+ def parse_sinatra_param(file_hash, content_type = nil)
74
+ if UploadableIO.sinatra_param?(file_hash)
75
+ @io_or_path = file_hash[:tempfile]
76
+ @content_type = content_type || file_hash[:type] || detect_mime_type(tempfile)
77
+ @filename = file_hash[:filename]
78
+ end
79
+ end
80
+
81
+ # takes a file object
82
+ def self.file_param?(file)
83
+ file.kind_of?(File)
84
+ end
85
+
86
+ def parse_file_object(file, content_type = nil)
87
+ if UploadableIO.file_param?(file)
88
+ @io_or_path = file
89
+ @content_type = content_type || detect_mime_type(file.path)
90
+ @filename = File.basename(file.path)
91
+ end
92
+ end
93
+
94
+ def parse_string_path(path, content_type = nil)
95
+ if path.kind_of?(String)
96
+ @io_or_path = path
97
+ @content_type = content_type || detect_mime_type(path)
98
+ @filename = File.basename(path)
99
+ end
100
+ end
101
+
102
+ def parse_io(io, content_type = nil)
103
+ if io.respond_to?(:read)
104
+ @io_or_path = io
105
+ @content_type = content_type
106
+ end
107
+ end
108
+
109
+ MIME_TYPE_STRATEGIES = [
110
+ :use_mime_module,
111
+ :use_simple_detection
112
+ ]
113
+
114
+ def detect_mime_type(filename)
115
+ if filename
116
+ MIME_TYPE_STRATEGIES.each do |method|
117
+ result = send(method, filename)
118
+ return result if result
119
+ end
120
+ end
121
+ nil # if we can't find anything
122
+ end
123
+
124
+ def use_mime_module(filename)
125
+ # if the user has installed mime/types, we can use that
126
+ # if not, rescue and return nil
127
+ begin
128
+ type = MIME::Types.type_for(filename).first
129
+ type ? type.to_s : nil
130
+ rescue
131
+ nil
132
+ end
133
+ end
134
+
135
+ def use_simple_detection(filename)
136
+ # very rudimentary extension analysis for images
137
+ # first, get the downcased extension, or an empty string if it doesn't exist
138
+ extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
139
+ case extension
140
+ when ""
141
+ nil
142
+ # images
143
+ when "jpg", "jpeg"
144
+ "image/jpeg"
145
+ when "png"
146
+ "image/png"
147
+ when "gif"
148
+ "image/gif"
149
+
150
+ # video
151
+ when "3g2"
152
+ "video/3gpp2"
153
+ when "3gp", "3gpp"
154
+ "video/3gpp"
155
+ when "asf"
156
+ "video/x-ms-asf"
157
+ when "avi"
158
+ "video/x-msvideo"
159
+ when "flv"
160
+ "video/x-flv"
161
+ when "m4v"
162
+ "video/x-m4v"
163
+ when "mkv"
164
+ "video/x-matroska"
165
+ when "mod"
166
+ "video/mod"
167
+ when "mov", "qt"
168
+ "video/quicktime"
169
+ when "mp4", "mpeg4"
170
+ "video/mp4"
171
+ when "mpe", "mpeg", "mpg", "tod", "vob"
172
+ "video/mpeg"
173
+ when "nsv"
174
+ "application/x-winamp"
175
+ when "ogm", "ogv"
176
+ "video/ogg"
177
+ when "wmv"
178
+ "video/x-ms-wmv"
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ # @private
185
+ # legacy support for when UploadableIO lived directly under Koala
186
+ UploadableIO = HTTPService::UploadableIO
187
+ end