Wednesday, November 25, 2009

Использование deb-пакетов для дистрибъюции кода

В этой статье я хочу рассказать, о том, как можно внедрить систему сборки deb-пакетов для некоторого абстрактного проекта. Плюсов в распространении и развёртывании ПО на основе пакетов несколько:

  • Атомарность пакета (представление продукта в виде одного файла);

  • Наличие скриптов пред/пост установки/удаления ПО;

  • Возможно указания зависимостей для ПО.

Кроме того, при развёртывании ПО на основе пакетов, а не на основе SVN, вы гарантировано защищены от проблем с .svn-папками.


Структура проекта


И так, рассмотрим некоторый web-проект my-app, находящийся под контролем SVN, со следующей файловой структурой:

/|-config
| |-parameters.ini
|-htdocs
| |-index.php
|-libs
|-templates


Структура пакета


Для сборки пакетов нам потребуется некоторый набор файлов и скриптов, который мы разместим в папке .package, которую в свою очередь добавим в корень проекта. Структура этой папки будет выглядеть следующим образом:

/|-.structure
| |-DEBIAN
| | |-conffiles
| | |-control
| | |-postinst
| | |-postrm
| | |-preinst
| | |-prerm
| | |-templates
| |-etc
| |-var
| | |-log
| | |-www
|-package.xml
|-package.properties

По сути каталог .structure и есть наш будущий пакет. Подкаталог DEBIAN содержит служебные файлы, о которых будет сказано ниже. Все остальные каталоги полностью повторяют иерархию каталогов файловой системы ОС Debian Linux. Файлы package.xml и package.properties содержат сценарии и настройки для создания пакетов с помощью утилиты Apache Ant. Но обо всём по порядку.

Каталог Debian


Каталог DEBIAN содержит файлы настроек проекта и скрипты пред/пост установки/удаления.

Файл conffiles содержит список конфигурационных файлов, которые не должны быть перезаписаны во время установки:

/etc/my-app/parameters.ini

В нашем случае здесь будет указан файл настроек проекта.

Файл control содержит общую информацию о пакете:

Package: my-app
Version: {{{VERSION}}}
Section: user
Priority: optional
Architecture: all
Installed-Size: 0
Maintainer: Mikhail Krestjaninoff
Depends: nginx, php5-common (>= 5.2), php5-cli
Description: My application

Отдельного внимания в этом файле заслуживают пункты Version и Depends. Мы специально указываем в качестве версии пакета константу {{{VERSION}}}, так как в дальнейшем при сборке пакета заменим её актуальным значением. В списке зависимостей мы указали только nginx и php, однако, если Ваш проект использует какие-либо дополнительные пакеты (например модули php), Вы можете явно перечислить их, уменьшив тем самым риски неверной работы проекта при выкладке на боевой сервер.

Файл preinst содержит сценарий предустановки:

#!/bin/sh

В нашем случае никаких предварительных действий не требуется, по этому файл пуст.

Файл postinst содержит сценарий постустановки. В данном случае сценарий будет выставлять права, настраивать конфиг с помощью утилиты debconf и создавать директории для временных файлов:

#!/bin/sh

if [ configure = "$1" ]; then

# Set permissions
chown -R www-data:www-data /etc/my-app/
chmod -R 0664 /etc/my-app/

chown -R www-data:www-data /var/www/my-app/
chmod -R 0664 /var/www/my-app/

chown -R www-data:www-data /var/log/my-app/
chmod -R 0664 /var/log/my-app/


# Set up configuration file
. /usr/share/debconf/confmodule

db_input critical db/dsn || true
db_go
db_fset db/dsn seen false || true
db_get db/dsn || true
DSN=$RET

DSN=`echo "$DSN" | sed 's/\//\\\\\//g'`;
sed -i s/{{DSN}}/$DSN/ /etc/my-app/parameters.ini


# Create directories for temporary files
CACHE_DIR="/var/www/my-app/templates/cache"
COMPILED_DIR="/var/www/my-app/templates/compiled"

if [ ! -d $CACHE_DIR ]
then
mkdir -p $CACHE_DIR
chown -R www-data:www-data $CACHE_DIR
chmod -R 0664 $CACHE_DIR
fi

if [ ! -d $COMPILED_DIR ]
then
mkdir -p $COMPILED_DIR
chown -R www-data:www-data $COMPILED_DIR
chmod -R 0664 $COMPILED_DIR
fi

fi

При необходимости в этот файл так же могут быть добавлены сценарии для создания БД проекта. С обновлением БД задача обстоит несколько сложнее. Однако, можно попробовать частично разрешить её хранением в проекте 2-х sql-файлов: с полным набором команд для воспроизведения структуры / данных (первичная установка) и изменениями относительно предыдущего релиза (для обновления).

Файл prerm содержит сценарий предудаления. В данном случае сценарий будет удалять каталоги для временных файлов:

#!/bin/sh

if [ remove == "$1" -o purge == "$1" ];
then
# Remove directories for temporary files
CACHE_DIR="/var/www/my-app/templates/cache"
if [ -e $CACHE_DIR ]; then
rm -rf $CACHE_DIR
fi

COMPILED_DIR="/var/www/my-app/templates/compiled"
if [ -e $COMPILED_DIR ]; then
rm -rf $COMPILED_DIR
fi
fi

Файл templates содержит шаблоны полей для утилиты debconf:

Template: db/dsn
Type: string
Default: postgres://user@passwd:localhost/my-app
Description: Database Source Name. Example: postgres://user@passwd:localhost/my-app

Подробнее о содержимом каталога DEBIAN можно почитать на opennet.


Скрипты сборки пакета


Теперь, что бы создать полноценный пакет на основе нашего проекта, нам остаётся заполнить файловую структуру пакета (файловую систему с корнем в каталоге .structure данными). Для этого придётся создать небольшой набор сценариев, копирующих нужные данные из файловой системы проекта в файловую систему пакета. Я использовал для этой цели Apache Ant, в результате чего у меня получились файлы package.xml и package.properties.

Файл package.properties содержит в себе настройки для сборки пакета:

package.name=my-app
package.version=1.0.0

Основной сценарий сборки содержится в файле packege.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project basedir=".." default="prepare" name="package-builder">

  <property file="./.package/package.properties" />
  <property name="location" value="."/>
  <property name="package.structure" value="${location}/.package/.structure"/>
  <property name="package.source" value="${location}/.package/source"/>
  <property name="package.target" value="${location}/.package/target"/>

  <!-- Init project -->
  <target name="init">
    <mkdir dir="${package.source}" />
    <mkdir dir="${package.target}" />
  </target>

  <!-- Clean project -->
  <target name="clean">
    <delete dir="${package.source}" />
    <delete dir="${package.target}" />
  </target>

  <!-- Prepare project files for package -->
  <target depends="init" name="prepare">

    <copy toDir="${package.source}">
      <fileset dir="${package.structure}">
        <include name="**/*" />
        <exclude name=".svn" />
    </fileset>
    </copy>

    <mkdir dir="${package.source}/etc/${package.name}" />
    <copy toDir="${package.source}/etc/${package.name}">
      <fileset dir="config">
        <include name="**/*" />
        <exclude name=".svn" />
      </fileset>
    </copy>

    <mkdir dir="${package.source}/var/www/${package.name}/htdocs" />
    <copy toDir="${package.source}/var/www/${package.name}/htdocs">
      <fileset dir="htdocs">
        <include name="**/*" />
        <exclude name=".svn" />
      </fileset>
    </copy>

  <mkdir dir="${package.source}/var/www/${package.name}/libs" />
    <copy toDir="${package.source}/var/www/${package.name}/libs">
      <fileset dir="libs">
        <include name="**/*" />
        <exclude name=".svn" />
      </fileset>
    </copy>

    <mkdir dir="${package.source}/var/www/${package.name}/templates" />
    <copy toDir="${package.source}/var/www/${package.name}/templates">
      <fileset dir="templates">
        <include name="**/*" />
        <exclude name=".svn" />
        <exclude name="cache" />
        <exclude name="compiled" />
      </fileset>
    </copy>

  </target>

  <!-- Build packege -->
  <target depends="prepare" name="build">

    <!-- Change owner to root -->
    <exec executable="fakeroot" dir="${package.source}">
      <arg line="chown -R root:root ." />
    </exec>

    <!-- Allow execution for installation scripts -->
    <exec executable="fakeroot" dir="${package.source}/DEBIAN">
      <arg line="chmod 0755 preinst postinst prerm postrm" />
    </exec>

    <!-- Set package version -->
    <exec executable="sed">
      <arg line="-i s/{{{VERSION}}}/${package.version}/ ${package.source}/DEBIAN/control" />
    </exec>

    <!-- Build package -->
    <exec executable="dpkg-deb">
      <arg line="--build ${package.source} ${package.target}/${package.name}_${package.version}_all.deb" />
    </exec>

  </target>

</project>


* This source code was highlighted with Source Code Highlighter.

Сборка пакета


И так, создание каталога .package завершено. Теперь мы можем закоммитить его в SVN вместе с остальным проектом. Когда же нам понадобится собрать пакет (например, отдать релиз/метку для тестирования или выкладки в бой), будет достаточно перейти в каталог .package и выполнить команду ant -f package.xml build, которая создаст для нас в каталоге .package/target новый deb-пакет, готовый к использованию!

Read more...

Monday, November 23, 2009

Фонетический поиск

Если когда-нибудь перед Вами встанет задача реализации фонетического поиска, первым делом рекомендую прочитать довольно интересную статью с хабра. В статье довольно просто разжёван алгоритм soundex со своими недостатками, и представлена его доработка в виде алгоритма Daitch-Mokotoff. Однако, у обоих этих алгоритмов есть недочёт: абсолютно разным словам они могут ставить в соответствие один и тот-же отпечаток. К примеру, слова "хостинг" и "гостиница" будут равны.

Для обхода этой проблемы коллегой по цеху было предложено довольно интересное решение - использование метрики Левенштейна. Другими словами, если Вы организуете фонетический поиск и получаете два одинаковых отпечатка для двух слов - не торопитесь считать их одинаковыми: проверьте расстояние между ними с помощью PHP-функции levenshtein. Если расстояние окажется слишком большим, скорее всего Ваш фонетический алгоритм ошибся.
Read more...

Tuesday, August 18, 2009

Рекурсия в SQL-запросах

С помощью SQL запросов можно творить чудеса! :) И как оказалось среди этих чудес есть рекурсия. В данном посте я хочу рассказать, как реализовать обход дерева с помощью SQL-запроса.

Первым делом создадим таблицу, с которой будем далее работать:
CREATE TABLE "tree" (
  "id" INTEGER DEFAULT NEXTVAL('"tree_id_seq"'::TEXT) NOT NULL,
  "parent_id" INTEGER ,
  "name" VARCHAR(255) NOT NULL,
  
  PRIMARY KEY ("id"),
  FOREIGN KEY ("parent_id") REFERENCES "tree" ("id") ON DELETE CASCADE,
  UNIQUE( "name" )
);


* This source code was highlighted with Source Code Highlighter.

и добавим в неё данные:
INSERT INTO "tree" ("parent_id", "name") VALUES (NULL, 'root');
INSERT INTO "tree" ("parent_id", "name") VALUES (1, 'A');
INSERT INTO "tree" ("parent_id", "name") VALUES (1, 'B');
INSERT INTO "tree" ("parent_id", "name") VALUES (2, 'AA');
INSERT INTO "tree" ("parent_id", "name") VALUES (2, 'AB');
INSERT INTO "tree" ("parent_id", "name") VALUES (3, 'BA');
INSERT INTO "tree" ("parent_id", "name") VALUES (3, 'BB');


* This source code was highlighted with Source Code Highlighter.

Теперь для обхода полученного дерева нам достаточно выполнить следующий запрос:
-- Временная таблица для получения потомков
WITH RECURSIVE child( node_id ) AS
(
  -- Начальная выборка (элемент, с которого начинается выборка)
  VALUES( 1 )
  
    UNION ALL

  -- Рекурсивная выборка
  SELECT
    t."id"   AS "node_id"
  FROM
    "child" c
  JOIN
    "tree" t ON ( c."node_id" = t."parent_id" )
  WHERE
    t."parent_id" = node_id
)

-- Получение потомков
SELECT "node_id" FROM "child"


* This source code was highlighted with Source Code Highlighter.

Результатом работы запроса будут следующие данные:
  name_id
  1
  2
  3
  4
  5
  6
  7

Read more...