lib/file_reader.ex

defmodule FileReader do
  @moduledoc """
    Helper module to accelerate linear file reads. Reads next `buffer_size`
    bytes and keeps in state.
  """
  defstruct [:fp, :module, :buffer_size, :offset, :chunk]

  def new(fp, loc, opts \\ []) do
    module = Keyword.get(opts, :module, :file)
    buffer_size = Keyword.get(opts, :buffer_size, 64_000)

    %FileReader{
      fp: fp,
      module: module,
      buffer_size: buffer_size,
      offset: loc,
      chunk: ""
    }
  end

  def read(state = %FileReader{buffer_size: buffer_size, chunk: chunk}, n)
      when is_integer(n) and n < buffer_size do
    if byte_size(chunk) < n do
      case read_chunk(state) do
        :eof -> :eof
        state -> read(state, n)
      end
    else
      ret = binary_part(chunk, 0, n)
      chunk = binary_part(chunk, n, byte_size(chunk) - n)
      {%FileReader{state | chunk: chunk}, ret}
    end
  end

  def read(state = %FileReader{fp: fp, module: module, offset: offset, chunk: chunk}, n)
      when is_integer(n) do
    case module.pread(fp, offset, n - byte_size(chunk)) do
      {:ok, binary} ->
        {%FileReader{state | offset: offset + byte_size(binary), chunk: ""}, chunk <> binary}

      :eof ->
        :eof
    end
  end

  defp read_chunk(
         state = %FileReader{
           fp: fp,
           module: module,
           buffer_size: buffer_size,
           chunk: chunk,
           offset: offset
         }
       ) do
    case module.pread(fp, offset, buffer_size) do
      {:ok, binary} ->
        %FileReader{state | chunk: chunk <> binary, offset: offset + byte_size(binary)}

      :eof ->
        :eof
    end
  end
end