zachdaniel

zachdaniel

Creator of Ash

AshStorage - Attachment and file management for Ash Framework

Introducing AshStorage! Attachment and file management that slots directly into your resources :smiling_face_with_sunglasses:

I had hoped to get this to a releasable state before sharing. It’s not quite there yet but I’m announcing it anyway as I know the Ash community is waiting.

It isn’t on hex yet, but I’ve gotten quite a bit of the roadmap done and its in a decent place to accept contributions from the community, and for beta testers to give it a shot.

Examples:

defmodule MyApp.Post do
  use Ash.Resource,
    extensions: [AshStorage]

  storage do
    service {AshStorage.Service.S3, bucket: "my-app-uploads"}
    blob_resource MyApp.StorageBlob
    attachment_resource MyApp.StorageAttachment

    has_one_attached :cover_image do
      analyzer MyApp.ImageDimensions,
        write_attributes: [width: :image_width, height: :image_height]

      variant :thumbnail, {MyApp.Resize, width: 200, height: 200}, generate: :eager
      variant :hero, {MyApp.Resize, width: 1200, height: 630}, generate: :oban
      variant :webp, {MyApp.Convert, format: :webp}
    end

    has_many_attached :documents do
      analyzer MyApp.FileInfo
      analyzer MyApp.PdfPageCount, analyze: :oban

      variant :preview, MyApp.PdfThumbnail, generate: :oban
    end
  end

  attributes do
    uuid_primary_key :id
    attribute :title, :string, allow_nil?: false, public?: true
    attribute :image_width, :integer, public?: true
    attribute :image_height, :integer, public?: true
  end

  actions do
    create :create do
      accept [:title]
      argument :cover_image, Ash.Type.File, allow_nil?: true

      change {AshStorage.Changes.HandleFileArgument,
              argument: :cover_image, attachment: :cover_image}
    end
  end
end
defmodule MyApp.StorageBlob do
    use Ash.Resource,
      extensions: [AshStorage.BlobResource, AshOban]

    postgres do
      table "storage_blobs"
      repo MyApp.Repo
    end

    oban do
      triggers do
        trigger :run_pending_analyzers do
          action :run_pending_analyzers
          read_action :read
          where expr(pending_analyzers == true)
          scheduler_cron("* * * * *")
        end

        trigger :run_pending_variants do
          action :run_pending_variants
          read_action :read
          where expr(pending_variants == true)
          scheduler_cron("* * * * *")
        end

        trigger :purge_blob do
          action :purge_blob
          read_action :read
          where expr(pending_purge == true)
          scheduler_cron("* * * * *")
        end
      end
    end

    attributes do
      uuid_primary_key :id
    end
  end

  # Create with file — dimensions written to parent, thumbnail generated instantly
  post = Ash.create!(Post, %{title: "Hello", cover_image: upload})
  post.image_width  #=> 1920

  # Variant URLs — thumbnail ready, hero generating in background
  post = Ash.load!(post, [:cover_image_thumbnail_url, :cover_image_hero_url])
  post.cover_image_thumbnail_url  #=> "https://my-app-uploads.s3.amazonaws.com/..."

  # On-demand — webp variant generated on first request
  post = Ash.load!(post, :cover_image_webp_url)

https://github.com/ash-project/ash_storage

Most Liked

hwuethrich

hwuethrich

FYI, I just managed to get caching (client- and server-side) working with plug_http_cache:

{:plug_http_cache, "~> 0.4.0"},
{:http_cache, "~> 0.4.0"},
{:plug_cache_control, "~> 1.1"},
{:http_cache_store_disk, "~> 0.3.0"} # or http_cache_store_memory

config.ex:

config :http_cache_store_disk,
  cache_dir: "priv/cache",
  memory_limit: 256 * 1024 * 1024

Note: Had to use fixed memory_limit because my Mac is running low on memory, and the store rejects if memory >70% by default.

In my router.ex:

pipeline :cache do
  plug PlugHTTPCache, store: :http_cache_store_disk
  plug PlugCacheControl, directives: [:public, max_age: {24, :hour}]
end

scope "/" do
  pipe_through [:browser, :cache]

  forward "/storage", AshStorage.Plug.Proxy,
    service:
      {AshStoragePGLO.Service,
       lo_resource: AshStorageTest.Gallery.StorageLO, base_url: "/storage"}
end

Ideally, the proxy should set etag/date headers based on the blob meta data (maybe using plug_http_validator).

Could also be useful when using proxy with S3 and other external services.

pierrelegall

pierrelegall

I was working on a non-Ash Elixir package to manage S3 use for dev env (stored in the filesystem) and test env (stored in an ETS). See: GitHub - pierrelegall/ps3: S3-compatible storage plug for Elixir/Phoenix applications designed for dev & test environments. · GitHub

Thank you to solve this the Ash way!

I’ll be happy to beta test it as soon as possible :slight_smile:

munksgaard

munksgaard

Really cool! Any plans/ideas for something like ash_storage_phoenix for making handling files in forms easier, or what’s the plan there?

Where Next?

Popular in Announcing Top

msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43806 214
New
maltoe
Hello! Came here to announce ChromicPDF, a pet project PDF generator I’ve been working on for the past few months. Why another PDF gener...
New
bryanjos
Hi, I wanted share a small library we at Revelry Labs made for rendering react components from the server side. There are instructions fo...
New
kip
Image is an image processing library for Elixir. It is based upon the fabulous vix library that provides a libvips wrapper for Elixir. I...
622 19010 194
New
RobertDober
Earmark is a pure-Elixir Markdown converter. It is intended to be used as a library (just call Earmark.as_html), but can also be used as...
239 12673 134
New
cjen07
parameterized pipe in elixir: |n> edit: negative index in |n> and mixed usage with |> are supported example: use ParamPipe ...
New
treble37
Just looking for a little feedback on a tiny helper library I built - Sometimes I find the need to convert maps with atom keys to maps w...
New
markmark206
simple_feature_flags is a tiny package that lets you turn features on or off based on which environment (e.g. localhost, staging, product...
New
mplatts
With HEEX released we decided to start a components library using Tailwind CSS - check it out here: Petal Components. We also have a boi...
New
tfwright
After working on it for a couple of months and using it in production for most of that time, today I’ve released LiveAdmin, a LiveView ba...
New

Other popular topics Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 127536 1222
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

We're in Beta

About us Mission Statement