From ea273fe149ce87aa5ad0df47aa78fb6dc0128742 Mon Sep 17 00:00:00 2001 From: DecafMango Date: Mon, 9 Dec 2024 17:40:51 +0300 Subject: [PATCH 1/2] add rest just for files --- lib/backend/files/file.ex | 20 +++++++ lib/backend/files/files.ex | 28 ++++++++++ lib/backend/schemas/file.ex | 12 ---- lib/backend/schemas/user.ex | 12 ---- .../controllers/file_controller.ex | 56 +++++++++++++++++++ lib/backend_web/router.ex | 6 +- mix.lock | 2 +- .../migrations/20241129212331_add_user.exs | 10 ---- .../migrations/20241129212656_add_file.exs | 4 +- 9 files changed, 112 insertions(+), 38 deletions(-) create mode 100644 lib/backend/files/file.ex create mode 100644 lib/backend/files/files.ex delete mode 100644 lib/backend/schemas/file.ex delete mode 100644 lib/backend/schemas/user.ex create mode 100644 lib/backend_web/controllers/file_controller.ex delete mode 100644 priv/repo/migrations/20241129212331_add_user.exs diff --git a/lib/backend/files/file.ex b/lib/backend/files/file.ex new file mode 100644 index 0000000..2c37e75 --- /dev/null +++ b/lib/backend/files/file.ex @@ -0,0 +1,20 @@ +defmodule Backend.Files.File do + @moduledoc """ + PostgreSQL schema: `File` + > Managed by *Ecto ORM* + """ + use Ecto.Schema + import Ecto.Changeset + + @derive {Jason.Encoder, only: [:id, :name, :content]} + schema "files" do + field :name, :string + field :content, :string + end + + def changeset(file, attrs) do + file + |> cast(attrs, [:name, :content], empty_values: [:content]) + |> validate_required([:name]) + end +end diff --git a/lib/backend/files/files.ex b/lib/backend/files/files.ex new file mode 100644 index 0000000..4ad5649 --- /dev/null +++ b/lib/backend/files/files.ex @@ -0,0 +1,28 @@ +defmodule Backend.Files do + @moduledoc """ + Context for File + """ + alias Backend.Repo + alias Backend.Files.File + + def get_file(id) do + Repo.get(File, id) + end + + def create_file(attrs \\ %{}) do + %File{} + |> File.changeset(attrs) + |> Repo.insert() + end + + def save_file(file) do + file + |> File.changeset(%{}) + |> Repo.update() + end + + def delete_file(file) do + file + |> Repo.delete() + end +end diff --git a/lib/backend/schemas/file.ex b/lib/backend/schemas/file.ex deleted file mode 100644 index b89c1d9..0000000 --- a/lib/backend/schemas/file.ex +++ /dev/null @@ -1,12 +0,0 @@ -defmodule Backend.File do - @moduledoc """ - PostgreSQL schema: `File` - > Managed by *Ecto ORM* - """ - use Ecto.Schema - - schema "files" do - field :filename, :string - field :created_at, :date - end -end diff --git a/lib/backend/schemas/user.ex b/lib/backend/schemas/user.ex deleted file mode 100644 index 162a871..0000000 --- a/lib/backend/schemas/user.ex +++ /dev/null @@ -1,12 +0,0 @@ -defmodule Backend.User do - @moduledoc """ - PostgreSQL schema: `User` - > Managed by *Ecto ORM* - """ - use Ecto.Schema - - schema "users" do - field :username, :string - field :password, :string - end -end diff --git a/lib/backend_web/controllers/file_controller.ex b/lib/backend_web/controllers/file_controller.ex new file mode 100644 index 0000000..4d97003 --- /dev/null +++ b/lib/backend_web/controllers/file_controller.ex @@ -0,0 +1,56 @@ +defmodule BackendWeb.FileController do + alias Backend.Files + use BackendWeb, :controller + + def get_file(conn, %{"id" => id}) do + file = Files.get_file(id) + + case file do + nil -> + conn + |> put_status(404) + |> json(%{"message" => "File with id " <> id <> " does not exist"}) + + _ -> + conn + |> put_status(200) + |> json(file) + end + end + + def create_file(conn, %{"name" => name}) do + case Files.create_file(%{"name" => name, "content" => ""}) do + {:ok, file} -> + conn + |> put_status(201) + |> json(file) + + {:error, _} -> + conn + |> put_status(500) + |> json(%{"message" => "Error occured"}) + end + end + + def delete_file(conn, %{"id" => id}) do + file = Files.get_file(id) + + if file == nil do + conn + |> put_status(404) + |> json(%{"message" => "File with id " <> id <> " not found"}) + else + case Files.delete_file(file) do + {:ok, _} -> + conn + |> put_status(200) + |> json(%{"message" => "File with id " <> id <> " has been deleted successfully"}) + + {:error, _} -> + conn + |> put_status(500) + |> json(%{"message" => "Error occured"}) + end + end + end +end diff --git a/lib/backend_web/router.ex b/lib/backend_web/router.ex index bab5e1d..70eb75f 100644 --- a/lib/backend_web/router.ex +++ b/lib/backend_web/router.ex @@ -6,7 +6,7 @@ defmodule BackendWeb.Router do plug :fetch_session plug :fetch_live_flash plug :put_root_layout, html: {BackendWeb.Layouts, :root} - plug :protect_from_forgery + # plug :protect_from_forgery plug :put_secure_browser_headers end @@ -18,6 +18,10 @@ defmodule BackendWeb.Router do pipe_through :browser get "/", PageController, :home + + get "/file/:id", FileController, :get_file + post "/file", FileController, :create_file + delete "/file/:id", FileController, :delete_file end # Other scopes may use custom stacks. diff --git a/mix.lock b/mix.lock index 381c9de..ea856cf 100644 --- a/mix.lock +++ b/mix.lock @@ -19,7 +19,7 @@ "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, - "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized"]}, + "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, diff --git a/priv/repo/migrations/20241129212331_add_user.exs b/priv/repo/migrations/20241129212331_add_user.exs deleted file mode 100644 index cc07447..0000000 --- a/priv/repo/migrations/20241129212331_add_user.exs +++ /dev/null @@ -1,10 +0,0 @@ -defmodule Backend.Repo.Migrations.AddUser do - use Ecto.Migration - - def change do - create table(:users) do - add :username, :string - add :password, :string - end - end -end diff --git a/priv/repo/migrations/20241129212656_add_file.exs b/priv/repo/migrations/20241129212656_add_file.exs index 7e35229..0f9d2e0 100644 --- a/priv/repo/migrations/20241129212656_add_file.exs +++ b/priv/repo/migrations/20241129212656_add_file.exs @@ -3,8 +3,8 @@ defmodule Backend.Repo.Migrations.AddFile do def change do create table(:files) do - add :filename, :string - add :created_at, :date + add :name, :string, null: false + add :content, :binary end end end From 563f7ee095ff32d532825740e9189fd6274cbfb1 Mon Sep 17 00:00:00 2001 From: DecafMango Date: Wed, 11 Dec 2024 00:25:07 +0300 Subject: [PATCH 2/2] add file manager --- lib/backend/application.ex | 4 +- lib/backend/files/file.ex | 6 +- lib/backend/files/file_manager.ex | 57 +++++++++++++++++++ lib/backend/files/file_process.ex | 49 ++++++++++++++++ lib/backend/files/file_registry.ex | 23 ++++++++ lib/backend/files/files.ex | 8 ++- lib/backend_web/channels/room_channel.ex | 17 +++++- .../migrations/20241129212656_add_file.exs | 2 +- 8 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 lib/backend/files/file_manager.ex create mode 100644 lib/backend/files/file_process.ex create mode 100644 lib/backend/files/file_registry.ex diff --git a/lib/backend/application.ex b/lib/backend/application.ex index 230fc68..5ef4648 100644 --- a/lib/backend/application.ex +++ b/lib/backend/application.ex @@ -17,7 +17,9 @@ defmodule Backend.Application do # Start a worker by calling: Backend.Worker.start_link(arg) # {Backend.Worker, arg}, # Start to serve requests, typically the last entry - BackendWeb.Endpoint + BackendWeb.Endpoint, + Backend.Files.FileRegistry, + Backend.Files.FileManager ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/backend/files/file.ex b/lib/backend/files/file.ex index 2c37e75..f273bf7 100644 --- a/lib/backend/files/file.ex +++ b/lib/backend/files/file.ex @@ -12,9 +12,7 @@ defmodule Backend.Files.File do field :content, :string end - def changeset(file, attrs) do - file - |> cast(attrs, [:name, :content], empty_values: [:content]) - |> validate_required([:name]) + def changeset(file, attr) do + cast(file, attr, [:name, :content]) end end diff --git a/lib/backend/files/file_manager.ex b/lib/backend/files/file_manager.ex new file mode 100644 index 0000000..0abd7d9 --- /dev/null +++ b/lib/backend/files/file_manager.ex @@ -0,0 +1,57 @@ +defmodule Backend.Files.FileManager do + @moduledoc """ + FileManager used for creating and interacting with FileProcess + """ + + alias Backend.Files.FileProcess + + def start_link() do + IO.puts("Starting file manager") + + DynamicSupervisor.start_link( + name: __MODULE__, + strategy: :one_for_one + ) + end + + defp start_child(file_id) do + DynamicSupervisor.start_child( + __MODULE__, + {Backend.Files.FileProcess, file_id} + ) + end + + @spec child_spec(any()) :: %{ + id: Backend.Files.FileManager, + start: {Backend.Files.FileManager, :start_link, []}, + type: :supervisor + } + def child_spec(_arg) do + %{ + id: __MODULE__, + start: {__MODULE__, :start_link, []}, + type: :supervisor + } + end + + def get_or_start_process(file_id) do + case Registry.lookup(Backend.Files.FileRegistry, file_id) do + [] -> + # Процесс не существует — запускаем новый + start_child(file_id) + :ok + + [{_pid, _}] -> + # Процесс уже существует + :ok + end + end + + def update_file_content(file_id, content) do + # Убедимся, что процесс существует + get_or_start_process(file_id) + + # Обновляем содержимое файла в процессе + FileProcess.update_content(file_id, content) + end +end diff --git a/lib/backend/files/file_process.ex b/lib/backend/files/file_process.ex new file mode 100644 index 0000000..3d7b210 --- /dev/null +++ b/lib/backend/files/file_process.ex @@ -0,0 +1,49 @@ +defmodule Backend.Files.FileProcess do + @moduledoc """ + FileProcess used to cashing file's text (just for reduce interaction with DB) + """ + + use GenServer, restart: :temporary + alias Backend.Files + + # Время бездействия (например, 5 секунд) + @timeout 5_000 + + # API + def start_link(file_id) do + GenServer.start_link(__MODULE__, file_id, name: via_tuple(file_id)) + end + + def update_content(file_id, new_content) do + GenServer.call(via_tuple(file_id), {:update_content, new_content}) + end + + defp via_tuple(file_id) do + {:via, Registry, {Backend.Files.FileRegistry, file_id}} + end + + # Callbacks + def init(file_id) do + # Загружаем файл из БД при запуске + file = Files.get_file(file_id) + {:ok, %{file: file, timer: reset_timer(nil)}} + end + + def handle_call({:update_content, new_content}, _from, %{file: file, timer: timer} = state) do + new_file = %{file | content: new_content} + {:reply, :ok, %{state | file: new_file, timer: reset_timer(timer)}} + end + + def handle_info(:timeout, %{file: file} = state) do + # Сохраняем в БД перед завершением + Files.save_file(file) + {:stop, :normal, state} + end + + defp reset_timer(timer) do + # Отменяем старый таймер, если он существует + if timer, do: Process.cancel_timer(timer) + # Устанавливаем новый таймер + Process.send_after(self(), :timeout, @timeout) + end +end diff --git a/lib/backend/files/file_registry.ex b/lib/backend/files/file_registry.ex new file mode 100644 index 0000000..e8771f2 --- /dev/null +++ b/lib/backend/files/file_registry.ex @@ -0,0 +1,23 @@ +defmodule Backend.Files.FileRegistry do + @moduledoc """ + Registy used for looking for active file processes pids + """ + + def start_link do + IO.puts("Starting file registry") + + Registry.start_link(name: __MODULE__, keys: :unique) + end + + def via_tuple(key) do + {:via, Registry, {__MODULE__, key}} + end + + def child_spec(_) do + Supervisor.child_spec( + Registry, + id: __MODULE__, + start: {__MODULE__, :start_link, []} + ) + end +end diff --git a/lib/backend/files/files.ex b/lib/backend/files/files.ex index 4ad5649..9e88272 100644 --- a/lib/backend/files/files.ex +++ b/lib/backend/files/files.ex @@ -16,9 +16,13 @@ defmodule Backend.Files do end def save_file(file) do + # Это сделано специально, так как без обнуления контента cast думает, что ничего не изменилось + content = file.content + file = %{file | content: ""} + file - |> File.changeset(%{}) - |> Repo.update() + |> File.changeset(%{content: content}) + |> Repo.update!() end def delete_file(file) do diff --git a/lib/backend_web/channels/room_channel.ex b/lib/backend_web/channels/room_channel.ex index 57b3d32..02861a8 100644 --- a/lib/backend_web/channels/room_channel.ex +++ b/lib/backend_web/channels/room_channel.ex @@ -2,14 +2,25 @@ defmodule BackendWeb.FileChannel do @moduledoc """ Websocket channel for each .mds file """ + alias Backend.Files + alias Backend.Files.FileManager use Phoenix.Channel - def join("file:" <> _room_id, _params, socket) do - {:ok, socket} + def join("file:" <> file_id, _params, socket) do + case Files.get_file(file_id) do + nil -> {:error, %{reason: "File with id " <> file_id <> " does not exist"}} + _ -> {:ok, assign(socket, :file_id, file_id)} + end end def handle_in("edit", %{"content" => content}, socket) do - # Рассылка обновлений всем клиентам + # Получаем file_id из socket.assigns + file_id = socket.assigns.file_id + + # Вызываем FileManager для обновления содержимого файла + FileManager.update_file_content(file_id, content) + + # Рассылаем обновления всем клиентам broadcast!(socket, "update", %{content: content}) {:noreply, socket} end diff --git a/priv/repo/migrations/20241129212656_add_file.exs b/priv/repo/migrations/20241129212656_add_file.exs index 0f9d2e0..ec46f1c 100644 --- a/priv/repo/migrations/20241129212656_add_file.exs +++ b/priv/repo/migrations/20241129212656_add_file.exs @@ -4,7 +4,7 @@ defmodule Backend.Repo.Migrations.AddFile do def change do create table(:files) do add :name, :string, null: false - add :content, :binary + add :content, :string end end end