defmodule Fsdb do use GenServer @rowre ~r/row\.[0-9]+/ @rowid "row.id" ########################################## # Public API ########################################## def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts) end def stop(pid) do GenServer.cast(pid, :stop) end def create(pid, table) do GenServer.call(pid, {:create, table}) end def drop(pid, table) do GenServer.call(pid, {:drop, table}) end def list(pid, table) do GenServer.call(pid, {:list, table}) end def insert(pid, table, row) do GenServer.call(pid, {:insert, table, row}) end def fetch(pid, table, id) do GenServer.call(pid, {:fetch, table, id}) end def update(pid, table, id, row) do GenServer.call(pid, {:update, table, id, row}) end def delete(pid, table, id) do GenServer.call(pid, {:delete, table, id}) end ########################################## # GenServer Implementation ########################################## def init(opts) do #path expansion should happend only once at config time #to ensure it repeats even under current directory change path = Keyword.fetch!(opts, :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({: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) File.mkdir_p!(folder) file = Path.join(folder, @rowid) :ok = dump_(file, 0) end defp drop_(path, table) do folder = Path.join(path, table) File.rm_rf!(folder) :ok #rm returns list of files deleted 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 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