Packages

Easy file uploads for Elixir/Phoenix

Current section

2 Versions

Jump to

Compare versions

15 files changed
+577 additions
-494 deletions
  @@ -1,10 +1,8 @@
1 1 # Dropkick
2 2
3 - Dropkick is a highly experimental library that provides easy to use uploads for the Elixir/ Phoenix ecosystem.
3 + Dropkick is a highly experimental library that provides easy to use uploads for the Elixir/ Phoenix ecosystem.
4 4 This is a opinionated library focused on developer ergonomics that you can use to provide file uploads in any Phoenix project.
5 5
6 - Some inspiration was taken from other projects like [Capsule](https://github.com/elixir-capsule/capsule) and [Waffle](https://github.com/elixir-waffle/waffle) as well as Ruby's [Shrine](https://shrinerb.com/).
7 -
8 6 ## Installation
9 7
10 8 ```elixir
  @@ -15,9 +13,71 @@ def deps do
15 13 end
16 14 ```
17 15
16 + ## Usage
17 +
18 + ### Setup
19 +
20 + - Add a map column to your database table: `add(:avatar, :map)`
21 + - Add a `Dropkick.File` field to your ecto schema: `field(:avatar, Dropkick.File)`
22 +
23 + ### Configuration
24 +
25 + Add the following configuration to your `config.exs`:
26 +
27 + ```elixir
28 + config :dropkick,
29 + repo: MyAppRepo,
30 + storage: Dropkick.Storage.Disk,
31 + folder: "uploads"
32 + ```
33 +
34 + ### Uploader
35 +
36 + Define an uplodader for your application:
37 +
38 + ```elixir
39 + defmodule MyApp.Uploader do
40 + use Dropkick.Uploader
41 +
42 + # Defines where to store the user avatar through pattern matching
43 + def storage_prefix({user, :avatar}), do: "avatars/#{user.id}"
44 +
45 + # You can also implement a list of callbacks that allow you to
46 + # customize what happens in your upload pipeline
47 + # def process(file, _scope), do: {:ok, file}
48 + # def before_store(file, _scope), do: {:ok, file}
49 + # def after_store(file, _scope), do: {:ok, file}
50 + # def before_delete(file, _scope), do: {:ok, file}
51 + # def after_delete(file, _scope), do: {:ok, file}
52 + end
53 + ```
54 +
55 + ### Save the files
56 +
57 + ```elixir
58 + import Dropkick.Context
59 +
60 + def create_user(user, attrs) do
61 + user
62 + |> User.changeset(attrs)
63 + |> insert_with_files(MyApp.Uploader)
64 + end
65 +
66 + def update_user(user, attrs) do
67 + user
68 + |> User.changeset(attrs)
69 + |> update_with_files(MyApp.Uploader)
70 + end
71 + ```
72 +
18 73 ## Missing bits
19 74
20 - - Increase test coverage
21 - - Implement more image transformations
22 - - Add video transformations
23 - - Add support to S3 storage
\ No newline at end of file
75 + - Add integration for file transformations
76 + - Add integration with [Briefly](https://hexdocs.pm/briefly) to make transformation/ cleanup of temporary files easier.
77 + - Support other types of storages (S3, Azure, etc)
78 + - Add strategy to allow cleaning up old files after update
79 + - Improve documentation and examples for modules and functions
80 + - Add examples of using libraries for processing files:
81 + - [`image`](https://hexdocs.pm/image)
82 + - [`ex_optimizer`](https://hexdocs.pm/ex_optimizer)
83 + - [`mogrify`](https://hexdocs.pm/mogrify)
  @@ -5,32 +5,38 @@
5 5 {<<"files">>,
6 6 [<<"lib">>,<<"lib/dropkick">>,<<"lib/dropkick/storage">>,
7 7 <<"lib/dropkick/storage/memory.ex">>,<<"lib/dropkick/storage/disk.ex">>,
8 - <<"lib/dropkick/uploader.ex">>,<<"lib/dropkick/attachment.ex">>,
8 + <<"lib/dropkick/file.ex">>,<<"lib/dropkick/uploader.ex">>,
9 9 <<"lib/dropkick/storage.ex">>,<<"lib/dropkick/application.ex">>,
10 - <<"lib/dropkick/transform.ex">>,<<"lib/dropkick/attachable.ex">>,
11 - <<"lib/dropkick.ex">>,<<"mix.exs">>,<<"README.md">>]}.
10 + <<"lib/dropkick/context.ex">>,<<"lib/dropkick/task.ex">>,
11 + <<"lib/dropkick/changeset.ex">>,<<"lib/dropkick.ex">>,<<"mix.exs">>,
12 + <<"README.md">>]}.
12 13 {<<"licenses">>,[<<"AGPL-3.0-only">>]}.
13 14 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/thiagomajesk/dropkick">>}]}.
14 15 {<<"name">>,<<"dropkick">>}.
15 16 {<<"requirements">>,
16 - [[{<<"app">>,<<"jason">>},
17 - {<<"name">>,<<"jason">>},
18 - {<<"optional">>,false},
19 - {<<"repository">>,<<"hexpm">>},
20 - {<<"requirement">>,<<"~> 1.0">>}],
21 - [{<<"app">>,<<"ecto">>},
17 + [[{<<"app">>,<<"ecto">>},
22 18 {<<"name">>,<<"ecto">>},
23 19 {<<"optional">>,false},
24 20 {<<"repository">>,<<"hexpm">>},
25 21 {<<"requirement">>,<<"~> 3.0">>}],
26 - [{<<"app">>,<<"plug">>},
27 - {<<"name">>,<<"plug">>},
28 - {<<"optional">>,false},
29 - {<<"repository">>,<<"hexpm">>},
30 - {<<"requirement">>,<<"~> 1.0">>}],
31 22 [{<<"app">>,<<"image">>},
32 23 {<<"name">>,<<"image">>},
33 24 {<<"optional">>,false},
34 25 {<<"repository">>,<<"hexpm">>},
35 - {<<"requirement">>,<<"~> 0.30.0">>}]]}.
36 - {<<"version">>,<<"0.0.0">>}.
26 + {<<"requirement">>,<<"~> 0.30.0">>}],
27 + [{<<"app">>,<<"jason">>},
28 + {<<"name">>,<<"jason">>},
29 + {<<"optional">>,false},
30 + {<<"repository">>,<<"hexpm">>},
31 + {<<"requirement">>,<<"~> 1.0">>}],
32 + [{<<"app">>,<<"infer">>},
33 + {<<"name">>,<<"infer">>},
34 + {<<"optional">>,false},
35 + {<<"repository">>,<<"hexpm">>},
36 + {<<"requirement">>,<<"~> 0.2.4">>}],
37 + [{<<"app">>,<<"sizeable">>},
38 + {<<"name">>,<<"sizeable">>},
39 + {<<"optional">>,false},
40 + {<<"repository">>,<<"hexpm">>},
41 + {<<"requirement">>,<<"~> 1.0">>}]]}.
42 + {<<"version">>,<<"0.0.1">>}.
  @@ -1,119 +1,3 @@
1 1 defmodule Dropkick do
2 - @moduledoc """
3 - This module provides functions that you can use to interact directly with uploads and attachments.
4 - """
5 -
6 - alias Dropkick.Attachment
7 -
8 - @doc """
9 - Caches an attachable by saving it on the provided directory under the "cache" prefix by default.
10 - When an attachable is cached we won't calculate any metadata information, the file is only
11 - saved to the directory. This function is usually usefull when you are doing async uploads -
12 - where you first save the file to a temporary location and only after some confirmation you actually move
13 - the file to its final destination. You probably want to clean this directory from time to time.
14 - """
15 - def cache(attachable, opts \\ []) do
16 - opts = Keyword.put(opts, :prefix, "cache")
17 -
18 - with {:ok, atch} <- put(attachable, opts) do
19 - {:ok, Map.replace!(atch, :status, :cached)}
20 - end
21 - end
22 -
23 - @doc """
24 - Stores an attachable by saving it on the provided directory under the "store" prefix by default.
25 - When an attachable is stored we'll calculate metadata information before moving the file to its destination.
26 - """
27 - def store(upload, opts \\ []) do
28 - opts = Keyword.put(opts, :prefix, "store")
29 -
30 - with {:ok, atch} <- put(upload, opts) do
31 - {:ok, Map.replace!(atch, :status, :stored)}
32 - end
33 - end
34 -
35 - @doc """
36 - Moves an attachble from its current destination to another one.
37 - This function can be used to "promote" cached attachments without having
38 - to worry about cleaning up the temporary directory.
39 - """
40 - def move(%Attachment{status: :cached} = atch, opts \\ []) do
41 - dest = Keyword.fetch!(opts, :dest)
42 -
43 - with {:ok, atch} <- copy(atch, dest, move: true) do
44 - {:ok, Map.replace!(atch, :status, :stored)}
45 - end
46 - end
47 -
48 - @doc """
49 - Creates a version of the attachment with some transformation.
50 - Transformations validated against an attachment `content_type`.
51 - The current transformations supported are:
52 -
53 - ## `image/*`
54 - Image transformations uses the [`image`](https://hexdocs.pm/image) library behind the scenes
55 -
56 - - `{:thumbnail, size, opts}`: Generates an image thumbnail, receives the same options
57 - as [`Image.thumbnail/3`](https://hexdocs.pm/image/Image.html#thumbnail/3)
58 - """
59 - def transform(%Attachment{} = atch, transforms) do
60 - Enum.map(transforms, fn
61 - {:thumbnail, size, params} ->
62 - Task.Supervisor.async_nolink(Dropkick.TransformTaskSupervisor, fn ->
63 - with {:ok, transform} <- Dropkick.Transform.thumbnail(atch, size, params),
64 - {:ok, version} <- store(transform, folder: Path.dirname(transform.key)) do
65 - {:ok, Map.update!(atch, :versions, fn versions -> [version | versions] end)}
66 - end
67 - end)
68 -
69 - transform ->
70 - raise "Not a valid transform param #{inspect(transform)}"
71 - end)
72 - end
73 -
74 - def url(%Attachment{} = atch, opts \\ []) do
75 - version = Keyword.get(opts, :version)
76 - atch = (version && version(atch, version)) || atch
77 - Path.join(Keyword.get(opts, :host, "/"), atch.key)
78 - end
79 -
80 - def version(%Attachment{} = atch, version) do
81 - Enum.find(atch.versions, &(&1.version == version))
82 - end
83 -
84 - def contextualize(%Attachment{} = atch) do
85 - key = Dropkick.Attachable.key(atch)
86 -
87 - %{
88 - filename: Path.basename(key),
89 - extension: Path.extname(key),
90 - directory: Path.dirname(key)
91 - }
92 - end
93 -
94 - @doc """
95 - Calls the underlyning storage's `put` function.
96 - Check the module `Dropkick.Storage` for documentation about the available options.
97 - """
98 - def put(upload, opts \\ []), do: storage!().put(upload, opts)
99 -
100 - @doc """
101 - Calls the underlyning storage's `read` function.
102 - Check the module `Dropkick.Storage` for documentation about the available options.
103 - """
104 - def read(upload, opts \\ []), do: storage!().read(upload, opts)
105 -
106 - @doc """
107 - Calls the underlyning storage's `copy` function.
108 - Check the module `Dropkick.Storage` for documentation about the available options.
109 - """
110 - def copy(upload, dest, opts \\ []), do: storage!().copy(upload, dest, opts)
111 -
112 - @doc """
113 - Calls the underlyning storage's `delete` function.
114 - Check the module `Dropkick.Storage` for documentation about the available options.
115 - """
116 - def delete(upload, opts \\ []), do: storage!().delete(upload, opts)
117 -
118 - defp storage!(), do: Application.fetch_env!(:dropkick, :storage)
2 + @moduledoc File.read!("README.md")
119 3 end
  @@ -1,88 +0,0 @@
1 - defprotocol Dropkick.Attachable do
2 - @spec key(struct() | binary()) :: String.t()
3 - def key(upload)
4 -
5 - @spec name(struct() | binary()) :: String.t()
6 - def name(upload)
7 -
8 - @spec content(struct() | binary()) :: {:ok, iodata()} | {:error, String.t()}
9 - def content(upload)
10 - end
11 -
12 - defimpl Dropkick.Attachable, for: BitString do
13 - def key(path), do: path
14 -
15 - def name(path), do: Path.basename(path)
16 -
17 - def content(path) do
18 - if Regex.match?(~r"^http(s?)\:\/\/.+", path) do
19 - Dropkick.Attachable.content(URI.new!(path))
20 - else
21 - case File.read(Path.expand(path)) do
22 - {:error, reason} -> {:error, "Could not read path: #{reason}"}
23 - success_tuple -> success_tuple
24 - end
25 - end
26 - end
27 - end
28 -
29 - defimpl Dropkick.Attachable, for: Plug.Upload do
30 - def key(%Plug.Upload{path: path}), do: path
31 -
32 - def name(%Plug.Upload{filename: name}), do: name
33 -
34 - def content(%Plug.Upload{path: path}) do
35 - case File.read(path) do
36 - {:error, reason} -> {:error, "Could not read path: #{reason}"}
37 - success_tuple -> success_tuple
38 - end
39 - end
40 - end
41 -
42 - defimpl Dropkick.Attachable, for: URI do
43 - def key(%URI{path: path}), do: path
44 -
45 - def name(%URI{path: path}), do: Path.basename(path)
46 -
47 - def content(%URI{} = uri) do
48 - uri = String.to_charlist(URI.to_string(uri))
49 -
50 - case :httpc.request(:get, {uri, []}, [], body_format: :binary) do
51 - {:ok, {{'HTTP/1.1', 200, 'OK'}, _headers, content}} ->
52 - {:ok, content}
53 -
54 - {:ok, {{'HTTP/1.1', code, _}, _headers, _}} ->
55 - {:error, "Unsuccessful response code: #{code}"}
56 -
57 - {:error, {reason, _}} ->
58 - {:error, "Could not read path: #{reason}"}
59 - end
60 - end
61 - end
62 -
63 - defimpl Dropkick.Attachable, for: Dropkick.Attachment do
64 - def key(%{key: key}), do: key
65 -
66 - def name(%{filename: name}), do: name
67 -
68 - def content(attachment) do
69 - case attachment do
70 - %{storage: nil} ->
71 - raise "No storage defined for this attachment"
72 -
73 - %{storage: storage} when is_atom(storage) ->
74 - storage.read(attachment)
75 -
76 - %{storage: storage} ->
77 - raise("#{inspect(storage)} is not a module")
78 - end
79 - end
80 - end
81 -
82 - defimpl Dropkick.Attachable, for: Dropkick.Transform do
83 - def key(%{key: key}), do: key
84 -
85 - def name(%{filename: name}), do: name
86 -
87 - def content(%{content: content}), do: content
88 - end
  @@ -1,29 +0,0 @@
1 - defmodule Dropkick.Attachment do
2 - @derive {Jason.Encoder, only: [:key]}
3 - @enforce_keys [:key, :storage, :filename]
4 - defstruct [:key, :status, :storage, :filename, :content_type, :metadata, versions: []]
5 -
6 - use Ecto.Type
7 -
8 - def type, do: :map
9 -
10 - def cast(%__MODULE__{} = atch), do: {:ok, atch}
11 - def cast(atch) when is_map(atch), do: {:ok, struct(__MODULE__, atch)}
12 - def cast(_), do: :error
13 -
14 - def load(data) when is_map(data) do
15 - data =
16 - Enum.map(data, fn
17 - {"versions", v} -> {:versions, Enum.map(v, &load/1)}
18 - {"status", v} -> {:status, String.to_existing_atom(v)}
19 - {"storage", v} -> {:storage, String.to_existing_atom(v)}
20 - {k, v} -> {String.to_existing_atom(k), v}
21 - end)
22 -
23 - {:ok, struct!(__MODULE__, data)}
24 - end
25 -
26 - def dump(%__MODULE__{} = atch), do: {:ok, Map.from_struct(atch)}
27 - def dump(data) when is_map(data), do: {:ok, data}
28 - def dump(_), do: :error
29 - end
Loading more files…