Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
@@ -0,0 +1,149 @@
# frozen_string_literal: true
module Ethon
class Easy
# This module contains all the logic around the callbacks,
# which are needed to interact with libcurl.
#
# @api private
module Callbacks
# :nodoc:
def self.included(base)
base.send(:attr_accessor, *[:response_body, :response_headers, :debug_info])
end
# Set writefunction and headerfunction callback.
# They are called by libcurl in order to provide the header and
# the body from the request.
#
# @example Set callbacks.
# easy.set_callbacks
def set_callbacks
Curl.set_option(:writefunction, body_write_callback, handle)
Curl.set_option(:headerfunction, header_write_callback, handle)
Curl.set_option(:debugfunction, debug_callback, handle)
@response_body = String.new
@response_headers = String.new
@headers_called = false
@debug_info = Ethon::Easy::DebugInfo.new
end
# Returns the body write callback.
#
# @example Return the callback.
# easy.body_write_callback
#
# @return [ Proc ] The callback.
def body_write_callback
@body_write_callback ||= proc do |stream, size, num, object|
headers
result = body(chunk = stream.read_string(size * num))
@response_body << chunk if result == :unyielded
result != :abort ? size * num : -1
end
end
# Returns the header write callback.
#
# @example Return the callback.
# easy.header_write_callback
#
# @return [ Proc ] The callback.
def header_write_callback
@header_write_callback ||= proc {|stream, size, num, object|
result = headers
@response_headers << stream.read_string(size * num)
result != :abort ? size * num : -1
}
end
# Returns the debug callback. This callback is currently used
# write the raw http request headers.
#
# @example Return the callback.
# easy.debug_callback
#
# @return [ Proc ] The callback.
def debug_callback
@debug_callback ||= proc {|handle, type, data, size, udata|
message = data.read_string(size)
@debug_info.add type, message
print message unless [:data_in, :data_out].include?(type)
0
}
end
def set_progress_callback
if Curl.version_info[:version] >= "7.32.0"
Curl.set_option(:xferinfofunction, progress_callback, handle)
else
Curl.set_option(:progressfunction, progress_callback, handle)
end
end
# Returns the progress callback.
#
# @example Return the callback.
# easy.progress_callback
#
# @return [ Proc ] The callback.
def progress_callback
@progress_callback ||= proc { |_, dltotal, dlnow, ultotal, ulnow|
progress(dltotal, dlnow, ultotal, ulnow)
0
}
end
# Set the read callback. This callback is used by libcurl to
# read data when performing a PUT request.
#
# @example Set the callback.
# easy.set_read_callback("a=1")
#
# @param [ String ] body The body.
def set_read_callback(body)
@request_body_read = 0
readfunction do |stream, size, num, object|
size = size * num
body_size = if body.respond_to?(:bytesize)
body.bytesize
elsif body.respond_to?(:size)
body.size
elsif body.is_a?(File)
File.size(body.path)
end
left = body_size - @request_body_read
size = left if size > left
if size > 0
chunk = if body.respond_to?(:byteslice)
body.byteslice(@request_body_read, size)
elsif body.respond_to?(:read)
body.read(size)
else
body[@request_body_read, size]
end
stream.write_string(
chunk, size
)
@request_body_read += size
end
size
end
end
# Returns the body read callback.
#
# @example Return the callback.
# easy.read_callback
#
# @return [ Proc ] The callback.
def read_callback
@read_callback
end
end
end
end
@@ -0,0 +1,47 @@
# frozen_string_literal: true
module Ethon
class Easy
# This class is used to store and retreive debug information,
# which is only saved when verbose is set to true.
#
# @api private
class DebugInfo
MESSAGE_TYPES = Ethon::Curl::DebugInfoType.to_h.keys
class Message
attr_reader :type, :message
def initialize(type, message)
@type = type
@message = message
end
end
def initialize
@messages = []
end
def add(type, message)
@messages << Message.new(type, message)
end
def messages_for(type)
@messages.select {|m| m.type == type }.map(&:message)
end
MESSAGE_TYPES.each do |type|
eval %Q|def #{type}; messages_for(:#{type}); end|
end
def to_a
@messages.map(&:message)
end
def to_h
Hash[MESSAGE_TYPES.map {|k| [k, send(k)] }]
end
end
end
end
@@ -0,0 +1,31 @@
# frozen_string_literal: true
module Ethon
class Easy
# This module contains class methods for feature checks
module Features
# Returns true if this curl version supports zlib.
#
# @example Return wether zlib is supported.
# Ethon::Easy.supports_zlib?
#
# @return [ Boolean ] True if supported, else false.
def supports_zlib?
!!(Curl.version_info[:features] & Curl::VERSION_LIBZ)
end
# Returns true if this curl version supports AsynchDNS.
#
# @example
# Ethon::Easy.supports_asynch_dns?
#
# @return [ Boolean ] True if supported, else false.
def supports_asynch_dns?
!!(Curl.version_info[:features] & Curl::VERSION_ASYNCHDNS)
end
alias :supports_timeout_ms? :supports_asynch_dns?
end
end
end
@@ -0,0 +1,107 @@
# frozen_string_literal: true
require 'ethon/easy/util'
require 'ethon/easy/queryable'
module Ethon
class Easy
# This class represents a form and is used to send a payload in the
# request body via POST/PUT.
# It handles multipart forms, too.
#
# @api private
class Form
include Ethon::Easy::Util
include Ethon::Easy::Queryable
# Return a new Form.
#
# @example Return a new Form.
# Form.new({})
#
# @param [ Hash ] params The parameter with which to initialize the form.
#
# @return [ Form ] A new Form.
def initialize(easy, params, multipart = nil)
@easy = easy
@params = params || {}
@multipart = multipart
end
# Return a pointer to the first form element in libcurl.
#
# @example Return the first form element.
# form.first
#
# @return [ FFI::Pointer ] The first element.
def first
@first ||= FFI::MemoryPointer.new(:pointer)
end
# Return a pointer to the last form element in libcurl.
#
# @example Return the last form element.
# form.last
#
# @return [ FFI::Pointer ] The last element.
def last
@last ||= FFI::MemoryPointer.new(:pointer)
end
# Return if form is multipart. The form is multipart
# when it contains a file or multipart option is set on the form during creation.
#
# @example Return if form is multipart.
# form.multipart?
#
# @return [ Boolean ] True if form is multipart, else false.
def multipart?
return true if @multipart
query_pairs.any?{|pair| pair.respond_to?(:last) && pair.last.is_a?(Array)}
end
# Add form elements to libcurl.
#
# @example Add form to libcurl.
# form.materialize
def materialize
query_pairs.each { |pair| form_add(pair.first.to_s, pair.last) }
end
private
def form_add(name, content)
case content
when Array
Curl.formadd(first, last,
:form_option, :copyname, :pointer, name,
:form_option, :namelength, :long, name.bytesize,
:form_option, :file, :string, content[2],
:form_option, :filename, :string, content[0],
:form_option, :contenttype, :string, content[1],
:form_option, :end
)
else
Curl.formadd(first, last,
:form_option, :copyname, :pointer, name,
:form_option, :namelength, :long, name.bytesize,
:form_option, :copycontents, :pointer, content.to_s,
:form_option, :contentslength, :long, content ? content.to_s.bytesize : 0,
:form_option, :end
)
end
setup_garbage_collection
end
def setup_garbage_collection
# first is a pointer to a pointer. Since it's a MemoryPointer it will
# auto clean itself up, but we need to clean up the object it points
# to. So this results in (pseudo-c):
# form_data_cleanup_handler = *first
# curl_form_free(form_data_cleanup_handler)
@form_data_cleanup_handler ||= FFI::AutoPointer.new(@first.get_pointer(0), Curl.method(:formfree))
end
end
end
end
@@ -0,0 +1,61 @@
# frozen_string_literal: true
module Ethon
class Easy
# This module contains the logic around adding headers to libcurl.
#
# @api private
module Header
# Return headers, return empty hash if none.
#
# @example Return the headers.
# easy.headers
#
# @return [ Hash ] The headers.
def headers
@headers ||= {}
end
# Set the headers.
#
# @example Set the headers.
# easy.headers = {'User-Agent' => 'ethon'}
#
# @param [ Hash ] headers The headers.
def headers=(headers)
headers ||= {}
header_list = nil
headers.each do |k, v|
header_list = Curl.slist_append(header_list, compose_header(k,v))
end
Curl.set_option(:httpheader, header_list, handle)
@header_list = header_list && FFI::AutoPointer.new(header_list, Curl.method(:slist_free_all))
end
# Return header_list.
#
# @example Return header_list.
# easy.header_list
#
# @return [ FFI::Pointer ] The header list.
def header_list
@header_list
end
# Compose libcurl header string from key and value.
# Also replaces null bytes, because libcurl will complain
# otherwise.
#
# @example Compose header.
# easy.compose_header('User-Agent', 'Ethon')
#
# @param [ String ] key The header name.
# @param [ String ] value The header value.
#
# @return [ String ] The composed header.
def compose_header(key, value)
Util.escape_zero_byte("#{key}: #{value}")
end
end
end
end
@@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'ethon/easy/http/actionable'
require 'ethon/easy/http/post'
require 'ethon/easy/http/get'
require 'ethon/easy/http/head'
require 'ethon/easy/http/put'
require 'ethon/easy/http/delete'
require 'ethon/easy/http/patch'
require 'ethon/easy/http/options'
require 'ethon/easy/http/custom'
module Ethon
class Easy
# This module contains logic about making valid HTTP requests.
module Http
# Set specified options in order to make a HTTP request.
# Look at {Ethon::Easy::Options Options} to see what you can
# provide in the options hash.
#
# @example Set options for HTTP request.
# easy.http_request("www.google.com", :get, {})
#
# @param [ String ] url The url.
# @param [ String ] action_name The HTTP action name.
# @param [ Hash ] options The options hash.
#
# @option options :params [ Hash ] Params hash which
# is attached to the url.
# @option options :body [ Hash ] Body hash which
# becomes the request body. It is a PUT body for
# PUT requests and a POST for everything else.
# @option options :headers [ Hash ] Request headers.
#
# @return [ void ]
#
# @see Ethon::Easy::Options
def http_request(url, action_name, options = {})
fabricate(url, action_name, options).setup(self)
end
private
# Return the corresponding action class.
#
# @example Return the action.
# Action.fabricate(:get)
# Action.fabricate(:smash)
#
# @param [ String ] url The url.
# @param [ String ] action_name The HTTP action name.
# @param [ Hash ] options The option hash.
#
# @return [ Easy::Ethon::Actionable ] The request instance.
def fabricate(url, action_name, options)
constant_name = action_name.to_s.capitalize
if Ethon::Easy::Http.const_defined?(constant_name)
Ethon::Easy::Http.const_get(constant_name).new(url, options)
else
Ethon::Easy::Http::Custom.new(constant_name.upcase, url, options)
end
end
end
end
end
@@ -0,0 +1,157 @@
# frozen_string_literal: true
require 'ethon/easy/http/putable'
require 'ethon/easy/http/postable'
module Ethon
class Easy
module Http
# This module represents a Http Action and is a factory
# for more real actions like GET, HEAD, POST and PUT.
module Actionable
QUERY_OPTIONS = [ :params, :body, :params_encoding ]
# Create a new action.
#
# @example Create a new action.
# Action.new("www.example.com", {})
#
# @param [ String ] url The url.
# @param [ Hash ] options The options.
#
# @return [ Action ] A new action.
def initialize(url, options)
@url = url
@options, @query_options = parse_options(options)
end
# Return the url.
#
# @example Return url.
# action.url
#
# @return [ String ] The url.
def url
@url
end
# Return the options hash.
#
# @example Return options.
# action.options
#
# @return [ Hash ] The options.
def options
@options
end
# Returns the query options hash.
#
# @example Return query options.
# action.query_options
#
# @return [ Hash ] The query options.
def query_options
@query_options
end
# Return the params.
#
# @example Return params.
# action.params
#
# @return [ Params ] The params.
def params
@params ||= Params.new(@easy, query_options.fetch(:params, nil))
end
# Return the form.
#
# @example Return form.
# action.form
#
# @return [ Form ] The form.
def form
@form ||= Form.new(@easy, query_options.fetch(:body, nil), options.fetch(:multipart, nil))
end
# Get the requested array encoding. By default it's
# :typhoeus, but it can also be set to :rack.
#
# @example Get encoding from options
# action.params_encoding
#
def params_encoding
@params_encoding ||= query_options.fetch(:params_encoding, :typhoeus)
end
# Setup everything necessary for a proper request.
#
# @example setup.
# action.setup(easy)
#
# @param [ easy ] easy the easy to setup.
def setup(easy)
@easy = easy
# Order is important, @easy will be used to provide access to options
# relevant to the following operations (like whether or not to escape
# values).
easy.set_attributes(options)
set_form(easy) unless form.empty?
if params.empty?
easy.url = url
else
set_params(easy)
end
end
# Setup request with params.
#
# @example Setup nothing.
# action.set_params(easy)
#
# @param [ Easy ] easy The easy to setup.
def set_params(easy)
params.escape = easy.escape?
params.params_encoding = params_encoding
base_url, base_params = url.split('?')
base_url << '?'
base_url << base_params.to_s
base_url << '&' if base_params
base_url << params.to_s
easy.url = base_url
end
# Setup request with form.
#
# @example Setup nothing.
# action.set_form(easy)
#
# @param [ Easy ] easy The easy to setup.
def set_form(easy)
end
private
def parse_options(options)
query_options = {}
options = options.dup
QUERY_OPTIONS.each do |query_option|
if options.key?(query_option)
query_options[query_option] = options.delete(query_option)
end
end
return options, query_options
end
end
end
end
end
@@ -0,0 +1,29 @@
# frozen_string_literal: true
module Ethon
class Easy
module Http
# This class knows everything about making requests for custom HTTP verbs.
class Custom
include Ethon::Easy::Http::Actionable
include Ethon::Easy::Http::Postable
def initialize(verb, url, options)
@verb = verb
super(url, options)
end
# Setup easy to make a request.
#
# @example Setup.
# custom.set_params(easy)
#
# @param [ Easy ] easy The easy to setup.
def setup(easy)
super
easy.customrequest = @verb
end
end
end
end
end
@@ -0,0 +1,25 @@
# frozen_string_literal: true
module Ethon
class Easy
module Http
# This class knows everything about making DELETE requests.
class Delete
include Ethon::Easy::Http::Actionable
include Ethon::Easy::Http::Postable
# Setup easy to make a DELETE request.
#
# @example Setup customrequest.
# delete.setup(easy)
#
# @param [ Easy ] easy The easy to setup.
def setup(easy)
super
easy.customrequest = "DELETE"
end
end
end
end
end
@@ -0,0 +1,24 @@
# frozen_string_literal: true
module Ethon
class Easy
module Http
# This class knows everything about making GET requests.
class Get
include Ethon::Easy::Http::Actionable
include Ethon::Easy::Http::Postable
# Setup easy to make a GET request.
#
# @example Setup.
# get.set_params(easy)
#
# @param [ Easy ] easy The easy to setup.
def setup(easy)
super
easy.customrequest = "GET" unless form.empty?
end
end
end
end
end
@@ -0,0 +1,24 @@
# frozen_string_literal: true
module Ethon
class Easy
module Http
# This class knows everything about making HEAD requests.
class Head
include Ethon::Easy::Http::Actionable
include Ethon::Easy::Http::Postable
# Setup easy to make a HEAD request.
#
# @example Setup.
# get.set_params(easy)
#
# @param [ Easy ] easy The easy to setup.
def setup(easy)
super
easy.nobody = true
end
end
end
end
end
@@ -0,0 +1,24 @@
# frozen_string_literal: true
module Ethon
class Easy
module Http
# This class knows everything about making OPTIONS requests.
class Options
include Ethon::Easy::Http::Actionable
include Ethon::Easy::Http::Postable
# Setup easy to make a OPTIONS request.
#
# @example Setup.
# options.setup(easy)
#
# @param [ Easy ] easy The easy to setup.
def setup(easy)
super
easy.customrequest = "OPTIONS"
end
end
end
end
end
@@ -0,0 +1,24 @@
# frozen_string_literal: true
module Ethon
class Easy
module Http
# This class knows everything about making PATCH requests.
class Patch
include Ethon::Easy::Http::Actionable
include Ethon::Easy::Http::Postable
# Setup easy to make a PATCH request.
#
# @example Setup.
# patch.setup(easy)
#
# @param [ Easy ] easy The easy to setup.
def setup(easy)
super
easy.customrequest = "PATCH"
end
end
end
end
end
@@ -0,0 +1,26 @@
# frozen_string_literal: true
module Ethon
class Easy
module Http
# This class knows everything about making POST requests.
class Post
include Ethon::Easy::Http::Actionable
include Ethon::Easy::Http::Postable
# Setup easy to make a POST request.
#
# @example Setup.
# post.setup(easy)
#
# @param [ Easy ] easy The easy to setup.
def setup(easy)
super
if form.empty?
easy.postfieldsize = 0
easy.copypostfields = ""
end
end
end
end
end
end
@@ -0,0 +1,32 @@
# frozen_string_literal: true
module Ethon
class Easy
module Http
# This module contains logic for setting up a [multipart] POST body.
module Postable
# Set things up when form is provided.
# Deals with multipart forms.
#
# @example Setup.
# post.set_form(easy)
#
# @param [ Easy ] easy The easy to setup.
def set_form(easy)
easy.url ||= url
form.params_encoding = params_encoding
if form.multipart?
form.escape = false
form.materialize
easy.httppost = form.first.read_pointer
else
form.escape = easy.escape?
easy.postfieldsize = form.to_s.bytesize
easy.copypostfields = form.to_s
end
end
end
end
end
end
@@ -0,0 +1,27 @@
# frozen_string_literal: true
module Ethon
class Easy
module Http
# This class knows everything about making PUT requests.
class Put
include Ethon::Easy::Http::Actionable
include Ethon::Easy::Http::Putable
# Setup easy to make a PUT request.
#
# @example Setup.
# put.setup(easy)
#
# @param [ Easy ] easy The easy to setup.
def setup(easy)
super
if form.empty?
easy.upload = true
easy.infilesize = 0
end
end
end
end
end
end
@@ -0,0 +1,25 @@
# frozen_string_literal: true
module Ethon
class Easy
module Http
# This module contains logic about setting up a PUT body.
module Putable
# Set things up when form is provided.
# Deals with multipart forms.
#
# @example Setup.
# put.set_form(easy)
#
# @param [ Easy ] easy The easy to setup.
def set_form(easy)
easy.upload = true
form.escape = true
form.params_encoding = params_encoding
easy.infilesize = form.to_s.bytesize
easy.set_read_callback(form.to_s)
end
end
end
end
end
@@ -0,0 +1,116 @@
# frozen_string_literal: true
module Ethon
class Easy
# This module contains the methods to return informations
# from the easy handle. See http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html
# for more information.
module Informations
# Holds available informations and their type, which is needed to
# request the informations from libcurl.
AVAILABLE_INFORMATIONS = {
# Return the available HTTP auth methods.
:httpauth_avail => :long,
# Return the total time in seconds for the previous
# transfer, including name resolution, TCP connection, etc.
:total_time => :double,
# Return the time, in seconds, it took from the start
# until the first byte was received by libcurl. This
# includes pre-transfer time and also the time the
# server needs to calculate the result.
:starttransfer_time => :double,
# Return the time, in seconds, it took from the start
# until the SSL/SSH connect/handshake to the remote
# host was completed. This time is most often very near
# to the pre-transfer time, except for cases such as HTTP
# pipelining where the pre-transfer time can be delayed
# due to waits in line for the pipeline and more.
:appconnect_time => :double,
# Return the time, in seconds, it took from the start
# until the file transfer was just about to begin. This
# includes all pre-transfer commands and negotiations
# that are specific to the particular protocol(s) involved.
# It does not involve the sending of the protocol-
# specific request that triggers a transfer.
:pretransfer_time => :double,
# Return the time, in seconds, it took from the start
# until the connect to the remote host (or proxy) was completed.
:connect_time => :double,
# Return the time, in seconds, it took from the
# start until the name resolution was completed.
:namelookup_time => :double,
# Return the time, in seconds, it took for all redirection steps
# include name lookup, connect, pretransfer and transfer before the
# final transaction was started. time_redirect shows the complete
# execution time for multiple redirections. (Added in 7.12.3)
:redirect_time => :double,
# Return the last used effective url.
:effective_url => :string,
# Return the string holding the IP address of the most recent
# connection done with this curl handle. This string
# may be IPv6 if that's enabled.
:primary_ip => :string,
# Return the last received HTTP, FTP or SMTP response code.
# The value will be zero if no server response code has
# been received. Note that a proxy's CONNECT response should
# be read with http_connect_code and not this.
:response_code => :long,
:request_size => :long,
# Return the total number of redirections that were
# actually followed.
:redirect_count => :long,
# URL a redirect would take you to, had you enabled redirects (Added in 7.18.2)
:redirect_url => :string,
# Return the bytes, the total amount of bytes that were uploaded
:size_upload => :double,
# Return the bytes, the total amount of bytes that were downloaded.
# The amount is only for the latest transfer and will be reset again
# for each new transfer. This counts actual payload data, what's
# also commonly called body. All meta and header data are excluded
# and will not be counted in this number.
:size_download => :double,
# Return the bytes/second, the average upload speed that curl
# measured for the complete upload
:speed_upload => :double,
# Return the bytes/second, the average download speed that curl
# measured for the complete download
:speed_download => :double
}
AVAILABLE_INFORMATIONS.each do |name, type|
eval %Q|def #{name}; Curl.send(:get_info_#{type}, :#{name}, handle); end|
end
# Returns true if this curl version supports zlib.
#
# @example Return wether zlib is supported.
# easy.supports_zlib?
#
# @return [ Boolean ] True if supported, else false.
# @deprecated Please use the static version instead
def supports_zlib?
Kernel.warn("Ethon: Easy#supports_zlib? is deprecated and will be removed, please use Easy#.")
Easy.supports_zlib?
end
end
end
end
@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Ethon
class Easy
class Mirror
attr_reader :options
alias_method :to_hash, :options
INFORMATIONS_TO_MIRROR = Informations::AVAILABLE_INFORMATIONS.keys +
[:return_code, :response_headers, :response_body, :debug_info]
INFORMATIONS_TO_LOG = [:effective_url, :response_code, :return_code, :total_time]
def self.from_easy(easy)
options = {}
INFORMATIONS_TO_MIRROR.each do |info|
options[info] = easy.send(info)
end
new(options)
end
def initialize(options = {})
@options = options
end
def log_informations
Hash[*INFORMATIONS_TO_LOG.map do |info|
[info, options[info]]
end.flatten]
end
INFORMATIONS_TO_MIRROR.each do |info|
eval %Q|def #{info}; options[#{info}]; end|
end
end
end
end
@@ -0,0 +1,64 @@
# frozen_string_literal: true
module Ethon
class Easy
# This module contains the logic to prepare and perform
# an easy.
module Operations
# Returns a pointer to the curl easy handle.
#
# @example Return the handle.
# easy.handle
#
# @return [ FFI::Pointer ] A pointer to the curl easy handle.
def handle
@handle ||= FFI::AutoPointer.new(Curl.easy_init, Curl.method(:easy_cleanup))
end
# Sets a pointer to the curl easy handle.
# @param [ ::FFI::Pointer ] Easy handle that will be assigned.
def handle=(h)
@handle = h
end
# Perform the easy request.
#
# @example Perform the request.
# easy.perform
#
# @return [ Integer ] The return code.
def perform
@return_code = Curl.easy_perform(handle)
if Ethon.logger.debug?
Ethon.logger.debug { "ETHON: performed #{log_inspect}" }
end
complete
@return_code
end
# Clean up the easy.
#
# @example Perform clean up.
# easy.cleanup
#
# @return the result of the free which is nil
def cleanup
handle.free
end
# Prepare the easy. Options, headers and callbacks
# were set.
#
# @example Prepare easy.
# easy.prepare
#
# @deprecated It is no longer necessary to call prepare.
def prepare
Ethon.logger.warn(
"ETHON: It is no longer necessary to call "+
"Easy#prepare. It's going to be removed "+
"in future versions."
)
end
end
end
end
@@ -0,0 +1,50 @@
# frozen_string_literal: true
module Ethon
class Easy
# This module contains the logic and knowledge about the
# available options on easy.
module Options
attr_reader :url
def url=(value)
@url = value
Curl.set_option(:url, value, handle)
end
def escape=( b )
@escape = b
end
def escape?
return true if !defined?(@escape) || @escape.nil?
@escape
end
def multipart=(b)
@multipart = b
end
def multipart?
!!@multipart
end
Curl.easy_options(nil).each do |opt, props|
method_name = "#{opt}=".freeze
unless method_defined? method_name
define_method(method_name) do |value|
Curl.set_option(opt, value, handle)
value
end
end
next if props[:type] != :callback || method_defined?(opt)
define_method(opt) do |&block|
@procs ||= {}
@procs[opt.to_sym] = block
Curl.set_option(opt, block, handle)
nil
end
end
end
end
end
@@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'ethon/easy/util'
require 'ethon/easy/queryable'
module Ethon
class Easy
# This class represents HTTP request parameters.
#
# @api private
class Params
include Ethon::Easy::Util
include Ethon::Easy::Queryable
# Create a new Params.
#
# @example Create a new Params.
# Params.new({})
#
# @param [ Hash ] params The params to use.
#
# @return [ Params ] A new Params.
def initialize(easy, params)
@easy = easy
@params = params || {}
end
end
end
end
@@ -0,0 +1,154 @@
# frozen_string_literal: true
module Ethon
class Easy
# This module contains logic about building
# query parameters for url or form.
module Queryable
# :nodoc:
def self.included(base)
base.send(:attr_accessor, :escape)
base.send(:attr_accessor, :params_encoding)
end
# Return wether there are elements in params or not.
#
# @example Return if params is empty.
# form.empty?
#
# @return [ Boolean ] True if params is empty, else false.
def empty?
@params.empty?
end
# Return the string representation of params.
#
# @example Return string representation.
# params.to_s
#
# @return [ String ] The string representation.
def to_s
@to_s ||= query_pairs.map{ |pair|
return pair if pair.is_a?(String)
if escape && @easy
pair.map{ |e| @easy.escape(e.to_s) }.join("=")
else
pair.join("=")
end
}.join('&')
end
# Return the query pairs.
#
# @example Return the query pairs.
# params.query_pairs
#
# @return [ Array ] The query pairs.
def query_pairs
@query_pairs ||= build_query_pairs(@params)
end
# Return query pairs build from a hash.
#
# @example Build query pairs.
# action.build_query_pairs({a: 1, b: 2})
# #=> [[:a, 1], [:b, 2]]
#
# @param [ Hash ] hash The hash to go through.
#
# @return [ Array ] The array of query pairs.
def build_query_pairs(hash)
return [hash] if hash.is_a?(String)
pairs = []
recursively_generate_pairs(hash, nil, pairs)
pairs
end
# Return file info for a file.
#
# @example Return file info.
# action.file_info(File.open('fubar', 'r'))
#
# @param [ File ] file The file to handle.
#
# @return [ Array ] Array of informations.
def file_info(file)
filename = File.basename(file.path)
[
filename,
mime_type(filename),
File.expand_path(file.path)
]
end
private
def mime_type(filename)
if defined?(MIME) && t = MIME::Types.type_for(filename).first
t.to_s
else
'application/octet-stream'
end
end
def recursively_generate_pairs(h, prefix, pairs)
case h
when Hash
encode_hash_pairs(h, prefix, pairs)
when Array
if params_encoding == :rack
encode_rack_array_pairs(h, prefix, pairs)
elsif params_encoding == :multi
encode_multi_array_pairs(h, prefix, pairs)
elsif params_encoding == :none
pairs << [prefix, h]
else
encode_indexed_array_pairs(h, prefix, pairs)
end
end
end
def encode_hash_pairs(h, prefix, pairs)
h.each_pair do |k,v|
key = prefix.nil? ? k : "#{prefix}[#{k}]"
pairs_for(v, key, pairs)
end
end
def encode_indexed_array_pairs(h, prefix, pairs)
h.each_with_index do |v, i|
key = "#{prefix}[#{i}]"
pairs_for(v, key, pairs)
end
end
def encode_rack_array_pairs(h, prefix, pairs)
h.each do |v|
key = "#{prefix}[]"
pairs_for(v, key, pairs)
end
end
def encode_multi_array_pairs(h, prefix, pairs)
h.each_with_index do |v, i|
key = prefix
pairs_for(v, key, pairs)
end
end
def pairs_for(v, key, pairs)
case v
when Hash, Array
recursively_generate_pairs(v, key, pairs)
when File, Tempfile
pairs << [key, file_info(v)]
else
pairs << [key, v]
end
end
end
end
end
@@ -0,0 +1,136 @@
# frozen_string_literal: true
module Ethon
class Easy
# This module contains the logic for the response callbacks.
# The on_complete callback is the only one at the moment.
#
# You can set multiple callbacks, which are then executed
# in the same order.
#
# easy.on_complete { p 1 }
# easy.on_complete { p 2 }
# easy.complete
# #=> 1
# #=> 2
#
# You can clear the callbacks:
#
# easy.on_complete { p 1 }
# easy.on_complete { p 2 }
# easy.on_complete.clear
# easy.on_complete
# #=> []
module ResponseCallbacks
# Set on_headers callback.
#
# @example Set on_headers.
# request.on_headers { p "yay" }
#
# @param [ Block ] block The block to execute.
def on_headers(&block)
@on_headers ||= []
@on_headers << block if block_given?
@on_headers
end
# Execute on_headers callbacks.
#
# @example Execute on_headers.
# request.headers
def headers
return if @headers_called
@headers_called = true
if defined?(@on_headers) and not @on_headers.nil?
result = nil
@on_headers.each do |callback|
result = callback.call(self)
break if result == :abort
end
result
end
end
# Set on_complete callback.
#
# @example Set on_complete.
# request.on_complete { p "yay" }
#
# @param [ Block ] block The block to execute.
def on_complete(&block)
@on_complete ||= []
@on_complete << block if block_given?
@on_complete
end
# Execute on_complete callbacks.
#
# @example Execute on_completes.
# request.complete
def complete
headers unless @response_headers.empty?
if defined?(@on_complete) and not @on_complete.nil?
@on_complete.each{ |callback| callback.call(self) }
end
end
# Set on_progress callback.
#
# @example Set on_progress.
# request.on_progress {|dltotal, dlnow, ultotal, ulnow| p "#{dltotal} #{dlnow} #{ultotal} #{ulnow}" }
#
# @param [ Block ] block The block to execute.
def on_progress(&block)
@on_progress ||= []
if block_given?
@on_progress << block
set_progress_callback
self.noprogress = 0
end
@on_progress
end
# Execute on_progress callbacks.
#
# @example Execute on_progress.
# request.body(1, 1, 1, 1)
def progress(dltotal, dlnow, ultotal, ulnow)
if defined?(@on_progress) and not @on_progress.nil?
@on_progress.each{ |callback| callback.call(dltotal, dlnow, ultotal, ulnow) }
end
end
# Set on_body callback.
#
# @example Set on_body.
# request.on_body { |chunk| p "yay" }
#
# @param [ Block ] block The block to execute.
def on_body(&block)
@on_body ||= []
@on_body << block if block_given?
@on_body
end
# Execute on_body callbacks.
#
# @example Execute on_body.
# request.body("This data came from HTTP.")
#
# @return [ Object ] If there are no on_body callbacks, returns the symbol :unyielded.
def body(chunk)
if defined?(@on_body) and not @on_body.nil?
result = nil
@on_body.each do |callback|
result = callback.call(chunk, self)
break if result == :abort
end
result
else
:unyielded
end
end
end
end
end
@@ -0,0 +1,28 @@
# frozen_string_literal: true
module Ethon
class Easy # :nodoc:
# This module contains small helpers.
#
# @api private
module Util
# Escapes zero bytes in strings.
#
# @example Escape zero bytes.
# Util.escape_zero_byte("1\0")
# #=> "1\\0"
#
# @param [ Object ] value The value to escape.
#
# @return [ String, Object ] Escaped String if
# zero byte found, original object if not.
def escape_zero_byte(value)
return value unless value.to_s.include?(0.chr)
value.to_s.gsub(0.chr, '\\\0')
end
extend self
end
end
end