defmodule Chunker.DiscBased.WriteableFile do @moduledoc false use GenServer alias Chunker.AlreadyClosedError alias Chunker.DiscBased.Helper alias Chunker.DiscBased.ReadOnlyFile defstruct path: nil, chunked_path: nil, pid: nil, chunk_size: 1024 * 1024 ## Client ## def new(path, chunk_size) do with {:ok, chunked_path} <- mkdir_if_nonexistant(path <> ".chunked"), :ok <- create_chunk_map(chunked_path), {:ok, pid} <- GenServer.start_link(__MODULE__, :ok, []) do {:ok, %__MODULE__{path: path, chunked_path: chunked_path, pid: pid, chunk_size: chunk_size}} else err -> err end end def append_chunk(chunked_file, data) do with true <- Process.alive?(chunked_file.pid), :ok <- GenServer.call(chunked_file.pid, {:append_chunk, chunked_file, data}) do {:ok, chunked_file} else false -> {:error, %AlreadyClosedError{}} err -> err end end def insert_chunk(chunked_file, data, index) when is_integer(index) and index >= 0 do with true <- Process.alive?(chunked_file.pid), :ok <- GenServer.call(chunked_file.pid, {:insert_chunk, chunked_file, data, index}) do {:ok, chunked_file} else false -> {:error, %AlreadyClosedError{}} err -> err end end def remove_chunk(chunked_file, index) when is_integer(index) and index >= 0 do with true <- Process.alive?(chunked_file.pid), :ok <- GenServer.call(chunked_file.pid, {:remove_chunk, chunked_file, index}) do {:ok, chunked_file} else false -> {:error, %AlreadyClosedError{}} err -> err end end def get_chunk(chunked_file, index) when is_integer(index) and index >= 0 do with {:ok, chunks} <- Helper.read_chunk_map(chunked_file), {:ok, chunk_path} <- Helper.mapped_chunk_path(chunked_file, chunks, index) do File.read(chunk_path) else err -> err end end def length(chunked_file) do case Helper.read_chunk_map(chunked_file) do {:ok, chunks} -> {:ok, Kernel.length(chunks)} err -> err end end def commit(chunked_file) do with true <- Process.alive?(chunked_file.pid), {:ok, path} <- GenServer.call(chunked_file.pid, {:commit, chunked_file}) do ReadOnlyFile.new(path, chunked_file.chunk_size) else false -> {:error, %AlreadyClosedError{}} err -> err end end def path(chunked_file), do: chunked_file.path def remove(chunked_file) do case File.rm_rf(chunked_file.chunked_path) do {:ok, _} -> :ok err -> err end end def close(chunked_file) do case Process.alive?(chunked_file.pid) do true -> GenServer.stop(chunked_file.pid) false -> {:error, %AlreadyClosedError{}} end end def closed?(chunked_file) do !Process.alive?(chunked_file.pid) end ## Server ## @impl true def init(:ok) do {:ok, nil} end @impl true def handle_call({:insert_chunk, chunked_file = %__MODULE__{}, data, index}, _from, state) do result = Helper.add_chunk(chunked_file, data, index, fn chunks, chunk_index, index -> List.insert_at(chunks, index, chunk_index) end) {:reply, result, state} end @impl true def handle_call({:remove_chunk, chunked_file = %__MODULE__{}, index}, _from, state) do result = with {:ok, chunks} <- Helper.read_chunk_map(chunked_file), {:ok, chunk_path} <- Helper.mapped_chunk_path(chunked_file, chunks, index), :ok <- File.rm(chunk_path), new_chunks <- List.delete_at(chunks, index) do Helper.write_chunk_map(chunked_file, new_chunks) else err -> err end {:reply, result, state} end @impl true def handle_call({:commit, chunked_file = %__MODULE__{}}, _from, state) do result = with {:ok, target} <- Helper.file_stream(chunked_file.path), {:ok, chunks} <- Helper.read_chunk_map(chunked_file), :ok <- chunks |> Stream.map(&Helper.chunk_path(chunked_file, &1)) |> Stream.flat_map(&File.stream!(&1, [:read], 4096)) |> Stream.into(target) |> Stream.run(), :ok <- remove(chunked_file) do {:ok, chunked_file.path} else err -> err end {:stop, :normal, result, state} end @impl true def terminate(reason, _state) do {:shutdown, reason} end ## Helper functions ## defp mkdir_if_nonexistant(path) do case File.mkdir(path) do :ok -> {:ok, path} {:error, :eexist} -> {:ok, path} err -> err end end defp create_chunk_map(path) do chunk_map_path = Path.join(path, "chunk_map") File.touch(chunk_map_path) end end defimpl Chunker.ChunkedFile, for: Chunker.DiscBased.WriteableFile do alias Chunker.DiscBased.WriteableFile alias Chunker.DiscBased.Helper defdelegate insert_chunk(chunked_file, data, index), to: WriteableFile defdelegate remove_chunk(chunked_file, index), to: WriteableFile def get_chunk(chunked_file, index) when is_integer(index) and index >= 0 do with {:ok, chunks} <- Helper.read_chunk_map(chunked_file), {:ok, chunk_path} <- Helper.mapped_chunk_path(chunked_file, chunks, index) do File.read(chunk_path) else err -> err end end defdelegate length(chunked_file), to: WriteableFile defdelegate commit(chunked_file), to: WriteableFile def writeable?(_), do: true def path(chunked_file), do: chunked_file.path defdelegate remove(chunked_file), to: WriteableFile defdelegate close(chunked_file), to: WriteableFile defdelegate closed?(chunked_file), to: WriteableFile end