Uploads
This commit is contained in:
parent
8715db806a
commit
56d2df5140
|
@ -1,3 +1,5 @@
|
|||
.DS_Store
|
||||
|
||||
# The directory Mix will write compiled artifacts to.
|
||||
/_build/
|
||||
|
||||
|
@ -32,3 +34,5 @@ npm-debug.log
|
|||
# we ignore priv/static. You may want to comment
|
||||
# this depending on your deployment strategy.
|
||||
/priv/static/
|
||||
|
||||
uploads/
|
||||
|
|
|
@ -74,3 +74,5 @@ config :phoenix, :stacktrace_depth, 20
|
|||
|
||||
# Initialize plugs at runtime for faster development compilation
|
||||
config :phoenix, :plug_init_mode, :runtime
|
||||
|
||||
config :wiki, :upload_dir, "uploads"
|
||||
|
|
|
@ -7,6 +7,7 @@ defmodule Wiki.Content do
|
|||
alias Wiki.Repo
|
||||
|
||||
alias Wiki.Content.Page
|
||||
alias Wiki.Content.Upload
|
||||
|
||||
@doc """
|
||||
Returns the list of pages.
|
||||
|
@ -103,4 +104,12 @@ defmodule Wiki.Content do
|
|||
def change_page(%Page{} = page, attrs \\ %{}, options \\ []) do
|
||||
Page.changeset(page, attrs, options)
|
||||
end
|
||||
|
||||
def get_upload(%Page{id: page_id}, id) do
|
||||
get_upload(page_id, id)
|
||||
end
|
||||
|
||||
def get_upload(page_id, upload_id) do
|
||||
Repo.one(from u in Upload, where: u.id == ^upload_id and u.page_id == ^page_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,8 @@ defmodule Wiki.Content.Page do
|
|||
content_encryption_key: String.t() | nil,
|
||||
html: String.t() | nil,
|
||||
title: String.t(),
|
||||
user: Wiki.Accounts.User.t()
|
||||
user: Wiki.Accounts.User.t(),
|
||||
uploads: [Wiki.Content.Upload.t()]
|
||||
}
|
||||
|
||||
schema "pages" do
|
||||
|
@ -23,6 +24,7 @@ defmodule Wiki.Content.Page do
|
|||
field :title, :string
|
||||
|
||||
belongs_to :user, Wiki.Accounts.User
|
||||
has_many :uploads, Wiki.Content.Upload
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
defmodule Wiki.Content.Upload do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@type t() :: %__MODULE__{
|
||||
relative_path: String.t(),
|
||||
content_type: String.t(),
|
||||
encryption_iv: binary(),
|
||||
page: Wiki.Content.Page.t()
|
||||
}
|
||||
|
||||
schema "uploads" do
|
||||
field :relative_path, :string
|
||||
field :content_type, :string
|
||||
field :encryption_iv, :binary
|
||||
|
||||
belongs_to :page, Wiki.Content.Page
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(upload, attrs) do
|
||||
upload
|
||||
|> cast(attrs, [:relative_path, :content_type, :encryption_iv, :page_id])
|
||||
|> validate_required([:relative_path, :content_type, :encryption_iv])
|
||||
end
|
||||
|
||||
@spec create_from_plug(upload :: Plug.Upload.t(), key :: binary()) :: t()
|
||||
def create_from_plug(upload, key) do
|
||||
{encrypted_file_path, iv} = encrypt_contents(upload, key)
|
||||
|
||||
changeset(%__MODULE__{}, %{
|
||||
content_type: upload.content_type,
|
||||
relative_path: encrypted_file_path,
|
||||
encryption_iv: iv
|
||||
})
|
||||
end
|
||||
|
||||
@spec encrypt_contents(upload :: Plug.Upload.t(), key :: binary()) :: {String.t(), binary()}
|
||||
defp encrypt_contents(upload, key) do
|
||||
filename = "#{Ecto.UUID.generate()}.enc"
|
||||
output_path = Path.join(upload_dir(), filename)
|
||||
|
||||
File.mkdir_p(upload_dir())
|
||||
:ok = File.touch(output_path)
|
||||
|
||||
{:ok, input} = File.open(upload.path, [:read, :binary])
|
||||
{:ok, output} = File.open(output_path, [:write, :binary])
|
||||
|
||||
iv = :crypto.strong_rand_bytes(16)
|
||||
state = :crypto.crypto_init(:aes_256_ctr, key, iv, true)
|
||||
|
||||
:ok = encrypt_loop(state, input, output)
|
||||
|
||||
final_data = :crypto.crypto_final(state)
|
||||
:ok = IO.binwrite(output, final_data)
|
||||
|
||||
:ok = File.close(input)
|
||||
:ok = File.close(output)
|
||||
|
||||
{filename, iv}
|
||||
end
|
||||
|
||||
@spec encrypt_loop(state :: :crypto.crypto_state(), input :: IO.device(), output :: IO.device()) ::
|
||||
:ok
|
||||
|
||||
defp encrypt_loop(state, input, output) do
|
||||
# why 4k?
|
||||
case IO.binread(input, 4096) do
|
||||
:eof ->
|
||||
:ok
|
||||
|
||||
data ->
|
||||
encrypted_data = :crypto.crypto_update(state, data)
|
||||
:ok = IO.binwrite(output, encrypted_data)
|
||||
|
||||
encrypt_loop(state, input, output)
|
||||
end
|
||||
end
|
||||
|
||||
@spec upload_dir() :: String.t()
|
||||
defp upload_dir() do
|
||||
dir = Application.get_env(:wiki, :upload_dir)
|
||||
|
||||
if is_nil(dir) do
|
||||
raise "expected `:wiki, :upload_dir` to be configured"
|
||||
end
|
||||
|
||||
dir
|
||||
end
|
||||
|
||||
@spec decrypt_content(upload :: t(), key :: binary()) :: binary()
|
||||
def decrypt_content(%__MODULE__{relative_path: filename, encryption_iv: iv}, key) do
|
||||
path = Path.join(upload_dir(), filename)
|
||||
encrypted_data = File.read!(path)
|
||||
|
||||
:crypto.crypto_one_time(:aes_256_ctr, key, iv, encrypted_data, false)
|
||||
end
|
||||
|
||||
@spec delete_file(upload :: t()) :: :ok
|
||||
def delete_file(%__MODULE__{relative_path: filename}) do
|
||||
File.rm(Path.join(upload_dir(), filename))
|
||||
end
|
||||
end
|
|
@ -1,10 +1,22 @@
|
|||
defmodule WikiWeb.PageController do
|
||||
use WikiWeb, :controller
|
||||
|
||||
alias Wiki.Repo
|
||||
alias Wiki.Content
|
||||
alias Wiki.Content.Page
|
||||
alias Wiki.Content.{Page, Upload}
|
||||
|
||||
plug :get_page when action in [:show, :edit, :update, :delete]
|
||||
plug :get_page
|
||||
when action in [
|
||||
:show,
|
||||
:edit,
|
||||
:update,
|
||||
:delete,
|
||||
:create_upload,
|
||||
:get_upload,
|
||||
:delete_upload
|
||||
]
|
||||
|
||||
plug :get_upload_plug when action in [:get_upload, :delete_upload]
|
||||
|
||||
defp get_page(%Plug.Conn{path_params: %{"id" => id}} = conn, _opts) do
|
||||
case Content.get_page(id) do
|
||||
|
@ -29,6 +41,20 @@ defmodule WikiWeb.PageController do
|
|||
end
|
||||
end
|
||||
|
||||
defp get_upload_plug(%Plug.Conn{path_params: %{"upload_id" => id}} = conn, _opts) do
|
||||
page = conn.assigns.page
|
||||
|
||||
case Content.get_upload(page, id) do
|
||||
nil ->
|
||||
conn
|
||||
|> send_resp(404, "Not found")
|
||||
|> halt()
|
||||
|
||||
upload ->
|
||||
assign(conn, :upload, upload)
|
||||
end
|
||||
end
|
||||
|
||||
def index(conn, _params) do
|
||||
pages = Content.list_pages()
|
||||
render(conn, "index.html", pages: pages)
|
||||
|
@ -59,12 +85,13 @@ defmodule WikiWeb.PageController do
|
|||
end
|
||||
|
||||
def show(conn, _params) do
|
||||
rendered_content = Content.Renderer.render(conn.assigns.page)
|
||||
render(conn, "show.html", page: conn.assigns.page, content: rendered_content)
|
||||
page = Repo.preload(conn.assigns.page, :uploads)
|
||||
rendered_content = Content.Renderer.render(page)
|
||||
render(conn, "show.html", page: page, content: rendered_content)
|
||||
end
|
||||
|
||||
def edit(conn, _params) do
|
||||
page = conn.assigns.page
|
||||
page = conn.assigns.page |> Repo.preload(:uploads)
|
||||
changeset = Content.change_page(page)
|
||||
render(conn, "edit.html", page: page, changeset: changeset)
|
||||
end
|
||||
|
@ -83,6 +110,40 @@ defmodule WikiWeb.PageController do
|
|||
end
|
||||
end
|
||||
|
||||
def create_upload(conn, %{"file" => %Plug.Upload{} = file}) do
|
||||
page = conn.assigns.page
|
||||
key = get_session(conn, :content_encryption_key)
|
||||
key = Base.decode16!(key, case: :lower)
|
||||
|
||||
changeset =
|
||||
Upload.create_from_plug(file, key)
|
||||
|> Upload.changeset(%{page_id: page.id})
|
||||
|
||||
{:ok, _upload} = Repo.insert(changeset)
|
||||
|
||||
redirect(conn, to: Routes.page_path(conn, :edit, page.id))
|
||||
end
|
||||
|
||||
def get_upload(conn, _params) do
|
||||
upload = conn.assigns.upload
|
||||
key = get_session(conn, :content_encryption_key)
|
||||
key = Base.decode16!(key, case: :lower)
|
||||
|
||||
data = Upload.decrypt_content(upload, key)
|
||||
|
||||
conn
|
||||
|> put_resp_header("content-type", upload.content_type)
|
||||
|> send_resp(200, data)
|
||||
end
|
||||
|
||||
def delete_upload(conn, _params) do
|
||||
page = conn.assigns.page
|
||||
upload = conn.assigns.upload
|
||||
Upload.delete_file(upload)
|
||||
Repo.delete(upload)
|
||||
redirect(conn, to: Routes.page_path(conn, :edit, page.id))
|
||||
end
|
||||
|
||||
def delete(conn, _params) do
|
||||
page = conn.assigns.page
|
||||
{:ok, _page} = Content.delete_page(page)
|
||||
|
|
|
@ -43,7 +43,11 @@ defmodule WikiWeb.Endpoint do
|
|||
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
|
||||
|
||||
plug Plug.Parsers,
|
||||
parsers: [:urlencoded, :multipart, :json],
|
||||
parsers: [
|
||||
:urlencoded,
|
||||
{:multipart, length: 5_000_000_000},
|
||||
:json
|
||||
],
|
||||
pass: ["*/*"],
|
||||
json_decoder: Phoenix.json_library()
|
||||
|
||||
|
|
|
@ -67,6 +67,9 @@ defmodule WikiWeb.Router do
|
|||
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
|
||||
|
||||
resources "/pages", PageController
|
||||
post "/pages/:id/uploads", PageController, :create_upload
|
||||
get "/pages/:id/uploads/:upload_id", PageController, :get_upload
|
||||
delete "/pages/:id/uploads/:upload_id", PageController, :delete_upload
|
||||
end
|
||||
|
||||
scope "/", WikiWeb do
|
||||
|
|
|
@ -2,4 +2,35 @@
|
|||
|
||||
<%= render "form.html", Map.put(assigns, :action, Routes.page_path(@conn, :update, @page)) %>
|
||||
|
||||
<h2>Uploaded Files</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Encrypted Filename</th>
|
||||
<th>Content Type</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<%= for upload <- @page.uploads do %>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="<%= Routes.page_path(@conn, :get_upload, @page.id, upload.id) %>">
|
||||
<%= upload.relative_path %>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<code><%= upload.content_type %></code>
|
||||
</td>
|
||||
<td>
|
||||
<%= form_tag Routes.page_path(@conn, :delete_upload, @page.id, upload.id), method: :delete, style: "margin: 0;" do %>
|
||||
<%= submit "Delete", style: "margin: 0;" %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
<%= form_tag Routes.page_path(@conn, :create_upload, @page.id), method: :post, multipart: true do %>
|
||||
<input type="file" name="file">
|
||||
<%= submit "Upload" %>
|
||||
<% end %>
|
||||
|
||||
<span><%= link "Back", to: Routes.page_path(@conn, :index) %></span>
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
<h1>Show Page</h1>
|
||||
|
||||
<ul>
|
||||
|
||||
<li>
|
||||
<strong>Title:</strong>
|
||||
<%= @page.title %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>Content:</strong>
|
||||
<%= raw(@content) %>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<h1><%= @page.title %></h1>
|
||||
<span><%= link "Edit", to: Routes.page_path(@conn, :edit, @page) %></span>
|
||||
<span><%= link "Back", to: Routes.page_path(@conn, :index) %></span>
|
||||
|
||||
<div>
|
||||
<%= raw(@content) %>
|
||||
</div>
|
||||
|
||||
<%= for upload <- @page.uploads do %>
|
||||
<div>
|
||||
<%= cond do %>
|
||||
<% String.starts_with?(upload.content_type, "image/") -> %>
|
||||
<img src="<%= Routes.page_path(@conn, :get_upload, @page.id, upload.id) %>" />
|
||||
<% String.starts_with?(upload.content_type, "video/") -> %>
|
||||
<video controls src="<%= Routes.page_path(@conn, :get_upload, @page.id, upload.id) %>"></video>
|
||||
<% String.starts_with?(upload.content_type, "audio/") -> %>
|
||||
<audio controls src="<%= Routes.page_path(@conn, :get_upload, @page.id, upload.id) %>"></audio>
|
||||
<% true -> %>
|
||||
<a href="<%= Routes.page_path(@conn, :get_upload, @page.id, upload.id) %>"><%= upload.relative_path %></a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -7,7 +7,7 @@ defmodule Wiki.Repo.Migrations.CreatePages do
|
|||
add :encrypted_content, :binary
|
||||
add :encrypted_content_iv, :binary
|
||||
add :encrypted_content_tag, :binary
|
||||
add :user_id, references(:users, on_delete: :nothing)
|
||||
add :user_id, references(:users, on_delete: :delete_all)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
defmodule Wiki.Repo.Migrations.CreateUploads do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:uploads) do
|
||||
add :relative_path, :string
|
||||
add :content_type, :string
|
||||
add :encryption_iv, :binary
|
||||
add :page_id, references(:pages, on_delete: :delete_all)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create index(:uploads, [:page_id])
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue