Skip to content

Add REST for files #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/backend/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions lib/backend/files/file.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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, attr) do
cast(file, attr, [:name, :content])
end
end
57 changes: 57 additions & 0 deletions lib/backend/files/file_manager.ex
Original file line number Diff line number Diff line change
@@ -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
49 changes: 49 additions & 0 deletions lib/backend/files/file_process.ex
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions lib/backend/files/file_registry.ex
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions lib/backend/files/files.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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
# Это сделано специально, так как без обнуления контента cast думает, что ничего не изменилось
content = file.content
file = %{file | content: ""}

file
|> File.changeset(%{content: content})
|> Repo.update!()
end

def delete_file(file) do
file
|> Repo.delete()
end
end
12 changes: 0 additions & 12 deletions lib/backend/schemas/file.ex

This file was deleted.

12 changes: 0 additions & 12 deletions lib/backend/schemas/user.ex

This file was deleted.

17 changes: 14 additions & 3 deletions lib/backend_web/channels/room_channel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions lib/backend_web/controllers/file_controller.ex
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion lib/backend_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
10 changes: 0 additions & 10 deletions priv/repo/migrations/20241129212331_add_user.exs

This file was deleted.

4 changes: 2 additions & 2 deletions priv/repo/migrations/20241129212656_add_file.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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, :string
end
end
end
Loading