Category: Rails

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.

Rails 3.0.1 na 3.0.2 i I18n oraz problemy z modułami

Przechodząc z Rails 3.0.1 na 3.0.2 umknęła mi jedna rzecz. Nikt nigdzie nie wspomniał o tym, że zmianie ulega sposób budowania tłumaczeń dla I18n.

O ile zmiana na pola defaultowe, jest całkiem fajna (a i tak ją stosowałem dawniej, przy pomocy pewnego "hacka" ;) ), o tyle zmiany odnośnie klas w modułach, przyprawiły mnie o 25 minutowe wkurzenie.

Zanim jednak do tego przejdziemy, wspomnę o tej fajniejszej zmianie. Otóż dawniej, kiedy chcieliśmy przetłumaczyć nazwy atrybutów z różnych modeli, musieliśmy (stosowanie tricków się nie liczy) wypisywać wszystkie atrybuty dla wszystkich modeli. Kończyło się to sytuację gdzie np. pole "Imię" dla modeli User i Admin było tłumaczone tak:

activerecord:
    models:
      user: Użyszkodnik
      admin: Bóg
    attributes:
      user:
         name: Imię
      admin:
         name: Imię

było to trochę niewygodne. W nowym I18n, możemy zrobić to tak:

attributes:
   name: Imię

Oczywiście jeśli mamy model gdzie "name" to np. nazwa firmy, możemy wciąż dla tego wyjątku, dopisać całość w węźle Activerecord.

Druga zmiana też jest dobra, jednak trochę niespodziewana (a przynajmniej niezapowiedziana). Dawniej, polonizując modele w modułach, robiło się to tak(przykład dla klasy Profile::Admin):

activerecord:
    models:
      "profile/base": Profil
    attributes:
      "profile/admin":
        views: Ilość odwiedzin
        name: Imię
        surname: Nazwisko
        city: Miasto

jak widać powyżej, kiedy mieliśmy do czynienia z klasą w module, postępowaliśmy wg. schematu: moduł/model.

Nadeszło nowe i lepsze (ale cholerka, mogli powiadomić!). Od teraz moduł będzie stanowił gałąź-rodzica w pliku translacji, tak jak w poniższym przykładzie:

activerecord:
    models:
      profile:
         base: Profil
    attributes:
      profile:
         admin:
           views: Ilość odwiedzin
           name: Imię
           surname: Nazwisko
           city: Miasto

Takie zagnieżdżanie jest dużo wygodniejsze :)

Copyright © 2026 Closer to Code

Theme by Anders NorenUp ↑