30 сентября 2009 г.

Проблемы при установке модуля BlogRewrites на сервер

На локальной машине всё работало, но перестало на сервере. Почему же? А потому что была лишняя заглавная буква в названиях.
  • Название модуля. Было BlogRewrite, надо Blogrewrite. Названия модулей можно делать только с одной заглавной буквой в начале, что бы потом не было неожиданных проблемм. Название модуля используется во всех названиях классов модуля, и кое-где в конфигах. Везде BlogRewrite надо заменить на Blogrewrite.
  • Название класса контроллера. Было Mage_BlogRewrite_Adminhtml_BlogRewriteController, надо Mage_Blogrewrite_Adminhtml_BlogrewriteController.
  • Имена файлов. В Windows файловая система регистро-независима, в линуксах - зависима (что за бред!). Поэтому нужно переименовать все файлы и папки где есть BlogRewrite в Blogrewrite. Например, app/code/local/Mage/BlogRewrite должно стать app/code/local/Mage/Blogrewrite.

После таких простых переименовываний (и столько потраченного времени), всё заработало.

Таким образом - во всех именах, модулей, контроллеров, действий, файлов, и т.д., нужно использовать только одну заглавную первую букву. Если использовать более одной заглавной буквы - когда-нибудь перестанет работать.

18 сентября 2009 г.

Интеграция Wordpress в Magento - ЧПУ, часть 2

После создания модуля, генерирующего реврайты для вордпресовских адресов, я подумал и решил, что такая идея не будет работать на 100%. Вот почему:
  • Кроме адресов постов, категорий, тэгов, и т.д., т.е. тех адресов, для которых мы делаем реврайты, есть другие адреса, такие, как action формы, отправляющей комментарий, адрес редактирования поста, адрес подписки на комментарии каждого поста. И это только те адреса, которые я заметил, при том все эти адреса ЧПУ, т.е. они просто не будут работать в Magento без реврайтов.
  • Некоторые страницы будут иметь так много постов, что они не будут влезать на одну страницу. Т.е. адреса "следующая страница с постами" не будут работать.
К счастью, я нашёл решение этих проблем, что не позволит выкинуть результат предыдущей работы с автосозданием реврайтов для блога :)

Тепер подробнее.

Проблема с ЧПУ

Сначала рассмотрим проблему со множеством ЧПУ.

Источник проблемы в том, что в настройках Wordpress указана ЧПУ схема адресов. Т.е. абсолютно все адреса, даже для которых мы не писали реврайты, будут ЧПУ. А зная, что такие адреса в Magento работать не будут, получается, что блог будет выдавать "страница не найдена" в неожиданных местах :)

Отсюда следует, что если поставить не-ЧПУ схему, все адреса станут не-ЧПУ, и начнут работать. Это и надо сделать.

Но мы же делали реврайты, т.е. некоторые адреса могут быть ЧПУ. Поэтому мы будем модифицировать шаблоны, выводящие адреса, меняя некоторые из них на ЧПУ.

Для этого я поступил не совсем красиво - вылез в файл index.php, что находится в самом корне Magento. Думаю, есть решение по-красивее, но я его не знаю :)

В этот файл я добавил следующий код (после включения хедеров вордпресса):
...
require_once $mageFilename;

define('WP_USE_THEMES', true);
require('wordpress/wp-blog-header.php');

// bof Blogrewrite module code
$oldPermalink = get_option('permalink_structure');

function ChangePermalink() {
    global $wp_rewrite, $oldPermalink;
    $wp_rewrite->set_permalink_structure("/%year%/%monthnum%/%postname%/");
}

function RestorePermalink() {
    global $wp_rewrite, $oldPermalink;
    $wp_rewrite->set_permalink_structure($oldPermalink);
}
// eof Blogrewrite module code

#Varien_Profiler::enable();
...
Т.е. сначала я запоминаю текущую схему адресов. Вообще-то это достаточно бесполезное занятие т.к. схема должна быть только "по-умолчанию", т.е. $oldPermalink == ''. Т.е. вместо запоминания текущей схемы можно использовать пустую строку. Сделал это, наверное, на будущее...

Затем я добавил две функции. Одна меняет схему адресов на ЧПУ (что автоматически меняет схемы адресов категорий, тэгов и т.д.), другая восставнавливает предыдущую схему (не-ЧПУ, пустая строка).

По вкусу можно добавить вызов RestorePermalink(); в конец файла :) Наверное так будет лучше.

Теперь, когда нам нужна ЧПУ схема, мы вызываем ChangePermalink(). Это мы будем делать, когда будем генерировать реврайты (т.к. для получения вордпресовских ссылок на посты/категории/и т.д. мы используем специальные вордпресовские функции, которые возвращают адреса, соответствующие текущей схеме). Так же мы будем вызывать эту функцию во время формирования блока с ссылками на посты, категории, тэги (который находится слева или справа). И последнее место - вывод контента - содержимого постов, список постов категории и т.д.

После того, как мы поработаем с ЧПУ, вызываем RestorePermalink() - восстанавливаем изменённую схему адресов.

Итак, файлы и код, которые используют эти функции:
  • Mage_Blogrewrite_Adminhtml_BlogrewriteController::makerewritesAction()

    Где-нибудь вначале метода нужно вызвать ChangePermalink, где-нибудь в конце - RestorePermalink.
  • Шаблон блока со списком постов/категорий/и т.д. находится в файле app/design/frontend/default/sunnyD/template/blog/menu.phtml. Т.к. мы создали реврайты для всех ссылок, которые выводятся в этом файле, то вызываем ChangePermalink в начале файла, RestorePermalink - в конце.
  • wordpress/wp-content/themes/Wordpress-theme/magento/magento.php

    Это файл-шаблон, используемый для отображения "контента", содержимого блога - списка постов, отдельного поста. Выводится по-середине страницы :)

    Те адреса, для которых мы писали реврайты, мы выводим с включёнными ЧПУ. Остальные адреса - с выключенными ЧПУ.

    Например, мы знаем, что ссылка на пост может быть ЧПУ. Поэтому код может выглядеть так:
    <?php wp_reset_query(); ?>
    
    <?php if (have_posts()) : ?>
    
      <?php while (have_posts()) : the_post(); ?>
    
       <div class="post" id="post-<?php the_ID(); ?>">
                    <?php ChangePermalink(); ?>
        <h2><a href="<?php the_permalink() ?>" rel="bookmark" title="Permanent Link to <?php the_title_attribute(); ?>"><?php the_title(); ?></a></h2>
                    <?php RestorePermalink(); ?>
    ...

Pagination

Ремонтирование разбиения на страницы оказалось проще. Когда мы получаем список категорий, постов, тэгов, месячных архивов, мы можем узнать количество постов, удовлетворяющих выбранному критерию. Ещё мы можем узнать выводимое количество постов на странице:
$postsPerPage = get_option('posts_per_page');
А, зная это, мы легко можем подсчитать количество страниц, необходимое для отображения всех постов, удовлетворяющих выбранному критерию, и добавить соответствующих реврайтов:
$pagesCount = ceil($totalPostsCount / $postsPerPage);
Вот как был модифицирован код:
  • Для категорий:
    // Update rewrites for categories
    $cats = get_categories();
    foreach ($cats as $_c) {
        $this->_makeRewrite('blog_cat/' . $_c->term_id,
            trim(substr(get_category_link($_c->term_id), strlen($baseUrl)), '/'),
            'blog/index/index/cat/' . $_c->term_id);
    
        // Also add rewrites for other pages, e.g. page/2, page/3 etc
        for ($i = 2; $i <= ceil($_c->count / $postsPerPage); $i++) {
            $pages++;
            $this->_makeRewrite('blog_cat/' . $_c->term_id . '/page/' . $i,
                trim(substr(get_category_link($_c->term_id), strlen($baseUrl)), '/') . "/page/$i",
                'blog/index/index/cat/' . $_c->term_id . '/page/' . $i);
        }
    }
  • Для тэгов:
    // Update rewrites for tags
    $tags = get_tags();
    foreach ($tags as $_t) {
        $this->_makeRewrite('blog_tag/' . $_t->term_id,
            trim(substr(get_tag_link($_t->term_id), strlen($baseUrl)), '/'),
            'blog/index/index/tag/' . $_t->term_id);
    
        // Also add rewrites for other pages, e.g. page/2, page/3 etc
        for ($i = 2; $i <= ceil($_t->count / $postsPerPage); $i++) {
            $pages++;
            $this->_makeRewrite('blog_tag/' . $_t->term_id . '/page/' . $i,
                trim(substr(get_category_link($_t->term_id), strlen($baseUrl)), '/') . "/page/$i",
                'blog/index/index/tag/' . $_t->term_id . '/page/' . $i);
        }
    }
  • Для месячных архивов:
    $query = "SELECT DISTINCT YEAR(post_date) AS `year`, MONTH(post_date) AS `month`, count(ID) as posts FROM $wpdb->posts $join $where GROUP BY YEAR(post_date), MONTH(post_date) ORDER BY post_date DESC $limit";
    $archive = $wpdb->get_results($query);
    if ($archive) {
        $afterafter = $after;
        foreach ((array) $archive as $_a) {
            $id = sprintf("%4d%02d", $_a->year, $_a->month);
    
            $this->_makeRewrite('blog_month/' . $id,
                trim(substr(get_month_link($_a->year, $_a->month), strlen($baseUrl)), '/'),
                'blog/index/index/m/' . $id);
    
            // Also add rewrites for other pages, e.g. page/2, page/3 etc
            for ($i = 2; $i <= ceil($_a->posts / $postsPerPage); $i++) {
                $pages++;
                $this->_makeRewrite('blog_month/' . $id . '/page/' . $i,
                    trim(substr(get_month_link($_a->year, $_a->month), strlen($baseUrl)), '/') . "/page/$i",
                    'blog/index/index/m/' . $id . '/page/' . $i);
            }
        }
    }

Итог

Итак:
  • Выставляем схему адресов в Wordpress "по-умолчанию". Это гарантирует нам, что любые ссылки, сгенерированне вордпрессом будут работать сами по себе.
  • Во время генерации реврайтов для постов/категорий/тэгов/месячных архивов устанавливаем схему адресов в ЧПУ. Генерируем реврайты, не забываем про "многостраничные страницы". Благодаря реврайтам пользователь сможет зайти на блог по ЧПУ (это относится к постам, категориям, тэгам, месячным архивам).
  • Модифицируем шаблоны, генерирующие вордпресовские адреса, включая ЧПУ схему для тех элементов, для которых мы создали реврайт на предыдущем шаге, выключая ЧПУ для тех элементов, для которых реврайтов нет
В итоге в основном пользователь будет видеть ЧПУ. Остальные адреса, для которых мы не сделали реврайтов, будут не-ЧПУ. Т.е. теперь блог будет 100% рабочий, разве что не 100% ЧПУ.

PS. Ну и недостаток, который есть у данной схемы: смена структуры адресов не самая быстрая операция. При этом она может выполняться несколько десятков раз (при отображении 20 постов на одной странице смен будет 20 * 3 = 60). Ну, Magento сама по себе весьма не скоростная, будем надеяться эти новые смены никто не заметит :)

PPS. Ещё я добавил загрузку реврайта по Request Path, если реврайт не найден по Id Path. Это сделано для того, что бы код без проблем заработал в магазине, где уже есть некоторые реврайты, добавленные вручную.
protected function _makeRewrite($idPath, $requestPath, $targetPath, $permamentRedirect = false) {
    $this->_urlRewrite->loadByIdPath($idPath);
    if (!$this->_urlRewrite->getId())
        $this->_urlRewrite->loadByRequestPath($requestPath);
    ...
}
Код попытается найти реврайт по Id Path, скорее всего не найдёт (Id Path заполнялся вручную и не совпадает с генерируемым нашим модулем). Если затем не попробовать загрузить реврайт по Request Path, Magento упадёт при попытке создать новый реврайт с тем же Request Path, который уже есть в системе (Request Path однозначно идентифицирует реврайт, это уникальное поле). Добавляя дополнительную загрузку по Request Path мы уберегаем себя от подобных падений - вместо падения Magento загрузит уже существующий, добавленный вручную реврайт, и обновит его.

16 сентября 2009 г.

Простой бэкап, Python + DropBox, часть 2

Новая версия скрипта для бэкапа. Или другая версия...

Логика работы с файлами-списками не поменялась. Поменялся только параметр скрипта - теперь это имя архива без расширения, скрипт сам добавит .tar.gz.

Новое:
  • Создание архива переместилось из внешнего архиватора внутрь скрипта. Создаётся tar.gz архив.
  • Все пути теперь системо-зависимы, т.е. скрипт должен работать на линуксах тоже
Кроме приятного нового, повились неприятный минус - скрипт стал жрать кучу памяти.

Сделать сжатый архив с помощью модуля tarfile не вышло. Архив создаётся и сжимается, но внтури архива пути почему-то съезжают.

Т.е. нужно создать архив d:\dropbox\my dropbox\backup\backup.tar.gz, он там появляется. Но если открыть этот gz архив, tar архив будет глубоко внутри по пути dropbox\my dropbox\backup\backup.tar. Т.е. полный путь до данных получается такой: d:\dropbox\my dropbox\backup\backup.tar.gz\dropbox\my dropbox\backup\backup.tar\.... В принципе с этим можно жить, но некрасиво...

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

Надо бы ещё покопать в эту сторону и найти лучшее решение...
import sys
import os
import os.path
import tarfile
import time
import gzip

__author__="race1"
__date__ ="$09.09.2009 19:55:06$"

if __name__ == "__main__":
    if (len(sys.argv) < 2):
        print("Usage: backup.py ")
        print("E.g.: \"backup.py d:\\dropbox\\my dropbox\\backup\"")
        exit(1)

    RootFoldersFile = "tobackup.lst"
    IgnoreFoldersFile = "ignore.lst"
    ExtraFile = "extra.lst"
    OutputArchive = sys.argv[1]
    OutputArchiveTar = OutputArchive + ".tar"
    OutputArchiveTarGz = OutputArchive + ".tar.gz"

    # Only mandatory file is RootFoldersFile, check that it exists
    if (not os.path.isfile(RootFoldersFile)):
        print("%s doesn't exist" % RootFoldersFile)
        exit(1)

    # Read root folders from file
    rootFolders = [i.strip() for i in open(RootFoldersFile, "r").readlines()]

    # Read list of folders that need to be igonred (if specified)
    ignoreList = []
    if (os.path.isfile(IgnoreFoldersFile)):
        ignoreList = [i.strip().lower() for i in open(IgnoreFoldersFile, "r").readlines()]

    # Delete old archive if exists
    if (os.path.isfile(OutputArchiveTar)):
        os.unlink(OutputArchiveTar)

    if (os.path.isfile(OutputArchiveTarGz)):
        os.unlink(OutputArchiveTarGz)

    filesCount = 0

    # Open output archive for writing (we'll gzip it later)
    try:
        tar = tarfile.open(OutputArchiveTar, "w")
    except IOError as err:
        print(err)
        exit(1)

    lastTime = time.time()
    for rootFolder in rootFolders:
        for root, dirs, files in os.walk(rootFolder):
            for file in files:
                tar.add(os.path.join(root, file))
                filesCount += 1

                # Show number of processed files each second
                if (time.time() - lastTime >= 1):
                    print("%d files..." % (filesCount))
                    lastTime = time.time()

            # Skip ignored folders
            for dir in dirs:                
                if (dir.lower() in ignoreList or (os.path.join(root, dir).lower() in ignoreList)):
                    dirs.remove(dir)

    # Add some files from extra files list
    if (os.path.isfile(ExtraFile)):
        for file in [i.strip() for i in open(ExtraFile, "r").readlines()]:
            tar.add(file)
            
    tar.close()

    print("Compressing...")

    # Gzip output archive
    try:
        gz = gzip.open(OutputArchiveTarGz, "wb")
    except IOError as err:
        print(err)
        exit(1)
    
    gz.write(open(OutputArchiveTar, "rb").read())
    gz.close()

    os.unlink(OutputArchiveTar)
        
    print("Output: %s, %d file(s), %d bytes" % (OutputArchiveTarGz, filesCount, os.path.getsize(OutputArchiveTarGz)))
    print("Done")

15 сентября 2009 г.

Интеграция Wordpress в Magento - ЧПУ

Уже давно у нас в в Magento был внедрён Wordpress, специальным модулем-плагином для Magento. Но, используя такой метод внедрения, есть проблема с адресами в Wordpress - можно использовать только простые адреса, вида ?p=<номер поста>, ?cat=<категория> и т.д. Это связано с тем, что обработку всех адресов берёт на себя Magento, т.е. все адреса должны быть в специальном формате - <module name>/<controller name>/<controller action>[/<param1>/<value1>/<param2>/<value2>...]. Если же в Wordpress включить ЧПУ адреса, например, вида <year>/<month>/<post title>, такой адрес придёт в Magento, которая попытается найти модуль <year>, в нём найти контроллер <month>, и выполнить метод <post title>. Естественно ничего такого в Magento нет, поэтому будет ошибка "страница не найдена".

Тем не менее, в Magento можно использовать ЧПУ. Для этого надо писать реврайты. Magento сама создаёт по реврайту на каждый продукт и категорию. Rewrite говорит, какой произвольный адрес перенаправить на какой корректный "Magento-адрес". Например, адрес /benq-2220hd нужно заменить адресом /catalog/product/view/id/496.

Реврайты бывают "внтуренние" и "с редиректом". Внутренние работают внутри :) Т.е. пользователь ввёл один адрес, он видит его в адресной строке, а Magento внутри себя вызывает другой адрес для обработки (более точно - другое действие другого контроллера). Реврайт с редиректом наоборот сразу бросается в глаза - браузер пользователя перенаправляется с одного адреса на другой.

Но даже с реврайтами есть проблема. Модуль, использующийся для интеграции Wordpress в Magento, не позволял использовать ЧПУ. Я его немного переделал, и теперь стало возможным написать адрес вида blog/index/index/param1/value1/param2/value2, и эти параметры будут переданы Wordpress, который на их основе выполнит запрос. Т.е. раньше приходилось писать реврайты с редиректом на не-ЧПУ адрес (например, на /blog/?p=123), сейчас же можно писать "внутренние" реврайты. А подсмотреть, какие нужно передавать параметры, можно в документации на Wordpress. Т.е., если перейти по адресу /blog/index/index/p/123, Wordpress покажет пост с id=123. Кроме p есть другие параметры, cat - для категорий, tag - для тэгов, m - для архивов, и т.д.

Сначала мы писали реврайты для постов вручную. Но это очень неудобно, медленно, не расширяемо и т.д., и я подумал что это дело можно автоматизировать. Сейчас будем этим заниматься :)

Мы напишем модуль, который сам будет создавать реврайты на все посты/категории/тэги/архивы блога/rss. Делать это будет специальная волшебная кнопка "пыщь" в админке :)

Создание модуля

Создадим модуль Blogrewrite в пространстве имён Mage. Что бы сделать свою страницу в админке нужно иметь такой config.xml:
<?xml version="1.0"?>
<config>
    <global>
        <helpers>
            <Blogrewrite>
                <class>Mage_Blogrewrite_Helper</class>
            </Blogrewrite>
        </helpers>
    </global>

    <admin>
        <routers>
            <Blogrewrite>
                <use>admin</use>
                <args>
                    <module>Mage_Blogrewrite</module>
                    <frontName>Blogrewrite</frontName>
                </args>
            </Blogrewrite>
        </routers>
    </admin>

    <adminhtml>
        <menu>
            <catalog module="catalog">

            <children>
                <Blogrewrite translate="title" module="Blogrewrite">
                    <title>Blog Rewrite</title>
                    <action>Blogrewrite/adminhtml_Blogrewrite</action>
                </Blogrewrite>
            </children>

            </catalog>
        </menu>
    </adminhtml>
</config>
В секции admin мы говорим, что наш модуль будет доступен по адресу Blogrewrite (www.example.com/Blogrewrite). Т.о. контроллер нашего модуля - Mage_Blogrewrite_Adminhtml_BlogrewriteController.

В секции adminhtml мы добавляем новый пункт меню, в меню Catalog, и говорим, какое действие нужно выполнить, когда пользователь нажмёт на этом пункте меню - модуль Blogrewrite, контроллер Mage_Blogrewrite_Adminhtml_BlogrewriteController, действие по-умолчанию - index.

Действие index всего лишь отображает блок Mage_Blogrewrite_Block_Adminhtml_Blogrewrite:
public function indexAction() {
    $this->_initAction();
    $this->getLayout()->getBlock('head')
         ->setCanLoadRulesJs(true);
    $this->_addContent($this->getLayout()->createBlock('Blogrewrite/adminhtml_Blogrewrite')
         ->setCanLoadRulesJs(true));
    $this->renderLayout();
}
Этот блок находится в файле Block/Adminhtml/Blogrewrite.php:
<?php
class Mage_Blogrewrite_Block_Adminhtml_Blogrewrite extends Mage_Adminhtml_Block_Template {
    public function __construct() {
        parent::__construct();
        $this->setTemplate('Blogrewrite/index.phtml');
    }
}
Блок всего лишь выводит шаблонный файл index.phtml, который должен быть в app/design/adminhtml/default/default/template/Blogrewrite/index.phtml. В шаблоне - одна кнопка, которая вызывает действие makerewrites нашего контроллера:
<div class="content-header">
    <table cellspacing="0">
        <tr>
            <td>
                <h3 class="head-dashboard"><?php echo $this->__('Blog Rewrite') ?></h3>
            </td>
        </tr>
    </table>
</div>

<form action="<?php print $this->getUrl('*/*/makerewrites'); ?>">
    <button type="submit">Make Blog Rewrites</button>
</form>
Итак, модуль готов, он виден в админке, форма с кнопкой отображается. Надо делать собственно добавление реврайтов

Создаём rewrites

Как создать реврайт? Для этого есть модель core/url_rewrite, мы можем получить её так:
Mage::getModel('core/url_rewrite');
Можно загрузить данные из базы по некоторым параметрам, нам вполне хватит загрузки по Id Path:
Mage::getModel('core/url_rewrite')->loadByIdPath($idPath);
Если запись с таким Id Path есть в базе, она загрузится в модель, иначе нет :)

После того как мы загрузили данные из базы (или если данных нет), нужно задать параметры реврайта:
Mage::getModel('core/url_rewrite')
    ->loadByIdPath($idPath)
    ->setIdPath($idPath)
    ->setRequestPath($requestPath)
    ->setTargetPath($targetPath)
    ->setDescription('Automagically generated, Blogrewrite module')
    ->setIsSystem(0);
И, наконец, сохраняем реврайт:
->save();
Для удобства вынесем создание/обновление реврайта в отдельную функцию:
protected function _makeRewrite($idPath, $requestPath, $targetPath, $permamentRedirect = false) {
    $this->_urlRewrite->loadByIdPath($idPath);
    $this->_urlRewrite->setIdPath($idPath)
          ->setRequestPath($requestPath)
          ->setTargetPath($targetPath)
          ->setOptions($permamentRedirect ? 'RP' : '')
          ->setDescription('Automagically generated, Blogrewrite module')
          ->setIsSystem(0);
    $this->_urlRewrite->save();
}
Ещё я добавил новый параметр $permanentRedirect, который, соответственно, делает реврайт редиректом (по-умолчанию реврайты создаются "внутренними").

Если сохранить реврайт так, как выше, без указания магазина (Store Id), он добавится для всех магазинов. Это хорошо :)

makerewritesAction

Создание реврайтов будет происходить в действии makerewrites контроллера Mage_Blogrewrite_Adminhtml_BlogrewriteController:
<?php
class Mage_Blogrewrite_Adminhtml_BlogrewriteController extends Mage_Adminhtml_Controller_Action {
...
    public function makerewritesAction() {
...
    }
...
}
?>
Что бы создать реврайт, нужно знать, для чего его создавать. Т.е. нужно перебрать все посты, категории, архивы, и т.д., узнать их адреса, и создать по реврайту, перенаправляя эти адреса на правильные.

Сейчас будет в основном Wordpress часть.

Wordpress

Мы будем пользоваться функциями Wordpress для получения ЧПУ постов/категорий и т.д. Отсюда следует два замечения:
  • Пермалинки в настройках Wordpress должны быть настроены на ЧПУ. Иначе мы будем делать ревайты на адреса вида ?p=123. Нам этого не надо, эти адреса и так работают.

    С другой стороны, схема ЧПУ может быть любой. Всё что нужно сделать после смены вида пермалинков - нажать кнопку "пыщь" на странице нашего модуля :)
  • Все функции, возвращающие адреса, возвращают полные, абсолютные адреса. Реврайты же нужно создавать на относительные адреса (иначе они просто не будут работать). Поэтому нужно удалять доменную часть. Для этого получаем базовый адрес Magento - вызываем метод Mage::getBaseUrl(Mage_Core_Model_Store::URL_TYPE_WEB), и получаем, например, http://www.example.com/. Далее при получении адреса для реврайта, будем отсекать эту часть:
    trim(substr(get_permalink(), strlen($baseUrl)), '/')
    Кроме того, нужно убрать последний слэш, без него реврайты тоже не сработают. trim это делает.

Перебор постов

Для перебора постов можно воспользоваться "циклом", loop. Но по-умолчанию Wordpress загружает лишь несколько постов в цикл. А нам надо все. Это делается вызовом метода query_posts. После этого запускаем цикл, перебирающий все посты. На каждой итерации добавляем/обновляем реврайт:
// Query all published posts
query_posts(array('post_status' => 'publish', 'showposts' => -1));
while (have_posts()) {
    the_post();

    // get_permalink returns full absolute url. We need to remove domain info.
    // Also remove trailing slash, it's important.
    // E.g. was http://www.example.com/blog/2009/02/title
    //   become                                             blog/2009/02/title
    $this->_makeRewrite('blog/' . get_the_ID(),
        trim(substr(get_permalink(), strlen($blogUrl) - 4), '/'),
        'blog/index/index/p/' . get_the_ID());

    $posts++;
}

Перебор категорий

Для получения всех категорий достаточно вызвать функцию get_categories. Для получения адреса категории есть функция get_category_link.
// Update rewrites for categories
$cats = get_categories();
foreach ($cats as $_c) {
    $this->_makeRewrite('blog_cat/' . $_c->term_id,
        trim(substr(get_category_link($_c->term_id), strlen($blogUrl) - 4), '/'),
        'blog/index/index/cat/' . $_c->term_id);
}

Перебор тэгов

Тэги так же доступны вызовом всего одной функции get_tags, а для получения адреса есть функция get_tag_url:
// Update rewrites for tags
$tags = get_tags();
foreach ($tags as $_t) {
    $this->_makeRewrite('blog_tag/' . $_t->term_id,
        trim(substr(get_tag_link($_t->term_id), strlen($blogUrl) - 4), '/'),
        'blog/index/index/tag/' . $_t->term_id);
}

Перебор архивов

Я посмотрел, как архивы выводятся у нас на сайте. Это задаётся в шаблонном файле app/design/frontend/default/sunnyD/template/blog/menu.phtml, а именно - вызов функции wp_get_archives('type=monthly'). Эта функция возвращает уже отформатированный html. К сожалению, нет нормального способа выбрать нужные мне ссылки. Пришлось скопировать код этой функции к себе и немного его переделать:
// Update rewrites for archive (from Wordpress core file wp-includes\general-template.php,
// function wp_get_archives, from line 753)
global $wpdb;
$defaults = array(
    'type' => 'monthly', 'limit' => '',
    'format' => 'html', 'before' => '',
    'after' => '', 'show_post_count' => false,
    'echo' => 1
);
$r = wp_parse_args('', $defaults);
$where = apply_filters('getarchives_where', "WHERE post_type = 'post' AND post_status = 'publish'", $r);
$join = apply_filters('getarchives_join', "", $r);
$query = "SELECT DISTINCT YEAR(post_date) AS `year`, MONTH(post_date) AS `month`, count(ID) as posts FROM $wpdb->posts $join $where GROUP BY YEAR(post_date), MONTH(post_date) ORDER BY post_date DESC $limit";
$archive = $wpdb->get_results($query);
if ($archive) {
    $afterafter = $after;
    foreach ((array) $archive as $_a) {
        $this->_makeRewrite('blog_month/' . $_a->year . $_a->month,
            trim(substr(get_month_link($_a->year, $_a->month), strlen($blogUrl) - 4), '/'),
            'blog/index/index/m/' . $_a->year . $_a->month);
    }
}

RSS

Последние ссылки без реврайтов - подписка на новости. Их две - подписка на новые посты, и новые комментарии.
$feeds = trim(substr(get_bloginfo('rss2_url'), strlen($baseUrl)), '/');
$feedsComments = trim(substr(get_bloginfo('comments_rss2_url'), strlen($baseUrl)), '/');

$this->_makeRewrite('blog_feeds', $feeds, 'blog/index/index/feed/rss2');
$this->_makeRewrite('blog_comments_feeds', $feedsComments, 'blog/index/index/feed/comments-rss2');

Заключение

Итак, мы сделали по реврайту на посты, категории, тэги, месячные архивы и rss. Теперь пользователи будут видеть ЧПУ, относящиеся к блогу, в адресной строке браузера. Так же адреса, связанные с Wordpress, выводимые на странице магазина, будут ЧПУ (т.к. это указано в настройках Wordpress).

Но всё же есть одна неприятность. Страницы просмотра категории, или тэга, могут иметь несколько страниц. Например, blog/my-category/page/2. Такой адрес выдаст страницу "не найдено" :(

11 сентября 2009 г.

Magento, подписка на новости во время чекаута

Задача - добавить галочку "Получать новости" к одному из шагов чекаута (checkout - "проход через кассу").



Сразу стало ясно, что придётся писать новый модуль, т.к. модификации дизайнерских файлов здесь не хватит. Как можно добавить своё поле к какому-либо шагу чекаута, можно подсмотреть у модулей Desitex Checkoutnewsletter (он добавляет галочку "подписать на новости" во второй шаг, где надо указать billing address) и у Biebersdorf CustomerOrderComment (он добавляет поле для добавления комментария в последний шаг - страницу подтверждения заказа).

Опускаю процесс копания в указанных модулях и поиска решения :) В итоге, что бы всё получилось, нужно:
  • Модифицировать дизайнерский файл, который рисует нужную страницу чекаута - добавить туда галочку;
  • Каким-либо образом подписаться на событие "сохранение страницы чекаута", что бы:
  • Запомнить состояние галочки в текущей сессии
  • Обработать событие "оформление заказа", возникающее когда пользователь уже оформил заказ, перед тем как сайт перенаправит его на сайт для оплаты (например, PayPal, AlliedWallet). Здесь надо извлечь сохранённое значение из сессии и подписать пользователя на рассылку, если он этого хочет.

Создание модуля

Прежде всего создадим модуль. Пусть он будет в пространстве имён Mage, а называться будет NewsletterSubscribe.

Сначала нужно сказать Magento, что наш модуль есть - создаём файл Mage_NewsletterSubscribe.xml в папке app/etc/modules:
<?xml version="1.0"?>
<config>
    <modules>
        <Mage_NewsletterSubscribe>
            <active>true</active>
            <codePool>local</codePool>
        </Mage_NewsletterSubscribe>
    </modules>
</config>
Согласно XML, модуль активен и находится в пуле local.

Далее создаём папку где будет находится новый модуль - app/code/local/Mage/NewsletterSubscribe, и файл конфигурации config.xml в папке app/code/local/Mage/NewsletterSubscribe/etc:
<?xml version="1.0"?>
<config>
    <global>
        <helpers>
            <newslettersubscribe>
                <class>Mage_NewsletterSubscribe_Helper</class>
            </newslettersubscribe>
        </helpers>
    </global>
</config>
Без хелпера модуль не будет работать как надо, а будет вместо этого падать. Поэтому дадим Magento хелпер, пусть и пустой - файл Data.php в папке Helper:
<?php

class Mage_NewsletterSubscribe_Helper_Data extends Mage_Core_Helper_Abstract {

}

Модификация страницы чекаута

Файл, рисующий нужную страницу чекаута - app\design\frontend\default\sunnyD\template\checkout\onepage\payment\methods.phtml. Добавляем галочку:
...
<?php /* bof Subscribe for newsletter checkbox */ ?>
<dt>Join Our Mailing List</dt>
<dd>
    <input type="checkbox" name="NewsletterSubscribe" id="NewsletterSubscribe" checked="checked" />
    <label for="NewsletterSubscribe"><?php echo Mage::helper('newslettersubscribe')->__('I would like to receive the Century Supplements newsletter') ?></label>
</dd>
<?php /* eof Subscribe for newsletter checkbox */ ?>
...
Да, теперь мы видим нашу галочку на странице выбора метода оплаты. Но почему же она неактивна? А потому что она сделана неактивной JS кодом, расположенным в конце файла:
<script type="text/javascript">payment.init();</script>
Не разбирался зачем он нужен, но в данном случае он делает неактивными все тэги <input>. Выходит, нам надо активировать нашу галочку после выполнения этого кода:
<script type="text/javascript">payment.init();</script>

<script type="text/javascript">$('NewsletterSubscribe').disabled = false;</script>
Теперь галочка стала активна, идём дальше.

Событие "сохранение страницы чекаута"

Я подсмотрел как это делает Checkoutnewsletter. В конфигурационном файле модуля есть строки, которые видимо перехватывают действия, связанные со всем чекаутом, всеми его страницами:
<?xml version="1.0"?>
<config>
    ...
    <global>
        <models>
         <checkout>
          <rewrite>
           <type_onepage>Desitex_Checkoutnewsletter_Model_Checkout_Type_Onepage</type_onepage>
          </rewrite>
         </checkout>
    ...
</config>
Стандартный класс Mage_Checkout_Model_Type_Onepage заменяется классом модуля Desitex_Checkoutnewsletter_Model_Checkout_Type_Onepage (который наследуется от оригинального класса Mage_Checkout_Model_Type_Onepage). В этом классе переопределён всего один метод:
<?php

class Desitex_Checkoutnewsletter_Model_Checkout_Type_Onepage extends Mage_Checkout_Model_Type_Onepage
{
    public function saveBilling($data, $customerAddressId)
    {
        if (isset($data['is_subscribed']) && !empty($data['is_subscribed'])){
            $this->getCheckout()->setCustomerIsSubscribed(1);
        }
        else {
            $this->getCheckout()->setCustomerIsSubscribed(0);
        }
        return parent::saveBilling($data, $customerAddressId);
    }
}
Очевидно, действие saveBilling возникает когда пользователь переходит со страницы ввода billing address (нажимает кнопку Continue). Здесь модуль сохраняет значение своей галочки "подписываться ли на новости" в текущей сессии (или чекауте...). После этого вызывает оригинальный метод стандартного класса.

Мы поступим подобным образом - сделаем класс, отнаследуем его от стандартного, и переопределяем только один метод, возникающий при сохранении формы на нашей странице. Метод будет сохранять значение нашей галочки. Класс поместим в файл Model/Onepage.php. :
<?php

class Mage_NewsletterSubscribe_Model_Onepage extends Mage_Checkout_Model_Type_Onepage {
    public function savePayment($data) {
        if (isset($_POST['NewsletterSubscribe'])){
            $this->getCheckout()->setNewsletterSubscribe((bool) $_POST['NewsletterSubscribe']);
        }
        else {
            $this->getCheckout()->setNewsletterSubscribe(false);
        }
        return parent::savePayment($data);
    }
}

Здесь меня немного настиг ступор. Значение галочки находится среди значений формы, но из текущего места у меня нет доступа к этим переменным. Т.е. доступа к объекту Magento Request, хранящему все GET и POST переменные. Доступны разные интересные объекты типа Quote, Checkout и т.д., с разным интересными данными, но не значениями формы. Я почти отчаялся, соображая что переписывать код ядра очень плохо, но потом вспомнил, что это Php, а значит в любом месте доступны переменные $_GET и $_POST :) Проблема была решена.

Теперь скажем Magento, что бы вместо стандартного класса брал наш. Редактируем etc/config.xml:
<?xml version="1.0"?>
<config>
    <global>
        <models>
            <checkout>
                <rewrite>
                    <type_onepage>Mage_NewsletterSubscribe_Model_Onepage</type_onepage>
                </rewrite>
            </checkout>
        </models>
    ...
</config>

Здесь готово. Только видимо есть одно ограничение - переопределить стандартный класс может только один модуль. Мой метод не вызывался, пока я не убрал переопределение у модуля Checkoutnewsletter. Т.е. это не обычное событие, на которое может подписаться произвольное количество слушателей. Потенциальные трудноотлаживаемые проблемы в будущем :(

Событие "оформление заказа"

В отличие от предыдущего "события", оформление заказа это "настоящее" событие checkout_type_onepage_save_order. Что бы подписаться на него нужно изменить конфиг модуля etc/config.xml:
<?xml version="1.0"?>
<config>
    <global>
        ...
        <events>
            <checkout_type_onepage_save_order>
                <observers>
                    <mage_newslettersubscribe_observer>
                        <type>singleton</type>
                        <class>newslettersubscribe/observer</class>
                        <method>onOrderSave</method>
                    </mage_newslettersubscribe_observer>
                </observers>
            </checkout_type_onepage_save_order>
        </events>
    </global>
</config>
Здесь мы указали какой метод у какого класса вызвать (Mage_NewsletterSubscribe_Model_Observer::onOrderSave), когда пользователь оформит заказ. Теперь создадим этот класс и метод - файл /Model/Observer.php:
<?php
class Mage_NewsletterSubscribe_Model_Observer extends Mage_Core_Helper_Abstract {
    public function onOrderSave($observer) {
        $isCustomerSubscribed = (bool) Mage::getSingleton('checkout/session')->getNewsletterSubscribe();
        if ($isCustomerSubscribed) {
            $quote = $observer->getEvent()->getQuote();
            $session = Mage::getSingleton('core/session');
            try {
                $status = Mage::getModel('newsletter/subscriber')->subscribe($quote->getBillingAddress()->getEmail());
                if ($status == Mage_Newsletter_Model_Subscriber::STATUS_NOT_ACTIVE){
                    $session->addSuccess(Mage::helper('checkoutnewsletter')->__('Confirmation request has been sent regarding your newsletter subscription'));
                }
            }
            catch (Mage_Core_Exception $e) {
                $session->addException($e, Mage::helper('checkoutnewsletter')->__('There was a problem with the newsletter subscription: %s', $e->getMessage()));
            }
            catch (Exception $e) {
                $session->addException($e, Mage::helper('checkoutnewsletter')->__('There was a problem with the newsletter subscription'));
            }
        }

        return $this;
    }
}
Этот код я взял из модуля Checkoutnewsletter, только переделал его что бы он работал :) К счастью в Magento есть класс, позволяющий подписывать пользователей на новости. По-сути всё что нужно сделать - вызвать Mage::getModel('newsletter/subscriber')->subscribe(<user email>);

Итог

В итоге получился небольшой модуль, выполняющий поставленную задачу :)

10 сентября 2009 г.

Джаббер чат на веб-странице

Прочитав пост на хабре про онлайн чат для сайта через джаббер, мне стало интересно - а как оно работает и как такое можно сделать самому, без готовых приложений. В итоге у меня получилась очень простая заготовка "чата для сайта через джаббер". К сожалению у меня нет выделенного сервера с линуксом для тестов, поэтому был использован локальный компьютер с Win7 (и сервером Apache).

Как это вообще должно работать: пользователь заходит на сайт, и видит окошко, куда можно разговаривать. После того как пользователь послал сообщение, оно прилетает на указанный джаббер аккаунт. Получатель этого сообщения может написать ответ и оно придёт посетителю сайта.
Что для этого нужно:
  • Jabber сервер, можно публичный, можно локальный. Я выбрал Openfire и установил его локально. Сервер должен поддерживать Bosh - XEP-0124: Bidirectional-streams Over Synchronous HTTP, об этом чуть позже.
  • JS библиотека, которая будет реализовывать джаббер-клиент на сайте. Я взял Strophe. Это достаточно низкоуровневая библиотека, в которой нет функций типа "ПослатьСообщение(Куда, Текст)". Для достижения нужных действий нужно вручную составлять команды джаббер серверу (в XML). Удобные средства для создания XML в Strophe есть :)

BOSH

JS не умеет создавать TCP соединения с другим сервером/клиентом, что необходимо для реализации джаббер-клиента. JS может посылать только HTTP запросы. Поэтому нужен специальный механизм, который позволит работать с TCP соединениями посредством HTTP. Это и есть BOSH.

По-умолчанию BOSH в Openfire включён и имеет адрес http://localhost:7070/http-bind. Но если указать этот адрес при соединении, ничего не выходит. Проблема хорошо описана здесь, что бы заработало нужно написать редирект для апача и раскомментировать модули proxy_module и proxy_http_module:

httpd.conf:
ProxyRequests Off
ProxyPass /http-bind http://127.0.0.1:7070/http-bind/
ProxyPassReverse /http-bind http://127.0.0.1:7070/http-bind/

Чат на сайте

Итак, всё очень просто, устанавливаем Openfire, создаём пользователя, например, site, и создаём тестовую страницу (в качестве основы я брал строфовский пример echobot). На странице есть поле, куда пользователь пишет своё сообщение, и поле с историей чата.

Пишем обработчик загрузки страницы, который будет логинится на джаббер сервер:
$(function () {
    connection = new Strophe.Connection('/http-bind');
    connection.connect('site@r1c', 'site', onConnect);
});
В обработчике onConnect мы добавляем "слушателя" на события, связанные с приходом сообщений (в случае успешного логина):
function onConnect(status){
    if (status == Strophe.Status.CONNECTING) {
        log('Strophe is connecting.');
    }
    else if (status == Strophe.Status.CONNFAIL) {
        log('Strophe failed to connect.');
    }
    else if (status == Strophe.Status.DISCONNECTING) {
        log('Strophe is disconnecting.');
    }
    else if (status == Strophe.Status.DISCONNECTED) {
        log('Strophe is disconnected.');
    }
    else if (status == Strophe.Status.CONNECTED) {
        log('Strophe is connected, ' + connection.jid);
        
        connection.addHandler(onMessage, null, 'message', null, null, null);
        connection.send($pres().tree());
    }
}
Обработчику onMessage приходят XML данные. Разбираем их что бы узнать текст сообщения, кто отправитель и т.д.:
function onMessage(msg) {
    var to = msg.getAttribute('to');
    var from = msg.getAttribute('from');
    var type = msg.getAttribute('type');
    var elems = msg.getElementsByTagName('body');

    if (type == "chat" && elems.length > 0) {
        var body = elems[0];
    
        AddText(Strophe.getText(body), 'in');
    }

    // we must return true to keep the handler alive.  
    // returning false would remove it after it finishes.
    return true;
}
Обработчик кнопки "Отправить" формирует XML с необходимыми данными и просит Strophe отправить их. В качестве принимающего используется пользователь admin:
function Send(message) {
    var msg = $msg({to: 'admin@r1c', from: connection.jid, type: 'chat'}).c('body').t(document.URL + '\n' + message);
    connection.send(msg.tree());
    
    AddText(message, 'out');
    $('input#message').val('');
}

Итог



В итоге получился очень простой набросок, позволяющий общаться по джабберу через веб-страницу. Мне было интересно узнать как это работает в принципе, поэтому я не стал копать дальше. Что-то мне подсказывает там есть куча подводных камней :)

Можно придумать ещё очень много усовершенствований. Например:
  • Нормальный интерфейс чата на сайте :)
  • Автоматическая регистрация новых пользователей на джаббер-сервере, если в этом есть необходимость. Для этого можно использовать In-band registration - регистрация через XML команды джаббера, а не через веб-интерфейс.
  • Сохранение состояния чата при переходе между разными страницами сайта
  • Сохранение истории чата на сервере
  • и т.д.

9 сентября 2009 г.

Простой бэкап, Python + DropBox

Бэкапы я не делал давно. Было лень, да и данные я не терял уже несколько лет. Но недавно задумался об этом и решил-таки сделать что-то такое, что резервно сохраняло бы то, что не очень хочется потерять. Просто что бы скучно не было, и немного попрактиковаться :)

Для начала нужно определиться где хранить копии файлов. По-моему DropBox это очень хороший выбор - это сервис для синхронизации данных на разных компьютерах. Бесплатно дают 2 Гб, плюс программу, которая отображает выбранную локальную папку на ихнее онлайн хранилище. Добавил файл в эту специальную папку, и он добавился на всех компьютерх, на которых стоит специальная программа, настроенная на тот же аккаунт. Плюс доступ через веб-интерфейс. Для бэкапа это очень удобно - не надо заморчиваться как скопировать файлы на FTP, а просто копируем их в специальную папку. Всё работу по заливке/синхронизации сделает DropBox клиент.

Дальше нужно собственно скопировать необходимые файлы. Для этого был написан крохотульный скрипт на питоне, backup.py. Этот скрипт создаёт текстовый список файлов, которые нужно скопировать, и передаёт его WinRar'у, который создаёт архив с указанным именем.

Сначала я хотел полностью поручить WinRar'у создание бэкапа, написав немного кода в батниках, но к сожалению он не умеет пропускать заданные папки (только файлы). Поэтому пришлось писать промежуточный скрипт, который создаёт список файлов.

У скрипта есть единственный параметр - имя выходного файла-архива со скопированными файлами.

Алгоритм работы скрипта:
  1. Читает из текущей папки файл tobackup.lst - там хранится список папок, которые нужно скопировать. Каждая строка - отдельная папка. Например:
    d:\projects
    d:\www
  2. Читает список папок, которые нужно исключить из бэкапа. Это опциональный файл igonre.lst в текущей папке. Это либо полный путь (d:\projects\old), либо просто имя папки (.svn). Например:
    .svn
    d:\projects\old
  3. Создаёт текстовый список файлов для копирования list.lst (в текущей папке). После этого добавляет в конец файла-списка опциональный файл extra.lst (из текущей папки). Там содержится список файлов, а не папок, для включения. Например, мы хотим сохранить настройки php, но из всей папки d:\programs\php нам нужен только один файл php.ini. Поэтому вместо добавления папки с php мы добавляем только один файл php.ini в extra.lst.
  4. Вызывает архиватор, который создаёт архив с файлами из списка. Параметры архиватора - сохранять полный путь файлов (вместе с диском) и создать архив без сжатия.

В итоге после работы скрипта мы будем иметь архив, созданный как раз в папке DropBox, клиент которого сам загрузит его на сервер.

Я не зря указывал, что скрипт берёт файлы из текущей папки. Благодаря этому делать разные "профили" очень просто - достаточно создать новую папку, создать там файл tobackup.lst, опциональные ignore.lst и extra.lst, и новый профиль готов! Для удобства можно сделать батник, который будет вызывать backup.py и передавать ему имя файла-архива который должен получиться.

Сейчас, например, у меня есть две папки-профиля, projects (для бэкапа текущих проектов) и other (для бэкапа настроек программ). Сам скрипт лежит в папке core (на том же уровне что и папки профилей), вместе с WinRar'ом.

Папка core:
Rar.exe
WinRAR.exe
rarreg.key
backup.py

Папка projects:
tobackup.lst:
d:\projects
d:\svn

ignore.lst:
.svn
d:\projects\old

backup.bat:
..\core\backup.py "d:\dropbox\my dropbox\backup\dev.rar"

Батник backup.bat вызывает скрипт, передеаёт ему имя получающегося файла-архива, который будет создан в папке, связанной с DropBox. Скрипт берёт файлы-списки из текущей папки (projects в данном случае). Один запуск батника - новый бэкап готов, и DropBox грузит его на сервер :)

Осталось прикрутить запуск по расписанию, но меня устроит и ручной запуск несколько раз в месяц :)

backup.py:
import subprocess
import sys
import os.path
import os

__author__="race1"
__date__ ="$09.09.2009 19:55:06$"

if __name__ == "__main__":
    if (len(sys.argv) < 2):
        print("Usage: backup.py ")
        print("E.g.: \"backup.py d:\\dropbox\\my dropbox\\backup.rar\"")
        exit(1)

    ScriptFolder = os.path.dirname(sys.argv[0])
    ArchiverFile = ScriptFolder + "\\winrar.exe"
    RootFoldersFile = "tobackup.lst"
    IgnoreFoldersFile = "ignore.lst"
    ExtraFile = "extra.lst"
    FilelistFile = "list.lst"
    OutputArchive = sys.argv[1]

    # Only mandatory file is RootFoldersFile, check that it exists
    if (not os.path.isfile(RootFoldersFile)):
        print("%s doesn't exist" % RootFoldersFile)
        exit(1)

    # Archiver file also should exist
    if (not os.path.isfile(ArchiverFile)):
        print("%s doesn't exist" % ArchiverFile)
        exit(1)

    # Read root folders from file
    rootFolders = [i.strip() for i in open(RootFoldersFile, "r").readlines()]

    # Read list of folders that need to be igonred (if specified)
    ignoreList = []
    if (os.path.isfile(IgnoreFoldersFile)):
        ignoreList = [i.strip().lower() for i in open(IgnoreFoldersFile, "r").readlines()]

    # Open filelist file for writing list of files
    out = open(FilelistFile, "w")
    filesCount = 0

    for rootFolder in rootFolders:
        for root, dirs, files in os.walk(rootFolder):
            for file in files:
                out.write(root + "\\" + file + "\n")
                filesCount += 1

            for dir in dirs:
                # Skip ignored folders
                if (dir.lower() in ignoreList or ("%s\%s" % (root, dir)).lower() in ignoreList):
                    dirs.remove(dir)

    # Append some files from extra files list
    if (os.path.isfile(ExtraFile)):
        out.writelines(open(ExtraFile, "r").readlines())
        
    out.close()

    print("Added %d file(s)" % (filesCount))

    # Delete old archive if exists
    if (os.path.isfile(OutputArchive)):
        os.unlink(OutputArchive)

    # Call archiver (winrar)
    subprocess.call(ArchiverFile + " a -m0 -ep3 \"" + OutputArchive + "\" @list.lst")

    os.unlink(FilelistFile)

    print("Done")