<commit_before>require 'rest-client'
require 'json'
require 'base64'
require 'active_support/hash_with_indifferent_access'

# Client interface for Filepicker's REST API
class FilepickerClient
  FP_FILE_PATH = "https://www.filepicker.io/api/file/"    # Path that Filepicker file handles are located under
  FP_API_PATH = "https://www.filepicker.io/api/store/S3"  # Path to access the Filepicker API

  DEFAULT_POLICY_EXPIRY = 5 * 60  # 5 minutes (short for security, but allows for some wiggle room)

  # Creates a client that will use the given Filepicker key and secret for requests and signing operations
  # @param api_key [String] Filepicker API key
  # @param api_secret [String] Filepicker API secret
  # @param filepicker_cert [OpenSSL::X509::Certificate] Optional certificate for verifying HTTPS connections to Filepicker
  def initialize(api_key, api_secret, filepicker_cert=nil)
    @api_key = api_key
    @api_secret = api_secret
    @filepicker_cert = filepicker_cert
  end

  # Create policies and signatures for Filepicker operations.
  #
  # Allowed Options:
  #
  # * expiration_start - Time from which the expiry value should start
  # * expiry - Seconds until the signature should expire (defaults to DEFAULT_POLICY_EXPIRY)
  # * call - Filepicker calls to allow (String, Symbol or Array of the following: 'read', 'stat', 'convert', 'write', 'writeUrl', 'pick', 'store', 'storeUrl')
  # * handle - Handle of the specific file to grant permissions for
  # * path - Path in the storage that Filepicker uploads to that the operations should be restricted to
  # * min_size - Minimum allowed upload size
  # * max_size - Maximum allowed upload size
  #
  # @param options [Hash] Options for generating the desired signature
  # @return [Hash] The policy generated with the encoded policy and signature for use in Filepicker requests
  def sign(options={})
    options = convert_hash(options)
    options[:expiration_start] ||= Time.now
    options[:expiry] ||= DEFAULT_POLICY_EXPIRY

    policy = {
      'call' => options[:call]
    }

    # Restrict the scope of the operation to either the specified file or the path
    if options[:handle]
      policy['handle'] = options[:handle]
    elsif options[:path]
      policy['path'] = (options[:path] + '/').gsub /\/+/, '/' # ensure path has a single, trailing '/'
    end

    if options[:min_size]
      policy['minsize'] = options[:min_size].to_i
    end

    if options[:max_size]
      policy['maxsize'] = options[:max_size].to_i
    end

    # Set expiration for <expiry> seconds from expiration start
    policy['expiry'] = (options[:expiration_start] + options[:expiry]).to_i.to_s

    # Generate policy in URL safe base64 encoded JSON
    encoded_policy = Base64.urlsafe_encode64(policy.to_json)

    # Sign policy using our API secret
    signature = OpenSSL::HMAC.hexdigest('sha256', @api_secret, encoded_policy)

    return convert_hash(
      policy: convert_hash(policy),
      encoded_policy: encoded_policy,
      signature: signature
    )
  end

  # Get Filepicker URI for the file with the given handle.
  # @param handle [String] Handle for the file in Filepicker
  # @return [URI] URI for the file in Filepicker
  def file_uri(handle)
    URI.parse(FP_FILE_PATH + handle)
  end

  # Get Filepicker URI for the file with the given handle signed for read and convert calls.
  # The expiry value returned with the URI indicates the time the URI will expire at.
  # @param handle [String] Handle for the file in Filepicker
  # @param expiry [Fixnum] Number of seconds until the URI should expire
  # @return [Hash] Hash with URI signed for read and convert calls (:uri) and the expiry for the URI (:expiry)
  def file_read_uri_and_expiry(handle, expiry=DEFAULT_POLICY_EXPIRY)
    signage = sign(
      expiry: expiry,
      handle: handle,
      call: ['read', 'convert']
    )

    uri = file_uri(handle)
    uri.query = URI.encode_www_form(
      signature: signage[:signature],
      policy: signage[:encoded_policy]
    )

    return convert_hash(
      uri: uri,
      expiry: signage[:policy]['expiry'].to_i
    )
  end

  # Store the given file at the given storage path through Filepicker.
  # @param path [String] Path the file should be organized under in the destination storage
  # @param file [File] File to upload
  # @return [FilepickerClientFile] Object representing the uploaded file in Filepicker
  def store(file, path=nil)
    signage = sign(path: path, call: :store)

    uri = URI.parse(FP_API_PATH)
    uri.query = URI.encode_www_form(
      key: @api_key,
      signature: signage[:signature],
      policy: signage[:encoded_policy],
      path: signage[:policy]['path']
    )

    resource = get_fp_resource uri

    response = resource.post fileUpload: file

    if response.code == 200
      response_data = JSON.parse response.body
      file = FilepickerClientFile.new(response_data, self)

      return file
    else
      raise FilepickerClientError, "failed to store (code: #{response.code})"
    end
  end

  # Store the file located at the given URL under the target storage path through Filepicker.
  # @param path [String] Path the file should be organized under in the destination storage
  # @param file_url [String] URL to get the file to upload from
  # @return [FilepickerClientFile] Object representing the uploaded file in Filepicker
  def store_url(file_url, path=nil)
    signage = sign(path: path, call: :store)

    uri = URI.parse(FP_API_PATH)
    uri.query = URI.encode_www_form(
      key: @api_key,
      signature: signage[:signature],
      policy: signage[:encoded_policy],
      path: signage[:policy]['path']
    )

    resource = get_fp_resource uri

    response = resource.post url: file_url.to_s

    if response.code == 200
      response_data = JSON.parse response.body
      file = FilepickerClientFile.new response_data, self
<commit_msg>Fix store_url when path is not specified.<commit_after>require 'rest-client'
require 'json'
require 'base64'
require 'active_support/hash_with_indifferent_access'

# Client interface for Filepicker's REST API
class FilepickerClient
  FP_FILE_PATH = "https://www.filepicker.io/api/file/"    # Path that Filepicker file handles are located under
  FP_API_PATH = "https://www.filepicker.io/api/store/S3"  # Path to access the Filepicker API

  DEFAULT_POLICY_EXPIRY = 5 * 60  # 5 minutes (short for security, but allows for some wiggle room)

  # Creates a client that will use the given Filepicker key and secret for requests and signing operations
  # @param api_key [String] Filepicker API key
  # @param api_secret [String] Filepicker API secret
  # @param filepicker_cert [OpenSSL::X509::Certificate] Optional certificate for verifying HTTPS connections to Filepicker
  def initialize(api_key, api_secret, filepicker_cert=nil)
    @api_key = api_key
    @api_secret = api_secret
    @filepicker_cert = filepicker_cert
  end

  # Create policies and signatures for Filepicker operations.
  #
  # Allowed Options:
  #
  # * expiration_start - Time from which the expiry value should start
  # * expiry - Seconds until the signature should expire (defaults to DEFAULT_POLICY_EXPIRY)
  # * call - Filepicker calls to allow (String, Symbol or Array of the following: 'read', 'stat', 'convert', 'write', 'writeUrl', 'pick', 'store', 'storeUrl')
  # * handle - Handle of the specific file to grant permissions for
  # * path - Path in the storage that Filepicker uploads to that the operations should be restricted to
  # * min_size - Minimum allowed upload size
  # * max_size - Maximum allowed upload size
  #
  # @param options [Hash] Options for generating the desired signature
  # @return [Hash] The policy generated with the encoded policy and signature for use in Filepicker requests
  def sign(options={})
    options = convert_hash(options)
    options[:expiration_start] ||= Time.now
    options[:expiry] ||= DEFAULT_POLICY_EXPIRY

    policy = {
      'call' => options[:call]
    }

    # Restrict the scope of the operation to either the specified file or the path
    if options[:handle]
      policy['handle'] = options[:handle]
    elsif options[:path]
      policy['path'] = (options[:path] + '/').gsub /\/+/, '/' # ensure path has a single, trailing '/'
    end

    if options[:min_size]
      policy['minsize'] = options[:min_size].to_i
    end

    if options[:max_size]
      policy['maxsize'] = options[:max_size].to_i
    end

    # Set expiration for <expiry> seconds from expiration start
    policy['expiry'] = (options[:expiration_start] + options[:expiry]).to_i.to_s

    # Generate policy in URL safe base64 encoded JSON
    encoded_policy = Base64.urlsafe_encode64(policy.to_json)

    # Sign policy using our API secret
    signature = OpenSSL::HMAC.hexdigest('sha256', @api_secret, encoded_policy)

    return convert_hash(
      policy: convert_hash(policy),
      encoded_policy: encoded_policy,
      signature: signature
    )
  end

  # Get Filepicker URI for the file with the given handle.
  # @param handle [String] Handle for the file in Filepicker
  # @return [URI] URI for the file in Filepicker
  def file_uri(handle)
    URI.parse(FP_FILE_PATH + handle)
  end

  # Get Filepicker URI for the file with the given handle signed for read and convert calls.
  # The expiry value returned with the URI indicates the time the URI will expire at.
  # @param handle [String] Handle for the file in Filepicker
  # @param expiry [Fixnum] Number of seconds until the URI should expire
  # @return [Hash] Hash with URI signed for read and convert calls (:uri) and the expiry for the URI (:expiry)
  def file_read_uri_and_expiry(handle, expiry=DEFAULT_POLICY_EXPIRY)
    signage = sign(
      expiry: expiry,
      handle: handle,
      call: ['read', 'convert']
    )

    uri = file_uri(handle)
    uri.query = URI.encode_www_form(
      signature: signage[:signature],
      policy: signage[:encoded_policy]
    )

    return convert_hash(
      uri: uri,
      expiry: signage[:policy]['expiry'].to_i
    )
  end

  # Store the given file at the given storage path through Filepicker.
  # @param path [String] Path the file should be organized under in the destination storage
  # @param file [File] File to upload
  # @return [FilepickerClientFile] Object representing the uploaded file in Filepicker
  def store(file, path=nil)
    signage = sign(path: path, call: :store)

    uri = URI.parse(FP_API_PATH)
    uri.query = URI.encode_www_form(
      key: @api_key,
      signature: signage[:signature],
      policy: signage[:encoded_policy],
      path: signage[:policy]['path']
    )

    resource = get_fp_resource uri

    response = resource.post fileUpload: file

    if response.code == 200
      response_data = JSON.parse response.body
      file = FilepickerClientFile.new(response_data, self)

      return file
    else
      raise FilepickerClientError, "failed to store (code: #{response.code})"
    end
  end

  # Store the file located at the given URL under the target storage path through Filepicker.
  # @param path [String] Path the file should be organized under in the destination storage
  # @param file_url [String] URL to get the file to upload from
  # @return [FilepickerClientFile] Object representing the uploaded file in Filepicker
  def store_url(file_url, path=nil)
    signage = sign(path: path, call: :store)

    uri = URI.parse(FP_API_PATH)
    query_params = {
      key: @api_key,
      signature: signage[:signature],
      policy: signage[:encoded_policy]
    }
    query_params[:path] = signage[:policy]['path'] if path
    uri.query = URI.encode_www_form(query_params)

    resource = get_fp_resource uri

    response = resource.post url: file_url.to_s

    if response.code == 200
      response_data = JSON.parse response.body
      file = FilepickerClientFile.new response_data, self

{'fraction_non_alphanumeric': 0.08812357558875665, 'fraction_numerical': 0.004220477758082215, 'mean_word_length': 3.628125, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 7, 'https://': 4, 'lorem ipsum': 0, 'www.': 10, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4637865', 'n_tokens_mistral': 3569, 'n_tokens_neox': 3402, 'n_words': 1213}