Tag: paperclip

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

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 ↑