Skip to content
This repository has been archived by the owner on Mar 10, 2021. It is now read-only.

Commit

Permalink
Merge pull request #79 from infinitered/feature/new-image-uploader-op…
Browse files Browse the repository at this point in the history
…tion

adds feature to store files in the database as blob
  • Loading branch information
jamonholmgren authored Nov 23, 2016
2 parents c3587e1 + 10823d4 commit cffcc7d
Show file tree
Hide file tree
Showing 23 changed files with 296 additions and 56 deletions.
43 changes: 38 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,29 +159,62 @@ content type.
becomes...

```eex
<%= content(@conn, "Image identifier", :background_image, do: "http://placekitten.com/200/300")
%>
<%= content(@conn, "Image identifier", :background_image, do: "http://placekitten.com/200/300") %>
```

### Image Uploads

Thesis offers support for a few different ways to handle image uploads: store files in the database,
point to an uploader/adapter inside your custom app, or use one of the prebuilt adapters.

##### Store Files in Database
For smaller websites and/or website that are hosted on the cloud, thesis offers a no-setup-required image uploader.
Files are stored in a separate table and contain all of the needed metadata (name, file type, and blobs themselves).
Keep in mind as you upload more and more files, your database will grow quickly. Don't use this for high-traffic,
content-heavy web applications. Smaller personal websites are probably fine.

To enable, add this in your config/config.exs file:

```elixir
config :thesis,
uploader: Thesis.RepoUploader
```

##### Use Your Own Uploader Module

If you already set up file uploads in your custom app, point thesis to a module that can handle a `%Plug.Upload{}`
struct.

```elixir
config :thesis,
uploader: <MyApp>.<CustomUploaderModule>
```

The module should have an `upload/1` function that accepts a `%Plug.Upload{}` struct. This function should return either `{:ok, "path/to/file.jpg"}` tuple with an image url or path, or {:error, _}. You can view
[/lib/thesis/uploaders/repo_uploader.ex](https://github.com/infinitered/thesis-phoenix/blob/master/lib/thesis/uploaders/repo_uploader.ex)
for an example.

##### Use a Prebuilt Adapter
Included in Thesis is an adapter for Ospry.io, which is a service that
offers the first 1,000 images and 1 GB of monthly download bandwidth
for free.

1. Sign up at [https://ospry.io/sign-up](https://ospry.io/sign-up)
2. Verify your email
3. Create a production subdomain (assets.example.com)
3. Copy your production public key to the Thesis config:
4. Add a valid credit card if you anticipate exceeding Ospry.io limits.
5. Copy your production public key to the Thesis config:

```elixir
config :thesis,
uploader: Thesis.OspryUploader

config :thesis, Thesis.OspryUploader,
ospry_public_key: "pk-prod-abcdefghijklmnopqrstuvwxyz0123456789"
```

That's it! Restart your server and image content areas will now contain a
file upload field. _Note: You'll need to add a valid credit card if you
anticipate exceeding Ospry.io limits._
file upload field.

### Global Content Areas

Expand Down
10 changes: 6 additions & 4 deletions README_INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,19 @@ Run `mix deps.get`.
```elixir
config :thesis,
store: Thesis.EctoStore,
authorization: <MyApp>.ThesisAuth
authorization: <MyApp>.ThesisAuth,
uploader: Thesis.RepoUploader
# uploader: <MyApp>.<CustomUploaderModule>
# uploader: Thesis.OspryUploader
config :thesis, Thesis.EctoStore, repo: <MyApp>.Repo
# config :thesis, Thesis.OspryUploader,
# ospry_public_key: "pk-prod-asdfasdfasdfasdf"
# If you want to allow creating dynamic pages:
# config :thesis, :dynamic_pages,
# view: <MyApp>.PageView,
# templates: ["index.html", "otherview.html"],
# not_found_view: <MyApp>.ErrorView,
# not_found_template: "404.html"
# If you want to use Ospry.io file uploads:
# config :thesis, Thesis.OspryUploader,
# ospry_public_key: "pk-prod-asdfasdfasdfasdfasdf"
```

#### 3. Create `lib/thesis_auth.ex`
Expand Down
11 changes: 7 additions & 4 deletions apps/example/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,20 @@ config :logger, :console,
# Configure thesis content editor
config :thesis,
store: Thesis.EctoStore,
authorization: Example.ThesisAuth
authorization: Example.ThesisAuth,
uploader: Thesis.RepoUploader

# config :thesis, Thesis.OspryUploader,
# ospry_public_key: System.get_env("OSPRY_PUBLIC_KEY")

config :thesis, Thesis.EctoStore, repo: Example.Repo

# If you want to allow creating dynamic pages:
config :thesis, :dynamic_pages,
view: Example.PageView,
templates: ["dynamic.html"],
not_found_view: Example.ErrorView,
not_found_template: "404.html"
# If you want to use Ospry.io file uploads:
config :thesis, Thesis.OspryUploader,
ospry_public_key: System.get_env("OSPRY_PUBLIC_KEY")

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule Example.Repo.Migrations.CreateThesisFilesTable do
@moduledoc false
use Ecto.Migration

def change do
create table(:thesis_files) do
add :slug, :string
add :content_type, :string
add :filename, :string
add :data, :binary

timestamps
end
create unique_index(:thesis_files, [:slug])
end
end
15 changes: 9 additions & 6 deletions lib/mix/tasks/thesis.install.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ defmodule Mix.Tasks.Thesis.Install do
"add_meta_to_thesis_page_contents",
"add_indexes_to_tables",
"add_template_and_redirect_url_to_thesis_pages",
"change_content_default_for_page_content"
"change_content_default_for_page_content",
"create_thesis_files_table"
]
@template_files [
{"priv/templates/thesis.install/thesis_auth.exs", "lib/thesis_auth.ex"}
Expand Down Expand Up @@ -103,17 +104,19 @@ defmodule Mix.Tasks.Thesis.Install do
# Configure thesis content editor
config :thesis,
store: Thesis.EctoStore,
authorization: #{Mix.Phoenix.base}.ThesisAuth
config :thesis, Thesis.EctoStore, repo: #{Mix.Phoenix.base}.Repo
authorization: #{Mix.Phoenix.base}.ThesisAuth,
uploader: Thesis.RepoUploader
# uploader: <MyApp>.<CustomUploaderModule>
# uploader: Thesis.OspryUploader
config :thesis, Thesis.EctoStore, repo: <MyApp>.Repo
# config :thesis, Thesis.OspryUploader,
# ospry_public_key: "pk-prod-asdfasdfasdfasdf"
# If you want to allow creating dynamic pages:
# config :thesis, :dynamic_pages,
# view: #{Mix.Phoenix.base}.PageView,
# templates: ["index.html", "otherview.html"],
# not_found_view: #{Mix.Phoenix.base}.ErrorView,
# not_found_template: "404.html"
# If you want to use Ospry.io file uploads:
# config :thesis, Thesis.OspryUploader,
# ospry_public_key: "pk-prod-asdfasdfasdfasdfasdf"
"""
end
end
Expand Down
26 changes: 26 additions & 0 deletions lib/thesis/api_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,32 @@ defmodule Thesis.ApiController do
json conn, %{}
end

def upload_file(conn, %{"file" => ""}), do: json conn, %{path: ""}
def upload_file(conn, %{"file" => file}) do
case uploader.upload(file) do
{:ok, path} -> json conn, %{path: path}
{:error, _} -> json conn, %{path: ""}
end
end
def upload_file(conn, _), do: json conn, %{path: ""}

def show_file(conn, %{"slug" => slug}) do
file = store.file(slug)
do_show_file(conn, file)
end

defp do_show_file(conn, nil) do
conn
|> put_resp_content_type("text/plain; charset=UTF-8")
|> send_resp(404, "File Not Found")
end

defp do_show_file(conn, file) do
conn
|> put_resp_content_type(file.content_type)
|> send_resp(200, file.data)
end

defp ensure_authorized!(conn, _params) do
if auth.page_is_editable?(conn), do: conn, else: put_unauthorized(conn)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/thesis/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ defmodule Thesis.Config do
Application.get_env(:thesis, Thesis.EctoStore)[:repo]
end

def uploader do
Application.get_env(:thesis, :uploader)
end

def ospry_public_key do
Application.get_env(:thesis, Thesis.OspryUploader)[:ospry_public_key]
end
Expand Down
46 changes: 46 additions & 0 deletions lib/thesis/models/file.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
defmodule Thesis.File do
@moduledoc """
Represents a file.
"""
use Ecto.Schema
import Ecto.Changeset, only: [cast: 3, validate_required: 2]
import Thesis.Utilities

@type t :: %Thesis.File{
id: any,
slug: String.t,
content_type: String.t,
filename: String.t,
data: any,
inserted_at: any,
updated_at: any
}

schema "thesis_files" do
field :slug, :string
field :content_type, :string
field :filename, :string
field :data, :binary

timestamps
end

@valid_attributes [:slug, :content_type, :filename, :data]
@required_attributes [:slug, :content_type, :filename, :data]

@doc """
Changeset for File structs.
"""
def changeset(file, %Plug.Upload{} = f) do
file
|> cast(%{content_type: f.content_type}, [:content_type])
|> cast(%{filename: f.filename}, [:filename])
|> cast(%{data: File.read!(f.path)}, [:data])
|> cast(%{slug: generate_slug(f.filename)}, [:slug])
|> validate_required(@required_attributes)
end

defp generate_slug(name) do
random_string(10) <> "-" <> parameterize(name)
end
end
2 changes: 1 addition & 1 deletion lib/thesis/models/page.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ defmodule Thesis.Page do
def redirected?(_), do: true

@doc """
Changeset for PageContent structs.
Changeset for Page structs.
"""
def changeset(page, params \\ %{}) do
page
Expand Down
8 changes: 1 addition & 7 deletions lib/thesis/render.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ defmodule Thesis.Render do

import HtmlSanitizeEx, only: [basic_html: 1]
import Phoenix.HTML, only: [raw: 1, html_escape: 1, safe_to_string: 1]
import Thesis.Utilities
alias Thesis.{PageContent}

@doc false
Expand Down Expand Up @@ -91,11 +92,4 @@ defmodule Thesis.Render do
|> safe_to_string
end

defp parameterize(str) do
str = Regex.replace(~r/[^a-z0-9\-\s]/i, str, "")
Regex.split(~r/\%20|\s/, str)
|> Enum.join("-")
|> String.downcase
end

end
3 changes: 3 additions & 0 deletions lib/thesis/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ defmodule Thesis.Router do

put "/update", ApiController, :update
delete "/delete", ApiController, :delete

post "/files/upload", ApiController, :upload_file
get "/files/:slug", ApiController, :show_file
end
end
end
Expand Down
11 changes: 10 additions & 1 deletion lib/thesis/stores/ecto_store.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ defmodule Thesis.EctoStore do

import Thesis.Config
import Ecto.Query, only: [from: 2]
alias Thesis.{Page, PageContent}
alias Thesis.{Page, PageContent, File}

def page(slug) when is_binary(slug) do
repo.get_by(Page, slug: slug)
Expand Down Expand Up @@ -44,6 +44,15 @@ defmodule Thesis.EctoStore do
repo.all(from pc in PageContent, where: pc.page_id == ^page_id or is_nil(pc.page_id))
end

@doc """
Retrieves a file by slug.
"""
def file(nil), do: nil
def file(""), do: nil
def file(slug) do
repo.get_by(File, slug: slug)
end

@doc """
Updates a page and its page_contents and global content areas.
"""
Expand Down
10 changes: 0 additions & 10 deletions lib/thesis/uploaders/ospry_uploader.ex

This file was deleted.

23 changes: 23 additions & 0 deletions lib/thesis/uploaders/repo_uploader.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Thesis.RepoUploader do
@moduledoc """
Handles file uploads
"""

@behaviour Thesis.Uploader

import Thesis.Config
import Ecto.Query, only: [from: 2]
alias Thesis.File

def upload(file) do
changeset = File.changeset(%File{}, file)
do_upload(changeset)
end

defp do_upload(changeset) do
case repo.insert_or_update(changeset) do
{:ok, file} -> {:ok, "/thesis/files/" <> file.slug}
{:error, changeset} -> {:error, changeset}
end
end
end
21 changes: 21 additions & 0 deletions lib/thesis/utilities.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule Thesis.Utilities do
@moduledoc """
Module that provides helper functions.
"""

def parameterize(str) do
str = Regex.replace(~r/[^a-z0-9\-\s\.]/i, str, "")
Regex.split(~r/\%20|\s/, str)
|> Enum.join("-")
|> String.downcase
end

def random_string(length) do
length
|> :crypto.strong_rand_bytes
|> Base.url_encode64
|> String.replace("_", "")
|> String.downcase
|> binary_part(0, length)
end
end
1 change: 1 addition & 0 deletions lib/thesis/view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ defmodule Thesis.View do
templates = Enum.join(dynamic_templates, ",")
editor = content_tag(:div, "", id: "thesis-container",
data_ospry_public_key: ospry_public_key,
data_file_uploader: uploader,
data_redirect_url: redirect_url,
data_template: template,
data_templates: templates,
Expand Down
4 changes: 2 additions & 2 deletions priv/static/thesis.js

Large diffs are not rendered by default.

Loading

0 comments on commit cffcc7d

Please sign in to comment.