defmodule Fsdb.Server do @moduledoc """ Server module. """ use GenServer @rowre ~r/row\.[0-9]+/ @rowid "row.id" ########################################## # Public API ########################################## @doc """ Starts the GenServer. `state` *must* contain a valid path in the `:path` key. `opts` is optional and is passed verbatim to GenServer. Returns `{:ok, pid}`. ## Example ``` Fsdb.Server.start_link([path: "./fsdb"], [name: Fsdb.Server]) ``` """ def start_link(state, opts \\ []) do GenServer.start_link(__MODULE__, state, opts) end @doc """ Stops the GenServer. Returns `:ok`. """ def stop(pid) do GenServer.cast(pid, :stop) end ########################################## # GenServer Implementation ########################################## def init(state) do #path expansion should happend only once at config time #to ensure it repeats even under current directory change path = Keyword.fetch!(state, :path) File.mkdir_p!(path) {:ok, {path}} end def handle_cast(:stop, state={_path}) do {:stop, :normal, state} end def handle_call({:create, table}, _from, state={path}) do {:reply, create_(path, table), state} end def handle_call({:drop, table}, _from, state={path}) do {:reply, drop_(path, table), state} end def handle_call({:list, table}, _from, state={path}) do {:reply, list_(path, table), state} end def handle_call({:insert, table, row}, _from, state={path}) do {:reply, insert_(path, table, row), state} end def handle_call({:save, table, id, row}, _from, state={path}) do {:reply, save_(path, table, id, row), state} end def handle_call({:fetch, table, id}, _from, state={path}) do {:reply, fetch_(path, table, id), state} end def handle_call({:update, table, id, row}, _from, state={path}) do {:reply, update_(path, table, id, row), state} end def handle_call({:delete, table, id}, _from, state={path}) do {:reply, delete_(path, table, id), state} end ########################################## # Internal Implementation ########################################## defp create_(path, table) do folder = Path.join(path, table) if !File.exists?(folder) do File.mkdir_p!(folder) file = Path.join(folder, @rowid) :ok = dump_(file, 0) end :ok end defp drop_(path, table) do folder = Path.join(path, table) File.rm_rf!(folder) :ok #rm returns list of deleted files end defp list_(path, table) do folder = Path.join(path, table) for file <- File.ls!(folder), Regex.match?(@rowre, file) do parse_(Path.join(folder, file)) end end defp parse_(file) do txt = File.read!(file) |> :erlang.binary_to_list {:ok, tokens, _} = :erl_scan.string(txt) {:ok, row} = :erl_parse.parse_term(tokens) row end defp insert_(path, table, row) do folder = Path.join(path, table) id = next_(folder) file = Path.join(folder, rowid(id)) :ok = dump_(file, {id, row}) {id, row} end defp save_(path, table, id, row) do folder = Path.join(path, table) file = Path.join(folder, rowid(id)) :ok = dump_(file, {id, row}) {id, row} end defp next_(folder) do :ok = File.mkdir_p(folder) file = Path.join(folder, @rowid) id = parse_(file) + 1 :ok = dump_(file, id) id end defp dump_(file, row) do File.write! file, :io_lib.format("~p.", [row]) end defp fetch_(path, table, id) do folder = Path.join(path, table) file = Path.join(folder, rowid(id)) if File.exists?(file) do parse_(file) else {:not_found, table, id} end end defp rowid(id) do fid = Integer.to_string(id) |> String.rjust(6, ?0) "row.#{fid}" end defp update_(path, table, id, row) do folder = Path.join(path, table) file = Path.join(folder, rowid(id)) if File.exists?(file) do :ok = dump_(file, {id, row}) {id, row} else {:not_found, table, id} end end defp delete_(path, table, id) do folder = Path.join(path, table) file = Path.join(folder, rowid(id)) if File.exists?(file) do {id, row} = parse_(file) File.rm!(file) {id, row} else {:not_found, table, id} end end end