How to clean up ActiveStorage record and storage folder of development environment?

2.5k views Asked by At

According to Rails (edge6.0) guides, we can do maintenance work for ActiveStorage used in System Test and Integration Test by calling the following statement respectively

  # System Test
  FileUtils.rm_rf("#{Rails.root}/storage_test")

  # Integration Test
  FileUtils.rm_rf(Rails.root.join('tmp', 'storage'))

I want to know -

Are there any Rails built-in functions or rake commands or gems to do the following?

  1. remove the orphan blobs (ActiveStorage::Blob records those are no longer associated with any ActiveStorage::Attachment record)
  2. remove the orphan files (files those are no longer associated with any ActiveStorage::Blob record)

I do not see any relevant rake tasks with rails --tasks.

Currently, I am using

# remove blob not associated with any attachment
ActiveStorage::Blob.where.not(id: ActiveStorage::Attachment.select(:blob_id)).find_each do |blob|
  blob.purge # or purge_later
end

and this script to clean orphan files (via rails console)

# run these ruby statement in project rails console
# to remove the orphan file
include ActionView::Helpers::NumberHelper

dry_run = true
files = Dir['storage/??/??/*']

orphan = files.select do |f|
  !ActiveStorage::Blob.exists?(key: File.basename(f))
end

sum = 0
orphan.each do |f|
  sum += File.size(f)
  FileUtils.remove(f) unless dry_run
end

puts "Size: #{number_to_human_size(sum)}"
4

There are 4 answers

5
dft On BEST ANSWER

Well there is a easier way now, not sure when it changed, but there is one caveat (it leaves the empty folder):

# rails < 6.1
ActiveStorage::Blob.left_joins(:attachments).where(active_storage_attachments: { id: nil }).find_each(&:purge)

# rails >= 6.1
ActiveStorage::Blob.missing(:attachments).find_each(&:purge)

This will delete the record in the database and delete the physical file on disk, but leave the folders the file existed in.

0
fguillen On

I think the @Tun suggested solution in the question itself works properly. I write it in a Rake Tasks version in case this saves sometime to the next person coming here:

# Usage:
#   - rake "remove_orphan_blobs"
#   - rake "remove_orphan_blobs[false]"
desc "Remove blobs not associated with any attachment"
task :remove_orphan_blobs, [:dry_run] => :environment do |_t, args|
  dry_run = true unless args.dry_run == "false"

  puts("[#{Time.now}] Running remove_orphan_blobs :: INI#{" (dry_run activated)" if dry_run}")

  ActiveStorage::Blob.where.not(id: ActiveStorage::Attachment.select(:blob_id)).find_each do |blob|
    puts("Deleting Blob: #{ActiveStorage::Blob.service.path_for(blob.key)}#{" (dry_run activated)" if dry_run}")
    blob.purge unless dry_run
  end

  puts("[#{Time.now}] Running remove_orphan_blobs :: END#{" (dry_run activated)" if dry_run}")
end

# Usage:
#   - rake "remove_orphan_files"
#   - rake "remove_orphan_files[false]"
desc "Remove files not associated with any blob"
task :remove_orphan_files, [:dry_run] => :environment do |_t, args|
  include ActionView::Helpers::NumberHelper
  dry_run = true unless args.dry_run == "false"

  puts("[#{Time.now}] Running remove_orphan_files :: INI#{" (dry_run activated)" if dry_run}")

  files = Dir["storage/??/??/*"]
  orphan_files = files.select do |file|
    !ActiveStorage::Blob.exists?(key: File.basename(file))
  end

  sum = 0
  orphan_files.each do |file|
    puts("Deleting File: #{file}#{" (dry_run activated)" if dry_run}")
    sum += File.size(file)
    FileUtils.remove(file) unless dry_run
  end

  puts "Size Liberated: #{number_to_human_size(sum)}#{" (dry_run activated)" if dry_run}"

  puts("[#{Time.now}] Running remove_orphan_files :: END#{" (dry_run activated)" if dry_run}")
end
1
estani On
ActiveStorage::Blob.unattached.each(&:purge_later)

or just purge if you don't want it to happen in the background.

0
Eldar On

Work in rails 7.1.3:

ActiveStorage::Blob.unattached.each(&:purge)