IOStreams Any size. Any format. Anywhere.

File Extensions

IOStreams uses the extensions in the file name to determine which streams to apply when reading or writing a file. Multiple extensions are applied in order, so sample.csv.gz.pgp is first decrypted with PGP and then decompressed with GZip when read.

Supported extensions:

Extension Stream Read Write Required gem / program
.bz2 BZip2 Yes Yes bzip2-ffi
.enc Symmetric Encryption Yes Yes symmetric-encryption
.gz, .gzip GZip Yes Yes None (Ruby standard library)
.zip Zip Yes Yes rubyzip (read), zip_kit (write). On JRuby the built-in Java zip support is used for reading.
.pgp, .gpg PGP Yes Yes GnuPG command line program (gpg)
.xlsx, .xlsm Excel Spreadsheet Yes No creek

The gems above are soft dependencies: IOStreams does not require them for installation, they only need to be added to the Gemfile when the corresponding extension is used.

Compression options

GZip accepts a compression level when writing, from 0 (no compression) to 9 (best compression):

IOStreams.path("sample.csv.gz").option(:gz, level: 9).write(data)

BZip2 passes its options through to bzip2-ffi: block_size (1 to 9) and work_factor (0 to 250) when writing, and small and first_only when reading:

IOStreams.path("sample.csv.bz2").option(:bz2, block_size: 9).write(data)
IOStreams.path("sample.csv.bz2").option(:bz2, small: true).read

Reading ignores the options for writing, and writing ignores the options for reading, so the same path can be written and then read. An option that neither accepts, such as a misspelled one, raises an ArgumentError. See Options for reading and writing.

Reading an Excel Spreadsheet

Each row in the spreadsheet is converted into a CSV line, so the regular :line, :array, and :hash modes apply:

IOStreams.path("spreadsheet.xlsx").each(:hash) do |record|
  p record
end

Notes:

Character encoding

The special :encode stream converts the character encoding of the data being read or written. It is applied with option or stream rather than a file name extension:

IOStreams.path("sample.csv.gz").
  option(:encode, encoding: "UTF-8", cleaner: :printable, replace: "").
  each do |line|
    puts line
  end

Options:

Registering a custom extension

To add a new extension, register the format that reads and writes it, usually a module that answers reader_class, writer_class, compressed? and encrypted?, and extends IOStreams::StreamFormat, which checks the options supplied for the stream against those of its reader and writer. The reader and writer classes must implement .open that yields a stream implementing #read or #write respectively. See any of the streams under lib/io_streams for examples.

module MyXz
  extend IOStreams::StreamFormat

  def self.reader_class
    MyXz::Reader
  end

  def self.writer_class
    MyXz::Writer
  end

  # Whether data in this format is compressed, for `#compressed?` on a path or stream.
  def self.compressed?
    true
  end

  # Whether data in this format is encrypted, for `#encrypted?` on a path or stream.
  def self.encrypted?
    false
  end
end

IOStreams.register_extension(:xz, MyXz)

A format that is neither compressed nor encrypted can instead be registered with just its reader and writer classes:

IOStreams.register_extension(:xls, MyXls::Reader, MyXls::Writer)

To use a registered format for another extension, register the format that IOStreams.extensions returns:

IOStreams.register_extension(:tgz, IOStreams.extensions[:gz])

Similarly, to support a new storage location, supply a Path class for its URI scheme. See IOStreams::Paths::S3 for an example of what is required.

IOStreams.register_scheme(:gcs, MyGoogleCloudStoragePath)

When writing fails, the path class is responsible for not leaving an incomplete file behind, for example by writing to a temp file and only uploading it once it is complete, like the S3, SFTP and HTTP paths, or by removing the incomplete file, like the local file path. IOStreams does not delete the target of a failed write or copy.