среда, 27 мая 2009 г.

Интернационализация ns_sendmail

Понадобилось отправлять почту без вложений, а это как раз тот случай, когда вызов внешней утилиты оказывается слишком накладно. Потому сделал враппер для встроенной в AOL функции ns_sendmail, умеющий корректно обрабатывать не-ASCII символы.


# note: _ns_sendmail is exists!
if {[info commands orig_ns_sendmail] eq {}} {
rename ns_sendmail orig_ns_sendmail
package require base64
package require mime

# headers are ignored!
proc ns_sendmail {to from subject body args} {
set headerSet [ns_set create]
ns_set put $headerSet Content-Type {text/plain; charset=UTF-8}
ns_set put $headerSet Content-Transfer-Encoding base64
ns_set put $headerSet MIME-Version 1.0

orig_ns_sendmail $to \
$from \
[mime::word_encode utf-8 quoted-printable [encoding convertto utf-8 $subject]] \
[base64::encode [encoding convertto utf-8 $body]] \
$headerSet [lindex $args 1]
}
}


Обертка работает точно так же, как и базовая функция, за исключением того, что игнорирует дополнительные хидеры (можно их не игнорировать, изменения тривиальны).


Как в рассылке подсказывает Bernhard van Woerden, мой вариант не разбивает на несколько строк длинный subject и не кодирует заголовки From и To. Правда, предложенный им вариант функции вообще не работает, но идея понятна. Сделал обработку большого subject (поправил код выше).

Upd.
А вот на перле аналог, с поддержкой вложений: Отправка писем с авторизацией на SMTP-сервере из Perl (smtp auth perl)

понедельник, 25 мая 2009 г.

SQLite 3.6.14.2

Выпущено внеочередное обновление SQLite, то бишь версия 3.6.14.2. Обновление исправляет единственный баг, который приводит к выдаче неправильного результата запроса. Обнаружить этот баг сложно и нет способа на уровне приложения обойти, потому предлагается обновление.

Баг-репорт и патч доступны здесь:

http://www.sqlite.org/cvstrac/tktview?tn=3879
http://www.sqlite.org/cvstrac/chngview?cn=6677

Баг затрагивает только версии 3.6.14 и 3.6.14.1, для пользователей этих версий настоятельно рекомендуется обновиться.

Ну а я в ближайшее время обновлю версию SQLite в своем репозитории.

Well-known text (WKT) и Tcl

Well-known text - стандартный формат предоставления геоинформации во многих системах, в том числе, с этим форматом умеют работать OGR, Spatialite, PostGIS. Неплохо бы с этим форматом уметь и напрямую работать, из тиклевой программы. А реализовать это можно вот так:

1. Преобразование во вложенные тиклевские списки:

proc WKT_read {wkt} {
return [string map {, " " \
( " {" \
, " " \
) "} " \
} $wkt]
}

WKT_read {POINT(6 10)}
POINT {6 10}

WKT_read {LINESTRING(3 4,10 50,20 25)}
LINESTRING {3 4 10 50 20 25}

WKT_read {POLYGON((1 1,5 1,5 5,1 5,1 1),(2 2, 3 2, 3 3, 2 3,2 2))}
POLYGON { {1 1 5 1 5 5 1 5 1 1} {2 2 3 2 3 3 2 3 2 2} }

WKT_read {MULTIPOINT(3.5 5.6,4.8 10.5)}
MULTIPOINT {3.5 5.6 4.8 10.5}

WKT_read {MULTILINESTRING((3 4,10 50,20 25),(-5 -8,-10 -8,-15 -4))}
MULTILINESTRING { {3 4 10 50 20 25} {-5 -8 -10 -8 -15 -4} }

WKT_read {MULTIPOLYGON(((1 1,5 1,5 5,1 5,1 1),(2 2, 3 2, 3 3, 2 3,2 2)),((3 3,6 2,6 4,3 3)))}
MULTIPOLYGON { { {1 1 5 1 5 5 1 5 1 1} {2 2 3 2 3 3 2 3 2 2} } { {3 3 6 2 6 4 3 3} } }

WKT_read {GEOMETRYCOLLECTION(POINT(4 6),LINESTRING(4 6,7 10))}
GEOMETRYCOLLECTION {POINT {4 6} LINESTRING {4 6 7 10} }

WKT_read {POINT EMPTY}
POINT EMPTY

WKT_read {MULTIPOLYGON EMPTY}
MULTIPOLYGON EMPTY


2. Работа с полученными вложенными списками:

proc MULTILINESTRING {mline} {
if {$mline eq {EMPTY}} return
foreach line $mline {
puts "new line:"
LINESTRING $line
}
}
proc LINESTRING {line} {
if {$line eq {EMPTY}} return
foreach {x y} $line {
puts "x: $x, y: $y"
}
}

MULTILINESTRING { {3 4 10 50 20 25} {-5 -8 -10 -8 -15 -4} }
new line:
x: 3, y: 4
x: 10, y: 50
x: 20, y: 25
new line:
x: -5, y: -8
x: -10, y: -8
x: -15, y: -4

LINESTRING {3 4 10 50 20 25}
x: 3, y: 4
x: 10, y: 50
x: 20, y: 25


Представленные реализация MULTILINESTRING и LINESTRING умеют работать только с двумерными координатами, однако аналогично можно написать версию для обработки трехмерных координат. А представленный выше код подойдет, к примеру, для обработки трэков с gps-навигатора.

воскресенье, 24 мая 2009 г.

GPS конверторы - второе пришествие

Как говорится, все новое - это хорошо забытое старое. Вот и я вспомнил про сделанные когда-то онлайн-конверторы форматов трэков с GPS. Побудил меня к этому ничто иное, как главный двигатель прогресса, то бишь лень. Ну не хочется мне каждый раз вручную набирать команду преобразования трэка, а потом полученный файл закачивать на веб-сервер, откуда его можно просмотреть с помощью Google Maps (понятно, что Google Earth может и локальный файл просмотреть, но ведь хочется и друзьям ссылочку отправить).
Напоминаю адрес - https://mobigroup.ru/page/services.

Впрочем, образовалась некая проблема - трэков становится все больше (у меня навигатор Garin Oregon 300, трэки умеет сохранять в формате Mapsource в файлах с расширением mps, - в России, к сожалению, он не очень известен, может быть, надо обзорчик написать? если вам интересно, пишите, сделаем обзор), и искать среди них нужный все сложнее. Посему появляется желание сделать онлайн-систему хранения и управления трэками. Думаю, нужны следующие функции:

- Идентификация пользователей по OpenID
- Загрузка трэка и сохранение метаинформации и статистики данных GPS (экстент, протяженность трэка, время в пути, минимальная и максимальная скорости, минимальная и максимальная высота над уровнем моря)
- Преобразование в KML, GPX и, опционально, в другие форматы
- Просмотр всех трэков пользователя или трэков с определенным тэгом
- яваскрипт-интерфейс для доступа к трэкам (думаю, подойдет формат oEmbed)
- Поиск трэков по атрибутам - имени пользователя, тэгам
- Поиск по информации трэка - географическому экстенту, скорости движения и проч.
- Поиск всех пользователей, которые загрузили

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

А также следует предусмотреть возможность загрузки трэка только для разового просмотра (без указания имени пользователя и прочих атрибутов) и загрузки для постоянного хранения с возможностью поиска по атрибутам.

Что касается технической реализации, могу выделить, скажем, 100 Gb дискового пространства для хранения трэков и один поток для выполнения обработки. Размер трэка ограничим значением 2 Мб (просто потому, что трэков большего размера у меня нет).

Релиз Spatialite 2.3

На самом деле, релиз вышел уже энное время назад, но только сегодня я добрался до него. Итак, занимаюсь валидацией исходников. Бета-версию Spatialite 2.3 мне присылал автор, я ее интегрировал в дерево исходников SQLite в виде набора модулей, в том числе поправил некоторые баги, в том числе переполнение int в модуле VirtualText, который я много где использую (а обнаружил ошибку, когда начал загружать с помощью этого модуля телефонные номера в биллинг - по стандарту номера бывают до 15-ти знаков, и вот тут-то и нарисовались аккуратно прикопанные грабли). Как оказалось, этот самый неприятный баг автор почему-то забыл поправить и я снова отправил ему сообщение об этом прискорбном факте (или даже инциденте, уж больно баг неприятный, сразу и не заметить, ведь проблема не в коде, а в логике).

Итак, к настоящему моменту я проверил код VirtualText, VrtualShape, iconv (автор его по-другому называет, но для меня, черт побери, имя файла gg_utf8 и название функций с префиксом gaia* отнюдь не очевидно, но на эту тему мы еще с автором пободаемся). Первый и последний модуль уже включены в сборку SQLite 3.6.14.2, доступную в моем репозитории, сообщения о багах отправлены автору. Второй модуль тоже в порядке, но есть мысль сделать отдельный пакет с расширением Spatialite. Сам автор делает в точности наоборот - собирает SQLite как часть Spatialite, но с дебиановской системой пакетов это никак не вяжется, а еще получается, что яйцо учит курицу, ведь как раз Spatialite создан как расширение к SQLite. Ладно, предыдущую версию Spatialite я целиком включил в SQLite (разбив на модули), но Spatialite растет на глазах и, похоже, имеет право на отдельный пакет. Из минусов такого решения отмечу, что придется явно подгружать дополнительную библиотеку для работы с ГИС-данными. Ну и еще признаюсь, что VirtualText я по-прежнему оставлю модулем SQLite - без этого расширения как без рук.

Spatialite 2.3 требует для работы geos 3.1.0, который есть в sid. Ну, раз требует, сделал бэкпорт, который также можно взять у меня в репозитории. Из приятностей - появилась поддержка EXIF GPS, VirtualNetwork, VirtualFdo. И в самом деле, проект растет. Если предыдущая версия "догоняла" PostGIS, то новая версия предлагает собственные уникальные функции. А про скорость работы и говорить не приходится. Также замечу, что код Spatialite по-прежнему сделан качественно и легко читаем, притом взаимодействие непосредственно с SQLite занимает буквально строчки - конечно, это достигается благодаря движку SQLite, но тем не менее весьма радует. Если мои читатели помнят, от PostgreSQL я отказался как раз в результате монстрообразности кода самого постгреса и его расширений в частности (в своих проектах до сих пор использую эту СУБД, но расширения к ней писать не желаю, поскольку тратить тучу времени на описание взаимодействия с движком СУБД имхо потеря времени в чистом виде).

Касаемо сборки Spatialite в отдельный пакет отмечу, что этому препятствует ничто иное, как желание автора таскать свою собственную копию SQLite. Думаю, этот вопрос мы утрясем в ближайшее время, по крайней мере, когда предыдущие релизы вообще не собирались под дебианом (не хватало им хидеров от зависимостей, требовались исходники), автор охотно шел навстречу и принимал патчи.

Как я сейчас обнаружил, в debian unstable уже есть пакет со Spatialite. Причем мантейнер пакета тот же, что и у AOLServer -это очень даже хорошо, поскольку мантейнер весьма адекватный и реагирует быстро. Отправил ему сообщение насчет неисправленного бага в VirtualText, а себе сделал бэкпорт пакета. Заодно собрал новый gdal 1.6.1. В gdal 1.7.0 обещают поддержку Spatialite, только непонятно, когда же он выйдет.

Ставим так
sudo aptitude install libspatialite2

Теперь в шелле SQLite можно загрузить расширение командой:
.load /usr/lib/libspatialite.so.2.0.3

Да, именно *.so.2.0.3, в бинарях с офсайта нумерация точно такая же. Впрочем, это мелочь, не буду пока обращать внимания.

Выглядеть это должно примерно так:

$ sqlite3 track.db
SQLite version 3.6.14.1
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .load /usr/lib/libspatialite.so.2.0.3
SpatiaLite version ..: 2.3.0 Supported Extensions:
- 'VirtualShape' [direct Shapefile access]
- 'VirtualText' [direct CSV/TXT access]
- 'VirtualNetwork [Dijkstra shortest path]
- 'RTree' [Spatial Index - R*Tree]
- 'MbrCache' [Spatial Index - MBR cache]
- 'VirtualFDO' [FDO-OGR interoperability]
- 'SpatiaLite' [Spatial SQL - OGC]
PROJ.4 Rel. 4.6.1, 21 August 2008
GEOS version 3.1.0-CAPI-1.5.0


Получил ответ от автора Spatialite, он посмотрел мою тестовую базу и порадовал сообщением, что в его коде ошибок не обнаружено. Есть проблема обработки в GDAL сгенерированной в ogr2ogr геометрии в представлении WKT.


Any problem you noticed simply arise because inside the "tracks"
table there is a "foolish" geometry, corresponding to the row
identified by OGC_FID=3
Let you try:

SELECT NumPoints(GeomFromText(WKT_GEOMETRY)) FROM tracks;

As you can easily notice, this row stores a single vertex:

MULTILINESTRING ((44.036190015999999 56.322677638000002))

so, this is one is a genuine degenerated LINESTRING:
actually, this one is a POINT.


Таким образом, вопрос лишь в дополнительной проверке данных.


Получил ответ на очередное письмо автору и порадовался как исправлению найденных проблем, так и живому юмору, с которым написано письмо. Итак, внесены следующие доработки:

1. Добавлена проверка вырожденных WKT геометрий: для LINESTRINGs [<2 pts] и
для RING [<4 pts] все функции семейства GeomFromText() корректно возвращают NULL Geometry.

2. Ффункция gaiaIsResevedSqlName() исключена.

3. Модуль "my_text_module" переименован в "virtualtext_module".

4. Исправлена проблема с целочисленным переполнением в модуле VirtualText.

Изменить префикс gaiaConvertToUTF8() автор не согласился, поскольку этот префикс принят для всех функций проекта. Не могу не согласиться, хотя при включении кода модуля VirtualText в SQLite название выглядит немного странно :-)

Новую версию [rev.15] можно взять здесь (вскоре я соберу обновленный deb-пакет):

svn co https://www.gaia-gis.it/svn/libspatialite libspatialite-svn

Таким образом, проблемы работы с WKT почти решены. Если автор примет предложение о добавлении функций для конвертации простой геометрии в мульти-геометрию и обратно, то вопрос можно будет полностью закрыть. Да, преобразование из мульти-геометрий в простые необратимое, т.к. приводит к слиянию объектов, но наличие такой возможности намного упростит обработку данных. Сейчас же приходится усложнять код приложения, к примеру, когда получена геометрия MULTIPOINT с единственной точкой.

вторник, 19 мая 2009 г.

skylink-интернет с модемом ZTE MG478

0. Потребуются следующие файлы конфигурации:
/etc/wvdial/skylink.conf
[Dialer Defaults]

Init2 = ATZ
Init3 = ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
Stupid Mode = 1
Modem Type = Analog Modem
ISDN = 0
Phone = #777
Modem = /dev/ttyUSB0
Username = "mobile"
Dial Command = ATDT
Password = "internet"
Baud = 115200


/etc/wvdial/skylink-blocked.conf
[Dialer Defaults]

Init2 = ATZ
Init3 = ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
Stupid Mode = 1
Modem Type = Analog Modem
ISDN = 0
Phone = #777
Modem = /dev/ttyUSB0
Username = "mobile@blocked"
Dial Command = ATDT
Password = "internet"
Baud = 115200


1. В ядрах до 2.6.26 включительно следует загрузить вручную модуль usbserial:

sudo modprobe usbserial vendor=0x19d2 product=0xfffe

2. Если активен другой интерфейс, лучше отключить:

sudo ifdown wlan0

3. А теперь можно и подключиться к skylink:

а) при положительном балансе счета
sudo wvdial -C /etc/wvdial/skylink.conf

б) при отрицательном балансе
sudo wvdial -C /etc/wvdial/skylink-blocked.conf

4. Возможно, также потребуется перезапустить resolvconf:

/etc/init.d/resolvconf restart

Вот, в принципе, и все.

Перекодирование видео для Ritmix RF-7400 с помощью mencoder

Когда-то давно довелось поразвлекаться, разбираясь, каким образом сконвертировать видео для плеера Ritmix RF-7400. "Штатного" перекодировщика в комплекте почему-то не было, на сайте производителя - тоже, пришлось искать на форумах и потом запускать под виндой на виртуалке. Оказалось, используется стандартный mencoder, для которого генерится bat-файл с параметрами запуска. Вот что получилось в итоге:

mencoder -noodml "$1" -o "out_$1" -ofps 20.000 -vf-add scale=-2:128 -vf-add expand=176:128:::1 -ovc xvid -xvidencopts bitrate=384 -srate 44100 -oac mp3lame -lameopts vbr=0 -lameopts br=128 -lameopts vol=0 -lameopts mode=0 -lameopts aq=7 -lameopts padding=3 -af volnorm -xvidencopts max_bframes=0:nogmc:noqpel

Здесь $1 - имя исходного файла, а результат будет сохранен в файл с добавлением префикса "out_". Все, можно загружать в папку video на устройстве и смотреть.

воскресенье, 17 мая 2009 г.

Заметка об XML, или "А Баба-Яга - против!"

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


  • Конфигурационные файлы


  • Фактически, это замена юниксовым plain-text конфигурационным файлам и виндовым ini-файлам. Кроссплатформенный пример - VirtualBox. Можно придумать много резонов делать именно так, но на практике ясно одно - более неудобную реализацию еще поискать. Руками править плохо, делать скрипт для правки сложно (ага, есть некоторые утилиты, облегчающие жизнь, но с plain-text все намного проще). И самое плохое - разработчики, использующие xml для конфигов, для своего удобства и во избежание лишних затрат на парсинг xml дублируют информацию в разных файлах! Например, в VirtualBox при переносе виртуалки между хостами приходится править как основной конфиг, так и конфиги отдельных виртуальных машин. Как итог - явно провальная затея, лучше было бы вернуться к plain-text.

  • Обменный формат для _разных_ приложений


  • Опять же, теоретически идея здравая. Но в реальности "обменный" xml почти всегда спроектирован из рук вон плохо и разбирать его сущее мучение. Мне знакомы только отечественные приложения с таким обменным форматом, так что воздержусь от примеров, так сказать, "во избежание"... В качестве альтернативы представляется обоснованным использование баз данных в виде одного файла, например, SQLite. БД хороша тем, что предоставляет целостную модель данных с требуемыми ограничениями и типами данных.

  • Обменный формат в рамках одного приложения


  • Основная область, разумеется, веб - производительностью обычно пренебрегают (иногда вполне разумно, но чаще - наоборот), и здесь очень распространены "модные" технологии. Здесь мы видим, что на стороне сервера xml генерить накладно, передавать приходится чрезмерный объем данных, на стороне клиента (браузер) разбирать ресурсоемко и долго... В качестве аргументов "за" называют наличие множества библиотек, упрощающих создание xml (разумеется, библиотеки перегружены и намного увеличивают накладные расходы, но об этом редко вспоминают), удобство отладки (скорее, стоит говорить о возможности отладки, а не об удобстве, т.к. "глазами" увидеть ошибку в xml затруднительно), и некое малопонятное "удобство использования" (под которым обычно подразумевают возможность запихнуть вместо xml большой кусок xhtml). Безусловно, не обходится и без ссылок на "авторитеты" (это проблема Рунета в целом), причем опять-таки обсуждается не целесообразность, а лишь возможность применения. Впрочем, в настоящее время заметен эволюционный переход на формат JSON, который решает многие из названных проблем. Разве что пока не хватает средств CSS непосредственно для JSON, без необходимости преобразовывать JSON в элементы документа, такое развитие было бы вполне закономерным.

  • Формат документов


  • Безусловно, лучше, чем бинарный формат, но автоматизированная обработка документов (например, индексация для поиска) чрезвычайно затратна и требует специализированных парсеров для каждого формата. В очередной раз сталкиваемся с проблемой смешения содержания и представления. Вероятно, намного более выгодной будет реализация на основе встраиваемой СУБД (например, стиль оформления в БД может храниться в отдельной таблице и иметь ключ, указывающий на форматируемый элемент, а сам элемент содержания не иметь никакой привязки к оформлению). Представляется небесполезным обратить внимание на то, что при генерации документов на основе некоторой БД (которая заполняется через веб-формы или на основе автоматизированной обработки различных массивов информации) вся затея с xml-форматом документов начинает "дурно пахнуть" - формализованный набор данных превращается в слабо формализованный и смешанный с элементами оформления (xml), а при необходимости дальнейшей (автоматизированной) обработки с большими усилиями снова преобразуется в формализованное представление! Тем не менее, существует одно веское "но" - xml удобнее для представления иерархических данных. Именно последнее соображение и приводит к xml-форматам для сложных документов. Можно предположить, что развитие средств рекурсивного SQL во встаиваемых СУБД способно решить указанную проблему.

  • Формат хранения


  • Тут много говорить и не стоит - корень проблемы заключается в тех приложениях, которые создают документы в этом формате. Если формат хорошо продуман и компактен, его легко хранить и обрабатывать, если формат плох, то появляется море проблем. XML-ориентированные СУБД - это лишь интерфейс к готовым документам, а целесообразность хранения всего и вся в базе выходит за рамки нашего рассмотрения (как, впрочем, и за рамки здравого смысла...).


Итак, как можно видеть, использование xml во многом не решает старые проблемы, но притом создает новые. Безусловно, xml может быть использован в разных областях - но в каждой отдельной области существует заслуживающая внимания альтернатива, и зачастую более эффективная. С другой стороны, "эпоха xml" ознаменовала такой рост производительности вычислительных систем, когда требования интероперабельности и переносимости стали важнее, чем правившие в предыдущие годы требования производительности. Следует лишь внести "небольшую" корректировку - мы увидели очередной виток развития, повторящий многие уже пройденные достижения и неудачи, а отнюдь не "серебряную пулю".

суббота, 16 мая 2009 г.

Использование утилиты tidy для чистки html

Tidy утилита очень мощная и есть во многих дистрибутивах, вот только использовать ее из внешней программы немного не тривиально, т.к. утилита представляет собой потоковый обработчик. Когда-то мне пришлось поломать голову над тем, как же вызывать tidy корректно, а в итоге получилась нижеприведенная функция.


proc tidy {buf} {
if {$buf eq {}} {return {}}
set res {}
set cat [open "|cat" r+]
set io [ open "|/usr/bin/tidy -utf8 -asxhtml -q --show-body-only 1 -f /dev/null <@ $cat" r]
puts $cat $buf
flush $cat
close $cat
set res [read $io]
catch {close $io}
return $res
}


Поскольку я работаю с дистрибутивом debian, то путь задан непосредственно в коде. Разумеется, точно так же можно вызывать из тикля любую другую утилиту потоковой обработки, например, архиватор.

HAProxy vs. Pound

Некоторое время назад столкнулся с неприятным "притормаживанием" при выполнении запросов. Как выяснилось, проблема в реверс-прокси Pound - в режиме HTTPS его эффективность оказалась не слишком высокой. Трафик проекта, на котором это произошло, составляет около 200 Gb в месяц, или миллион "с гаком" запросов в сутки (весь трафик по протоколу HTTPS). Притом в режиме HTTP (по отзывам разработчиков в рассылке AOLServer) Pound выдерживает до 800 запросов в секунду (ну, правда, неизвестно, на каком железе), после чего приходится ставить другой прокси.

В качестве альтернативы выбрал HAProxy - и код понравился, и автор помог перенести конфиг Pound. Также оказалось очень полезным, что автор HAProxy написал патч к Stunnel4 для поддержки хидера X-forwarded-For (этот патч понадобилось немного адаптировать к версии stunnel4 в debian lenny, результат можно взять на сайте HAProxy или уже собранный пакет Stunnel4 в моем репозитории). Что приятно, после замены реверс-прокси в top-е его практически не видно, иногда заметен stunnel, но в общем и целом производительность связки HAProxy+Stunnel4 как минимум на порядок выше. Для поддержки условных редиректов следует обновить HAProxy, да и автор советовал, так что собрал пакет с последней версией, которая меня и устроила (брать все в том же репозитории).

Багфикс ns_httptime в AOLServer

Началось все с того, что обнаружил неприятную вещь - AOLServer 4.5.1 для файлов, созданных с 1-го по 10-е число любого месяца, никогда не отдает код ответа 304, а всегда заново их пересылает. Т.е. не работает кэширование, т.к. браузер сообщает, что актуальный файл в кэше есть, но сервер "не понимает". Обнаружил я эти "грабли" в процессе написания своей системы кэширования, которая еще и gzip-сжатие определенных типов файлов выполняет. Т.к. мне нужна работа в non-english локали, то я использовал debian-овский патч для ns_httptime, убирающий локалезависимость этой функции, а в нем и оказалась ошибка.
Исправленный патч можно взять здесь:

http://mobigroup.ru/files/aol4.5.1/httptime.c.diff


--- httptime.c.orig 2003-01-18 22:24:20.000000000 +0300
+++ httptime.c 2009-05-12 20:14:24.000000000 +0400
@@ -27,6 +27,9 @@
* version of this file under either the License or the GPL.
*/

+static char *weekdays_names[7] =
+{ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
+

/*
* time.c --
@@ -92,11 +95,14 @@
}

/*
- * This will most likely break if the locale is not an english one.
+ * Using snprintf instead of strftime to always use english names
* The format is RFC 1123: "Sun, 06 Nov 1997 09:12:45 GMT"
*/

- strftime(buf, 40, "%a, %d %b %Y %H:%M:%S GMT", tmPtr);
+ snprintf(buf, 40, "%s, %02d %s %d %02d:%02d:%02d GMT",
+ weekdays_names[tmPtr->tm_wday], tmPtr->tm_mday,
+ month_names[tmPtr->tm_mon], tmPtr->tm_year + 1900,
+ tmPtr->tm_hour, tmPtr->tm_min, tmPtr->tm_sec);

Ns_DStringAppend(pds, buf);
return pds->string;


Мантейнеру патч я уже отправил, а также он войдет в upstream. Как говорится, "заседание продолжается".

P.S. Ниже приведена та функция, которую я писал - для сжатия файлов с кэшированием на сервере сжатой версии (очевидно, что "на лету" сжимать статику при каждом запросе, как это делают с помощью nginx и подобных, решение явно бредовое).



# return original or gzipped file
# is used zlib module
# may cooperate with config "checkmodifiedsince" parameter
# may use fastpath cache
# config must have these sections:
#ns_section "ns/server/${servername}"
# ns_param gzipmin 512
#ns_section ns/server/dataset
# ns_param temp
proc ns_returnfilez {fname} {
if {[file isfile $fname]==0} {
ns_returnnotfound
return
}
# TODO: when file must be gzipped?
set ext [string tolower [file extension $fname]]
if {$ext eq {.html} ||
$ext eq {.htm} ||
$ext eq {.css} ||
$ext eq {.js} ||
$ext eq {.xml} ||
$ext eq {.xsl} ||
$ext eq {.docx} ||
$ext eq {.doc} ||
$ext eq {.mdb} ||
$ext eq {.xls} ||
$ext eq {.txt}
} {
set is_compress 1
} else {
set is_compress 0
}

# minimal gzipping file size set equal to config gzipping setting for ADP
set gzipmin [ns_config "ns/server/main" gzipmin]
if {[file size $fname] < $gzipmin} {
set is_compress 0
}

# TODO: check client compability
# client must send header like as "Accept-Encoding: gzip,deflate"

set mtime [file mtime $fname]

if {$is_compress == 1} {
# create hidden gzipped file in temp storage
# get temp storage path from config
set temp [ns_config "ns/server/dataset" temp]
set fname_hidden [file normalize [file join $temp www_pool .[ns_conn url]]].gz
# will create all non-existing parent directories
if {[file exists [file dirname $fname_hidden]]==0} {
file mkdir [file dirname $fname_hidden]
}
if {[file exists $fname_hidden]} {
set mtime_hidden [file mtime $fname_hidden]
# delete old gzipped file _or_ directory with same name from temp storage
if {$mtime >= $mtime_hidden} {
file delete -force $fname_hidden
}
}
}

if {$is_compress == 1} {
# send gzipped file
if {[file isfile $fname_hidden]==0} {
file copy -force $fname [file rootname $fname_hidden]
ns_zlib gzipfile [file rootname $fname_hidden]
}
ns_updateheader Content-Encoding gzip
ns_returnfile 200 [ns_guesstype $fname] $fname_hidden
} else {
# send original file
ns_returnfile 200 [ns_guesstype $fname] $fname
}
}

Tcl и JSON

Все чаще использую формат JSON, но средства его генерации в тикле... как бы это сказать... ужасают. В tcllib и вовсе неработающий код (да, deb-пакет tcllib пересобрал, заодно обновив версию, ибо безобразие это надо лечить). Пересмотрел множество реализаций, и что же - все авторы спотыкаются на несоответствии типов данных. Как пример:


set json [::json::encode [list object [list code [list string $code] msg [list string $msg] id [list number $id]]]]


Одним словом, интуитивно понятным такой код не назовешь.

Самое интересное, что JSON - нетипизированный формат. А раз нетипизированный, то нет никакой необходимости указывать эти самые типы, как это делается во многих реализациях. Значит, можно сделать следующим образом - тиклевскому array (в виде списка, разумеется) ставим в соответствие JSON object, а тиклевскому list - JSON array. В Tcl array будем рассматривать ключи как строки (и, если нужно, эскейпить и заключать в дойные кавычки). Отсюда получаем следующую реализацию:


# keys are not escaped!
# values may be any json serialized objects, arrays or strings
proc json::object {args} {
# XXX: Currently this API isn't symmetrical, as to create proper
# XXX: JSON text requires type knowledge of the input data
set json ""

dict for {key val} $args {
# key must always be a string, val may be a number, string or
# bare word (true|false|null)
append json "\"$key\": $val," \n
}

return "\{\n${json}\}"
}

# keys are not escaped!
# values will be escaped
proc json::object_escaped {args} {
# XXX: Currently this API isn't symmetrical, as to create proper
# XXX: JSON text requires type knowledge of the input data
set json ""

foreach {key val} $args {
# key must always be a string, val may be a number, string or
# bare word (true|false|null)
append json "\"$key\": [json::string_escaped $val]," \n
}

return "\{\n${json}\}"
}

# each argument must be prepared as correct json representation!
proc json::array {args} {
return \[[join $args ,]\]
}

# each argument must be prepared as correct json representation!
proc json::array_escaped {args} {
return \[[join [json::string_escaped $args] ,]\]
}

# see http://www.json.org/
proc json::string_escaped {val} {
if {[string is double -strict $val]==1
|| $val eq {true}
|| $val eq {false}
|| $val eq {null}
} {
return $val
}
# table of character substitutions
set meta {\" \\" \\ \\\\ \/ \\/ \b \\b \f \\f \n \\n \r \\r \t \\t}
return \"[string map $meta $val]\"
}


Следует отметить, что некоторые последовательности Unicode также следует обрабатывать, но пока не вижу в том потребности.

А вот и код функции ns_returnjson для AOLServer (замечу, что я собрал AOLServer с Tcl 8.5, который предоставляет возможность раскрытия списков с помощью конструкции {*}):

package require json

proc ns_returnjson {args} {
ns_return 200 text/plain [::json::object_escaped {*}$args]
}


Mime-тип указан "text/plain", т.к. иначе браузер предлагает сохранить полученный JSON-контент в файл. Да и в принципе непонятно, какой же тип "правильный" - то ли application/json, то ли text/x-json (или даже application/jsonrequest)...

В качестве ремарки примечание для javascript-девелоперов - в новых версиях браузеров перестала работать поддержка формата JSON многими яваскрипт-библиотеками. При указании типа данных JSON, Firebug выдает следующую ошибку "missing } in XML expression". Бороться с этим можно так:


// response = doc.body.innerHTML;
if (doc.body.getElementsByTagName("pre")[0] != undefined) {
response = doc.body.getElementsByTagName("pre")[0].innerHTML;
} else {
response = doc.body.innerHTML;
}


Как ни странно, готового решения вышеназванной проблемы в сети найти не удалось, пришлось разбираться самому. Как же достает временами этот самый яваскрипт...

Upd. 2009-10-24

В ИЕ6 некорректно работает разбор json, сгенерированного приведенным выше модулем. Приведенный ниже патч решает проблему.


--- json.tcl.old 2009-06-01 11:32:18.000000000 +0400
+++ json.tcl 2009-10-24 11:53:00.000000000 +0400
@@ -253,14 +253,12 @@
# XXX: Currently this API isn't symmetrical, as to create proper
# XXX: JSON text requires type knowledge of the input data
set json ""
-
dict for {key val} $args {
# key must always be a string, val may be a number, string or
# bare word (true|false|null)
- append json "\"$key\": $val," \n
+ lappend json \"$key\":$val
}
-
- return "\{\n${json}\}"
+ return \{[join $json ,]\}
}

# keys are not escaped!
@@ -269,14 +267,12 @@
# XXX: Currently this API isn't symmetrical, as to create proper
# XXX: JSON text requires type knowledge of the input data
set json ""
-
foreach {key val} $args {
# key must always be a string, val may be a number, string or
# bare word (true|false|null)
- append json "\"$key\": [json::string_escape $val]," \n
+ lappend json \"$key\":[json::string_escape $val]
}
-
- return "\{\n${json}\}"
+ return \{[join $json ,]\}
}

# each argument must be prepared as correct json representation!


Upd.
Как подсказывает Anton Kovalenko, при обработке строк вида "01" в моей реализации будет потерян "0". Поправить можно или проверкой для чисел, не начинаются ли они с нуля, или представлением всех чисел в виде строки. Пока не решил, какой вариант лучше.

(C) Alexey Pechnikov aka MBG, mobigroup.ru