ironsand
ironsand

Reputation: 15151

How to add a method to URI module

I want to download a binary file from http or https URL like:

URI('https://www.google.com/favicon.ico').download('google.ico')

I wrote the method for it like this:

module URI
  def download(file)
    File.open(file, 'wb') {|f| f.write(open(self).read)}
  end
end

This method ends up with an error ArgumentError: extra arguments, while following code is working.

file = 'google.ico'
url = 'https://www.google.com/favicon.ico')
File.open(file, 'wb') {|f| f.write(open(url).read)}

What am I doing wrong? How should I fix it?

Upvotes: 1

Views: 264

Answers (2)

ironsand
ironsand

Reputation: 15151

I thought I'm calling Kernel.open from open-uri, but inside the module URI OpenURI::OpenRead is called.

At first I added binding.pry

module URI
  def download(file)
    binding.pry
    File.open(file, 'wb') {|f| f.write(open(self).read)}
  end
end

And checked which method is called like this:

pry(#<URI::HTTPS>)> show-method open

From: /Users/ironsand/.rbenv/versions/2.4.3/lib/ruby/2.4.0/open-uri.rb @ line 720:
Owner: OpenURI::OpenRead
Visibility: public
Number of lines: 3

def open(*rest, &block)
  OpenURI.open_uri(self, *rest, &block)
end

pry(#<URI::HTTPS>)> exit
pry(main)> show-method open

From: /Users/ironsand/.rbenv/versions/2.4.3/lib/ruby/2.4.0/open-uri.rb @ line 29:
Owner: Kernel
Visibility: private
Number of lines: 11

def open(name, *rest, &block) # :doc:
  if name.respond_to?(:open)
    name.open(*rest, &block)
  elsif name.respond_to?(:to_str) &&
        %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ name &&
        (uri = URI.parse(name)).respond_to?(:open)
    uri.open(*rest, &block)
  else
    open_uri_original_open(name, *rest, &block)
  end
end

To use proper method I should have called the method explicitly.

module URI
  def download(file)
    File.open(file, 'wb') {|f| f.write(OpenURI.open_uri(self).read)}
  end
end

The code above works as I expected.

Upvotes: 1

lacostenycoder
lacostenycoder

Reputation: 11206

URI module doesn't download file. File class doesn't either. open comes from open-uri stdlib which is why it works in your 2nd example. If you have curl in your system this should work:

module URI
  def self.download(file_url)
    filename = file_url.split('/').last
    `curl -O #{file_url}`
  end
end

If you DON'T have curl use open-uri

require 'open-uri'
module URI
  def self.download(file_url)
    filename = file_url.split('/').last
    File.open(filename, "wb") do |saved_file|
      open(file_url, "rb") do |read_file|
        saved_file.write(read_file.read)
      end
    end
  end
end

And call it like this

URI.download('https://www.google.com/favicon.ico')

Note, URI behaves like more like a class so you need to define the method on the base object self otherwise you'll need to create an instance, but since it's just a module, use def self.some_method(some_arg) to be able to call URL.some_method(some_arg)

While this works, it is not recommended for production. Why do you wanna monkey patch URI when you can simply write your own module which does this?

You're better off doing something like this:

module SimpleFileDownload
  require 'open-uri'
  def self.download(file_url)
    filename = file_url.split('/').last
    File.open(filename, "wb") do |saved_file|
      open(file_url, "rb") do |read_file|
        saved_file.write(read_file.read)
      end
    end
  end
end

and call it like:

SimpleFileDownload.download('https://www.google.com/favicon.ico')

Upvotes: 1

Related Questions