Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion external/activestorage_ex_rails/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ ruby '2.4.2'
gem 'rails', '5.2.2.1'

# Database
gem 'pg'
gem 'pg', '~> 0.18'

# Server
gem 'puma', '~> 3.11'
Expand Down
4 changes: 2 additions & 2 deletions external/activestorage_ex_rails/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ GEM
nio4r (2.3.1)
nokogiri (1.10.3)
mini_portile2 (~> 2.4.0)
pg (1.1.4)
pg (0.21.0)
public_suffix (3.0.3)
puma (3.12.1)
rack (2.0.7)
Expand Down Expand Up @@ -199,7 +199,7 @@ DEPENDENCIES
interactive_editor
listen (>= 3.0.5, < 3.2)
mini_magick (~> 4.8)
pg
pg (~> 0.18)
puma (~> 3.11)
rails (= 5.2.2.1)
sass-rails (~> 5.0)
Expand Down
18 changes: 18 additions & 0 deletions lib/activestorage/blob_stats.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule ActivestorageEx.Activestorage.BlobStats do
@moduledoc ~S"""
Interop with ActiveStorage requires data be a specific shape before persistence.

This module simply contains a struct that represents how AS expects blob stats (not generated attributes like `key`)
to look before inserting in the database.

There is some overlap with %ActivestorageEx.Blob{} but that is our internal representation, this is ActiveStorage's.
This is not used interally and is only to ensure shape on the way out
"""

@enforce_keys [:filename, :content_type, :metadata, :checksum, :byte_size]
defstruct filename: nil,
content_type: nil,
metadata: nil,
checksum: nil,
byte_size: nil
end
39 changes: 39 additions & 0 deletions lib/blob.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,45 @@
defmodule ActivestorageEx.Blob do
alias ActivestorageEx.Activestorage.BlobStats

@enforce_keys [:key, :content_type, :filename]
defstruct key: nil,
content_type: nil,
filename: nil

def analyze!(image_path) do
image =
image_path
|> Mogrify.open()
|> Mogrify.verbose()

%BlobStats{
content_type: "image/#{image.format}",
byte_size: size_on_disk(image.path),
filename: filename(image),
checksum: compute_checksum!(image.path),
metadata: %{
identified: true,
analyzed: true,
height: image.height,
width: image.width
}
}
end

defp size_on_disk(image_path) do
%{size: size} = File.stat!(image_path)

size
end

defp filename(image) do
Path.basename(image.path, image.ext)
end

defp compute_checksum!(image_path) do
File.stream!(image_path, [], 2048)
|> Enum.reduce(:crypto.hash_init(:md5), fn (line, acc) -> :crypto.hash_update(acc, line) end)
|> :crypto.hash_final
|> Base.encode64
end
end
32 changes: 21 additions & 11 deletions lib/service/disk_service.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,29 +62,27 @@ defmodule ActivestorageEx.DiskService do
Saves an `%Image{}` to disk, as determined by a given `%Blob{}` or `%Variant{}` key

## Parameters
- `image`: A `%Mogrify.Image{}` that isn't persisted
- `image`: A `%Mogrify.Image{}` that isn't persisted _or_ a String.t() path to an image
- `key`: The blob or variant's key. File location will be based off this.
Directories _will_ be created
## Examples
Uploading an `%Image{}` to disk from a `%Blob{}` key

```
image = %Mogrify.Image{}
image = %Mogrify.Image{} | String.t()
blob = %Blob{}

DiskService.upload(image, blob.key) # %Mogrify.Image{}
```
"""
def upload(image, key) do
with :ok <- make_path_for(key) do
image
|> Mogrify.save()
|> rename_image(key)
def upload(%Mogrify.Image{} = image, key) do
do_upload(image, key)
end

:ok
else
{:error, err} -> {:error, err}
end
def upload(image_path, key) when is_binary(image_path) do
image_path
|> Mogrify.open()
|> do_upload(key)
end

@doc """
Expand Down Expand Up @@ -195,6 +193,18 @@ defmodule ActivestorageEx.DiskService do
|> File.exists?()
end

def do_upload(%Mogrify.Image{} = image, key) do
with :ok <- make_path_for(key) do
image
|> Mogrify.save()
|> rename_image(key)

:ok
else
{:error, err} -> {:error, err}
end
end

defp make_path_for(key) do
key
|> path_for()
Expand Down
36 changes: 23 additions & 13 deletions lib/service/s3_service.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,14 @@ defmodule ActivestorageEx.S3Service do
{:ok, filepath}
end

def upload(image, key) do
saved_image = image |> Mogrify.save()

with {:ok, image_io} <- File.read(saved_image.path),
{:ok, _} <- put_object_for(key, image_io) do
remove_temp_file(saved_image.path)

:ok
else
{:error, err} ->
remove_temp_file(saved_image.path)
def upload(%Mogrify.Image{} = image, key) do
do_upload(image, key)
end

{:error, err}
end
def upload(image_path, key) when is_binary(image_path) do
image_path
|> Mogrify.open()
|> do_upload(key)
end

def delete(key) do
Expand Down Expand Up @@ -74,6 +68,22 @@ defmodule ActivestorageEx.S3Service do
end
end

defp do_upload(%Mogrify.Image{} = image, key) do
saved_image = Mogrify.save(image)

with {:ok, image_io} <- File.read(saved_image.path),
{:ok, _} <- put_object_for(key, image_io) do
remove_temp_file(saved_image.path)

:ok
else
{:error, err} ->
remove_temp_file(saved_image.path)

{:error, err}
end
end

defp remove_temp_file(filepath) do
File.rm(filepath)
end
Expand Down
49 changes: 49 additions & 0 deletions test/blob_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule ActivestorageExTest.BlobTest do
use ExUnit.Case

alias ActivestorageEx.Blob
alias ActivestorageEx.Activestorage.BlobStats

@image_filepath "test/files/image.jpg"

describe "analyze!/1" do
test "Returns %BlobStats{}" do
assert %BlobStats{} = Blob.analyze!(@image_filepath)
end

test "Returns blob size on disk" do
known_byte_size = 156_746

assert %{byte_size: ^known_byte_size} = Blob.analyze!(@image_filepath)
end

test "Returns blob checksum" do
known_checksum = "o0K/S+b6DobHNNfe6EGCKA=="

assert %{checksum: ^known_checksum} = Blob.analyze!(@image_filepath)
end

test "Returns blob content_type" do
known_content_type = "image/jpeg"

assert %{content_type: ^known_content_type} = Blob.analyze!(@image_filepath)
end

test "Returns blob filename" do
known_filename = "image"

assert %{filename: ^known_filename} = Blob.analyze!(@image_filepath)
end

test "Returns blob metadata" do
known_metadata = %{
identified: true,
analyzed: true,
height: 720,
width: 1080
}

assert %{metadata: ^known_metadata} = Blob.analyze!(@image_filepath)
end
end
end
14 changes: 13 additions & 1 deletion test/service/disk_service_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ defmodule ActivestorageExTest.DiskServiceTest do
end

describe "DiskService.upload/2" do
test "An image is sucessfully saved to disk" do
test "A %Mogrify.Image{} is sucessfully saved to disk" do
Application.put_env(:activestorage_ex, :root_path, "test/files")
image = Mogrify.open("test/files/image.jpg")
key = "test_key"
Expand All @@ -60,6 +60,18 @@ defmodule ActivestorageExTest.DiskServiceTest do
File.rm(DiskService.path_for(key))
end

test "An image is successfully saved to disk from string path" do
Application.put_env(:activestorage_ex, :root_path, "test/files")
image_path = "test/files/image.jpg"
key = "test_key"

DiskService.upload(image_path, key)

assert File.exists?(DiskService.path_for(key))

File.rm(DiskService.path_for(key))
end

test "Image directory is created if it doesn't exist" do
Application.put_env(:activestorage_ex, :root_path, "test/files")
image = Mogrify.open("test/files/image.jpg")
Expand Down
20 changes: 19 additions & 1 deletion test/service/s3_service_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ defmodule ActivestorageExTest.S3ServiceTest do
end

describe "S3Service.upload/2" do
test "An image is sucessfully saved to s3" do
test "A %Mogrify.Image{} is sucessfully saved to s3" do
image = Mogrify.open("test/files/image.jpg")

S3Service.upload(image, @test_key)
Expand All @@ -60,6 +60,16 @@ defmodule ActivestorageExTest.S3ServiceTest do
delete_test_image()
end

test "An image is sucessfully saved to s3 from string path" do
image_path = "test/files/image.jpg"

S3Service.upload(image_path, @test_key)

assert S3Service.exist?(@test_key)

delete_test_image()
end

test "An image with a complex path is sucessfully saved to s3" do
image = Mogrify.open("test/files/image.jpg")
key = "variants/new_key"
Expand All @@ -83,6 +93,14 @@ defmodule ActivestorageExTest.S3ServiceTest do
refute S3Service.exist?(@test_key)
end

test "No error is thrown if a file doesn't exist" do
key = "super_fake_test_key"

refute S3Service.exist?(key)

:ok = S3Service.delete(key)
end

test "An image with a complex path is sucessfully deleted from s3" do
key = "variants/new_key"
upload_test_image(key)
Expand Down