defmodule Mongo.GridFs.Download do @moduledoc """ The main entry point for downloading files from the grid-fs specified by the bucket struct. """ alias BSON.ObjectId alias Mongo.GridFs.Bucket @type result :: {:error, :unknown} | {:error, :length_is_zero} | {:error, :not_found} | {:ok, Mongo.cursor} @doc """ Opens a Stream from which the application can read the contents of the stored file specified by fileId. The fileId can be a string, an ObjectId or just a map with the keys `length` and `_id`. In case of the map the function tries to stream the chunks described by the `length` and the `_id` values. Returns a Stream. """ @spec open_download_stream(Bucket.t, String.t | BSON.ObjectId.t | map()) :: result def open_download_stream(%Bucket{topology_pid: topology_pid, opts: opts} = bucket, file_id) when is_binary(file_id) do topology_pid |> Mongo.find_one(Bucket.files_collection_name(bucket), %{"_id" => ObjectId.decode!(file_id)}, opts) |> stream_chunk(bucket) end def open_download_stream(%Bucket{topology_pid: topology_pid, opts: opts} = bucket, %BSON.ObjectId{} = oid) do topology_pid |> Mongo.find_one(Bucket.files_collection_name(bucket), %{"_id" => oid}, opts) |> stream_chunk(bucket) end def open_download_stream(bucket, %{"length" => _, "_id" => _} = file) do stream_chunk(file, bucket) end @doc """ Same as above, but returns also the file document. """ @spec find_and_stream(Bucket.t, String.t) :: {result, BSON.document} def find_and_stream(bucket, file_id) def find_and_stream(%Bucket{topology_pid: topology_pid, opts: opts} = bucket, file_id) when is_binary(file_id) do file = Mongo.find_one(topology_pid, Bucket.files_collection_name(bucket), %{"_id" => ObjectId.decode!(file_id)}, opts) {stream_chunk(file, bucket), file} end def find_and_stream(%Bucket{topology_pid: topology_pid, opts: opts} = bucket, file_id) do file = Mongo.find_one(topology_pid, Bucket.files_collection_name(bucket), %{"_id" => file_id}, opts) {stream_chunk(file, bucket), file} end def find_one_file(%Bucket{topology_pid: topology_pid, opts: opts} = bucket, filename) when is_binary(filename) do Mongo.find_one(topology_pid, Bucket.files_collection_name(bucket), %{"filename" => filename}, opts) end def find_one_file(%Bucket{topology_pid: topology_pid, opts: opts} = bucket, %BSON.ObjectId{} = file_id) do Mongo.find_one(topology_pid, Bucket.files_collection_name(bucket), %{"_id" => file_id}, opts) end ## # In case that the file map is nil we return :error # defp stream_chunk(nil, _bucket), do: {:error, :not_found} ## # However, when downloading a zero length stored file the driver MUST NOT issue a query against the chunks # collection, since that query is not necessary. For a zero length file, drivers return either an empty # stream or send nothing to the provided stream (depending on the download method). ## defp stream_chunk(%{"length" => 0}, _bucket), do: {:error, :length_is_zero} ## # Streaming the chunks with `file_id` sorted ascending by n # defp stream_chunk(%{"_id" => id}, %Bucket{topology_pid: topology_pid, opts: opts} = bucket) do opts = Keyword.merge(opts, [sort: [n: 1]]) stream = topology_pid |> Mongo.find(Bucket.chunks_collection_name(bucket), %{files_id: id}, opts) |> Stream.map(fn map -> map["data"].binary end) {:ok, stream} end ## # catch up for other cases # defp stream_chunk(_, _bucket), do: {:error, :unknown} end