Tag: plugin

Paperclip, interpolations (interpolacje) i zmiany nazwy plików

Wstęp

Są sytuacje w których chcemy aby nasze pliki z Paperclipa miały konkretne nazwy. Np. takie odpowiadające nazwom obiektów do których są dopięte (np po atrybucie name). Dla leniwych - gotowy kod na githubie.

Interpolacje

Paperclip daje nam taką możliwość dzięki interpolacjom. Możemy w bardzo prosty sposób tworzyć własne reguły nazewnictwa. Aby tego dokonać, modyfikujemy (lub tworzymy jeśli go nie mamy) plik config/initializers/paperclip_extensions.rb wpisując w nim (przykładowo):

Paperclip.interpolates :instance_name do |attachment, style|
  attachment.instance.name.to_url
end

gdzie metoda to_url (opisana tutaj) zamienia tekst na jego odpowiednik w formie nadającej się do linków (a także na nazwy plików, katalogów, itp). Przykładowo z nazwy "Przykładowa nazwa" powstanie "przykladowa-nazwa". Dzięki temu, możemy sobie poskładać nazwę dla pliku w modelu w taki oto sposób:

  has_attached_file :photo,
    :url => "/images/photos/:instance_name_:style.:extension"

Pozwala nam to tworzyć ładniejsze linki do obrazków.

Interpolacja po parametrach == problem

Interpolując z pomocą parametrów, nazwijmy to "zmiennych", pojawia się nam pewien problem. Paperclip nie zmienia samoczynnie nazw plików już utworzonych, tak więc zmieni ścieżki do plików w bazie, nie zmieniając położenia (bądź nazw) samych plików. W skutek tego jeśli utworzymy sobie model z polem :name, po którym interpolujemy, a następnie zaktualizujemy wartość tego pola, utracimy dostęp do plików będących już na serwerze. Przykładowo:

  1. Mamy model z polem :name oraz plikiem :image
  2. Interpolujemy nazwę pliku po atrybucie :name: 'images/:instance_name.:extension'
  3. Tworzymy obiekt, przypisując name => "Susanoo", dołączamy zdjęcie
  4. Otrzymujemy obiekt ze ścieżką do pliku => "images/susanoo.jpg"
  5. Aktualizujemy nazwę w naszym obiekcie, zmieniając ją na "Nokogiri". Paperclip zmieni scieżkę w bazie, na "images/nokogiri.jpg", jednak nie zmieni nazw plików, przez co w systemie plików dalej będzie plik "images/susanoo.jpg", zamiast "images/nokogiri.jpg"

Rozwiązanie - has_interpolated_attached_file

Rozwiązaniem takiej sytuacji jest wykorzystanie callbacków z ActiveRecord. Musimy "przechwycić'' stary (poprawny) adres pliku na serwerze a następnie przenieść go pod nowy adres. Musimy także zachować transakcyjność. Aby tego dokonać wykorzystamy trzy callbacki:

  1. before_update - zapamiętamy w nim nasz "stary", poprawny adres pliku - dla każdego ze stylów.
  2. after_update - mając stare nazwy oraz nowe (bo już po aktualizacji), w tym callbacku przeniesiemy pliki pod nowe adresy
  3. after_rollback - gdyby coś poszło nie tak, wycofamy zmiany wprowadzone w plikach tak aby odzyskały swoje pierwotne położenie

Całość zamkniemy w metodzie która będzie rozszerzeniem do Paperclipa, dzięki czemu będziemy mogli z niej korzystać zamiast standardowego has_attached_file.

before_update

Najpierw before_update. Tutaj sprawa jest prosta. Tworzymy sobie zmienną instancyjną która będzie hashem przechowującym. oryginalne nazwy plików (dla wszystkich styli jeśli są). Cała sztuczka polega na tym, że aby uzyskać dostęp do pierwotnych nazw. musimy odpytać bazę danych o samych siebie ale z bazy - tak żeby mieć te "poprzednie" ścieżki z nazwami. Reszta to już tylko iteracja po wartościach i przypisanie do hasha:

      before_update do |record|
        @interpolated_names = {} unless @interpolated_names
        @interpolated_names[name] = {} unless @interpolated_names[name]
        old_record = self.class.find(record.id)
        (record.send(name).styles.keys+[:original]).each do |style|
          @interpolated_names[name][style] = old_record.send(name).path(style)
        end
      end

after_update

Mamy już potrzebne nam stare i nowe ścieżki. Nie pozostaje nam nic innego, jak je pozamieniać:

      after_update do |record|
        (record.send(name).styles.keys+[:original]).each do |style|
          orig_path = @interpolated_names[name][style]
          dest_path = record.send(name).path(style)
          if orig_path && dest_path && File.exist?(orig_path) && orig_path != dest_path
            FileUtils.move(orig_path, dest_path)
          end
        end
      end

after_rollback

Dla pewności, musimy zapewnić sobie także możliwość odwrócenia zmian. Jak to zrobić? Po prostu pozamieniamy nazwy plików z powrotem, wracając do punktu wyjścia:

      after_rollback do |record|
        return unless @interpolated_names
        (record.send(name).styles.keys+[:original]).each do |style|
          dest_path = @interpolated_names[name][style]
          orig_path = record.send(name).path(style)
          if orig_path && dest_path && File.exist?(orig_path) && orig_path != dest_path
            FileUtils.move(orig_path, dest_path)
          end
        end
      end

Całość

Całość zamkniemy w metodzie has_interpolated_attached_file. Dodając do niej przy okazji oryginalny has_attached_file, co pozwoli nam korzystać z naszej metody zamiast has_attached_file:

require 'fileutils'
# Some usefull paperclip extensions
module Paperclip
  module ClassMethods

    def has_interpolated_attached_file name, options = {}

      # Get old pathes to all files from file and save in instance variable
      before_update do |record|
        @interpolated_names = {} unless @interpolated_names
        @interpolated_names[name] = {} unless @interpolated_names[name]
        old_record = self.class.find(record.id)
        (record.send(name).styles.keys+[:original]).each do |style|
          @interpolated_names[name][style] = old_record.send(name).path(style)
        end
      end

      # If validation has been passed - move files to a new location
      after_update do |record|
        (record.send(name).styles.keys+[:original]).each do |style|
          orig_path = @interpolated_names[name][style]
          dest_path = record.send(name).path(style)
          if orig_path && dest_path && File.exist?(orig_path) && orig_path != dest_path
            FileUtils.move(orig_path, dest_path)
          end
        end
      end

      # If renaming (or other callbacks) went wrong - restore old names to files
      after_rollback do |record|
        return unless @interpolated_names
        (record.send(name).styles.keys+[:original]).each do |style|
          dest_path = @interpolated_names[name][style]
          orig_path = record.send(name).path(style)
          if orig_path && dest_path && File.exist?(orig_path) && orig_path != dest_path
            FileUtils.move(orig_path, dest_path)
          end
        end
      end

      has_attached_file name, options
    end

  end
end

Źródełka dostępne na moim Githubie.

Paperclip – walidacja rozmiaru obrazków(grafik) – podejście drugie

Nie tak dawno temu opublikowałem wpis w którym pokazuję jak zrobić walidację minimalnego rozmiaru obrazka z wykorzystaniem Paperclipa. Podejście to było (i jest) proste, jednak okazało się niewystarczające (niewygodne) gdy mamy do czynienia z kilkoma plikami graficznymi na model. Z tego względu przebudowałem tę metodę, tak aby wydzielić ja do samego Paperclipa. Mimo że wyszła wersja 2.3.8 Paperclipa, sam korzystam z lekko zmodyfikowanej przeze mnie 2.3.6, która działa na Rails3. Kod ten powinien działać na każdej wersji Paperclipa.

module Paperclip
  module ClassMethods
    def validates_attachment_minimum_resolution name, options = {}
      validation_options = options.dup
      validates_each(name, validation_options) do |record, attr, value|
        unless record.errors.include?(name)
          m_width  = options[:width]
          m_height = options[:height]
          message = options[:message] || "must be bigger."
          message = message.call if message.is_a?(Proc)
          image = record.send(name)
          if image && image.queued_for_write[:original]
            dimensions = Paperclip::Geometry.from_file(image.queued_for_write[:original])
            if dimensions.width < m_width || dimensions.height < m_height
              if record.errors.method(:add).arity == -2
                record.errors.add(:"#{name}", message)
              else
                record.errors.add(:"#{name}", :inclusion, :default => options[:message], :value => value)
              end
            end
          end
        end
      end
    end
  end
end

Wrzucamy do /lib/extensions, podpinamy w /config/application.rb:

require 'extensions/paperclip'

I używamy. A jak działa? Załóżmy że mamy mieć miniaturkę (:thumb), która ma mieć rozdzielczość minimum 300x300 i być w formacie JPG:

  TYPES = ['image/jpeg', 'image/jpg', 'image/pjpeg']
  FILE_MAX_SIZE = 2048

  has_attached_file :thumb, :styles => {:mini => "120x95\#",
                  :standard => "190x150\#", :big => "300x300>"},
                  :url => "/images/models/thumb/:id_:style.:extension",
                  :default_style => :standard

  validates_attachment_presence :thumb,
    :message =>  'musi być dołączony'
  validates_attachment_size :thumb, :less_than => FILE_MAX_SIZE.kilobytes,
    :message =>  "musi mieć mniej niż #{FILE_MAX_SIZE} kb"
  validates_attachment_content_type :thumb, :content_type => TYPES,
    :message =>  "ma nieprawidłowy format"
  validates_attachment_minimum_resolution :thumb,
    :width => 300, :height => 300,
    :message => "mus mieć minimum 300px na 300px"

Ważne jest aby sprawdzać typ pliku przed sprawdzeniem rozmiarów, ponieważ walidacja ta nie uruchomi się jeśli plik zostanie uznany przez wcześniejsze walidacje za niepoprawny.

Warto zauważyć, że można to internacjonalizować. Mamy w kodzie taki fragment:

message = message.call if message.is_a?(Proc)

Dzięki któremu możemy zrobić w kodzie tak:

:message => lambda{I18n.t("activerecord.errors.messages.paperclip.res", {
    :width => 300, :height => 300})}

I mieć inny komunikat zależnie od języka.

Warto wspomnieć też, że walidacja ta (tak jak reszta paperclipowych) przeprowadzana jest tylko jeśli wymuszamy podstawową walidację obecności pliku:

  validates_attachment_presence :thumb

Aby uruchamiać walidację tylko jeśli plik został do formularza dołączony (ale formularz może być bez pliku), można zrobić tak:

  validates_attachment_presence :thumb,
    :message =>  "musi istnieć",
    :if => Proc.new { |imports| imports.thumb.file? }

Lub, można też dopisać sobie taką metodę do naszego rozszerzenia i będzie to za nas robione automatycznie:

    def validates_attachment_if_included name, options = {}
      options[:if] = Proc.new { |imports| imports.send(name).file? }
      validates_attachment_presence name, options
    end

Przykład:

  validates_attachment_if_included :thumb

Paperclip minimum resolution validation extension

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑