diff --git a/core/systems/assignment/connection_view_storage.ex b/core/systems/assignment/connection_view_storage.ex
index 054803713..030fb1bc8 100644
--- a/core/systems/assignment/connection_view_storage.ex
+++ b/core/systems/assignment/connection_view_storage.ex
@@ -1,9 +1,7 @@
defmodule Systems.Assignment.ConnectionViewStorage do
use CoreWeb, :live_component
- alias Systems.{
- Assignment
- }
+ alias Systems.Assignment
@impl true
def update(%{event: :disconnect}, %{assigns: %{assignment: assignment}} = socket) do
@@ -28,8 +26,7 @@ defmodule Systems.Assignment.ConnectionViewStorage do
@impl true
def render(assigns) do
~H"""
-
-
+
"""
end
end
diff --git a/core/systems/assignment/connector_popup_storage.ex b/core/systems/assignment/connector_popup_storage.ex
index 8b7257b4f..e0816cc7d 100644
--- a/core/systems/assignment/connector_popup_storage.ex
+++ b/core/systems/assignment/connector_popup_storage.ex
@@ -4,6 +4,8 @@ defmodule Systems.Assignment.ConnectorPopupStorage do
import CoreWeb.UI.Dialog
+ require Logger
+
alias Systems.{
Assignment,
Storage
@@ -62,11 +64,12 @@ defmodule Systems.Assignment.ConnectorPopupStorage do
end
@impl true
- def compose(:storage_endpoint_form, %{storage_endpoint: storage_endpoint}) do
+ def compose(:storage_endpoint_form, %{storage_endpoint: storage_endpoint, entity: entity}) do
%{
module: Storage.EndpointForm,
params: %{
- endpoint: storage_endpoint
+ endpoint: storage_endpoint,
+ key: Assignment.Private.storage_endpoint_key(entity)
}
}
end
diff --git a/core/systems/assignment/crew_page.ex b/core/systems/assignment/crew_page.ex
index 677c88b38..f155b571d 100644
--- a/core/systems/assignment/crew_page.ex
+++ b/core/systems/assignment/crew_page.ex
@@ -114,7 +114,7 @@ defmodule Systems.Assignment.CrewPage do
socket =
socket
|> decline_member()
- |> store("onboarding", "{\"status\":\"consent declined\"}")
+ |> store("onboarding", nil, "{\"status\":\"consent declined\"}")
socket =
if embedded? do
@@ -130,8 +130,8 @@ defmodule Systems.Assignment.CrewPage do
end
@impl true
- def handle_event("store", %{key: key, data: data}, socket) do
- {:noreply, socket |> store(key, data)}
+ def handle_event("store", %{key: key, group: group, data: data}, socket) do
+ {:noreply, socket |> store(key, group, data)}
end
@impl true
@@ -161,12 +161,14 @@ defmodule Systems.Assignment.CrewPage do
}
} = socket,
key,
+ group,
data
) do
meta_data = %{
remote_ip: remote_ip,
timestamp: Timestamp.now() |> DateTime.to_unix(),
- key: key
+ key: key,
+ group: group
}
if storage_info = Storage.Private.storage_info(assignment) do
diff --git a/core/systems/assignment/crew_work_view.ex b/core/systems/assignment/crew_work_view.ex
index 51a03e165..703e7bbf7 100644
--- a/core/systems/assignment/crew_work_view.ex
+++ b/core/systems/assignment/crew_work_view.ex
@@ -354,13 +354,13 @@ defmodule Systems.Assignment.CrewWorkView do
end
end
- defp handle_feldspar_event(socket, %{
+ defp handle_feldspar_event(%{assigns: %{selected_item: {%{group: group}, _}}} = socket, %{
"__type__" => "CommandSystemDonate",
"key" => key,
"json_string" => json_string
}) do
socket
- |> send_event(:parent, "store", %{key: key, data: json_string})
+ |> send_event(:parent, "store", %{key: key, group: group, data: json_string})
|> Frameworks.Pixel.Flash.put_info("Donated")
end
diff --git a/core/systems/storage/_private.ex b/core/systems/storage/_private.ex
index 977c1dd60..4981dc7ed 100644
--- a/core/systems/storage/_private.ex
+++ b/core/systems/storage/_private.ex
@@ -13,13 +13,15 @@ defmodule Systems.Storage.Private do
Application.get_env(:core, :storage)
end
+ def build_special(:builtin), do: %Storage.BuiltIn.EndpointModel{}
+ def build_special(:yoda), do: %Storage.Yoda.EndpointModel{}
def build_special(:aws), do: %Storage.AWS.EndpointModel{}
def build_special(:azure), do: %Storage.Azure.EndpointModel{}
- def build_special(:yoda), do: %Storage.Yoda.EndpointModel{}
+ def backend_info(%Storage.BuiltIn.EndpointModel{}), do: {:builtin, Storage.BuiltIn.Backend}
+ def backend_info(%Storage.Yoda.EndpointModel{}), do: {:yoda, Storage.Yoda.Backend}
def backend_info(%Storage.AWS.EndpointModel{}), do: {:aws, Storage.AWS.Backend}
def backend_info(%Storage.Azure.EndpointModel{}), do: {:azure, Storage.Azure.Backend}
- def backend_info(%Storage.Yoda.EndpointModel{}), do: {:yoda, Storage.Yoda.Backend}
def storage_info(%{storage_endpoint: %{} = storage_endpoint, external_panel: external_panel}) do
if endpoint = Storage.EndpointModel.special(storage_endpoint) do
diff --git a/core/systems/storage/built_in/backend.ex b/core/systems/storage/built_in/backend.ex
new file mode 100644
index 000000000..b6a80a0ae
--- /dev/null
+++ b/core/systems/storage/built_in/backend.ex
@@ -0,0 +1,43 @@
+defmodule Systems.Storage.BuiltIn.Backend do
+ @behaviour Systems.Storage.Backend
+
+ alias CoreWeb.UI.Timestamp
+ alias Systems.Storage.BuiltIn
+
+ def store(%{"key" => folder}, panel_info, data, meta_data) do
+ identifier = identifier(panel_info, meta_data)
+ special().store(folder, identifier, data)
+ end
+
+ def store(_, _, _, _) do
+ {:error, :endpoint_key_missing}
+ end
+
+ defp identifier(%{"participant" => participant}, %{"key" => meta_key, "group" => group})
+ when not is_nil(group) do
+ ["participant=#{participant}", "source=#{group}", meta_key]
+ end
+
+ defp identifier(%{"participant" => participant}, %{"key" => meta_key}) do
+ ["participant=#{participant}", meta_key]
+ end
+
+ defp identifier(%{"participant" => participant}, _) do
+ timestamp = Timestamp.now() |> DateTime.to_unix()
+ ["participant=#{participant}", "#{timestamp}"]
+ end
+
+ defp identifier(_, _) do
+ timestamp = Timestamp.now() |> DateTime.to_unix()
+ ["participant=?", "#{timestamp}"]
+ end
+
+ defp settings do
+ Application.fetch_env!(:core, Systems.Storage.BuiltIn)
+ end
+
+ defp special do
+ # Allow mocking
+ Access.get(settings(), :special, BuiltIn.LocalFS)
+ end
+end
diff --git a/core/systems/storage/built_in/endpoint_form.ex b/core/systems/storage/built_in/endpoint_form.ex
new file mode 100644
index 000000000..156bc8c89
--- /dev/null
+++ b/core/systems/storage/built_in/endpoint_form.ex
@@ -0,0 +1,47 @@
+defmodule Systems.Storage.BuiltIn.EndpointForm do
+ use CoreWeb.LiveForm, :fabric
+ use Fabric.LiveComponent
+
+ require Logger
+
+ alias Systems.Storage.BuiltIn.EndpointModel, as: Model
+
+ @impl true
+ def update(%{model: model, key: key}, socket) do
+ attrs =
+ if Map.get(model, :key) != nil do
+ %{}
+ else
+ %{key: key}
+ end
+
+ changeset =
+ Model.changeset(model, attrs)
+ |> Model.validate()
+
+ changeset =
+ if model.id do
+ Map.put(changeset, :action, :update)
+ else
+ Map.put(changeset, :action, :insert)
+ end
+
+ {
+ :ok,
+ socket
+ |> send_event(:parent, "update", %{changeset: changeset})
+ }
+ end
+
+ @impl true
+ def handle_event(_, _payload, socket) do
+ {:noreply, socket}
+ end
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ """
+ end
+end
diff --git a/core/systems/storage/built_in/endpoint_model.ex b/core/systems/storage/built_in/endpoint_model.ex
new file mode 100644
index 000000000..926be1bfb
--- /dev/null
+++ b/core/systems/storage/built_in/endpoint_model.ex
@@ -0,0 +1,43 @@
+defmodule Systems.Storage.BuiltIn.EndpointModel do
+ use Ecto.Schema
+ use Frameworks.Utility.Schema
+
+ import Ecto.Changeset
+
+ @fields ~w(key)a
+ @required_fields @fields
+
+ @derive {Jason.Encoder, only: @fields}
+ schema "storage_endpoints_builtin" do
+ field(:key, :string)
+ timestamps()
+ end
+
+ def changeset(endpoint, params) do
+ endpoint
+ |> cast(params, @fields)
+ end
+
+ def validate(changeset) do
+ changeset
+ |> validate_required(@required_fields)
+ |> unique_constraint(:key)
+ end
+
+ def ready?(endpoint) do
+ changeset =
+ endpoint
+ |> changeset(%{})
+ |> validate()
+
+ changeset.valid?()
+ end
+
+ def preload_graph(:down), do: []
+
+ defimpl Frameworks.Concept.ContentModel do
+ alias Systems.Storage.BuiltIn
+ def form(_), do: BuiltIn.EndpointForm
+ def ready?(endpoint), do: BuiltIn.EndpointModel.ready?(endpoint)
+ end
+end
diff --git a/core/systems/storage/built_in/local_fs.ex b/core/systems/storage/built_in/local_fs.ex
new file mode 100644
index 000000000..d3f5c830f
--- /dev/null
+++ b/core/systems/storage/built_in/local_fs.ex
@@ -0,0 +1,21 @@
+defmodule Systems.Storage.BuiltIn.LocalFS do
+ @behaviour Systems.Storage.BuiltIn.Special
+ use CoreWeb, :verified_routes
+
+ @impl true
+ def store(folder, identifier, data) do
+ filename = Enum.join(identifier, "_") <> ".json"
+ folder_path = get_full_path(folder)
+ File.mkdir(folder_path)
+ file_path = Path.join(folder_path, filename)
+ File.write!(file_path, data)
+ end
+
+ defp get_full_path(folder) do
+ Path.join(get_root_path(), folder)
+ end
+
+ def get_root_path do
+ Application.get_env(:core, :upload_path)
+ end
+end
diff --git a/core/systems/storage/built_in/s3.ex b/core/systems/storage/built_in/s3.ex
new file mode 100644
index 000000000..8c314d44e
--- /dev/null
+++ b/core/systems/storage/built_in/s3.ex
@@ -0,0 +1,35 @@
+defmodule Systems.Storage.BuiltIn.S3 do
+ @behaviour Systems.Storage.BuiltIn.Special
+ alias ExAws.S3
+
+ @impl true
+ def store(folder, identifier, data) do
+ filename = Enum.join(identifier, "_") <> ".json"
+ filepath = Path.join(folder, filename)
+ object_key = object_key(filepath)
+ content_type = content_type(object_key)
+ bucket = Access.fetch!(settings(), :bucket)
+
+ S3.put_object(bucket, object_key, data, content_type: content_type)
+ |> backend().request!()
+ end
+
+ defp object_key(filepath) do
+ if prefix = Access.get(settings(), :prefix, nil) do
+ Path.join(prefix, filepath)
+ else
+ filepath
+ end
+ end
+
+ defp content_type(name), do: MIME.from_path(name)
+
+ defp settings do
+ Application.fetch_env!(:core, Systems.Storage.BuiltIn.S3)
+ end
+
+ defp backend do
+ # Allow mocking
+ Access.get(settings(), :s3_backend, ExAws)
+ end
+end
diff --git a/core/systems/storage/built_in/special.ex b/core/systems/storage/built_in/special.ex
new file mode 100644
index 000000000..45187db42
--- /dev/null
+++ b/core/systems/storage/built_in/special.ex
@@ -0,0 +1,7 @@
+defmodule Systems.Storage.BuiltIn.Special do
+ @callback store(
+ folder :: binary(),
+ identifier :: list(binary()),
+ data :: binary()
+ ) :: any()
+end
diff --git a/core/systems/storage/delivery.ex b/core/systems/storage/delivery.ex
index 1dfc495ad..068ab0992 100644
--- a/core/systems/storage/delivery.ex
+++ b/core/systems/storage/delivery.ex
@@ -20,14 +20,21 @@ defmodule Systems.Storage.Delivery do
{:error, error}
_ ->
- Logger.debug("Data delivery succeeded")
+ Logger.info("Data delivery succeeded")
:ok
end
end
def deliver(backend, endpoint, panel_info, data, meta_data) do
Logger.warn("[Storage.Delivery] deliver")
- backend.store(endpoint, panel_info, data, meta_data)
+
+ try do
+ backend.store(endpoint, panel_info, data, meta_data)
+ rescue
+ e ->
+ Logger.error(Exception.format(:error, e, __STACKTRACE__))
+ reraise e, __STACKTRACE__
+ end
end
def deliver(
diff --git a/core/systems/storage/endpoint_form.ex b/core/systems/storage/endpoint_form.ex
index bc47e9951..d5439541a 100644
--- a/core/systems/storage/endpoint_form.ex
+++ b/core/systems/storage/endpoint_form.ex
@@ -3,7 +3,9 @@ defmodule Systems.Storage.EndpointForm do
use Fabric.LiveComponent
alias Frameworks.Concept
- alias Frameworks.Pixel
+ alias Frameworks.Pixel.RadioGroup
+ alias Frameworks.Pixel.Annotation
+ alias Frameworks.Pixel.Panel
alias Systems.{
Storage
@@ -11,7 +13,7 @@ defmodule Systems.Storage.EndpointForm do
@impl true
def update(
- %{id: id, endpoint: endpoint},
+ %{id: id, endpoint: endpoint, key: key},
socket
) do
{
@@ -19,11 +21,12 @@ defmodule Systems.Storage.EndpointForm do
socket
|> assign(
id: id,
- endpoint: endpoint
+ endpoint: endpoint,
+ key: key
)
|> update_special_type()
|> update_special()
- |> update_special_title()
+ |> update_special_annotation()
|> compose_child(:type_selector)
|> compose_child(:special_form)
}
@@ -39,16 +42,25 @@ defmodule Systems.Storage.EndpointForm do
assign(socket, special: special)
end
- defp update_special_title(%{assigns: %{special_type: nil}} = socket) do
- socket
- |> assign(special_title: nil)
+ defp update_special_annotation(%{assigns: %{special_type: nil}} = socket) do
+ assign(socket, annotation: nil)
end
- defp update_special_title(%{assigns: %{special_type: special_type}} = socket) do
- special_title = Storage.ServiceIds.translate(special_type)
+ defp update_special_annotation(%{assigns: %{special_type: special_type}} = socket) do
+ annotation =
+ case special_type do
+ :builtin -> dgettext("eyra-storage", "builtin.annotation")
+ :yoda -> dgettext("eyra-storage", "yoda.annotation")
+ :centerdata -> dgettext("eyra-storage", "centerdata.annotation")
+ :aws -> dgettext("eyra-storage", "aws.annotation")
+ :azure -> dgettext("eyra-storage", "azure.annotation")
+ end
+
+ annotation_title = Storage.ServiceIds.translate(special_type)
socket
- |> assign(special_title: special_title)
+ |> assign(annotation: annotation)
+ |> assign(annotation_title: annotation_title)
end
@impl true
@@ -56,7 +68,7 @@ defmodule Systems.Storage.EndpointForm do
items = Storage.ServiceIds.labels(special_type, Storage.Private.allowed_service_ids())
%{
- module: Pixel.RadioGroup,
+ module: RadioGroup,
params: %{
items: items
}
@@ -69,11 +81,12 @@ defmodule Systems.Storage.EndpointForm do
end
@impl true
- def compose(:special_form, %{special: special}) do
+ def compose(:special_form, %{special: special, key: key}) do
%{
module: Concept.ContentModel.form(special),
params: %{
- model: special
+ model: special,
+ key: key
}
}
end
@@ -109,7 +122,7 @@ defmodule Systems.Storage.EndpointForm do
special_changeset: nil,
special: special
)
- |> update_special_title()
+ |> update_special_annotation()
|> compose_child(:type_selector)
|> compose_child(:special_form)
}
@@ -139,10 +152,20 @@ defmodule Systems.Storage.EndpointForm do
<.child name={:type_selector} fabric={@fabric} />
+ <%= if @annotation do %>
+ <.spacing value="M" />
+
+ <:title>
+
+ <%= @annotation_title %>
+
+
+ <.spacing value="XS" />
+
+
+ <% end %>
<%= if get_child(@fabric, :special_form) do %>
- <.spacing value="L" />
-
<%= @special_title %>
- <.spacing value="XS" />
+ <.spacing value="M" />
<.child name={:special_form} fabric={@fabric} />
<% end %>
diff --git a/core/systems/storage/endpoint_model.ex b/core/systems/storage/endpoint_model.ex
index 1e15ac9dd..06a750013 100644
--- a/core/systems/storage/endpoint_model.ex
+++ b/core/systems/storage/endpoint_model.ex
@@ -13,17 +13,18 @@ defmodule Systems.Storage.EndpointModel do
require Storage.ServiceIds
schema "storage_endpoints" do
+ belongs_to(:builtin, Storage.BuiltIn.EndpointModel, on_replace: :delete)
+ belongs_to(:yoda, Storage.Yoda.EndpointModel, on_replace: :delete)
+ belongs_to(:centerdata, Storage.Centerdata.EndpointModel, on_replace: :delete)
belongs_to(:aws, Storage.AWS.EndpointModel, on_replace: :delete)
belongs_to(:azure, Storage.Azure.EndpointModel, on_replace: :delete)
- belongs_to(:centerdata, Storage.Centerdata.EndpointModel, on_replace: :delete)
- belongs_to(:yoda, Storage.Yoda.EndpointModel, on_replace: :delete)
timestamps()
end
@fields ~w()a
@required_fields @fields
- @special_fields ~w(aws azure centerdata yoda)a
+ @special_fields ~w(builtin yoda centerdata aws azure)a
def changeset(endpoint, params) do
endpoint
diff --git a/core/systems/storage/service_ids.ex b/core/systems/storage/service_ids.ex
index b72c2c46e..7ef226aaf 100644
--- a/core/systems/storage/service_ids.ex
+++ b/core/systems/storage/service_ids.ex
@@ -3,5 +3,5 @@ defmodule Systems.Storage.ServiceIds do
Defines list of supported storage services
"""
use Core.Enums.Base,
- {:storage_service_ids, [:aws, :azure, :yoda]}
+ {:storage_service_ids, [:builtin, :yoda, :aws, :azure]}
end
diff --git a/core/test/core_web/controllers/user_session_controller_test.exs b/core/test/core_web/controllers/user_session_controller_test.exs
index f5ba33ad1..0b82cccff 100644
--- a/core/test/core_web/controllers/user_session_controller_test.exs
+++ b/core/test/core_web/controllers/user_session_controller_test.exs
@@ -25,7 +25,7 @@ defmodule CoreWeb.UserSessionControllerTest do
test "redirects if already logged in", %{conn: conn} do
conn = get(conn, ~p"/user/signin")
- assert redirected_to(conn) == "/project"
+ assert redirected_to(conn) =~ "/"
end
end
diff --git a/core/test/core_web/live/user/confirm_token_test.exs b/core/test/core_web/live/user/confirm_token_test.exs
index abb69c4b4..d68b43e33 100644
--- a/core/test/core_web/live/user/confirm_token_test.exs
+++ b/core/test/core_web/live/user/confirm_token_test.exs
@@ -103,8 +103,7 @@ defmodule CoreWeb.Live.User.ConfirmToken.Test do
setup [:login_as_member]
test "opening activation mail with expired (invalid) token should redirect", %{conn: conn} do
- {:error, {:redirect, %{to: "/project"}}} =
- live(conn, Routes.live_path(conn, ConfirmToken, "abc"))
+ {:error, {:redirect, %{to: _}}} = live(conn, Routes.live_path(conn, ConfirmToken, "abc"))
end
end
end
diff --git a/core/test/systems/storage/builtin/backend_test.exs b/core/test/systems/storage/builtin/backend_test.exs
new file mode 100644
index 000000000..3c6f83b2c
--- /dev/null
+++ b/core/test/systems/storage/builtin/backend_test.exs
@@ -0,0 +1,89 @@
+defmodule Systems.Storage.BuiltIn.BackendTest do
+ use ExUnit.Case, async: true
+
+ import Mox
+
+ alias Systems.Storage.BuiltIn.Backend
+ alias Systems.Storage.BuiltIn.MockSpecial
+
+ setup :verify_on_exit!
+
+ setup do
+ initial_config = Application.get_env(:core, Systems.Storage.BuiltIn)
+
+ Application.put_env(:core, Systems.Storage.BuiltIn, special: MockSpecial)
+
+ on_exit(fn ->
+ Application.put_env(:core, Systems.Storage.BuiltIn, initial_config)
+ end)
+
+ :ok
+ end
+
+ describe "store/4" do
+ test "unknown folder" do
+ assert {:error, :endpoint_key_missing} = Backend.store(%{}, %{}, "data", %{})
+ end
+
+ test "unknown participant" do
+ expect(MockSpecial, :store, fn _, identifier, data ->
+ assert ["participant=?", _unix_timestamp] = identifier
+ assert "data" = data
+ :ok
+ end)
+
+ assert :ok = Backend.store(%{"key" => "assignment=1"}, %{}, "data", %{})
+ end
+
+ test "folder + participant" do
+ expect(MockSpecial, :store, fn folder, identifier, _data ->
+ assert "assignment=1" = folder
+ assert ["participant=1", _unix_timestamp] = identifier
+ :ok
+ end)
+
+ assert :ok = Backend.store(%{"key" => "assignment=1"}, %{"participant" => 1}, "data", %{})
+ end
+
+ test "folder + participant + meta key" do
+ expect(MockSpecial, :store, fn folder, identifier, _data ->
+ assert "assignment=1" = folder
+ assert ["participant=1", "session=1"] = identifier
+ :ok
+ end)
+
+ assert :ok =
+ Backend.store(%{"key" => "assignment=1"}, %{"participant" => 1}, "data", %{
+ "key" => "session=1"
+ })
+ end
+
+ test "folder + participant + meta key + group" do
+ expect(MockSpecial, :store, fn folder, identifier, _data ->
+ assert "assignment=1" = folder
+ assert ["participant=1", "source=apple", "session=1"] = identifier
+ :ok
+ end)
+
+ assert :ok =
+ Backend.store(%{"key" => "assignment=1"}, %{"participant" => 1}, "data", %{
+ "key" => "session=1",
+ "group" => "apple"
+ })
+ end
+
+ test "folder + participant + meta key + group=nil" do
+ expect(MockSpecial, :store, fn folder, identifier, _data ->
+ assert "assignment=1" = folder
+ assert ["participant=1", "session=1"] = identifier
+ :ok
+ end)
+
+ assert :ok =
+ Backend.store(%{"key" => "assignment=1"}, %{"participant" => 1}, "data", %{
+ "key" => "session=1",
+ "group" => nil
+ })
+ end
+ end
+end
diff --git a/core/test/test_helper.exs b/core/test/test_helper.exs
index a2462e732..43a493d6b 100644
--- a/core/test/test_helper.exs
+++ b/core/test/test_helper.exs
@@ -27,3 +27,4 @@ Mox.defmock(BankingClient.MockClient, for: BankingClient.API)
Application.put_env(:core, BankingClient, client: BankingClient.MockClient)
Mox.defmock(Systems.Storage.MockBackend, for: Systems.Storage.Backend)
+Mox.defmock(Systems.Storage.BuiltIn.MockSpecial, for: Systems.Storage.BuiltIn.Special)