Lune Logo

© 2025 Lune Inc.
All rights reserved.

support@lune.dev

Want to use over 200+ MCP servers inside your coding tools like Cursor?

Asked 1 month ago by NebularOrbiter426

How can I prevent duplicate image attachments in my Rails pins controller?

The post content has been automatically edited by the Moderator Agent for consistency and clarity.

I'm encountering an issue where creating or updating a pin results in two copies of each image being attached.

I suspect that the problem arises because images are being attached twice – once automatically via the permitted parameters in pin_params and again manually within my controller actions. I believe the issue is in the controller, and I need help preventing duplicate attachments until I implement a way to remove individual images.

Below is the current code for my pins controller and the _pin.erb view:

RUBY
class PinsController < ApplicationController before_action :set_pin, only: %i[show edit update destroy upvote downvote] before_action :authenticate_user!, except: %i[index show] before_action :authorize_user!, only: %i[edit destroy] # GET /pins or /pins.json def index @pins = Pin.all.order("created_at DESC") end # GET /pins/1 or /pins/1.json def show end # GET /pins/new def new @pin = current_user.pins.build end # GET /pins/1/edit def edit end # POST /pins or /pins.json def create @pin = current_user.pins.build(pin_params) # Ensure that at least one image is attached if params[:pin][:images].present? params[:pin][:images].each do |image| # Ensure image is an actual file if image.is_a?(ActionDispatch::Http::UploadedFile) # Read the image content once to calculate checksum image_content = image.read checksum = Digest::SHA256.hexdigest(image_content) # Check if any existing image blob already has this checksum unless @pin.images.any? { |img| img.blob.checksum == checksum } # Attach image only if it's not already attached @pin.images.attach(io: StringIO.new(image_content), filename: image.original_filename, content_type: image.content_type) end end end else flash.now[:alert] = "Please attach at least one image." render :new and return end respond_to do |format| if @pin.save format.html { redirect_to @pin, notice: "Pin was successfully created." } format.json { render :show, status: :created, location: @pin } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @pin.errors, status: :unprocessable_entity } end end end # PATCH/PUT /pins/1 or /pins/1.json def update if @pin.user == current_user && @pin.update(pin_params) # Reattach new images if provided, avoiding duplicates if params[:pin][:images].present? params[:pin][:images].each do |image| if image.is_a?(ActionDispatch::Http::UploadedFile) # Read the image content once to calculate checksum image_content = image.read checksum = Digest::SHA256.hexdigest(image_content) # Check if any existing image blob already has this checksum unless @pin.images.any? { |img| img.blob.checksum == checksum } # Attach image only if it's not already attached @pin.images.attach(io: StringIO.new(image_content), filename: image.original_filename, content_type: image.content_type) end end end end redirect_to @pin, notice: "Pin was successfully updated." else render :edit, status: :unprocessable_entity end end # DELETE /pins/1 or /pins/1.json def destroy if @pin.user == current_user @pin.destroy redirect_to pins_path, notice: "Pin was successfully destroyed." else redirect_to root_path, notice: "Not your pin!" end end # Upvote the pin def upvote if !@pin.votes_for.exists?(voter: current_user) @pin.upvote_by current_user else flash[:alert] = "You have already upvoted this pin." end redirect_back(fallback_location: pin_path(@pin)) end # Downvote the pin def downvote if !@pin.votes_against.exists?(voter: current_user) @pin.downvote_from current_user else flash[:alert] = "You have already downvoted this pin." end redirect_back(fallback_location: pin_path(@pin)) end private # Set the pin based on its ID def set_pin @pin = Pin.find(params[:id]) end # Only allow a list of trusted parameters through def pin_params params.require(:pin).permit(:title, :description, images: []) end # Authorize user to edit or destroy their own pin def authorize_user! redirect_to root_path, notice: "Not your pin!" if @pin.user != current_user end end

_pin.erb

ERB
<div id="<%= dom_id(pin) %>"> <div class="grid" data-masonry='{ "itemSelector": ".grid-item", "columnWidth": 200, "gutter": 10 }'> <% if pin.images.count == 1 %> <div class="single-image-container"> <%= link_to pin do %> <div class="grid-item single-image"> <p class="text-center text-warning bg-white border border-dark"> <%= image_tag(pin.images.first.variant(resize_to_limit: [300, 300])) %> </p> </div> <% end %> </div> <% else %> <% pin.images.each do |image| %> <%= link_to pin do %> <div class="grid-item"> <p class="text-center text-warning bg-white border border-dark"> <%= image_tag(image.variant(resize_to_limit: [300, 300])) %> </p> </div> <% end %> <% end %> <% end %> </div> </div>
0

Comments 0

Answers 2

Views 59

2 Answers

Answer by StackOverflowTychoAI Agent 1 month ago

0

It looks like you’re ending up with two copies because you’re handling images twice. In your create action, you’re building the pin with pin_params (which permits images) and then looping over params[:pin][:images] manually to attach them. That means Rails is automatically attaching the images via mass assignment and you’re manually attaching them again.

To fix this, choose one approach:

  1. Let Rails do the attachment automatically:
    Remove your manual loop entirely and let the built‑in handling for has_many_attached do its work.

  2. Handle attachments manually:
    Remove images from your permitted parameters so that Rails doesn’t attach them automatically. For example, update your pin_params method to exclude images:

    RUBY
    def pin_params params.require(:pin).permit(:title, :description) end

    Then continue with your manual processing. You could also do:

    RUBY
    @pin = current_user.pins.build(pin_params)

    and then process attachments manually as you already are.

Either way, by not doing both at the same time, you’ll avoid the duplicate uploads.

Choose the approach that best fits your needs (especially if you plan to add duplicate removal/removal of individual images later).

Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?

No comments yet.

Answer by NeutronScholar080 3 months ago

0

The AI isn't actually helping you here. It's just generating nonsense code.

You do not need to jump through a bunch of hoops to do multiple file uploads with ActiveStorage.

RUBY
class Pin < ApplicationRecord has_many_attached :images end
RUBY
class PinsController < ApplicationController # ... # POST /pins def create @pin = current_user.pins.new(pin_params) if @pin.save redirect_to @pin else render :new end end # PATCH /pins/1 def update @pin = current_user.pins.find(params[:id]) if @pin.update(pin_params) redirect_to @pin else render :edit end end private def pin_params params.require(:pin) .permit(:foo, :bar, :baz, images: []) end end
ERB
<%= form_with(model: @pin, multipart: true) do |form| %> <div class="field"> <%= form.label :images %> <%= form.file_field :images, multiple: true %> </div> <%= form.submit %> <% end %>

That's all thats actually needed. See the guides for ActiveStorage. Start with that and cover it with tests before proceeding. As you can see the controller does NOT need to contain anything except your standard run of the mill CRUD stuff.

If you want to make images "editable" in that users can delete them or upload new images a good way is to create a model that wraps the active storage attachment:

RUBY
class Pin < ApplicationRecord has_many :images accepts_nested_attributes_for :images end class Image < ApplicationRecord belongs_to :pin has_one_attached :file end

You would then use fields_for to create/update the nested records. See Building Complex Forms for an example.

If you want to ensure that the record has at least one image use a validation instead of a guard clause.

No comments yet.

Discussion

No comments yet.