Instalacja Ruby on Rails, Apache, mySQL i Passengera pod kontrolą REE na serwerze pod kontrolą Debiana 4.0 etch

30 stycznia 2009, 18:07:59

Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Ruby on Rails, Techblog. | 7 komentarzy

Ostatnio dane mi było instalować pełen stos programów potrzebnych do uruchomienia aplikacji w Railsach na serwerze pod kontrolą systemu Debian 4.0 etch minimal w wersji 64-bitowej. Ponieważ proces ten jest dosyć długi, opisuję go poniżej.

Wymagania wstępne i założenia

Opis nie będzie zawierał prawie żadnych instrukcji jak zabezpieczyć serwer, przy okazji zakładam kilka rzeczy:

Czynności wstępne

Na początku warto upewnić się, że mamy aktualną listę pakietów. W tym celu warto wykonać następujące czynności:

$ apt-get update
$ apt-get upgrade

Instalujemy też potrzebne pakiety:

$ apt-get install apt-utils build-essential ruby1.8-dev imagemagick libmysqlclient15off \
 libmysqlclient15-dev mysql-server mysql-common mysql-client memcached libxslt1-dev libpcre3-dev \
 zlib1g-dev unzip gzip libssl-dev libreadline5-dev libmysql-ruby sqlite3 imagemagick man-db cron wget lynx curl bind9 -y

Od razu możemy zmienić hasło roota MySQL:

$ mysqladmin -u root password NEWPASSWORD

Instalacja Ruby

Żeby korzystać z jedynie Ruby Enterprise Edition można opuścić ten punkt.

Ruby instalujemy ze źrodeł. W momencie tworzenia opisu w oficjalnych repozytoriach Debiana znalazłem wersję 1.8.5, a najnowszą stabilną wersją Ruby na stronie Ruby-Lang była wersja 1.8.6-p286. Pobieramy i rozpakowujemy źródła Ruby:

$ cd /usr/src
$ wget ftp://ftp.ruby-lang.org/pub/ruby/1.8/ruby-1.8.6-p286.tar.gz
$ tar zxf ruby-1.8.6-p286.tar.gz
$ cd ruby-1.8.6-p286

Przed konfiguracją edytujemy plik ext/Setup w celu dołączenia potrzebnych rozszerzeń. Powyższy plik może wyglądać tak:

 #Win32API
 #bigdecimal
 curses
 #dbm
 digest
 digest/md5
 #digest/rmd160
 digest/sha1
 digest/sha2
 dl
 enumerator
 #etc
 #fcntl
 #gdbm
 iconv
 #io/wait
 #nkf
 #pty
 openssl
 #racc/cparse
 readline
 #sdbm
 socket
 stringio
 strscan
 syck
 syslog
 #tcltklib
 thread
 #tk
 #win32ole
 zlib

Zawartość pliku oznacza, że chcemy skompilować Ruby wraz z rozszerzeniami curses, digest (md5, sha1/2), dl, enumerator, iconv, openssl, readline, socket, stringio, strscan, syck, syslog, thread, zlib.

Przyszła kolej na konfigurację, kompilację i instalację:

$ ./configure --prefix=/usr/local --with-openssl-dir=/usr \
 --with-readline-dir=/usr --with-zlib-dir=/usr
$ make
$ make install
$ make install-doc

Ostatnia linijka jest opcjonalna i odpowiedzialna za wygenerowanie dokumentacji.

Po zainstalowaniu interpretera języka Ruby przychodzi kolej na Rubygems:

$ cd /usr/src
$ wget http://rubyforge.org/frs/download.php/45905/rubygems-1.3.1.tgz
$ tar zxf rubygems-1.3.1.tgz
$ cd rubygems-1.3.1/
$ ruby setup.rb
$ gem isntall rubygems-update
$ update_rubygems
$ gem source --add http://gems.github.com

Ostatnia linijka dodaje do źródeł gemów repozytorium Github

Pora na instalacje potrzebnych gemów, moja proponowana lista:

gem install actionmailer actionpack actionwebservice active_graph \
 activerecord activeresource activesupport addressable archive-tar-minitar \ 
 bcrypt-ruby BlueCloth builder capistrano capistrano-ext cgi_multipart_eof_fix \
 chronic color columnize daemons eventmachine extlib fastercsv fastthread \ 
 feedtools gem_plugin haml highline hoe hpricot image_science \
 InlineAttachment json libxml-ruby linecache memcache-client metaid \
 mime-types mini_magick mislav-will_paginate mongrel mongrel_cluster \
 mongrel_proctitle needle net-scp net-sftp net-ssh net-ssh-gateway nokogiri \
 oauth passenger progressbar rack rails rake rcov RedCloth rmagick rspec \
 rspec-rails rspec_hpricot_matchers ruby-debug ruby-debug-base rubyforge \
 RubyInline rubyzip thin thor tlsmail transaction-simple uuid uuidtools \
 webrat wirble xml-simple youtube ZenTest

Instalacja Ruby Enterprise Edition

Ponieważ będziemy uruchamiać aplikację Rails z użyciem Passengera, możemy zredukować zużycie pamięci korzystając ze specjalnie przygotowanej wersji interpretera. Instalujemy go ze źródeł:

$ cd /usr/src
$ wget http://rubyforge.org/frs/download.php/50087/ruby-enterprise-1.8.6-20090113.tar.gz
$ tar zxf ruby-enterprise-1.8.6-20090113.tar.gz
$ cd ruby-enterprise-1.8.6-20090113/
$ ./installer

Wraz z REE zostaną zainstalowane rubygems oraz pewne gemy. Dodatkowe gemy instalujemy jak wyżej, z tym, że korzystamy ze zmodyfikowanej wersji Ruby:

/opt/ruby-enterprise-1.8.6-20090113/bin/ruby -S /opt/ruby-enterprise-1.8.6-20090113/bin/gem install capistrano  # ... i inne gemy

Instalacja Apache

Zainstalowałem apache2 wraz z pakietami apache2-mpm-worker, apache2-utils oraz apache2-prefork-dev

 apt-get install apache2 apache2-mpm-worker apache2-utils apache2-prefork-dev

Instalacja i konfiguracja Passengera obsługiwanego przez REE

Ponieważ gem Passengera został zainstalowany wcześniej pozostaje wygenerować odpowiedni moduł dla apache2:

 /opt/ruby-enterprise-1.8.6-20090113/bin/passenger-install-apache2-module

Pora na konfigurację. Wykorzystamy modułowość apache2 oraz program a2enmod do załadowania modułu. Tworzymy pliki /etc/apache2/mods-available/passenger.load z zawartością taką, jak wygenerowała nam instalacja modułu Passengera, np.:

  LoadModule passenger_module /opt/ruby-enterprise-1.8.6-20090113/lib/ruby/gems/1.8/gems/passenger-2.0.6/ext/apache2/mod_passenger.so

i /etc/apache2/mods-available/passenger.conf:

 PassengerRoot /opt/ruby-enterprise-1.8.6-20090113/lib/ruby/gems/1.8/gems/passenger-2.0.6
 PassengerRuby /opt/ruby-enterprise-1.8.6-20090113/bin/ruby

Po zapisaniu tych plików ładujemy moduł:

$ a2enmod passenger

I to już praktycznie wszystko, jedyne, co zostaje do zrobienia, to utworzenie konfiguracji vhosta dla konkretnej aplikacji, np. w /etc/apache2/sites-available, uaktywnienie jej programem a2ensite i przeładowanie apache.

Mam nadzieję, że powyższy opis pozwoli komuś zaoszczędzić trochę czasu.

Tagi:

Dziwne zachowanie config.gem w połączeniu z rspec-rails w Rails 2.2

16 stycznia 2009, 17:23:13

Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Ruby on Rails, Techblog. | 3 komentarze

Pracując ostatnio nad projektem zauważyłem dziwne rzeczy dziejące się w aplikacji. Po pierwsze w trybie deweloperskim po jakimkolwiek błędzie nie wyświetlał się stack trace. Jedyne, co się wyświetlało, to error code 500.

Już sam ten fakt był dla mnie dość uciążliwy, chociaż w tym przypadku zawsze można było odwołać się do logów aplikacji. Dużo gorszą rzeczą było nadpisanie przez coś wartości stałej RAILS_ENV i ustawienie jej na 'test'. Co ciekawe, wartość ENV['RAILS_ENV'] nadal wskazywała na poprawne środowisko.

Kiedy po zainstalowaniu RPM aplikacja nie chciała z nim współpracować (oczywiście działo się tak przez źle wskazujące RAILS_ENV) trzeba było znaleźć winnego. Po nitce do kłębka znalazłem wpis dotyczący znikających stack trace'ów. Okazało się, że wycięcie z pliku config/environment.rb linijki:

config.gem 'rspec-rails', :lib => 'spec/rails'

rozwiązało oba problemy!

Winowajcą jest linijka

silence_warnings { RAILS_ENV = "test" }

w pliku lib/spec/rails.rb gema rspec-rails.

Powyższy problem zdaje się nie występować w przypadku korzystania z rspeca w formie pluginu.

Tagi:

Blokowe helpery w Ruby on Rails

08 lipca 2008, 10:28:11

Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Ruby on Rails, Techblog. | 4 komentarze

Bardzo przydatną rzeczą, którą możemy zrobić podczas refaktoryzacji kodu widoków jest dopisanie helperów akceptujących bloki kodu.

W Railsach standardowo mamy co najmniej kilka takich helperów, m.in. form_for, czy link_to (póki co w wersji EDGE). Nic nie stoi na przeszkodzie, aby dopisać swoje własne metody akceptujące bloki, które w widoczny sposób poprawiają czytelność kodu.

Jako przykład podam implementację rounded-box jako helper akceptujący blok. Końcowy efekt wyglądał będzie następująco:

<%- rounded_box do %>
Treść, która powinna znaleźć się w boksie
<% end -%>

Napisanie tego typu metod jest bardzo proste i opiera się na wykorzystaniu metody concat. Zakładam, że cały kod naszego boksu na postać (styl CSS zostawmy w spokoju):

<div class="rnd_container">
  <b class="rnd_top">
    <b class="rnd_b1"></b>
    <b class="rnd_b2"></b>
    <b class="rnd_b3"></b>
    <b class="rnd_b4"></b>
  </b>
<div class="rnd_content"> 
  <h2><p>Treść w boksie</p></h2>
</div>
  <b class="rnd_bottom">
    <b class="rnd_b4"></b>
    <b class="rnd_b3"></b>
    <b class="rnd_b2"></b>
    <b class="rnd_b1"></b>
  </b>
</div>

Powyższy kod można najprościej przetransportować do osobnego partiala i zapisac, np. w katalogu app/views/shared/_rounded_box.html.erb w lekko zmodyfikowanej wersji:

  <div class="rnd_container">
    <b class="rnd_top">
      <b class="rnd_b1"></b>
      <b class="rnd_b2"></b>
      <b class="rnd_b3"></b>
      <b class="rnd_b4"></b>
    </b>
  <div class="rnd_content"> 
    <h2><%= body %></h2>
  </div>
    <b class="rnd_bottom">
      <b class="rnd_b4"></b>
      <b class="rnd_b3"></b>
      <b class="rnd_b2"></b>
      <b class="rnd_b1"></b>
    </b>
  </div>

Sama metoda helpera wygląda następująco:

  def rounded_box(options={}, &block)
    options.merge!(:body => capture(&block))
    concat(render(:partial => "shared/rounded_box", :locals => options), block.binding)
  end

Jak widać oprócz bloku możemy przekazać do partiala swoje zmienne podając je jako parametry wywołania helpera. Cała magia polega na przechwyceniu zawartości bloku wywołaniem capture(&block) i skonkatenowaniem jej z naszym boksem.

Rozwiązanie szybkie, proste i jakże czytelne!

Tagi:

Manipulowanie czasem w testach w aplikacjach Ruby

05 lipca 2008, 00:43:31

Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Ruby on Rails, Techblog. | 1 komentarz

Czasem, szczególnie podczas pisania testów, zachodzi potrzeba manipulowania czasem, np. podczas testowania metod, które mają zwracać posortowane względem czasu utworzenia obiekty ActiveRecord.

W ostatnim projekcie podczas pisania spec-ów modeli korzystałem z takiego tworu:

describe SomeClass do
  describe "by_date" do
    it "should return objects sorted by creation date" do
      Time.advancing_by_days(-1) do
        @earlier = SomeClass.new(:some => "values")
      end
      @normal = SomeClass.new
      Time.advancing_by_days(1) do
        @later = SomeClass.new(:some => "other value")
      end
      SomeClass.by_date.should == [@later, @normal, @earlier]
    end
  end
end

O ile mi wiadomo, Railsy nie mają standardowo wbudowanych klas do manipulowania czasem, czy metodą Time.now.

Stworzenie metod realizujących powyższą funkcjonalność jest bardzo proste. Wystarczy stworzyć plik, np. time_extensions.rb z poniższą zawartością:

require 'time'

if !Time.respond_to?('old_now')
  Time.class_eval {
    @@advance_by_days = 0
    @@advance_by_minutes = 0
    cattr_accessor :advance_by_days, :advance_by_minutes

    class << Time
      alias old_now now
      def now
        if Time.advance_by_days != 0
          return Time.at(old_now.to_i + Time.advance_by_days * 60 * 60 * 24 + 1)
        elsif Time.advance_by_minutes != 0
          return Time.at(old_now.to_i + Time.advance_by_minutes * 60)
        else
          old_now
        end
      end
      def advancing_by_days(days=0)
        Time.advance_by_days = days
        yield
        Time.advance_by_days = 0
      end
      def advancing_by_minutes(minutes=0)
        Time.advance_by_minutes = minutes
        yield
        Time.advance_by_minutes = 0
      end
    end
  }
end

Jak widać korzystamy z możliwości, jakie daje monkey pathing i modyfikujemy standardową metodę Time.now, uprzednio tworząc do niej alias. W powyższym kodzie oprócz zmiany czasu o zadaną liczbę dni, możemy zmieniać go o zadaną liczbę minut.

Aby korzystać z powyższych metod w spec-ach aplikacji railsowej, wystarczy wrzucić plik z powyższym kodem do katalogu spec aplikacji i dopisać do pliku spec_helper.rb linijkę:

require 'time_extensions'

Tagi:

Dwie przydatne rzeczy przy pracy z RubyGems i Rake

21 czerwca 2008, 11:48:59

Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Techblog. | 3 komentarze

Ostatnio cierpię na brak czasu, stąd trochę zaniedbałem bloga. Dzisiaj krótka rzecz, która ułatwi pracę z gemami oraz z Rake. Przyznam się, że poniższe skrypty nie są mojego autorstwa, ja jedynie popularyzuję je.

Automatyczne otwieranie dokumentacji gemów w przeglądarce

Tworzymy funkcję w bashu gemdoc, która otworzy nam w przeglądarce dokumentacje RDoc podanego gema. Oczywiście obsłużymy autocomplete. Poniższy kod wklejamy do naszego pliku .bash_profile, po czym przeładowujemy ów plik poleceniem

$ source ~/.bash_profile
export GEMDIR=`gem env gemdir`

gemdoc() {
  open $GEMDIR/doc/`$(which ls) $GEMDIR/doc | grep $1 | sort | tail -1`/rdoc/index.html
}
_gemdocomplete() {
  COMPREPLY=($(compgen -W '$(`which ls` $GEMDIR/doc)' -- ${COMP_WORDS[COMP_CWORD]}))
  return 0
}
complete -o default -o nospace -F _gemdocomplete gemdoc

Funkcja pochodzi z bloga Stephena.

Automatyczne dopełnianie tasków Rake

Drugą przydatną funkcją jest automatyczne dopełnianie tasków Rake. Kod wklejamy jak powyżej do naszego .bash_profile.

export COMP_WORDBREAKS=${COMP_WORDBREAKS/\:/}

_rakecomplete() {
  COMPREPLY=($(compgen -W "`rake -s -T | awk '{{print $2}}'`" -- ${COMP_WORDS[COMP_CWORD]}))
  return 0
}
complete -o default -o nospace -F _rakecomplete rake

Tagi:

deferred?(env) w Thin, Ebb i Merb

18 kwietnia 2008, 11:04:35

Poziom: 0 | Kategoria: Komputerowo-internetowo, Merb, Ruby, Techblog. | Dodaj komentarz

Jak donosi Ezra Zygmuntowicz Merb doczekał się wsparcia dla deferred requests. Ale o co chodzi?

Porównując serwery operujące na zdarzeniach (event driven) z operującymi na wątkach (threaded) można dojść do wniosków, iż te operujące na zdarzeniach są o wiele szybsze przy serwowaniu krótkich żądań. Dzieje się tak, gdyż nie ma narzutu na odpalenie nowego wątku i przełączaniu kontekstu. Ry Dahl wspominał o tym w swojej prezentacji na RuPy. Jednak co z długimi requestami, takimi jak upload pliku, czy skomplikowane wyliczenia? Jak się okazuje jedne z najpopularniejszych i najszybszych serwerów do frameworków w Rubym, które wspierają interfejs RackThin oraz Ebb mają metodę deferred?(env) w ich Rackowym interfejsie. Oba serwery będą odpalały tę metodę na objekcie aplikacji przed jego wywołaniem. Taki trik pozwala adapterowi Rack zadecydować czy request powinien być obsłużony przez osobny wątek, czy w pętli zdarzeń. Aby przyspieszyć działanie serwera, wszystkie wolne akcje powinny być oznaczone flagą deferred i wywoływane w osobnych wątkach.

O ile nic nie wiadomo mi o wsparciu deferred?(env) w Railsach, o tyle, jak wspomniałem na początku, Merb doczekał się właśnie takiej funkcjonalności. Jeżeli chcemy poinformować nasz serwer, że jakieś akcje są wolne, do pliku init.rb powinniśmy dodać następującą linijkę:

Merb::Config[:deferred_actions] = ["/uploads/create", "reports/longaction"]

Powyższa linijka sprawi, że wszystkie akcje oprócz wymienionych będą obsługiwane przez event loop. Każdy request do jednej z powyższych akcji spowoduje odpalenie nowego wątku dla niej i nie będzie spowalniać naszej aplikacji.

Co ważne, aby korzystać z dobrodziejstw deferred?(env), należy zaopatrzyć się w najnowsze developerskie wersje zarówno serwerów, jak i Merba. Wszystko można znaleźć oczywiście na GitHubie.

Tagi:

Google App Engine — oddaj głos na wsparcie dla Ruby

10 kwietnia 2008, 13:48:00

Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Techblog. | 20 komentarzy

Jak już zapewne większość z Was zauważyło, Google zrobiło konkurencję dla Amazon Web Services. Google App Engine jest póki co w fazie beta i na razie obsługuje jedynie aplikacje napisane w Pythonie, jednak niebawem ma się pojawić wsparcie dla innych języków. Dlatego też warto już teraz dodać gwiazdkę i skomentować chęć dołączenia języka Ruby do języków wspieranych przez Google App Engine. Można to zrobić komentując Issue 29. Tak więć "star" that ticket!

Tagi:

Modelujemy znajomych w Ruby on Rails

05 kwietnia 2008, 17:51:05

Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Ruby on Rails, Techblog. | 12 komentarzy

Ponieważ jestem w trakcie pisania małego serwisu społecznościowego w ramach pracy dyplomowej, nadszedł czas na wymodelowanie sieci znajomych. Przeglądałem trochę książek, szukałem trochę po przeróżnych blogach. W końcu stworzyłem hybrydę z rzeczy, które znalazłem.

Piszę w Railsach 2.0.2, chciałem więc w pełni wykorzystać REST. Pierwsze pytania pojawiły się już na poziomie projektowania bazy danych: czy lepiej podwajać krotki i trzymać w nich jakieś dodatkowe atrybuty? Czy może lepiej oszczędzić miejsce redukując rozmiar tabeli do jednej krotki na znajomość kosztem bardziej złożonego zapytania? Stanęło na tym, że każda "znajomość" ma dwie krotki w tabeli pośredniej, a znajomych przeglądamy przeglądając atrubuty "friend_id" krotek wybranych po atrybucie "user_id". Zatem dostaliśmy relację wiele-wiele pomiędzy tabelą Users a nią samą. Nasz model usera zaczął wyglądać więc mniej więcej tak:

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :friendships, :dependent => :destroy
end

Model samej relacji "być znajomym" nabrał takiego kształtu:

# app/models/friendship.rb
class Friendship < ActiveRecord::Base
  belongs_to :user
  belongs_to :friend, :class_name => 'User', :foreign_key => 'friend_id'
end

Przyjęliśmy, że każde "zaproszenie" może mieć jeden z trzech stanów:

Stan zaproszenia przechowywany więc jest w tabeli pośredniej relacji wiele-wiele. Chcemy również w łatwy sposób docierać do naszych znajomych z poziomu usera. Z pomocą przychodzi nam metoda has_many :through, którą wykorzystujemy w modelu User (dla uproszczenia stany zaproszeń wyrażane są za pomocą stringów, innym sposobem, w moim mniemaniu lepszym, jest użycie, zamiast napisów, np. jakiś stałych liczbowych):

# app/models/user.rb
class User < ActiveRecord::Base
# (...)  
  has_many :friendships, :dependent => :destroy
  
  has_many :friends,
    :through => :friendships,
    :conditions => ["status = ?", 'zaakceptowane']
  
  has_many :requested_friends,
    :through => :friendships,
    :source => :friend,
    :conditions => ["status = ?", 'wyslane']
  
  has_many :pending_friends,
    :through => :friendships,
    :source => :friend,
    :conditions => ["status = ?", 'oczekujace']
  
  has_many :any_friends,
    :through => :friendships,
    :source => :friend
# (...)
end

Teraz dobrze jest zapewnić jakieś metody do zarządzania znajomościami. W tym celu utworzymy metody klasy Friendship, które będą wysyłały, akceptowały i odrzucały zaproszenia do znajomości:

# app/models/friendship.rb
class Friendship < ActiveRecord::Base

  belongs_to :user
  belongs_to :friend, :class_name => 'User', :foreign_key => 'friend_id'
  
  validates_presence_of :user_id, :friend_id
  
  def self.exists?(user, friend)
    not find_by_user_id_and_friend_id(user, friend).nil?
  end
  
  def self.request(user, friend)
    unless user == friend or Friendship.exists?(user, friend)
      transaction do
        create(:user => user, :friend => friend, :status => 'wyslane')
        create(:user => friend, :friend => user, :status => 'oczekujace')
      end
    end
  end
  
  def self.accept(user, friend)
    transaction do
      accepted_at = Time.now
      accept_one_side(user, friend, accepted_at)
      accept_one_side(friend, user, accepted_at)
    end
  end
  
  def self.breakup(user, friend)
    transaction do
      destroy(find_by_user_id_and_friend_id(user, friend))
      destroy(find_by_user_id_and_friend_id(friend, user))
    end
  end
  
  private
  def self.accept_one_side(user, friend, accepted_at)
    request = find_by_user_id_and_friend_id(user, friend)
    request.status = 'zaakceptowane'
    request.accepted_at = accepted_at
    request.save!
  end
end

Jak widać kluczowe akcje modelu wywoływane są w bloku metody transaction. To zapewnia nam transakcyjne wykonanie kodu, co zabda o spójność danych i nie dopuści do powstania sytuacji, w której podczas wysyłania zaproszenia od użytkownika A do użytkownika B w bazie pojawi się tylko krotka (A,B,'wyslane').

Można powiedzieć, że model jest już gotowy, pora zatem na kontroler, który będzie oferował podzbiór standardowych akcji: create, index, show oraz destroy. Akcja index zajmie się wyświetlaniem listy znajomych danego użytkownika, show przekieruje nas na profil użytkownika, create będzie odpowiedzialna za wysłanie lub zaakceptowanie zaproszenia, a destroy pozwoli na usunięcie kogoś z listy znajomych, jak również anulowanie wysłanego zaproszenia czy odrzucenie zaproszenia od niechcianej osoby. Całość umieściłem za filtrem :login_required (korzystam z pluginu restful_authentication) aby zarządzanie znajomymi było dostępne tylko po zalogowaniu.

Poszczególne akcje kontrolera nie są jakoś bardzo wyszukane, dla przykładu wkleję akcję destroy:

# app/controllers/friendships_controller.rb
class FriendshipsController < ApplicationController
# (...)
  def destroy
    # current_user zwraca aktualnie zalogowanego uzytkownika
    @user = current_user # dbamy o to, aby nikt nie usunal za nas naszych znajomych
    @friend = User.find(params[:id]) rescue nil
    if @friend
      if @user.any_friends.include?(@friend)
        Friendship.breakup(@user,@friend)
        flash[:notice] = 'Zaproszenie zostało usunięte'
      else
        flash[:notice] = 'Nie ma takiego zaproszenia'
      end
    else
      flash[:notice] = 'Nie ma takiego użytkownika'
    end
    redirect_to user_friendships_url(@user)
  end
# (...)

Zasób Friendships zagnieżdzony został przeze mnie w zasobie Users, co zapewnia taki fragment routes.rb:

# config/routes.rb
# (...)
 map.resources :users do |u|
    u.resources :friendships
  end
# (...)

Wydaje mi się, że takie rozwiązanie jest jednym z najprostszych, a mimo to zapewnia nam podstawowe rzeczy, które mogą okazać się przydatne, np. to, kto był stroną wysyłającą zaproszenie, czy kiedy zostało ono zaakceptowane.

Tagi:

Dynamiczne ładowanie plików z formularza w Ruby on Rails

23 marca 2008, 00:38:32

Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Ruby on Rails, Techblog. | 3 komentarze

Wstęp

Wyobraźmy sobie, że chcemy utworzyć bazę ogłoszeń, do których chcemy w łatwy i przyjemny sposób dodawać zdjęcia. W tej notce pokażę jak w dosyć prosty sposób wykorzystać Railsy do stworzenia prostego panelu do zarządzania takimi ogłoszeniami. Stworzymy formularz dodawania ogłoszeń, w którym oprócz możliwości dodawania ogłoszeń umieścimy prosty formularz, który umożliwi załadowanie zdjęcia na serwer i podpięcie go pod dodawane ogłoszenie. Oczywiście udostępnimy możliwość wyboru zdjęć już obecnych na serwerze, dla uproszczenia zakładamy, że zdjęcie może należeć tylko do jednego ogłoszenia.

Wymagania

Wszystko, czego potrzebujemy to:

Zaczynamy

Zacznijmy od stworzenia nowego projektu. Nie będę skupiał się na podstawach, na potrzeby tego opisu wykorzystam domyślną konfigurację railsów z bazą SQLite. W linii poleceń tworzymy nowy projekt:

$ rails demo
$ cd demo

Od razu stworzymy bazę danych, aby potem nie trzeba było do tego wracać:

$ rake db:create

Mając przygotowany projekt, zaopatrzmy się w odpowiednie pluginy. W tym celu odpalamy następujące polecenia:

$ script/plugin install http://svn.techno-weenie.net/projects/plugins/attachment_fu/
$ script/plugin install http://responds-to-parent.googlecode.com/svn/trunk

Środowisko mamy już przygotowane. Pora na utworzenie modeli. Ograniczymy się do najbardziej podstawowych rzeczy. Utworzymy model Advertisement, który będzie miał tytuł oraz opis. Potrzebny będzie również model Photo, który będzie miał atrybuty wymagane przez plugin attachment_fu. Wklepujemy zatem w konsoli:

$ script/generate model Advertisement name:string description:text

W przypadku modelu Photo nie będziemy podawać żadnych parametrów dla generatora:

$ script/generate model Photo

Po wygenerowaniu modeli, zanim zmigrujemy bazę danych, wyedytujemy plik migracji db/migrate/002_create_photos.rb, wypełniając go jak poniżej:

class CreatePhotos < ActiveRecord::Migration
  def self.up
    create_table :photos do |t|
      t.column :parent_id,  :integer
      t.column :content_type, :string
      t.column :filename, :string    
      t.column :thumbnail, :string 
      t.column :size, :integer
      t.column :width, :integer
      t.column :height, :integer
      t.column :advertisement_id, :integer
    end
  end

  def self.down
    drop_table :photos
  end
end

W tej chwili możemy już zmigrować bazę danych

$ rake db:migrate

Konfiguracja modeli

Pora na skonfigurowanie modeli, utworzenie relacji jeden-wiele między ogłoszeniami i zdjęciami oraz przygotowanie modelu Photo do pracy z pluginem attachment_fu (wykorzystanie relacji wiele-wiele pozostawiam jako ćwiczenie). W tym celu edytujemy pliki modeli jak poniżej

# app/models/advertisement.rb
class Advertisement < ActiveRecord::Base
  has_many :photos
end
# app/models/photo.rb
class Photo < ActiveRecord::Base
  belongs_to :advertisement
  has_attachment :content_type => :image,   # zalacznikiem jest obrazek
                 :storage => :file_system,  # trzymamy go w systemie plikow
                 :max_size => 2.megabytes,  # ograniczamy rozmiar do 2 MB
                 :resize_to => '1200x800>', # zmniejszamy plik wejsciowy
                 :thumbnails => {:thumb => '200x200>'} # tworzymy jedna miniature
  validates_as_attachment # walidujemy plik po zaladowaniu na serwer, przed zapisem do bazy danych
  # z powyzsza walidacja mialem problemy pod Windowsem, posiadaczom tego systemu radze poki co zakomentowac
  # powyzsza linijke
end

W tej chwili mamy już skonfigurowaną relację oraz przygotowany model pod umieszczanie na serwerze plików. Pora na następną część

Konfiguracja kontrolerów

Do prawidłowej pracy będziemy potrzebować dwa kontrolery. Jeden z nich, nazwijmy go advertisements będzie odpowiedzialny za zarządzanie ogłoszeniami, drugi — photos — będzie zajmował się zdjęciami. Wykorzystamy mechanizm REST. W tym celu tworzymy kontrolery z linii poleceń:

$ script/generate controller Advertisements index show new edit create update destroy
$ script/generate controller Photos index new edit create destroy

Aby skorzystać z REST musimy jeszcze dodać odpowiednie wpisy w pliku config/routes.rb:

ActionController::Routing::Routes.draw do |map|
  map.resources :advertisements
  map.resources :photos
  
  # ponizsze wpisy pozostawiamy
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end

Nadeszła pora na zaimplementowanie akcji potrzebnych do zarządzania naszymi zasobami. Są to standardowe akcje, pokażę zatem jedynie zawartość kontrolerów:

# app/controllers/advertisements_controller.rb
class AdvertisementsController < ApplicationController

  def index
    @advertisements = Advertisement.find(:all)
    respond_to do |format|
      format.html {}
      format.xml  { render :xml => @advertisements.to_xml }
    end
  end

  def show
    @advertisement = Advertisement.find(params[:id])
    respond_to do |format|
      format.html {}
      format.xml  { render :xml => @advertisement.to_xml }
    end
  end

  def new
    @advertisement = Advertisement.new
  end

  def edit
    @advertisement = Advertisement.find(params[:id])
  end

  def create
    @advertisement = Advertisement.new(params[:advertisement])
    respond_to do |format|
      if @advertisement.save
        format.html { redirect_to advertisement_url(@advertisement) }
        format.xml  { render :xml => @advertisement.to_xml }
      else
        format.html { render :action => 'new'}
        format.xml  { render :xml => @advertisement.errors.to_xml }
      end
    end
  end

  def update
    @advertisement = Advertisement.find(params[:id])
    respond_to do |format|
      if @advertisement.update_attributes(params[:advertisement])
        format.html { redirect_to advertisement_url(@advertisement) }
        format.xml  { render :xml => @advertisement.to_xml }
      else
        format.html { redirect_to edit_advertisement_url(@advertisement) }
        format.xml  { render :xml => @advertisement.errors.to_xml }
      end
    end
  end

  def destroy
    @advertisement = Advertisement.find(params[:id])
    respond_to do |format|
      if @advertisement.destroy
        format.html { redirect_to advertisements_url }
        format.xml  { head :ok }
      else
        format.html { redirect_to advertisement_url(@advertisement) }
        format.xml  { redirect_to @advertisement.errors.to_xml }
      end
    end
  end
end
# app/controllers/photos_controller.rb
class PhotosController < ApplicationController

  def index
    @photos = Photo.find(:all, :conditions => 'parent_id IS NULL') # wybieramy tylko zdjecia w oryginalnych rozmiarach
    respond_to do |format|
      format.html {}
      format.xml  { render :xml => @photos.to_xml }
    end
  end

  def new
    @photo = Photo.new
    @advertisements = Advertisement.find(:all)
  end

  def create
    @photo = Photo.new(params[:photo])
    respond_to do |format|
      if @photo.save
        format.html { redirect_to photos_url }
        format.xml  { render :xml => @photo.to_xml }
      else
        format.html { render :action => 'new' }
        format.xml  { render :xml => @photo.errors.to_xml }
      end
    end
  end

  def destroy
    @photo = Photo.find(params[:id])
    respond_to do |format|
      if @photo.destroy
        flash[:notice] = 'Zdjęcie zostało usunięte'
        format.html { redirect_to photos_url }
        format.xml  { head :ok }
      else
        flash[:error] = 'Wystąpił błąd podczas usuwania zdjęcia'
        format.html { redirect_to photos_url }
        format.xml  { render :xml => @photo.errors.to_xml }
      end
    end
  end
end

Warstwa modeli i kontrolerów jest już prawie gotowa. Pozostało dodanie widoków, za pomocą których będziemy mogli odczuć, czy to, co stworzyliśmy w ogóle działa.

Pora na widoki

W warstwie widoków ograniczę się do najprostrzego wyświetlania elementów, bez żadnych upiększaczy (przynajmniej na razie). Poniżej wklejam najprostsze możliwe widoki, które nie oferują nic poza podstawowymi opcjami. Widoki dla kontrolera advertisements_controller:

# app/views/advertisements/index.html.erb
<h1>Ogłoszenia</h1>
<ul>
<% for advertisement in @advertisements %>
  <li>
    <p><%= link_to h(advertisement.name), advertisement_url(advertisement) %></p>
    <p><%= truncate(h(advertisement.description), 20) %></p>
    <p><%= link_to 'Edytuj', edit_advertisement_url(advertisement) %></p>
    <p><%= link_to 'Usuń', advertisement_url(advertisement), :method => :delete, :confirm => 'Czy jesteś pewien?' %></p>
  </li>
<% end %>
</ul>
<p><%= link_to 'Dodaj nowe', new_advertisement_url %></p>

# app/views/advertisements/new.html.erb
<h1>Nowe ogłoszenie</h1>
<%= error_messages_for :advertisement %>

<% form_for @advertisement do |f| %>
  <p><label for="advertisement_name">Nazwa:</label></p>
  <p><%= f.text_field :name %></p>
  <p><label for="advertisement_description">Opis:</label></p>
  <p><%= f.text_area :description, :cols => 60, :rows => 20 %></p>
  <p><%= submit_tag 'Zapisz', :disable_with => "Proszę czekać..." %></p>
<% end %>
<p><%= link_to 'Powrót', advertisements_url %></p>

# app/views/advertisements/show.html.erb
<h1><%=h @advertisement.name %></h1>
<p><%=h @advertisement.description %></p>
<p><%= link_to 'Edytuj', edit_advertisement_url(@advertisement) %></p>
<p><%= link_to 'Usuń', advertisement_url(@advertisement), :method => :delete, :confirm => 'Czy jesteś pewien?' %></p>
<p><%= link_to 'Powrót', advertisements_url %></p>
<hr/>
<% unless @advertisement.photos.empty? %>
<p>Zdjęcia:</p>
<ul>
<% for photo in @advertisement.photos %>
  <li><%= link_to image_tag(photo.public_filename(:thumb)), photo.public_filename %></li>
<% end %>
</ul>
<% end %>

# app/views/advertisements/edit.html.erb
<h1>Edycja ogłoszenia</h1>
<%= error_messages_for :advertisement %>

<% form_for @advertisement do |f| %>
  <p><label for="advertisement_name">Nazwa:</label></p>
  <p><%= f.text_field :name %></p>
  <p><label for="advertisement_description">Opis:</label></p>
  <p><%= f.text_area :description, :cols => 60, :rows => 20 %></p>
  <p><%= submit_tag 'Zapisz', :disable_with => "Proszę czekać..." %></p>
<% end %>
<p><%= link_to 'Powrót', advertisements_url %></p>

Widoki dla kontrolera photos_controller:

# app/views/photos/index.html.erb
<h1>Zdjęcia</h1>
<ul>
<% for photo in @photos %>
  <li>
    <p><%= link_to image_tag(photo.public_filename(:thumb)), photo.public_filename %></p>
    <p><%= link_to 'Usuń', photo_url(photo), :method => :delete, :confirm => 'Czy jesteś pewien?' %></p>
  </li>
<% end %>
</ul>
<p><%= link_to 'Dodaj nowe', new_photo_url %></p>

# app/views/photos/new.html.erb
<h1>Nowe zdjęcie</h1>
<%= error_messages_for :photo %>

<% form_for @photo, :html => {:multipart => true} do |f| %>
  <p><label for="photo_uploaded_data">Plik ze zdjęciem:</label></p>
  <p><%= f.file_field :uploaded_data %></p>
  <p><label for="photo_advertisement_id">Ogłoszenie:</label></p>
  <p><%= f.collection_select :advertisement_id, @advertisements, :id, :name, {:prompt => true} %></p>
  <p><%= f.submit "Załaduj" %></p>
<% end %>
<p><%= link_to 'Powrót', photos_url %></p>

W tej chwili możemy już zobaczyć, jak to wszystko działa. Wystarczy odpalić serwer:

$ script/server

Po odpaleniu serwera wpisując w adresie przeglądarki adres http://localhost:3000/advertisements możemy administrować ogłoszeniami, natomiast pod adresem http://localhost:3000/photos możemy zarządzać zdjęciami.

No dobrze, ale na początku pisałem o dynamicznym ładowaniu zdjęć podczas pisania ogłoszenia oraz o możliwości wybierania zdjęć. Na pierwszy rzut pójdzie możliwość wyboru zdjęć spośród załadowanych i nie dołączonych do innych ogłoszeń

Wybór zdjęć przy dodawaniu ogłoszenia

Aby umożliwić dołączanie zdjęć podczas dodawania czy edycji ogłoszenia, skorzystamy z możliwości, jakie dostarcza metoda has_many w modelu ogłoszeń, a dokładnie z settera photo_ids. W tym celu należy dodać kilka rzeczy do kontrolera ogłoszeń i zmodyfikować odpowiednio widoki.

W metodach new oraz edit dodajemy następującą linijkę (oczywiście przeczymy tutaj zasadzie DRY, jednak chodzi tutaj o jak najprostsze pokazanie, o co chodzi):

@photos = Photo.find(:all, :conditions => 'parent_id IS NULL')

W tej chwili możemy już pokazać w formularzu dodawania/edycji ogłoszeń dostępne zdjęcia wraz z check boxami do ich zaznaczania. W tym celu dodajemy do formularza w plikach app/views/advertisements/{new,edit} gdzieś w bloku form_for, najlepiej po text area:

<div id="photos">
<% for photo in @photos %>
  <p><%= image_tag photo.public_filename(:thumb) %></p>
  <p><%= check_box_tag 'advertisement[photo_ids][]', photo.id, @advertisement.photos.include?(photo) %>
<% end %>
</div>

Od tej pory będziemy mogli zaznaczać zdjęcia, które będziemy chcieli dodać do ogłoszenia (pamiętajmy o tym, że mamy relację jeden-wiele, więc jeżeli tworząc lub edytując jakieś ogłoszenie zaznaczymy zdjęcie wcześniej zajęte, zostanie ono przypisane tylko do nowo tworzonego/edytowanego ogłoszenia — aby to zmienić należy zamienić relację z jeden-wiele na wiele-wiele). Zostaje tylko jeden problem. Kiedy będziemy chcieli usunąć z jakiegoś ogłoszenia wszystkie zdjęcia i odznaczymy wszystkie dostępne w formularzu, po kliknięciu 'Dodaj' lub 'Edytuj' nic się nie stanie. Dzieje się tak dlatego, że w tym przypadku Railsy nie przekażą do kontrolera zawartości parametru advertisement[photo_ids][]. Jednak jest bardzo prosty sposób, by to zmienić. Do kontrolera advertisements_controller w akcji update dodajemy na początku następującą linijkę:

params[:advertisement][:photo_ids] ||= []

Spowoduje ona, że w przypadku, gdy odznaczymy wszystkie zdjęcia podczas edycji ogłoszenia, zostaną rzeczywiście usunięte z relacji.

Dynamiczne dodawanie plików + a pinch of AJAX

Do zrobienia została nam jeszcze jedna część — dynamiczne ładowanie zdjęcia podczas dodawania lub edycji ogłoszenia i uaktualnianie formularza nowym zdjęciem. Nie da się tego zrobić przy pomocy samego AJAX-a, ponieważ ze względów bezpieczeństwa AJAX nie pozwala na przesyłanie lokalnych plików. Istnieje jednak prosty sposób, aby tę niedogodność ominąć. W tym celu utworzymy na stronie niewodoczny iframe i ustawimy na niego atrybut target formularza ładującego zdjęcie. Następnie, wykorzystując plugin responds_to_parent, uaktualnimy listę zdjęć w formularzu dotyczącym ogłoszenia.

Na początku zdefiniujmy sobie prosty layout oraz dołączmy potrzebne skrypty JavaScript. W tym celu tworzymy plik app/views/layouts/application.erb o nastepującej zawartości:

<html>
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
    <title>Demonstracja dynamicznego ładowania plików, &copy; 2008 GhandaL</title>
    <%= javascript_include_tag :defaults %>
  </head>
  <body>
    <% if flash[:notice] %>
      <div style="border: 1px green"><%= flash[:notice] %></div>
    <% end %>
    <% if flash[:error] %>
      <div style="border: 1px red"><%= flash[:error] %></div>
    <% end %>
    <%= yield :layout %>
  </body>
</html>

W celu ładowania pliku posłużymy się fragmentami kodu widoków, tzw. partials. Zanim jednak dopracujemy widoki, zmodyfikujemy odpowiednio kontroler photos_controller, aby uaktualniał stronę, która wywołała akcję create. W tym celu modyfikujemy akcję create w pliku app/controllers/photos_controller.rb tak, by wyglądała jak poniżej:

  def create
    @photo = Photo.new(params[:photo])
    respond_to do |format|
      if @photo.save
        format.html { redirect_to photos_url }
        format.xml  { render :xml => @photo.to_xml }
        format.js do
          responds_to_parent do
            render :update do |page|
              page.insert_html :bottom, "photos", :partial => 'photo', :object => @photo
            end
          end
        end
      else
        format.html { render :action => 'new' }
        format.xml  { render :xml => @photo.errors.to_xml }
        format.js do
          responds_to_parent do
            render :update do |page|
              # jakis kod uaktualniajacy, np. bledem, strone
            end
          end
        end
      end
    end
  end

Teraz możemy się zająć stworzeniem odpowiednich partials. Po pierwsze stworzymy kod, który wyświetli nowo załadowane zdjęcie. Tworzymy plik app/views/photos/_photo.erb o zawartości:

<p><%= image_tag photo.public_filename(:thumb) %></p>
<p><%= check_box_tag 'advertisement[photo_ids][]', photo.id, true %></p>

Do zrobienia została nam jeszcze jedynie modyfikacja widoków akcji new i edit kontrolera advertisements_controller. Gdzieś na końcu plików app/views/advertisements/{new,edit} dopisujemy następujący kod:

<iframe name="dummy" id="dummy" style="width:1px;height:1px;border:0"></iframe>
<% form_for :photo, :url => {:controller => :photos, :action => :create, :format => 'js'}, :html => {:target => 'dummy', :multipart => true} do |f| %>
  <p><label for="photo_uploaded_data">Plik ze zdjęciem:</label></p>
  <p><%= f.file_field :uploaded_data %></p>
  <p><%= f.submit 'Dodaj' %></p>
<% end %>

I to już wszystko. Od tej pory dodając i edytując ogłoszenia można już ładować zdjęcia bez potrzeby przeładowywania strony.

Konkluzja

Oczywiście powyższe rozwiązanie ma pewne wady. Przede wszystkim relacja jeden-wiele jest bardzo prymitywna w tym przypadku i zabrania dołączania jednego zdjęcia do kilku ogłoszeń. Jednak modyfikacja tego nie jest trudna, nie o tym jest również ta notka. Kod demonstracyjnej aplikacji możesz ściągnąć mojej strony. Mam nadzieję, że powyższy opis komuś się przyda. Jeśli zauważyłeś jakiś błąd lub chciałbyś wyrazić opinię o moim rozwiązaniu, zapraszam do komentowania.

Tagi:

Mój pierwszy VPS

11 marca 2008, 14:43:37

Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Ruby on Rails, Techblog, Web Design. | 10 komentarzy

Ponieważ zaczęły mnie denerwować timeouty w przekazywaniu żądań z Apache do mongreli na Rootnode zacząłem rozglądać się za czymś lepszym. Pod uwagę brałem VPS-y w cenie do ok. 60zł za miesiąc. Postanowiłem w najbliższych miesiącach przetestować kilka z nich. Na pierwszy ogień poszedł ProVPS. Za tym rozwiązaniem przemawia lokalizacja serwerów w Polsce, w Gdańsku, a co za tym idzie niskie pingi, w granicach 35-45ms. Jednak na tym plusy się kończą (porównując do reszty ofert branych pod uwagę). Za 49 zł netto miesięcznie dostajemy gwarantowane 128 MB RAM-u, rozszerzane do 512 MB, 5 GB powierzchni dyskowej, 25 GB transferu miesięcznego oraz 1 adres IP. Na tle konkurencji trochę blado.

Wczoraj aktywowano mi VPS i spędziłem kilka ładnych godzin na konfigurowaniu poszczególnych części systemu. Postanowiłem uruchomić minimum usług, głównie po to, by zmieścić się w drakońskich limitach pamięci. Niestety musiałem zrezygnować z mySQL, ponieważ nie znalazłem sposobu na ograniczenie konsumpcji pamięci do akceptowalnego na tym serwerze poziomu — po uruchomieniu mysqld konsumował ok. 100 MB pamięci operacyjnej pomimo przeróżnych konfiguracji znalezionych w sieci i w dokumentacji mySQL. Zaminiłem mySQL na PostgreSQL i tutaj małe zaskoczenie — domyślna instalacja postgresa zużywa ponad połowę mniej pamięci niż mySQL!

Gdy baza danych była już gotowa, nadszedł czas na serwer WWW. Wybór oczywiście padł na nginx, o wiele lżejszy od Apache'a serwer plus load balancer. Na VPS-ie będą uruchamiane jedynie statyczne pliki oraz aplikacje w Railsach i Merbie (w liczbie max. 3 — patrz pamięć). Z konfiguracją nginxa nie było żadnych problemów, świetną konfigurację dla aplikacji railsowych w obsługą cache przygotował Ezra Zygmuntowicz. Konfiguracja ta dostępna jest na jego stronie.

Następną rzeczą do zrobienia była kompilacja Ruby, Rubygems, ImageMagick oraz instalacja Subversion. Oczywiście po drodze były mniejsze komplikacje — a to zabrakło biblioteki zlib, a to nie zainstalowałem libsqlite3-dev. W każdym razie po mniej więcej 5 godzinach miałem już działającego Postgresa, nginxa, SVN, Ruby+Rails i całą resztę potrzebnych do życia aplikacji.

Nadszedł czas na sprawdzenie, czy praca nie poszła na marne. Zgrałem pisaną ostatnio aplikację, zmigrowałem bazę, ustawiłem dwie instancje serwera thin i uruchomiłem je. Et voilà! Wszystko ruszyło i działa jak należy i, co najważniejsze, naprawdę dużo szybciej niż na Rootnode, gdzie aplikacja stała poprzednio.

Nieprzespana noc z konfiguracją serwera za mną, ale za miesiąc prawdopodobnie czeka mnie następna, ponieważ, jak pisałem na wstępie, mam w planie przetestowanie kilku dostawców, którzy mają mniejszy miesięczny abonament i gwarantują 2x więcej pamięci, co dla mnie jest rzeczą ważną. Zastanawiam się, czy sensowne będzie kupowanie VPS-a w USA. Na mojej liście jest Slicehost oraz SilverRack z całkiem niezłą ofertą za niewielkie pieniądze. Z drugiej strony za zachodnią granicą mamy Host Europe, również z ciekawą ofertą i niskimi pingami.

Pomimo dużo większych nakładów czasowych myślę, że nie wrócę już do dzielonego hostingu. VPS i serwer dedykowany daje tę wolność, jakiej nie ma na żadnym dzielonym hostingu (Rootnode jest bardzo liberalne, ale, np. nie mogłem doprosić się o aktualizację Ruby do wersji 1.8.6, ponieważ ma "za dużo zależności"). Pamiętać trzeba oczywiście o tym, że samemu trzeba dbać o stabliność i bezpieczeństwo danych na takim serwerze, ale to z kolei wymusza pewną pracę i naukę, która później nie zginie.

PS. Jakby ktoś z czytających miał wiedzę na temat tego, jak ograniczyć zużycie pamięci przez mySQL, proszę o kontakt.

Tagi:

Ebb — szybciej, coraz szybciej!

05 marca 2008, 09:37:26

Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Techblog. | 4 komentarze

Na horyzoncie pojawił się nowy gracz na rynku serwerów do obsługi frameworków w języku Ruby i Python (w przyszłości) — jest nim Ebb. Serwer ten bije konkurentów na głowę, przynajmniej w szybkości i ilości requestów na sekundę. Ja zauważyłem, że zużywa troche więcej pamięci niż np. Mongrel. W swojej małej aplikacji używałem już Mongrela oraz młodszego Thina — nadszedł czas na przetestowanie Ebb. Na pierwszy rzut oka rzeczywiście wzrost wydajności widoczny jest niemal natychmiast.

Ebb jest na razie młodym serwerem i brak mu dokumentacji, a przynajmniej ja nie stwierdziłem takowej. Jednak aby odpalić aplikację w Railsach wystarczy wykonać kilka prostych poleceń:

$ gem install ebb

Następnie przejść do katalogu aplikacji Railsowej i odpalić

$ ebb_rails start

Pełna lista opcji, jakie oferuje Ebb jest następująca:

Usage: ebb_rails [options] start|stop

Server options:
    -p, --port PORT        use PORT (default: 3000)
    -e, --env ENV          Rails environment 
                                     (default: development)
    -c, --chdir PATH       Rails root dir 
                                     (default: current dir)
    -d, --daemonize        Daemonize
    -l, --log-file FILE    File to redirect output
    -P, --pid-file FILE    File to store PID
    -t, --timeout SEC      Request or command timeout in sec
                                     (default: 60)

Common options:
    -h, --help             Show this message
    -v, --version          Show version

W przypadku uruchamiania jako demon, konieczne jest podanie ścieżki do pliku z PID procesu.

Tagi:

pygglib 0.2 released

16 stycznia 2008, 18:37:35

Poziom: 0 | Kategoria: Komputerowo-internetowo, Python, Studia, Techblog. | 14 komentarzy

Można powiedzieć, że dzisiaj światło dzienne ujrzała wersja 0.2 pygglib — biblioteki do obsługi protokołu Gadu-Gadu napisanej w pythonie. Wersja ta implementuje już większą część protokołu opisanego na stronach EKG.

Przy okazji pisania testów i implementacji okazało się, że panowie z Gadu-Gadu znowu mieszają coś w protokole, poza tym opis na stronach EKG jest już trochę nieaktualny (szczegóły zostały wysłane na listę dyskusyjną ekg-devel). W każdym razie dzisiaj miała miejsce prezentacja projektu na zajęciach i muszę przyznać, że większość prac, która została przewidziana, została zaimplementowana. Niestety zabrakło czasu na obsługę bezpośrednich połączeń oraz takich rzeczy jak połączenia konferencyjne, czy rozmowy głosowe, jednak jestem zadowolony z tego, co zostało napisane.

Przy okazji muszę ponarzekać na pythona... Przyzwyczajony byłem do tej pory do składni języka Ruby i trochę trudno było mi się przestawić na wcięcia i dwukropki. Poza tym podobno python jest językiem obiektowym, więc tym bardziej denerwowało mnie na przykład to, że lista nie ma metody "len" bądź "length", tylko jej długość możemy sprawdzić funkcją len(lista), co już, według mnie, za bardzo obiektowe nie jest.

Na osobną notkę zasługuje również coś, co według mnie powinno zostać naprawione wieki temu — w pythonie nie da się (pod Windowsem i w sposób najbardziej naturalny) przechwytywać sygnału SIGINT po wciśnięciu Ctrl+C (czyli wyjątku KeyboardInterrupt) w programach wielowątkowych! Jest to absolutna kompromitacja. W programie demonstrującym działanie pygglib chciałem stworzyć proste echo, które byłoby wyłączane przez wciśnięcie Ctrl+C, jednak, ponieważ biblioteka korzysta z listenera i obsługuje zdarzenia, przez co jest wielowątkowa, stało się to niemożliwe. Pod linuxem można ten błąd w dosyć prosty sposób ominąć poprzez zastosowanie os.fork(), jednak pod Windowsem takiej opcji nie ma.

To tyle narzekania na Pythona. Zainteresowanych zapraszam do pobrania źródeł biblioteki oraz testowania. Ewentualnie błędy i sugestie proszę zgłaszać na Trac projektu.

EDIT: Wypuściliśmy wersję 0.2.1, w której poprawiony został odczyt wiadomości wysyłanych z niektórych wersji Gadu-Gadu, na końcu których występowały czasem śmieci. Link do źródeł został zaktualizowany.

Tagi:

GalleryMaker 0.2

07 grudnia 2007, 18:59:40

Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Techblog. | 7 komentarzy

W miarę wolnego czasu kontynuuję pisanie swojej małej biblioteki w języku Ruby do tworzenia indeksu zdjęć z miniaturkami. Dzisiaj światło dzienne ujrzała wersja 0.2, a wraz z nią możliwość dzielenia indeksu na kilka plików, co było dosyć dużą bolączką biblioteki (ładowanie kilkuset miniaturek trochę trwa).

Cały czas w TODO został system szablonów, w tej chwili nie mam czasu go zrobić. Nie wybrałem nawet koncepcji zarządzania tymi szablonami. W głowie mam kilka pomysłów — XML, Yaml, może Haml lub ERb. Na co się zdecyduję, jeszcze nie wiem.

Jeśli ktoś chciałby pobrać aktualną wersję (nie daję gwarancji, że cokolwiek zadziała, chociaż testowałem przez chwilę zarówno pod Windowsem, jak i pod Linuxem — Debian 4.0 i Ubuntu 7.10), zapraszam do repozytorium. Do zrobienia jest jeszcze dużo, począwszy od refaktoryzacji. Wszelkie uwagi i pomysły mile widziane.

Zapomniałem dodać, że do poprawnego działania wymagane jest posiadanie ImageMagick oraz biblioteki RMagick.

Tagi:

ntfs-3g » nie do końca taki super

23 grudnia 2006, 01:25:47

Poziom: 0 | Kategoria: Techblog, Z życia. | Dodaj komentarz

Pisałem niedawno o obsłudze zapisu i odczytu partycji ntfs zaszyfrowanej TrueCryptem. Ostatnio, ku mojemu zaskoczeniu i zdenerwowaniu zaczęły ginąć pliki. Pod Windowsem nadal były widoczne, jednak linux ich nie widział. Po krótkim dochodzeniu okazało się, że były to pliki z polskimi ogonkami.

Nie byłbym sobą, gdybym nie znalazł na to rozwiązania. Oczywiście jest banalnie proste, choć wydawać by się mogło, że zasugerowanie sterownikowi, że nazwy plików są w UTF-8 powinnno wystarczyć. Ostatecznie za poprawną pracę parycji nfts z odczytem i zapisem odpowiedzialna jest taka linijka w pliku /etc/fstab:

/dev/hdaX /media/dysk ntfs-3g defaults,locale=pl_PL.utf8 0 0

Tagi:

Truecrypt pod Linuxem z obsługą ntfs-3g

21 listopada 2006, 15:36:45

Poziom: 0 | Kategoria: Techblog. | 4 komentarze

Po ciężkim boju z kompilowaniem TrueCrypt pod Ubuntu 6.10 udało się i TC zaczął prawidłowo działać. Ale pozostała jeszcze jedna rzecz do zrobienia -- zmuszenie TC do korzystania ze sterownika ntfs-3g, ponieważ systemem plików na zaszyfrowanej partycji jest właśnie ntfs, a chciałbym mieć możliwość zapisywania plików nie tylko pod Windowsem.

Domyślnie TrueCrypt korzysta z domyślnego sterownika ntfs pracującego w trybie read-only. Praca z odczytem i zapisem jest możliwa w bardzo prosty sposób -- podczas montowania zaszyfrowanej partycji bądź pliku, wystarczy podać dodatkowy parametr --filesystem ntfs-3g zakładając, że mamy zainstalowany pakiet ntfs-3g.

Tagi:

Współdzielenie zakładek i ciastek w Firefoxie

18 września 2006, 20:39:42

Poziom: 0 | Kategoria: Komputerowo-internetowo, Techblog, Z życia. | 6 komentarzy

Ponieważ nudze się niesamowicie, nareszcie zrobiłem to, co chciałem od dawna, czyli współdzielenie ciastek i zakładek w Firefoxie między dwoma systemami w moim notebooku. Zdaje sobie sprawę, że Ameryki nie odkryłem oraz że sposób, który wykorzystałem jest banalny, jednak efekt mi się podoba. Jeśli ktoś chciałby zrobić to samo, służę pomocą.

Wymagania: Windows i Linux oraz partycja FAT z uprawnieniami odczytu i zapisu zamontowana pod Linuxem, na której znajduje się profil Firefoxa w wersji Windows. Cała zabawa sprowadza się, do podlinkowania plików cookies.txt, odpowiedzialnego za przechowywanie ciastek oraz bookmarks.html, czyli plik z zakładkami, do katalogu przechowującego profil Firefoxa w wersji Linuxowej. Pod Linuxem przechodzimy do katalogu z profilem Firefoxa (zwykle ~/.mozilla/firefox/<numerki>) i w konsoli wpisujemy (po uprzednim usunięciu plików cookies.txt i bookmarks.html):

ln -s /sciezka/do/pliku/na/partycji/fat/cookies.txt
ln -s /sciezka/do/pliku/na/partycji/fat/bookmarks.html

Od tej pory możemy cieszyć się współdzielonymi ciastkami i zakładkami. O plusach tego rozwiązania nie musze chyba nikomu pisać.