1
0

attachment_batch.rb 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. # frozen_string_literal: true
  2. class AttachmentBatch
  3. # Maximum amount of objects you can delete in an S3 API call. It's
  4. # important to remember that this does not correspond to the number
  5. # of records in the batch, since records can have multiple attachments
  6. LIMIT = ENV.fetch('S3_BATCH_DELETE_LIMIT', 1000).to_i
  7. MAX_RETRY = ENV.fetch('S3_BATCH_DELETE_RETRY', 3).to_i
  8. # Attributes generated and maintained by Paperclip (not all of them
  9. # are always used on every class, however)
  10. NULLABLE_ATTRIBUTES = %w(
  11. file_name
  12. content_type
  13. file_size
  14. fingerprint
  15. created_at
  16. updated_at
  17. ).freeze
  18. # Styles that are always present even when not explicitly defined
  19. BASE_STYLES = %i(original).freeze
  20. attr_reader :klass, :records, :storage_mode
  21. def initialize(klass, records)
  22. @klass = klass
  23. @records = records
  24. @storage_mode = Paperclip::Attachment.default_options[:storage]
  25. @attachment_names = klass.attachment_definitions.keys
  26. end
  27. def delete
  28. remove_files
  29. batch.delete_all
  30. end
  31. def clear
  32. remove_files
  33. batch.update_all(nullified_attributes)
  34. end
  35. private
  36. def batch
  37. klass.where(id: records.map(&:id))
  38. end
  39. def remove_files
  40. keys = []
  41. logger.debug { "Preparing to delete attachments for #{records.size} records" }
  42. records.each do |record|
  43. @attachment_names.each do |attachment_name|
  44. attachment = record.public_send(attachment_name)
  45. styles = BASE_STYLES | attachment.styles.keys
  46. next if attachment.blank?
  47. styles.each do |style|
  48. case @storage_mode
  49. when :s3
  50. logger.debug { "Adding #{attachment.path(style)} to batch for deletion" }
  51. keys << attachment.style_name_as_path(style)
  52. when :filesystem
  53. logger.debug { "Deleting #{attachment.path(style)}" }
  54. path = attachment.path(style)
  55. FileUtils.remove_file(path, true)
  56. begin
  57. FileUtils.rmdir(File.dirname(path), parents: true)
  58. rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR, Errno::EACCES
  59. # Ignore failure to delete a directory, with the same ignored errors
  60. # as Paperclip
  61. end
  62. when :fog
  63. logger.debug { "Deleting #{attachment.path(style)}" }
  64. retries = 0
  65. begin
  66. attachment.send(:directory).files.new(key: attachment.path(style)).destroy
  67. rescue Fog::OpenStack::Storage::NotFound
  68. logger.debug "Will ignore because file is not found #{attachment.path(style)}"
  69. rescue => e
  70. retries += 1
  71. if retries < MAX_RETRY
  72. logger.debug "Retry #{retries}/#{MAX_RETRY} after #{e.message}"
  73. sleep 2**retries
  74. retry
  75. else
  76. logger.error "Batch deletion from fog failed after #{e.message}"
  77. raise e
  78. end
  79. end
  80. when :azure
  81. logger.debug { "Deleting #{attachment.path(style)}" }
  82. attachment.destroy
  83. end
  84. end
  85. end
  86. end
  87. return unless storage_mode == :s3
  88. # We can batch deletes over S3, but there is a limit of how many
  89. # objects can be processed at once, so we have to potentially
  90. # separate them into multiple calls.
  91. retries = 0
  92. keys.each_slice(LIMIT) do |keys_slice|
  93. logger.debug { "Deleting #{keys_slice.size} objects" }
  94. bucket.delete_objects(delete: {
  95. objects: keys_slice.map { |key| { key: key } },
  96. quiet: true,
  97. })
  98. rescue => e
  99. retries += 1
  100. if retries < MAX_RETRY
  101. logger.debug "Retry #{retries}/#{MAX_RETRY} after #{e.message}"
  102. sleep 2**retries
  103. retry
  104. else
  105. logger.error "Batch deletion from S3 failed after #{e.message}"
  106. raise e
  107. end
  108. end
  109. end
  110. def bucket
  111. @bucket ||= records.first.public_send(@attachment_names.first).s3_bucket
  112. end
  113. def nullified_attributes
  114. @attachment_names.flat_map { |attachment_name| NULLABLE_ATTRIBUTES.map { |attribute| "#{attachment_name}_#{attribute}" } & klass.column_names }.index_with(nil)
  115. end
  116. def logger
  117. Rails.logger
  118. end
  119. end