Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for :writedata/:file options -- write body directly to a file #100

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions lib/ethon/curls/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ def set_option(option, value, handle, type = :easy)
s = value.to_s unless value.nil?
value = FFI::MemoryPointer.new(:char, s.bytesize)
value.put_bytes(0, s)
when :file
func=:ffipointer
# We expect that easy is passing us a FILE *
when :string_escape_null
func=:string
value=Util.escape_zero_byte(value) unless value.nil?
Expand Down Expand Up @@ -126,7 +129,8 @@ def set_option(option, value, handle, type = :easy)
:ffipointer => :objectpoint, # FFI::Pointer
:curl_slist => :objectpoint,
:buffer => :objectpoint, # A memory buffer of size defined in the options
:dontuse_object => :objectpoint, # An object we don't support (e.g. FILE*)
:dontuse_object => :objectpoint, # An object we don't support
:file => :objectpoint,
:cbdata => :objectpoint,
:callback => :functionpoint,
:debug_callback => :functionpoint,
Expand Down Expand Up @@ -227,7 +231,7 @@ def #{type.to_s.downcase}_options(rt=nil)
option :easy, :wildcardmatch, :bool, 197
## CALLBACK OPTIONS
option :easy, :writefunction, :callback, 11
option :easy, :file, :cbdata, 1
option :easy, :file, :file, 1
option_alias :easy, :file, :writedata
option :easy, :readfunction, :callback, 12
option :easy, :infile, :cbdata, 9
Expand Down
9 changes: 8 additions & 1 deletion lib/ethon/easy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require 'ethon/easy/response_callbacks'
require 'ethon/easy/debug_info'
require 'ethon/easy/mirror'
require 'ethon/easy/files'

module Ethon

Expand Down Expand Up @@ -39,6 +40,7 @@ class Easy
include Ethon::Easy::Http
include Ethon::Easy::Operations
include Ethon::Easy::ResponseCallbacks
include Ethon::Easy::Files

# Returns the curl return code.
#
Expand Down Expand Up @@ -216,7 +218,7 @@ class Easy
def initialize(options = {})
Curl.init
set_attributes(options)
set_callbacks
set_callbacks(options)
end

# Set given options.
Expand All @@ -231,6 +233,10 @@ def initialize(options = {})
# @see initialize
def set_attributes(options)
options.each_pair do |key, value|
if key == :file || key == :writedata
# TODO: modes other than 'w+'
value = open_file(value, 'w+')
end
method = "#{key}="
unless respond_to?(method)
raise Errors::InvalidOption.new(key)
Expand All @@ -252,6 +258,7 @@ def reset
@on_body = nil
@procs = nil
@mirror = nil
close_all_files
Curl.easy_reset(handle)
set_callbacks
end
Expand Down
13 changes: 10 additions & 3 deletions lib/ethon/easy/callbacks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@ def self.included(base)
#
# @example Set callbacks.
# easy.set_callbacks
def set_callbacks
Curl.set_option(:writefunction, body_write_callback, handle)
def set_callbacks(options = {})
if options[:file].nil? && options[:writedata].nil?
Curl.set_option(:writefunction, body_write_callback, handle)
@response_body = ""
else
# If we are writing to a file, set writefuntion to NULL
Curl.set_option(:writefunction, nil, handle)
@response_body = nil
end
Curl.set_option(:headerfunction, header_write_callback, handle)
Curl.set_option(:debugfunction, debug_callback, handle)
@response_body = ""
on_complete { close_all_files }
@response_headers = ""
@headers_called = false
@debug_info = Ethon::Easy::DebugInfo.new
Expand Down
18 changes: 18 additions & 0 deletions lib/ethon/easy/files.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module Ethon
class Easy
# This module contains logic for managing open files on an easy instance
module Files
def open_file(path, mode)
@open_files ||= []
fh = Libc.ffi_fopen(path, mode)
@open_files << fh
fh
end

def close_all_files
@open_files.each {|fh| Libc.ffi_fclose(fh)} unless @open_files.nil?
@open_files = []
end
end
end
end
13 changes: 13 additions & 0 deletions lib/ethon/libc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,22 @@ def self.windows?
Gem.win_platform?
end

def self.ffi_fclose(fh)
retv = fclose(fh)
fail SystemCallError.new(FFI.errno) unless retv == 0
end

def self.ffi_fopen(path, mode)
fh = fopen(path, mode)
fail SystemCallError.new(FFI.errno), path if fh == nil
return fh
end

unless windows?
attach_function :getdtablesize, [], :int
attach_function :free, [:pointer], :void
attach_function :fopen, [:string, :string], :pointer
attach_function :fclose, [:pointer], :int
end
end
end
7 changes: 7 additions & 0 deletions spec/ethon/easy/callbacks_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
easy.set_callbacks
end

[:file, :writedata].each do |file_opt|
it "sets @response_body to null if file passed" do
easy.set_callbacks({file_opt => '/some/file'})
expect(easy.instance_variable_get(:@response_body)).to be_nil
end
end

it "resets @response_body" do
easy.set_callbacks
expect(easy.instance_variable_get(:@response_body)).to eq("")
Expand Down
30 changes: 30 additions & 0 deletions spec/ethon/easy/files_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require 'spec_helper'

describe Ethon::Easy::Files do
let!(:easy) { Ethon::Easy.new }

describe "#open_file" do
path, mode = '/i/am/no/file', 'w'

before do
expect(Ethon::Libc).to receive(:ffi_fopen).with(path, mode)
end

it "calls ffi_fopen" do
easy.open_file(path, mode)
end

it "tracks the open files in @open_files" do
easy.open_file(path, mode)
expect(easy.instance_variable_get(:@open_files).count).to eq(1)
end
end

describe "#close_all_files" do
it "calls ffi_fclose on all open files" do
easy.instance_variable_set(:@open_files, [nil])
expect(Ethon::Libc).to receive(:ffi_fclose).exactly(1).times
easy.close_all_files
end
end
end
8 changes: 5 additions & 3 deletions spec/ethon/easy/response_callbacks_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,24 @@

context "when no block given" do
it "returns @#{callback_type}" do
expect(easy.send("#{callback_type}")).to eq([])
expect(easy.send("#{callback_type}")).to be_kind_of(Array)
end
end

context "when block given" do
it "stores" do
orig_size = easy.send(callback_type).count
easy.send(callback_type) { p 1 }
expect(easy.instance_variable_get("@#{callback_type}").size).to eq(1)
expect(easy.instance_variable_get("@#{callback_type}").size).to eq(orig_size + 1)
end
end

context "when multiple blocks given" do
it "stores" do
orig_size = easy.send(callback_type).count
easy.send(callback_type) { p 1 }
easy.send(callback_type) { p 2 }
expect(easy.instance_variable_get("@#{callback_type}").size).to eq(2)
expect(easy.instance_variable_get("@#{callback_type}").size).to eq(orig_size + 2)
end
end
end
Expand Down
18 changes: 17 additions & 1 deletion spec/ethon/easy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@
expect{ easy.set_attributes({:fubar => 1}) }.to raise_error(Ethon::Errors::InvalidOption)
end
end

context "when file passed" do
it "opens the file" do
[:file, :writedata].each do |file_opt|
path = '/i/am/not/a/file'
expect(easy).to receive(:open_file).with(path, 'w+')
easy.set_attributes({file_opt => path})
end
end
end
end
end

Expand All @@ -75,9 +85,10 @@
end

it "resets on_complete" do
orig_size = easy.on_complete.count
easy.on_complete { p 1 }
easy.reset
expect(easy.on_complete).to be_empty
expect(easy.on_complete.count).to eq(orig_size)
end

it "resets on_headers" do
Expand All @@ -86,6 +97,11 @@
expect(easy.on_headers).to be_empty
end

it "closes files" do
easy.reset
expect(easy.instance_variable_get(:@open_files)).to eq([])
end

it "resets on_body" do
easy.on_body { p 1 }
easy.reset
Expand Down