22

So I'm using Active Storage to upload multiple images attached to a Collection model. Everything works well except whenever I'm trying to purge/delete a single attachment from a collection.

The problem: For some reason all my images immediately get purged/deleted whenever I load the show page of a collection. Of course I only want to delete a file whenever I click the link. Does anyone know how to solve this problem?

My collection show view:

<div id="gallery">
  <% @collection.images.each do |image| %>
    <%= image_tag(image) %>
    <%= link_to 'Remove image', image.purge %>
  <% end %>
</div>

I've read documentation on http://edgeguides.rubyonrails.org/active_storage_overview.html#removing-files (see paragraph 4)

but sadly this doesn't give any information on how to specifically use the purge or purge_later method.

EDIT Currently changed my code to this (which still sadly doesn't work):

<div id="gallery">
  <% @collection.images.each do |image| %>
    <%= image_tag(image) %>
    <%= link_to 'Remove', delete_image_attachment_collections_url(image.signed_id),
                method: :delete,
                data: { confirm: 'Are you sure?' } %>
    <% end %>
</div>

With this in my collections_controller.rb

  def delete_image_attachment
    @image = ActiveStorage::Blob.find_signed(params[:id])
    @image.purge
    redirect_to root_path
  end

Which is giving me this error after I tried to delete an attached image:

Error I get when returning to the collection show after (trying to) delete an image.

Server log:

Started DELETE "/collections/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBYdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--3e75276d414b4c2040e02cf0afbc083e2337faa0/delete_image_attachment" for ::1 at 2018-03-29 19:06:55 +0200
Processing by CollectionsController#delete_image_attachment as HTML
  Parameters: {"authenticity_token"=>"60zIkeknxRYp/sJIWNwF+BrEftYHSCQvak34h8FkadPXgVPQSXN/sCoxI/6FU+jZbqQitES81fyqkmIx6XYp6w==", "id"=>"eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBYdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--3e75276d414b4c2040e02cf0afbc083e2337faa0"}
  ActiveStorage::Blob Load (0.1ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1 LIMIT $2  [["id", 90], ["LIMIT", 1]]
  ↳ app/controllers/collections_controller.rb:69
  Disk Storage (0.1ms) Deleted file from key: 8wpzqPQcWYjK2rVEejcU88FB
  Disk Storage (0.0ms) Deleted files by key prefix: variants/8wpzqPQcWYjK2rVEejcU88FB/
   (0.0ms)  BEGIN
  ↳ app/controllers/collections_controller.rb:70
  ActiveStorage::Blob Destroy (0.2ms)  DELETE FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1  [["id", 90]]
  ↳ app/controllers/collections_controller.rb:70
   (2.0ms)  COMMIT
  ↳ app/controllers/collections_controller.rb:70
  ActiveStorage::Attachment Load (0.2ms)  SELECT  "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = $1 AND "active_storage_attachments"."record_type" = $2 AND "active_storage_attachments"."name" = $3 LIMIT $4  [["record_id", 90], ["record_type", "ActiveStorage::Blob"], ["name", "preview_image"], ["LIMIT", 1]]
  ↳ app/controllers/collections_controller.rb:70
Redirected to http://localhost:3000/
Completed 302 Found in 5ms (ActiveRecord: 2.5ms)

Output of rake routes:

                            Prefix Verb   URI Pattern                                                                              Controller#Action
                              root GET    /                                                                                        home#index
                             about GET    /about(.:format)                                                                         pages#about
                           contact GET    /contact(.:format)                                                                       pages#contact
                          settings GET    /settings(.:format)                                                                  settings#edit
                       new_setting GET    /setting/new(.:format)                                                                   settings#new
                      edit_setting GET    /setting/edit(.:format)                                                                  settings#edit
                           setting GET    /setting(.:format)                                                                       settings#show
                                   PATCH  /setting(.:format)                                                                       settings#update
                                   PUT    /setting(.:format)                                                                       settings#update
                                   DELETE /setting(.:format)                                                                       settings#destroy
                                   POST   /setting(.:format)                                                                       settings#create
delete_image_attachment_collection DELETE /collections/:id/delete_image_attachment(.:format)                                       collections#delete_image_attachment
                       collections GET    /collections(.:format)                                                                   collections#index
                                   POST   /collections(.:format)                                                                   collections#create
                    new_collection GET    /collections/new(.:format)                                                               collections#new
                   edit_collection GET    /collections/:id/edit(.:format)                                                          collections#edit
                        collection GET    /collections/:id(.:format)                                                               collections#show
                                   PATCH  /collections/:id(.:format)                                                               collections#update
                                   PUT    /collections/:id(.:format)                                                               collections#update
                                   DELETE /collections/:id(.:format)                                                               collections#destroy
                rails_service_blob GET    /rails/active_storage/blobs/:signed_id/*filename(.:format)                               active_storage/blobs#show
         rails_blob_representation GET    /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
                rails_disk_service GET    /rails/active_storage/disk/:encoded_key/*filename(.:format)                              active_storage/disk#show
         update_rails_disk_service PUT    /rails/active_storage/disk/:encoded_token(.:format)                                      active_storage/disk#update
              rails_direct_uploads POST   /rails/active_storage/direct_uploads(.:format)                                           active_storage/direct_uploads#create

My routes.rb:

Rails.application.routes.draw do
  root 'home#index'

  get 'about', to: 'pages#about', as: :about
  get 'contact', to: 'pages#contact', as: :contact
  get 'instellingen', to: 'settings#edit'

  resource :setting
  resources :collections do
    member do
      delete :delete_image_attachment
    end
  end
end
Jason Swett
  • 43,526
  • 67
  • 220
  • 351
royketelaar
  • 1,389
  • 2
  • 11
  • 21

5 Answers5

28

You are looping through the collection of images and calling the purge method on each one. Instead you should be linking to a destroy method on your controller, something like the below taking into account your controller actions and guessing at your route names.

The error is because image object returns its full path and the link thinks that what you want to point to. Instead you just want its signed_id and want the link to call the route that has your delete_image_attachment path.

 <%= link_to 'Remove', delete_image_attachment_collections_url(image.signed_id),
                method: :delete,
                data: { confirm: 'Are you sure?' } %>

The destroy method would look something like this...

def delete_image_attachment
  @image = ActiveStorage::Blob.find_signed(params[:id])
  @image.purge
  redirect_to collections_url
end

The route should be something like so...

resources :collections do
  member do
    delete :delete_image_attachment
  end
end

Check out the rails routing guide for more fun routing facts.

dbugger
  • 15,868
  • 9
  • 31
  • 33
  • what kind of line would I need to add to the routes? I now have `delete '/rails/active_storage/blobs/:signed_id/', to: 'collections#delete_image_attachment', as: :delete_image_attachment`. Sorry, i'm kinda new at this. – royketelaar Mar 29 '18 at 13:52
  • Added an example, might need to play a bit with it, but that's the basic idea. – dbugger Mar 29 '18 at 14:40
  • I think I'm getting there.. but still errors sadly when I try to delete an attachment (see edit). I also added what happened on the server when I tried to delete. It seems like it deleted _something_ of the attachment which causes an error for the code when trying to show an image. – royketelaar Mar 29 '18 at 17:11
  • Yes, you now have a simple nil class issue. Odds are good you need to refresh your collection before showing it. – dbugger Mar 29 '18 at 17:26
  • I _kind of_ got it working now. Although it still has some problems (see the answer that I posted). Thanks for your help so far – royketelaar Apr 03 '18 at 16:58
  • @dbugger could routing concerns apply here? https://guides.rubyonrails.org/routing.html#routing-concerns – BenKoshy Aug 30 '18 at 16:08
  • 4
    Worked with `ActiveStorage::Attachment.find(params[:id])` instead of `ActiveStorage::Blob.find_signed(params[:id])`, where `id` is the `ActiveStorage::Attachment` table `id`. – iGian Apr 02 '19 at 11:43
  • @iGian your comment led me down the right path! I was really trying to delete the entire attachment and it's blob. Dbugger's answer seems to only try to delete the blob, but fails at the database level, due to a foreign key constraint. Deleting the attachment like you said, also deletes the blob. Thanks! – Alan Oct 16 '19 at 14:33
  • The solution only works if there are no attachments connected to the blob anymore( child record/ foreign_key error). If not you have to search for the attachment via image.id. Then purge the attachment. Active Storage will then delete the attachment first and then the blob. – Sebastian Peter Jul 24 '23 at 11:13
11

The following didn't work for me.

def delete_image_attachment
  @image = ActiveStorage::Blob.find_signed(params[:id])
  @image.purge
  redirect_to collections_url
end

So what i did is found the attachment and purged it. You can do purge_later which is recommended.

def delete_image_attachment
  @image = ActiveStorage::Blob.find_signed(params[:id])
  @image.attachments.first.purge
  redirect_to collections_url
end

This removed both attachment and blob record.

Praveen KJ
  • 630
  • 2
  • 11
  • 22
9

Thanks for your update regarding Blob vs Attachment! After purging the attachment I redirect_back to the form I came from like this:

def remove_attachment
  attachment = ActiveStorage::Attachment.find(params[:id])
  attachment.purge # or use purge_later
  redirect_back(fallback_location: collections_path)
end

Not the best solution to reload the entire page but works ...

Jason Swett
  • 43,526
  • 67
  • 220
  • 351
Max Weber
  • 109
  • 4
6

Okay I sort of solved my problem but I don't really understand what's happening.

Whenever I click on the "Remove" button I get this error:

It wants to redirect to the collection_url with ID 43 (while the ID of my collection is actually 6, 43 is probably the ID of the image attachment).

When I reload the same collection page manually, the picture is gone (so it sort of works) but nothing of this is ofcourse ideal.

Does someone know how I can improve my code so that the redirect_to in my controller points to the current collection ID instead of the Activestorage image attachment ID?

My files

View: collection/show.html.erb:

<div id="gallery">
  <% @collection.images.each do |image| %>
    <%= image_tag(image) %>
    <%= link_to 'Remove', delete_image_attachment_collection_url(image),
                    method: :delete,
                    data: { confirm: 'Are you sure?' } %>
  <% end %>
</div>

Controller: collections_controller.rb

class CollectionsController < ApplicationController
  before_action :set_collection, only: [:show, :edit, :update, :destroy]
  before_action :set_collections

  # GET /collections
  # GET /collections.json
  def index
  end

  # GET /collections/1
  # GET /collections/1.json
  def show
  end

  # GET /collections/new
  def new
    @collection = Collection.new
  end

  # GET /collections/1/edit
  def edit
  end

  # POST /collections
  # POST /collections.json
  def create
    @collection = Collection.new(collection_params)

    respond_to do |format|
      if @collection.save
        format.html { redirect_to @collection, notice: 'Fotocollectie is aangemaakt.' }
        format.json { render :show, status: :created, location: @collection }
      else
        format.html { render :new }
        format.json { render json: @collection.errors, status: :unprocessable_entity }
      end
    end

    # collection = Collection.create!(collection_params)
    # redirect_to collection
  end

  # PATCH/PUT /collections/1
  # PATCH/PUT /collections/1.json
  def update
    respond_to do |format|
      if @collection.update(collection_params)
        format.html { redirect_to @collection, notice: 'Fotocollectie is bijgewerkt.' }
        format.json { render :show, status: :ok, location: @collection }
      else
        format.html { render :edit }
        format.json { render json: @collection.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /collections/1
  # DELETE /collections/1.json
  def destroy
    @collection.destroy
    respond_to do |format|
      format.html { redirect_to collections_url, notice: 'Fotocollectie is verwijderd.' }
      format.json { head :no_content }
    end
  end

  def delete_image_attachment
    @image = ActiveStorage::Attachment.find(params[:id])
    @image.purge
    redirect_to @current_page
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_collection
      @collection = Collection.find(params[:id])
    end

    def set_collections
      @collections = Collection.all
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def collection_params
      params.require(:collection).permit(:title, :order, images: [])
    end
end

Model: collection.rb

class Collection < ApplicationRecord
  has_many_attached :images
end

Routes: routes.rb

Rails.application.routes.draw do
  root 'home#index'

  get 'about', to: 'pages#about', as: :about
  get 'contact', to: 'pages#contact', as: :contact
  get 'settings', to: 'settings#edit'

  resource :setting

  resources :collections do
    member do
      delete :delete_image_attachment
    end
  end
end
royketelaar
  • 1,389
  • 2
  • 11
  • 21
  • I don't know what your @current_page is, but you can use something like redirect_to collection_path(@image.record) or edit_collection_path(@image.record) if you want to go back to the collection show or edit. – hungmi Jul 31 '18 at 17:39
0

I used an approach similar to @dbugger's.

In the view:

<% @product.images.each do |image| %>
  <%= image_tag image, height: '300', width: '300' %>
  <%= link_to 'Remove', 
              delete_an_image_url(attachment_id: image.id, product_id: @product.id),
              method: :delete,
              data: { confirm: 'Are you sure?' } %>
<% end %>

In the controller

def delete_an_image
  @image = ActiveStorage::Attachment.find_by_id(delete_an_image_params[:attachment_id])
  @image.purge   
  redirect_to edit_product_path(delete_an_image_params[:product_id])
end

private

def delete_an_image_params
  params.permit(:authenticity_token, :commit, :_method, :product_id, :attachment_id)
end

In routes

delete '/delete_an_image/' => 'products#delete_an_image', as: :delete_an_image

If you arrived here just looking for how to delete an attachment via the rails console.

For a single attachment

user.image.purge

For multiple attachments:

user.images.purge

Source: https://edgeguides.rubyonrails.org/active_storage_overview.html#removing-files

stevec
  • 41,291
  • 27
  • 223
  • 311