Tag: sweeper

Using Rails Sweepers outside controllers

Sweeper outside?

Sometimes we modify some stuff without controllers (for example we publish some stuff via rake task ignited by cron). Unfortunately Rails does not handle it "out of the box", so we need to do it by ourselfs.

BaseSweeper

Let's create a base sweeper class where all the "magic" will happen:

class BaseSweeper < ActionController::Caching::Sweeper

  def after_save(data)
    expire_data(data)
  end

  def after_destroy(data)
    expire_data(data)
  end

  def expire_data
    @controller ||= ActionController::Base.new
  end

end

There's nothing special about this code, except:

@controller ||= ActionController::Base.new

This line will emulate (if needed) controller outside of standard Rails flow. Thanks to this we can sweep stuff whenever we need. Example:

class NewsSweeper < BaseSweeper

  observe News

  def expire_data(data)
      super
      expire_fragment /news_#{data.id}/
  end

end

There's one more thing to do. We need to add sweeper to observers in application.rb file (so our sweeper will react on any change in observed model):
application.rb:

    config.active_record.observers =
      "CommentObserver",
      "JakistamInnyObserver",
      # Sweepery
      "NewsSweeper"

Rails3, Paperclip i usuwanie oryginalnych obrazów (plików)

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.

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑