if match?({:module, _}, Code.ensure_compiled(ExAws.S3)) do defmodule Trunk.Storage.S3 do @moduledoc """ A `Trunk.Storage` implementation for Amazon’s S3 service. """ @behaviour Trunk.Storage @doc ~S""" Saves the file to Amazon S3. - `directory` - The directory (will be combined with the `filename` to form the S3 object key). - `filename` - The name of the file (will be combined with the `directory` to form the S3 object key). - `source_path` - The full path to the file to be stored. This is a path to the uploaded file or a temporary file that has undergone transformation - `opts` - The options for the storage system - `bucket:` (required) The S3 bucket in which to store the object - `ex_aws:` (optional) override options for `ex_aws` - All other options are passed through to S3 put object which means you can pass in anything accepted by `t:ExAws.S3.put_object_opts/0` including but not limited to `:acl`, `:meta`, `:content_type`, and `:content_disposition` ## Example: The file will be saved to s3.amazonaws.com/my-bucket/path/to/file.ext ``` Trunk.Storage.S3.save("path/to/", "file.ext", "/tmp/uploaded_file.ext", bucket: "my-bucket") """ @spec save( directory :: String.t(), filename :: String.t(), source_path :: String.t(), opts :: keyword ) :: :ok | {:error, :file.posix()} def save(directory, filename, source_path, opts \\ []) do key = directory |> Path.join(filename) bucket = Keyword.fetch!(opts, :bucket) ex_aws_opts = Keyword.get(opts, :ex_aws, []) save_opts = Keyword.drop(opts, [:bucket, :ex_aws]) with {:ok, source_data} <- File.read(source_path), {:ok, _result} <- put_object(bucket, key, source_data, save_opts, ex_aws_opts) do :ok else error -> error end end defp put_object(bucket, key, source_data, storage_opts, ex_aws_opts) do bucket |> ExAws.S3.put_object(key, source_data, storage_opts) |> ExAws.request(ex_aws_opts) end @doc ~S""" Copies the file within Amazon S3. - `directory` - The relative directory within which to find the file (will be combined with the `filename` to form the source S3 object key). - `filename` - The name of the file to be copied from (will be combined with the `directory` to form the source S3 object key). - `to_directory` - The relative directory within which to copy the file (will be combined with the `to_filename` to form the destination S3 object key). - `to_filename` - The name of the file to be copied to (will be combined with the `to_directory` to form the destination S3 object key). - `opts` - The options for the storage system - `path:` (required) The base path within which to save files - `acl:` (optional) The file mode to store the file (accepts octal `0o644` or string `"0644"`). See `File.chmod/2` for more info. ## Example: The file will be copied from s3.amazonaws.com/my-bucket/path/to/file.ext to s3.amazonaws.com/my-bucket/copied/to/file.copy ``` Trunk.Storage.S3.copy("path/to/", "file.ext", "copied/to/", "file.copy", bucket: "my-bucket") """ @spec copy( directory :: String.t(), filename :: String.t(), to_directory :: String.t(), to_filename :: String.t(), opts :: keyword ) :: :ok | {:error, :file.posix()} def copy(directory, filename, to_directory, to_filename, opts \\ []) do key = directory |> Path.join(filename) to_key = to_directory |> Path.join(to_filename) bucket = Keyword.fetch!(opts, :bucket) ex_aws_opts = Keyword.get(opts, :ex_aws, []) copy_opts = opts |> Keyword.drop([:bucket, :ex_aws]) |> Keyword.put(:metadata_directive, :REPLACE) with {:ok, _result} <- copy_object(bucket, key, to_key, copy_opts, ex_aws_opts) do :ok else error -> error end end defp copy_object(bucket, key, to_key, storage_opts, ex_aws_opts) do bucket |> ExAws.S3.put_object_copy(to_key, bucket, key, storage_opts) |> ExAws.request(ex_aws_opts) end def retrieve(directory, filename, destination_path, opts \\ []) do key = directory |> Path.join(filename) bucket = Keyword.fetch!(opts, :bucket) ex_aws_opts = Keyword.get(opts, :ex_aws, []) {:ok, %{body: data}} = get_object(bucket, key, ex_aws_opts) File.write(destination_path, data, [:binary, :write]) end defp get_object(bucket, key, ex_aws_opts) do bucket |> ExAws.S3.get_object(key) |> ExAws.request(ex_aws_opts) end @doc ~S""" Deletes the file from Amazon S3. - `directory` - The directory (will be combined with the `filename` to form the S3 object key. - `filename` - The name of the file (will be combined with the `directory` to form the S3 object key. - `opts` - The options for the storage system - `bucket:` (required) The S3 bucket in which to store the object - `ex_aws:` (optional) override options for `ex_aws` ## Example: The file will be removed from s3.amazonaws.com/my-bucket/path/to/file.ext ``` Trunk.Storage.S3.delete("path/to/", "file.ext", bucket: "my-bucket") """ @spec delete(String.t(), String.t(), keyword) :: :ok | {:error, :file.posix()} def delete(directory, filename, opts \\ []) do key = directory |> Path.join(filename) bucket = Keyword.fetch!(opts, :bucket) ex_aws_opts = Keyword.get(opts, :ex_aws, []) bucket |> ExAws.S3.delete_object(key) |> ExAws.request(ex_aws_opts) |> case do {:ok, _} -> :ok error -> error end end @doc ~S""" Generates a URL to the S3 object - `directory` - The directory (will be combined with the `filename` to form the S3 object key. - `filename` - The name of the file (will be combined with the `directory` to form the S3 object key. - `opts` - The options for the storage system - `bucket:` (required) The S3 bucket in which to store the object. - `virtual_host:` (optional) boolean indicator whether to generate a virtual host style URL or not. - `signed:` (optional) boolean whether to sign the URL or not. - `ex_aws:` (optional) override options for `ex_aws` ## Example: ``` Trunk.Storage.S3.build_url("path/to", "file.ext", bucket: "my-bucket") #=> "https://s3.amazonaws.com/my-bucket/path/to/file.ext" Trunk.Storage.S3.build_url("path/to", "file.ext", bucket: "my-bucket", virtual_host: true) #=> "https://my-bucket.s3.amazonaws.com/path/to/file.ext" Trunk.Storage.S3.build_url("path/to", "file.ext", bucket: "my-bucket", signed: true) #=> "https://s3.amazonaws.com/my-bucket/path/to/file.ext?X-Amz-Algorithm=AWS4-HMAC-SHA256&…" ``` """ def build_uri(directory, filename, opts \\ []) do key = directory |> Path.join(filename) bucket = Keyword.fetch!(opts, :bucket) ex_aws_opts = Keyword.get(opts, :ex_aws, []) config = ExAws.Config.new(:s3, ex_aws_opts) {:ok, url} = ExAws.S3.presigned_url(config, :get, bucket, key, opts) if Keyword.get(opts, :signed, false) do url else uri = URI.parse(url) %{uri | query: nil} |> URI.to_string() end end end end