Search code examples
ruby-on-railsrubyrails-activestorage

Count the number of times a specific user downloaded files through Active Storage


Both my Post & Comment models have has_one_attached :file. What I want to do is to count the number of times a specific user downloaded either a post's file or a comment's file. Create a sort of download_counter_cache on the User model that increments whenever this user downloads a file. How can I achieve this?

Update based on Max's answer

This is what I did for the moment:

# Migration file
class CreateDownloads < ActiveRecord::Migration[6.0]
  def change
    create_table :downloads do |t|
      t.references :user, null: false, foreign_key: true
      t.references :resource, polymorphic: true

      t.timestamps
    end
  end
end
# Routes.rb
 concern :downloadable do
   resources :downloads, only: :create
 end

 resources :posts, concerns: :downloadable do
   [...] # Routes such as like/dislike
   resources :comments, concerns: :downloadable do
     [...] # Routes such as like/dislike
   end
 end
# In posts/_post.html.erb
<%= link_to([@post, :downloads], html_options = {class: 'file w-100'}, method: :post) do %>
  [...]
<% end %>

My downloads_controller is exactly the same as suggested in Max's answer, as are my Post, Comment, User & Download models.

The problem is that whenever I try to download it redirects me to downloads#index which obviously does not exist. I am not sure how I am supposed to create the resource class.


Solution

  • max has a good answer, but if you just want to have only one column on a User model to count all downloads it could be done without a polymorphic join table.

    Let's add an attribute to the User module to store the counts:

      add_column :users, :download_count, :integer
    

    Then add a controller that will handle the counts and redirect to a downloaded file:

    class DownloadsController < ApplicationController
      def create
        # using find_by so it doesn't throw any errors
        resource = Post.find_by_id(params[:post_id]) || Comment.find_by_id(params[:comment_id])
    
        if resource
          current_user.increment!(:download_count)
          redirect_to rails_blob_path(resource.file, disposition: "attachment")
        else
          render nothing: true
        end
      end
    end
    

    Routes will look like:

      resources :downloads, only: :create
    

    Download link in view looks like:

    # for post file
    <%= link_to 'download file', downloads_path(post_id: @post.id), method: :post %>
    
    # for comment file
    <%= link_to 'download file', downloads_path(comment_id: comment.id), method: :post %>
    

    As simple as that.


    Update: changed the method from new to post to prevent double rendering.