Zaktualizowałem parę dni temu Paperclipa i oczywiście nie obyło się bez garści dziwnych problemów. Jednak nie to mnie najbardziej wkur zdenerwowało. W obecnej wersji zmieniono sposób obsługi sprawdzania rozmiaru plików (no chyba że to tak działa tylko u mnie…).

Załóżmy że mam limit rozmiaru ustawiony na 2MB. Przesyłam zdjęcie mające 5MB, ulega ono przekształceniu przez ImageMagicka i ma 1,5MB. Takie zdjęcie nie powinno być dopuszczone (limit pliku 2MB), jednak jest. Aby nie zapisywać oryginalnego pliku, maskowałem go w taki sposób:

  has_attached_file :avatar, :styles => {
                    :mini => "25x25\#",
                    :small => "50x50\#",
                    :medium => "75x75\#",
                    :big => "100x100\#",
                    :original => "200x200>"},

Ustawienie wartości :original powodowało nadpisanie pierwotnego pliku. Okazuje się jednak, że w obecnej wersji Paperclipa, ocena rozmiaru następuje na podstawie originala, po nadpisaniu rozmiaru. Przez takie zachowanie, musiałem opracować nową metodę kasowania pliku oryginalnego.

Postanowiłem wykorzystać do tego PostProcessor, jednak to podejście nie zakończyło się sukcesem. Wykorzystanie Observera do kasowania również nie działa. Observer uruchamia się jeszcze przed zapisaniem plików na dysk, więc nie mogą zostać usunięte.

Problem rozwiązałem stosując Observer wraz z Cronem i przeznaczonym do tego odpowiednim modelem. Utworzyłem prosty model do przechowywania ścieżki do pliku oraz timestampy. Dodałem do niego dwie metody. Jedna odpowiada za wykasowanie pojedyńczego pliku (którego odzwierciedleniem w bazie jest instancja klasy UselessFile) oraz drugą która ma wykasować wszystkie pliki. Należy przy tym pamiętać że należy kasować pliki których obiekt ma minimum parę sekund (żeby nie usunąć wpisu z bazy, pozostawiając plik). Ja ustawiłem domyślnie 5 minut (tak dla pewności).

Model UselessFile:

 
require 'fileutils'

# Musimy "jakoś" kasować oryginalne pliki robione przez paperclipa
# postprocessorem jakoś nie szło, to samo w callbackach, z racji tego powstał
# ten model. Zbiera on listę oryginalnych plików, by potem z crona je usuwac
class UselessFile < ActiveRecord::Base

  def destroy
    File.delete(self.path) if self.path && File.exists?(self.path)
    super
  end

  def self.delete_all
    self.sweep
  end

  def self.sweep
    time = 5.minutes.ago
    self.where("created_at < '#{time.to_s(:db)}' OR updated_at < '#{time.to_s(:db)}'").each { |el|
      el.destroy
    }
  end
end

Działanie jest bardzo proste, z tego względu podaruję sobie tłumaczenie ;)

Observer:

class ProfileObserver < ActiveRecord::Observer
  def after_save(profile)
    return unless profile.avatar?
    path = profile.avatar.url(:original).split('?').first
    path = File.join(Rails.root, 'public',path)
    UselessFile.create(:path => path)
  end
end

Tutaj najpierw sprawdzamy czy model ma avatar (plik) i jeśli tak to “wrzucamy” do bazy jego adres, tak aby w cronie usunąć odpowiadający mu plik.

Cron:

  desc "Czyszczenie bezużytecznych plików zgromadzonych w UselessFile"
  task :useless_files_sweeper => :environment do
    UselessFile.sweep
  end

Tyle :) Nie jest to może rozwiązanie najbardziej optymalne ale działa. Dodatkowo sam model UselessFile i zasadę jego działania możemy wykorzystać nie tylko z Paperclipem.