Tag: Rails

Rails + MySQLDump czyli robienie kopii zapasowej bazy danych z poziomu Railsów

Zabezpieczenie systemu jest (jak nie muszę przekonywać) bardzo ważną sprawą. Jedną z podstawowych metod zapewnienia bezpieczeństwa systemu jest regularne wykonywanie kopii zapasowych bazy danych. Wprawdzie w Ruby on Rails nie istnieje ujednolicony interfejs do wykonywania tej czynności, jednak bardzo prosto można napisać własny model wykonujący dumpa.

Model jest bardzo prosty. Jeśli chodzi o migrację to zawiera ona tylko timestampy (created_at, updated_at). Jeśli chodzi o klasę, to jej szkielet wygląda tak:

# coding: utf-8

require 'fileutils'

# Służy do wykonywania kopii zapasowych plików oraz bazy danych
# Działa dla MySQLa - robi dumpa bazy
class Backup < ActiveRecord::Base
  # Zawartość
end

Dalej nie jest trudniej :) Musimy ustalić prefiks dla nazwy pliku, który będzie przechowywał nasz zrzut:

  # Prefix do nazwy backupu bazy danych
  DB_NAME_PREFIX = 'db_'

Oraz nadpisać metodę create, która oprócz utworzenia wpisu w bazie danych, utworzy nam także nasz zrzut.

Aby wykonać zrzut, musimy mieć dostęp do kilku informacji:

  • Nazwa bazy
  • Login
  • Hasło
  • Host
  • Możliwość wykonywania poleceń zewnętrznych
  • Ścieżka do backupu

Tak naprawdę, nie potrzebujemy niczego szczególnego. Zanim jednak przystąpimy do tworzenia naszej metody create, omówmy sobie jeszcze jak ma wyglądać przechowywanie kopii zapasowych.

Proponowałbym utworzyć logiczną hierarchię dla plików i katalogów. W głównym katalogu aplikacji, tworzony będzie katalog backup/. W nim będą subkatalogi, których nazwy składać się będą z daty oraz czasu wykonania kopii. W środku takiego subkatalogu będzie spakowany zrzut bazy.

Idąc za ciosem, zdefiniujmy sobie metodę prywatną, która sprawdzi nam czy katalog backup/ oraz jego subkatalog istnieją. Metoda ta jest na tyle prosta, że nie ma co jej opisywać. Komentarze powinny wystarczyć:

  # Sprawdzamy czy ścieżka gdzie ma być backup istnieje i jeśli nie to ją
  # utworzymy
  # Jako parametr podajemy czas - to jest nazwa folderu jako sygnatura czasowa
  # momentu rozpoczecia backupowania, czas w formacie Time.now.strftime("%Y\-%m\-%d_%H\-%M\-%S")
  def check_path(time)
    # Sprawdzajmy czy struktura katalogow jest cala - od poczatku (wiem że to
    # narzut tych kilku sprawdzen, ale tak jest prościej)
    full_backup_path = "#{Rails.root}/backup/#{time}/"
    check_path = ''
    full_backup_path.split('/').each { |el|
      check_path += "#{el}/"
      Dir.mkdir(check_path) unless File.directory?(check_path)
    }
  end

Mając już metodę, która zapewni nam, że będzie gdzie przechowywać plik ze zrzutem, możemy przystąpić do pisania metody create.

Pierwszą rzeczą jaką musimy zrobić, jest zapamiętanie czasu w którym przystąpiliśmy do tworzenia backupu. Jest to bardzo istotne, ponieważ jeśli operowalibyśmy na Time.now mógłby on różnić się dla subkatalogu oraz nazwy pliku (i ew. wpisu w bazie), co prowadziłoby do wielu problemów. Ominiemy je jednak wykonując na początku metody taki fragment kodu:

  # Nadpisujemy domyślne create żeby wykonywało kopię
  def create
    # Potrzebujemy jeden czas dla plików i dla utworzenia obiektu modelu Backup
    time = Time.now
    super
    self.created_at = time
    self.save
    # Musimy mieć czas w takim formacie aby dało się tym nazwać pliki
    file_time = time.strftime("%Y\-%m\-%d_%H\-%M\-%S")

Zapamiętaliśmy w zmiennej time aktualny czas, zapisaliśmy go także jako wartość created_at dla backupu. Na końcu konwertujemy czas do formatu który będzie stanowił nazwę dla naszego subkatalogu (np 2010-09-20_19-12-56).

Mając już "globalny" czas, możemy sprawdzić (i jeśli trzeba - a trzeba na pewno ;)) i utworzyć strukturę katalogów dla kopii zapasowej. W tym celu wywołamy następujący kod (który napisaliśmy wcześniej):

     # Jeśli nie ma katalogu na backupy to go utwórz
    check_path file_time

Ustalmy teraz jak ma nazywać się plik z kopią zapasową bazy i gdzie ma się znajdować:

    # Ścieżka gdzie ma zapisać plik bazy danych
    backup_db = "#{Rails.root}/backup/#{file_time}/#{DB_NAME_PREFIX}#{file_time}.sql.bz2"

Teraz pozostało nam tylko wyciągnąć parametry dostępowe do bazy oraz wykonać dump:

    db = self.configurations[Rails.env]['database']
    db_user = self.configurations[Rails.env]['username']
    db_pass = self.configurations[Rails.env]['password']
    db_host = self.configurations[Rails.env]['host']

    # Zrzut bazy danych
    exec "mysqldump --add-drop-table -u #{db_user} -p#{db_pass} -h #{db_host} #{db} | bzip2 -c > #{backup_db}" if fork.nil?

Baza po zrzucie zostanie spakowana.

Mamy już kopię zapasową oraz odpowiadający jej model. Musimy jednak zapewnić sobie możliwość usuwania powstałych kopii. Aby tego dokonać nadpiszemy metodę destroy w taki sposób aby usuwając instancję modelu, usuwała także korespondujący katalog z kopią zapasową:

  # Usuwa stary backup oraz zadane pliki do niego
  def destroy
    return if self.new_record?
    dir = "#{Rails.root}/backup/#{self.created_at.strftime("%Y\-%m\-%d_%H\-%M\-%S")}"
    # Usuwamy katalog z backupem
    FileUtils.rm_rf(dir) if File.directory?(dir)
    super
  end

To by było na tyle. Zachęcam Was do utworzenia w analogiczny sposób, kodu odpowiedzialnego za backupowanie plików userów - np katalogu /public/uploads/.

Przygotowanie kodu aplikacji w Ruby on Rails do deployu – część III

Witam w ostatniej części tego miniporadnika nt. przygotowania kodu dla mniej fajnych klientów ;)

W tej części zajmiemy się wyczyszczeniem plików rb z oryginalnych komentarzy oraz "zamieszaniem" w tychże plikach. Uruchomimy także całą resztę kodu, tak aby wszystko ładnie funkcjonowało.

A więc do dzieła!

Zacznijmy od metody która usunie nam wszystkie linijki komentarzy, ze wszystkich możliwych plików.

Metoda jest całkiem prosta. Otwieramy plik rb i usuwamy wszystkie linijki zaczynające się na "#", za wyjątkiem magicznego # coding: utf-8. Najpierw otwieramy plik do odczytu, kopiujemy linijka po linijce. Następnie otwieramy plik do zapisu (co automatycznie go wyczyści) a następnie zapisujemy tylko te linijki które nie są komentarzami. Przy okazji uważamy na #{ które nie jest komentarzem ale mogło zostać za takowy uznane.

Usuwamy także puste linijki i pozbywamy się wcięć.

    # Usuwa komentarze (ale nie maskuje - tylko komentarze i białe znaki) z kodu
    def self.clean(path = './deploy/')
      Find.find(path) { |f|
        # Jeśli to nie plik rb to idz do nastepnego pliku
        next unless f[f.size-3, f.size-1] == '.rb'
        filename =  f
        # Wrzuć info na konsole co sie dzieje
        puts "Cleaning #{filename}"

        lines = []
        File.open(filename, "r").each_line {|l| lines << l }

        File.open(filename, 'w') {|f|
          lines.each { |l|
            a = l.strip

            # Jeśli to komentarz to przeskocz dalej
            next if (a[0] == "\#" && a != '# coding: utf-8' && a[0,2] != "\#\{") || a.size == 0

            f.write(a+"\n")
          }
        }
      }
    end

Po przepuszczeniu naszego "deploy" przez ten kod, otrzymamy pliki które nie mają ani komentarzy ani też wcięć.

Pozostało nam jeszcze losowo "nabałaganić" w plikach i spiąć deploy.rb w jedną sensowną całość. Może najpierw pobałaganimy ;)

Po pierwsze, maskowanie powinno dotyczyć tylko i wyłącznie plików .rb, stąd poniższa linijka, która powoduje że kod nie zostanie wykonany dla plików o innych rozszerzeniach:

next unless f[f.size-3, f.size-1] == '.rb'

Dalej jest kawałek kodu, analogiczny do tego z metody czyszczącej komentarze. Ładujemy zawartość pliku do pamięci, po czym czyścimy go i otwieramy do zapisu. Bałaganimy generując losowo od 0 do 64 linijek z losowymi ciągami lub losowymi fragmentami kodu oraz z losową ilością spacji na początku każdej linijki.

Jedyna rzecz na jaką musimy uwazać to polecenia które składają się z wielu linijek, a w których nie można umieszczać komentarzy (np. wielolinijkowe stringi). Rozwiązałem to stosunkowo prosto. Zliczam po prostu liczbę otwarć cudzysłowów i jeśli nie jest parzysta, tzn że kod jest wielolinijkowy. Do czasu aż znajdę następną linijkę z ilością nieparzystą (co oznaczać będzie domknięcie), nie wstawiam śmieci. Śmieci też wstawiam na początku i końcu każdego pliku.

    def self.mask(path)
      rand = Randomer.new
      Find.find(path) { |f|
        # Jeśli to nie plik rb to idz do nastepnego pliku
        next unless f[f.size-3, f.size-1] == '.rb'
        filename =  f
        # Wrzuć info na konsole co sie dzieje
        puts "Masking: #{filename}"

        lines = []
        File.open(filename, "r").each_line {|l| lines << l }

        File.open(filename, 'w') {|f|
          # Czy wyrazenie jest wieloliniowe (nie wsadza w takie komentów)
          multiline = false
      	  i = 0
          lines.each { |l|
            a = l.strip

            # Jeśli to komentarz to przeskocz dalej
            next if (a[0] == "\#" && a != '# coding: utf-8' && a[0,2] != "\#\{") || a.size == 0

            if a == '# coding: utf-8'
              f.write(a+"\n")
              rand(64).times { f.write(rand.class.spaces+'# '+rand.line+"\n") }
            else
              rand(64).times { f.write(rand.class.spaces+'# '+rand.line+"\n") } if i == 0
              line = (multiline ? '' : rand.class.spaces) + a + "\n"
              multiline = !multiline if line.count('"') % 2 == 1
              multiline = !multiline if line.count("'") % 2 == 1
              f.write(line)
            end
            (rand(64).times { f.write(rand.class.spaces+'# '+rand.line+"\n") }) unless multiline
            i+=1
          }
          rand(64).times { f.write(rand.class.spaces+'# '+rand.line+"\n") }
        }
      }
    end

Całość opakowujemy w klasę:

  # Maskuje pliki rb z podanego katalogu
  class Masker
    def self.mask(path)
      rand = Randomer.new
      Find.find(path) { |f|
        # Jeśli to nie plik rb to idz do nastepnego pliku
        next unless f[f.size-3, f.size-1] == '.rb'
        filename =  f
        # Wrzuć info na konsole co sie dzieje
        puts "Masking: #{filename}"

        lines = []
        File.open(filename, "r").each_line {|l| lines << l }

        File.open(filename, 'w') {|f|
          # Czy wyrazenie jest wieloliniowe (nie wsadza w takie komentów)
          multiline = false
      	  i = 0
          lines.each { |l|
            a = l.strip

            # Jeśli to komentarz to przeskocz dalej
            next if (a[0] == "\#" && a != '# coding: utf-8' && a[0,2] != "\#\{") || a.size == 0

            if a == '# coding: utf-8'
              f.write(a+"\n")
              rand(64).times { f.write(rand.class.spaces+'# '+rand.line+"\n") }
            else
              rand(64).times { f.write(rand.class.spaces+'# '+rand.line+"\n") } if i == 0
              line = (multiline ? '' : rand.class.spaces) + a + "\n"
              multiline = !multiline if line.count('"') % 2 == 1
              multiline = !multiline if line.count("'") % 2 == 1
              f.write(line)
            end
            (rand(64).times { f.write(rand.class.spaces+'# '+rand.line+"\n") }) unless multiline
            i+=1
          }
          rand(64).times { f.write(rand.class.spaces+'# '+rand.line+"\n") }
        }
      }
    end

    # Usuwa komentarze (ale nie maskuje - tylko komentarze i białe znaki) z kodu
    def self.clean(path = './deploy/')
      Find.find(path) { |f|
        # Jeśli to nie plik rb to idz do nastepnego pliku
        next unless f[f.size-3, f.size-1] == '.rb'
        filename =  f
        # Wrzuć info na konsole co sie dzieje
        puts "Cleaning #{filename}"

        lines = []
        File.open(filename, "r").each_line {|l| lines << l }

        File.open(filename, 'w') {|f|
          lines.each { |l|
            a = l.strip

            # Jeśli to komentarz to przeskocz dalej
            next if (a[0] == "\#" && a != '# coding: utf-8' && a[0,2] != "\#\{") || a.size == 0

            f.write(a+"\n")
          }
        }
      }
    end
  end

Po czym odpalamy całość kodu napisanego przez 3 części tutoriala:

  # Zrób kopię zapasową do której robimy deploy
  Backuper.run
  # Usuń co niepotrzebne w deployu
  Cleaner.run

  # Wyczysc komentarze z calosci plikow
  Masker.clean
  # I zamaskuj reszte kodu
  MASK_DIRS.each {|d| Masker.mask(d)}

Po tych operacjach otrzymujemy kod który:

  • Nie zawiera elementów niepotrzebnych klientowi
  • Nie zawiera plików tymczasowych
  • Jest "niemiły" dla użyszkodników

To by było na tyle. Używajcie tego ilekroć macie klientów których niekoniecznie darzycie zaufaniem.

Sprawdź także pozostałe dwie części tutoriala:

Copyright © 2025 Closer to Code

Theme by Anders NorenUp ↑