Записки программиста, обо всем и ни о чем. Но, наверное, больше профессионального.

2012-06-29

Resumable HTTP file upload

Кто о чем, а вшивый о бане. В продолжение этого поста: возобновляемая закачка файлов на веб-сервер с использованием HTML5 FileAPI + Node.js + Socket.io
я проверил работоспособность предлагаемого решения
Заодно и код причесал (см.полный листинг в конце поста)

Кому лень читать многабукаф, заявляю сразу — предлагаемое решение вполне работоспособно. Если нужен веб-сервис по надежной, возобновляемой заливке мега(гига)файлов недорого — берите, пользуйтесь.

Остальные читают дальше :)

В результате получилась как бы инструкция по созданию веб-сервиса по заливке файлов практически любого размера на сервер. На самых современных технологиях :)

Для создания сервиса мне понадобится:

Виртмашина с дебианчиком

Со страницы debian.org/devel/debian-installer
забрал образ debian-wheezy-DI-a1-i386-netinst.iso
и поставил из него систему в виртуалбоксе. Выделил 2 ядра и 512 мегабайт оперативки.

Свежепоставленную систему доточил напильником:
nano .bashrc # для рута и своей учетки

shopt -s histappend
shopt -s cmdhist
export PROMPT_COMMAND='history -a'
export HISTFILESIZE=1000
export HISTSIZE=1000
export HISTCONTROL=ignoreboth:erasedups

# репозитории
nano /etc/apt/sources.list

deb http://mirror.yandex.ru/debian/ testing main contrib non-free
deb-src http://mirror.yandex.ru/debian/ testing main contrib non-free
deb http://security.debian.org/ testing/updates main
deb-src http://security.debian.org/ testing/updates main
deb http://mirror.yandex.ru/debian/ testing-proposed-updates contrib non-free main
deb-src http://mirror.yandex.ru/debian/ testing-proposed-updates contrib non-free main

# доустановить пакеты
aptitude update; aptitude safe-upgrade; aptitude full-upgrade
aptitude install linux-headers-$(uname -r) dkms make gcc g++
aptitude install git-core curl build-essential openssl libssl-dev wget
Сервер к приему софта готов.

Node.js

Веб-сервер Node.js ставится из исходников традиционным образом. Я собрал и запустил ноду по инструкции с сайта разработчиков github.com/joyent/node/wiki/Installing-Node.js-via-package-manager
# mkdir -p /root/build/node && cd $_
# git clone https://github.com/joyent/node.git .
# git checkout v0.6.19
# ./configure --openssl-libpath=/usr/lib/ssl
# make
# make test
# make install
# node -v
Хоть и написано «установка ноды через пакетный менеджер», инфы по установке из исходников там достаточно.
Теперь нужно выйти из рута, можно поиграться с нодой, позапускать хелловорд и прочее.

Socket.io

Это будет модуль к Node.js. Установка модуля проста до безобразия
$ mkdir -p ~/node && cd $_
$ npm install socket.io

# после заливки программ на сервер, запустить тут веб-сервер
# node app.js
Типа, всё, сервер готов. Дальше пишем программу.

Код программы

см.в самом низу. Здесь приведу только сниппеты. Еще раз — код не мой а Габриэля Манрика, я его творение только слегка переписал. CSS & HTML вообще не трогал.
Файлов два: один — это HTML для скармливания браузеру; второй — JS код сервера.

Ключевые места программы, приблизительный ход выполнения
// на клиенте
// по щелчку на кнопке «старт» в сокет уходит событие fileMeta с именем и размером файла;
// на событие «чтение файла ридером» вешается обработчик, который отправляет в сокет событие fileData
//с именем файла и куском считанных бинарных данных
var socket = io.connect(); // connect at the same host / port as your website
...
socket.on('nextChunk', onSocketNextChunk);
socket.on('fileProcessed', onSocketFileProcessed);
...
function onClickStartUpload() {
...
  fReader = new FileReader();
...
  fReader.onload = function(evnt) {
   socket.emit('fileData', {'Name' : fileName, 'Data' : evnt.target.result});
  }
  socket.emit('fileMeta', {'Name' : fileName, 'Size' : selectedFile.size});
…

// на сервере, приняв событие fileMeta, код проверяет наличие файла и открывает его на запись;
// открытие файла отправляет в сокет (в браузер) событие nextChunk
// с номером следующего чанка и числом отработанных процентов
var app = require('http').createServer(httpResponder)
 , io = require('socket.io').listen(app)
...
app.listen(8080); // http://servername:8080/
io.sockets.on('connection', onSocketConnect);
...
function onSocketConnect(socket) {
 socket.on( 'fileMeta', function(data) { onSocketFileMeta(data, socket); } );
 socket.on( 'fileData', function(data) { onSocketFileData(data, socket); } );
}
…
function onSocketFileMeta (data, socket) {
...
  var stat = fs.statSync(fullName);
  if(stat.isFile()) {
   fObj.rcvdBytes = stat.size;
...
 fs.open(fullName, 'a', 0644, function(err, fd) { onFileOpen(err, fd, fName, socket); });
…
function getNextChunk(fObj, socket) {
 var pct = (fObj.rcvdBytes / fObj.fSize) * 100;
 var chunkNum = fObj.rcvdBytes / chunkSize;
 socket.emit('nextChunk', { 'ChunkNum' : chunkNum, 'Percent' : pct });

// на клиенте, обработчик события nextChunk считывает из файла следующий чанк данных,
// после чего обработчик события «чтение файла ридером» отправляет в сокет событие fileData
function onSocketNextChunk (data) {
...
 var blob = getNextBlob(selectedFile, nextByte, chunkSize);
 fReader.readAsBinaryString(blob);
…
function getNextBlob(fileObj, startPos, numBytes) {
 var endPos = startPos + Math.min( numBytes, (fileObj.size - startPos) );
 if(fileObj.webkitSlice)
  return fileObj.webkitSlice(startPos, endPos);

// на сервере, приняв событие fileData, код ветвится на три дорожки:
// в приемном буфере еще есть место — запросить следующий чанк;
// буфер полон — сбросить его на диск и запросить следующий чанк;
// все данные уже здесь — завершить запись в файл, переместить его в репозиторий и отправить уведомление браузеру
function onSocketFileData(data, socket) {
...
 if(fObj.rcvdBytes == fObj.fSize) {
  fs.write(fObj.fHandle, fObj.bytesBuf, null, 'Binary', function(err, written, buffer) {
   onFileWriteDone(err, written, buffer, fName, socket);
  });
 }
 else if(fObj.bytesBuf.length > bufMaxSize) {
  fs.write(fObj.fHandle, fObj.bytesBuf, null, 'Binary', function(err, written, buffer) {
   onFileWriteBuffer(err, written, buffer, fName, socket);
  });
 }
 else {
  getNextChunk(fObj, socket);
 }
…
function onFileWriteBuffer(err, written, buffer, fName, socket) {
 fObj.bytesBuf = '';
...
 getNextChunk(fObj, socket);
}
…
function onFileWriteDone(err, written, buffer, fName, socket) {
...
 fs.close(fObj.fHandle, function(err) {
  fObj.fHandle = '';
...
 });
...
 util.pump(inp, out, function() {
  fs.unlink(tempName, function () {
   socket.emit('fileProcessed', {'Preview' : 'thumbnail.jpg'});
  });
 });
}
… и так далее
Все на асинхронных событиях и коллбеках, довольно прикольно.

Запуск и тестирование

После написания файлов надо залить их на сервер
rsync -av "/home/valik/Desktop/node/" "node:node/"
на сервере запустить ноду
$ cd node; mkdir Repo; mkdir Temp; node app.js

После успешного запуска я провел тестирование на предмет — как оно в целом. Выяснилось, что в целом — неплохо. За исключением того, что очень долго выполняется финальная стадия, а именно перенос закачанного файла из папки Temp в папку Repo. Этот кусок кода надо переделать.

В общем, файл размером 8621 мегабайт был передан успешно, со средней скоростью 3.5 мегабайта в секунду. Этот же файл через rsync залился со скоростью 25 мб/сек. Откуда берется такое замедление — неясно, задача почти не дает нагрузки на ресурсы компа.

Браузер (Chrome 19) сожрал 1 гигабайт вирт.памяти, 128 реальной оперативки, 70% процессора (одного ядра). Утечек памяти нет.
Node.js сожрала 140 мегабайт вирт.памяти, 90 реальной, 40% процессора (одного ядра). Утечек памяти нет. В сумме, четырехядерный процессор был занят на 15%.

Файл передавался чанками по 3 мегабайта; серверный буфер 10 мегабайт, после его заполнения буфер сбрасывается на диск.
Финальный сброс буферов и перенос файла из временной папки в репозиторий занял около 3-х минут.

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

Вывод.
Технология рабочая, рекомендую к использованию.


Полный код аплоадера


index.html
<!DOCTYPE html>
<html>
<head>
 <meta http-equiv="Content-type" content="text/html; charset=utf-8">
 <title>File Uploader</title>
 <script src="/socket.io/socket.io.js"></script>

<script type="text/javascript" charset="utf-8">

window.addEventListener("load", onDocReady);

var socket = io.connect(); // connect at the same host / port as your website
var fReader = '';         // FileReader object
var fileName = '';        // selected file name
var selectedFile = '';     // file object
var mbBytes = 1048576;     // bytes in megabyte
var chunkSize = 3 * mbBytes; // chunk size in bytes

socket.on('nextChunk', onSocketNextChunk);
socket.on('fileProcessed', onSocketFileProcessed);

function onDocReady() {
 if(window.File && window.FileReader) { //These are the necessary HTML5 objects the we are going to use
  document.getElementById('UploadButton').addEventListener('click', onClickStartUpload);
  document.getElementById('FileBox').addEventListener('change', onFileChosen);
 }
 else {
  document.getElementById('UploadArea').innerHTML = "Your Browser Doesn't Support The File API. Please Update Your Browser";
 }
} // function onDocReady()


function onClickStartUpload() {
 if(selectedFile !== '') {
  fReader = new FileReader();
  fileName = document.getElementById('NameBox').value;
  htmlUploading();
  fReader.onload = function(evnt) {
   socket.emit('fileData', {'Name' : fileName, 'Data' : evnt.target.result});
  }
  socket.emit('fileMeta', {'Name' : fileName, 'Size' : selectedFile.size});
 }
 else {
  alert("Please Select A File");
 }
} // function onClickStartUpload()


function onFileChosen(evnt) {
 selectedFile = evnt.target.files[0];
 document.getElementById('NameBox').value = selectedFile.name;
} // function onFileChosen(evnt)


function onSocketNextChunk (data) { // socket.emit('nextChunk', { 'ChunkNum' : chunkNum, 'Percent' : pct });
 var pct = data['Percent'];
 var chunkNum = data['ChunkNum']; // chunk number (0 <= chunkNum < n; n = fSize/chunkSize)
 htmlUpdatePct(pct);
 var nextByte = chunkNum * chunkSize; //The Next Blocks Starting Position
 var blob = getNextBlob(selectedFile, nextByte, chunkSize);
 fReader.readAsBinaryString(blob);
} // function onSocketNextChunk (data)


function getNextBlob(fileObj, startPos, numBytes) {
 var endPos = startPos + Math.min( numBytes, (fileObj.size - startPos) );
 if(fileObj.webkitSlice)
  return fileObj.webkitSlice(startPos, endPos);
 else
  return fileObj.mozSlice(startPos, endPos);
} // function getNextBlob(fileObj, startPos, numBytes)


function htmlUploading() {
 var htmlContent = "<span id='NameArea'>Uploading " + selectedFile.name + " as " + fileName + "</span>";
 htmlContent += '<div id="ProgressContainer"><div id="ProgressBar"></div></div><span id="percent">0%</span>';
 htmlContent += "<span id='Uploaded'> - <span id='MB'>0</span>/" + Math.round(selectedFile.size / mbBytes) + "MB</span>";
 document.getElementById('UploadArea').innerHTML = htmlContent;
} // function htmlUploading()


function htmlUpdatePct(percent) {
 document.getElementById('ProgressBar').style.width = percent + '%';
 document.getElementById('percent').innerHTML = (Math.round(percent * 100) / 100) + '%';
 var mbDone = Math.round(((percent / 100.0) * selectedFile.size) / mbBytes);
 document.getElementById('MB').innerHTML = mbDone;
} // function htmlUpdatePct(percent)


function onSocketFileProcessed (data) { // socket.emit('fileProcessed', {'Preview' : 'thumbnail.jpg'});
 var htmlContent = "File Successfully Uploaded!"
 htmlContent += "<img id='Thumb' src='" + data['Preview'] + "' alt='" + fileName + "'><br>";
 htmlContent += "<button type='button' name='Upload' value='' id='Restart' class='Button'>Upload Another</button>";
 document.getElementById('UploadArea').innerHTML = htmlContent;
 document.getElementById('Restart').addEventListener('click', onRefresh);
 document.getElementById('UploadBox').style.width = '270px';
 document.getElementById('UploadBox').style.height = '270px';
 document.getElementById('UploadBox').style.textAlign = 'center';
 document.getElementById('Restart').style.left = '20px';
} // function onSocketFileProcessed (data)


function onRefresh() {
 location.reload(true);
}

</script>

<style type="text/css" media="screen">
body {
 background: #F9F9F9;
 font-family: Calibri;
 font-size: 18px;
}

h2 {
 font-size: 40px;
 margin-top: 6px;
 margin-bottom: 10px;
}

#Thumb {
 max-width: 230px;
 max-height: 130px;
}

#ProgressContainer {
 width: 396px;
 height: 36px;
 background: #F8F8F8;
 margin-top: 14px;
 border: 1px solid #E8E8E8;
 border-top: 1px solid #D8D8D8;

 -webkit-border-radius: 4px;
 -moz-border-radius: 4px;
 border-radius: 4px;
 padding: 2px;
}

#ProgressBar {
 height: 100%;
 width: 0%;

 -webkit-border-radius: 4px;
 -moz-border-radius: 4px;
 border-radius: 4px;
 background: -webkit-gradient( linear, left top, left bottom, from(#a50aad), color-stop(0.50, #6b0d6b), to(#4a074a));
}

#UploadBox {
 background: #FFF;
 padding: 20px;
 position: absolute;
 top: 50%;
 left: 50%;
 margin-left: -200px;
 margin-top: -150px;
 height: 200px;
 width: 400px;
 border: 1px solid #DFDFDF;

 -webkit-box-shadow: 0px 0px 16px 0px rgba(0,0,0,0.2);
 -moz-box-shadow: 0px 0px 16px 0px rgba(0,0,0,0.2);
 box-shadow: 0px 0px 16px 0px rgba(0,0,0,0.2);

 -webkit-border-radius: 11px;
 -moz-border-radius: 11px;
 border-radius: 11px;
}

button.Button {
 font-size: 18px;
 color: #ffffff;
 padding: 8px 30px;
 background: -webkit-gradient( linear, left top, left bottom, from(#a50aad), color-stop(0.50, #6b0d6b), to(#4a074a));

 -webkit-border-radius: 5px;
 -moz-border-radius: 5px;
 border-radius: 5px;
 border: 1px solid #5b139e;

 -webkit-box-shadow: 0px 1px 3px rgba(000,000,000,0.5), inset 0px 0px 3px rgba(255,255,255,0.4);
 -moz-box-shadow: 0px 1px 3px rgba(000,000,000,0.5), inset 0px 0px 3px rgba(255,255,255,0.4);
 box-shadow: 0px 1px 3px rgba(000,000,000,0.5), inset 0px 0px 3px rgba(255,255,255,0.4);
 text-shadow: 0px -1px 0px rgba(000,000,000,0.1), 0px 1px 0px rgba(145,035,145,1);
 position: absolute;
 bottom: 20px;
 right: 20px;
 cursor: pointer;
}

button.Button:hover {
 background: -webkit-gradient( linear, left top, left bottom, from(#a50aad), color-stop(0.80, #6b0d6b), to(#a50aad));
 color: #D3D3D3;
}

button.Button:active {
 background: -webkit-gradient( linear, left top, left bottom, from(#4a074a), color-stop(0.80, #6b0d6b), to(#a50aad));
}

input {
 margin-top: 10px;
 margin-bottom: 8px;
}

input[type=text] {
 border: 1px solid #CDCDCD;
 border-top: 1px solid #676767;

 -webkit-border-radius: 3px;
 -moz-border-radius: 3px;
 border-radius: 3px;
 font-size: 18px;
 padding: 2px;
 width: 300px;
 margin-left: 10px;
}

</style>
</head>
<body>
 <div id="UploadBox">
  <h2>Chunked File Uploader</h2>
  <span id='UploadArea'>
   <label for="FileBox">Choose A File: </label><input type="file" id="FileBox"><br>
   <label for="NameBox">Name: </label><input type="text" id="NameBox"><br>
   <button type='button' id='UploadButton' class='Button'>Upload</button>
  </span>
 </div>
</body>
</html>


app.js
//# -*- mode: javascript; coding: utf-8 -*-

var tempDir = 'Temp';
var targetDir = 'Repo';          // mkdir Temp; mkdir Repo; node app.js
var mbBytes = 1048576;           // bytes in megabyte
var chunkSize = 3 * mbBytes;     // chunk size in bytes
var bufMaxSize = 10485760;       // 10 MB

var app = require('http').createServer(httpResponder)
 , io = require('socket.io').listen(app)
 , fs = require('fs')
 , exec = require('child_process').exec
 , util = require('util')
 , filesList = {};

app.listen(8080); // http://servername:8080/
io.sockets.on('connection', onSocketConnect);


function httpResponder(req, res) {
 fs.readFile(__dirname + '/index.html',
  function (err, data) {
   if (err) {
    res.writeHead(500);
    return res.end('Error loading index.html');
   }
   res.writeHead(200);
   res.end(data);
  }
 );
} // function httpResponder(req, res)


function onSocketConnect(socket) {
 socket.on( 'fileMeta', function(data) { onSocketFileMeta(data, socket); } );
 socket.on( 'fileData', function(data) { onSocketFileData(data, socket); } );
} // function onSocketConnect(socket)


function onSocketFileMeta (data, socket) { // start recieve file
 // socket.emit('fileMeta', {'Name' : fileName, 'Size' : selectedFile.size});
 var fName = data['Name'], fSize = data['Size'];
 var fullName = tempDir + '/' + fName;
 var fObj = {  // Create a new Entry in The Files List
  fSize     : fSize, // file size
  bytesBuf  : '',    // data buffer, 10 MB max
  rcvdBytes : 0,     // count of recieved bytes
  fHandle   : ''     // file handle
 }

 try { // if file exist already
  var stat = fs.statSync(fullName);
  if(stat.isFile()) {
   fObj.rcvdBytes = stat.size;
  }
 }
 catch(err) { // It's a New File
  console.log(err);
 }

 filesList[fName] = fObj;
 fs.open(fullName, 'a', 0644, function(err, fd) { onFileOpen(err, fd, fName, socket); });
} // function onSocketFileMeta (data, socket)


function onSocketFileData(data, socket) { // socket.emit('fileData', {'Name' : fileName, 'Data' : evnt.target.result});
 var fName = data['Name'];
 var blob = data['Data']

 var fObj = filesList[fName];
 fObj.rcvdBytes += blob.length;
 fObj.bytesBuf += blob;
 filesList[fName] = fObj;

 if(fObj.rcvdBytes == fObj.fSize) { //If File is Fully Uploaded
  fs.write(fObj.fHandle, fObj.bytesBuf, null, 'Binary', function(err, written, buffer) {
   onFileWriteDone(err, written, buffer, fName, socket);
  });
 } // finish

 else if(fObj.bytesBuf.length > bufMaxSize) { //If the Data Buffer reaches 10MB
  fs.write(fObj.fHandle, fObj.bytesBuf, null, 'Binary', function(err, written, buffer) {
   onFileWriteBuffer(err, written, buffer, fName, socket);
  });
 }
 else {
  getNextChunk(fObj, socket);
 }
} // function onSocketFileData(data, socket)


function getNextChunk(fObj, socket) {
 var pct = (fObj.rcvdBytes / fObj.fSize) * 100;
 var chunkNum = fObj.rcvdBytes / chunkSize;
 socket.emit('nextChunk', { 'ChunkNum' : chunkNum, 'Percent' : pct });
} // function getNextChunk(fObj, socket)


function onFileOpen(err, fd, fName, socket) {
 var fObj = filesList[fName];
 if(err) {
  console.log(err);
 }
 else {
  fObj.fHandle = fd; // We store the file handler so we can write to it later
  filesList[fName] = fObj;
  getNextChunk(fObj, socket);
 }
} // function onFileOpen(err, fd, fName, socket)


function onFileWriteBuffer(err, written, buffer, fName, socket) { // buffer writed
 var fObj = filesList[fName];
 fObj.bytesBuf = ''; // Reset The Buffer
 filesList[fName] = fObj;
 getNextChunk(fObj, socket);
} // function onFileWriteBuffer(err, written, buffer, fName, socket)


function onFileWriteDone(err, written, buffer, fName, socket) {
 var fObj = filesList[fName];
 fs.close(fObj.fHandle, function(err) {
  fObj.fHandle = '';
  filesList[fName] = fObj;
 });
 var tempName = tempDir + '/' + fName;
 var inp = fs.createReadStream(tempName);
 var out = fs.createWriteStream(targetDir + '/' + fName);
 util.pump(inp, out, function() {
  fs.unlink(tempName, function () {
   socket.emit('fileProcessed', {'Preview' : 'thumbnail.jpg'});
//   exec("ffmpeg -i Video/" + Name  + " -ss 01:30 -r 1 -an -vframes 1 -f mjpeg Video/" + Name  + ".jpg", function(err){
//    socket.emit('Done', {'Image' : 'Video/' + Name + '.jpg'});
//   });
  });
 });
} // function onFileWriteDone(err, written, buffer, fName, socket)


2012-06-27

Смешно?

Что мы знаем про смех? Как часто мы вообще задумываемся над природой смеха? А юмор?
Есть мнение, что смех это механизм сброса напряжения — человек упал, опасность! Ой, он просто подскользнулся, ничего страшного — хахаха. Самая распространенная шутка. Работает безотказно.

Но не все так просто:

Я это повторяю каждый раз, и не устану, и сейчас опять скажу. В человеческой природе нет ни единого уникального свойства. Вероятный общий предок всех Hominidae, живший примерно 12-16млн лет назад, - вот это было инновационное решение. А мы,- просто топовая модель в линейке (что, в общем, тоже неплохо). Таким образом, смех вообще появился где-то 5-7 миллионов лет назад, на уровне общего предка гоминид, и в дальнейшем усложнялся и эволюционировал. В более-менее нынешнем виде смех появился, когда люди уверенно встали на ноги, т.е. где-то с хабилиса, 2млн лет назад.

(Не важное, но интересное дополнение. Если честно, кроме людей еще смеются крысы. Обычный серые пасюки в игривом и довольном настроении издают не слышимый человеческим ухом щебет в 50кГц, который функционально и ситуационно является аналогом гоминидного смеха.)
Как смех появился и зачем он нужен?
Смех социален, имеет смысл только в рамках межперсональных взаимодействий. Наедине с собою люди улыбаются и смеются в 30 раз реже (причем даже когда человек смеется «тихо сам с собою», все равно там скорее всего присутствует «виртуальное» социальное взаимодействие). Произошел от сигналов тревоги и основан на тех же нейробиологических механизмах, что и страх- вы вряд ли отличите на фМРТ мозг смеющегося человека от мозга испуганного человека.
Общих эволюционных теорий комического существует множество, они частично дополняются, часто конфликтуют. Из самых известных, - модели Фредериксона (расшаренный эмоциональный статус), Рамачандрана (ложная тревога), Оуэна (защита от читера), Ризолатти (автоматическое резонансное поведение). Я, как убежденный сторонник идеи групповой эволюции, здесь и далее собираюсь тенденциозно освещать симпатичные мне концепции и замалчивать все прочие
Мы такой успешный вид, потому что у нас есть сложное и пластичное поведение. А сложное поведение у нас есть, потому что у нас есть социальность. А социальность у нас есть, потому что мы умеем испытывать личные чувства к другим человеческими особям. То есть у нас есть персонально окрашенные эмоции. А они у нас есть, потому что мы способны к эмоциональному вовлечению. А эмоциональное вовлечение у нас есть, потому что есть специальные структуры и механизмы, за это отвечающие. Например, - инсулярный и префронтальный кортекс. Например,- зеркальные нейроны и нейроны фон Экономо
Причем, что самое забавное, юмора-то до недавних пор вообще не было. Все было,- смех, веселье, забавы и потешки, ирония и сарказм. Веселились всегда. Юмора не было. Горилла Коко, которую обучили языку жестов, научилась играть со смыслами,- надевать на голову папку для бумаг и говорить «шляпа» (нормальная кстати шутка, с моей дочерью прокатывает). Смех, радость и веселье в базисе своем,- изначально в нас заложены. Хихикающие пастушки, увлекаемые фавнами в кусты, или нажраться на дионисийских мистериях и кидать кости в горбуна, или хохот от пьянящего чувства победы, пока твой противник елозит коленями в собственных кишках,- это всегда было необычайно смешно. Ни одна мифология не обходится без трикстера,- от Койота и Ананси, до Ивана-дурака и Ходжи. Шуты и скоморохи, сатурналии и вакханалии, карнавалы и прочая масленица. Культура смеха и веселья есть у всех. Но сказать, чтобы это в нашем современном понимании было «юмором»,- очень сильная натяжка. Возможно, многим знакомо недоумение от комедий Шекспира или Мольера. Ну забавно, ну занимательно, но шутки-то где? Почему это комедия? Где смеяться? Ну забавное, ну занимательное, но вот чтобы бугага? Хм хм. Первые юмористы в истории,- это Дж.Джером и М.Твен


Очень интересно.

2012-06-26

T from ETL

Периодически бывает нужно переложить данные из емкости в емкость. Например из DBF в БД Оракла. По дороге, как водится, подчистить и подправить всякое. Для этого давно придумали кучу инструментов под общим названием ETL.

Вот еще один в обойму, на Python . Делает преобразование более декларативным.
Сравним:

решим пока в лоб.
new_data = {
        'name': sample_data['userNameFirst'],
        'second_name': sample_data['userNameSecond'],
        'password': hashlib.md5(sample_data['userPassword']).hexdigest(),
        'roles': [s.strip() for s in sample_data['userRoles'].split(',')]
    }
if 'userEmail' in sample_data:
    new_data['email'] = sample_data['userEmail']
new_data['title'] = sample_data.get('userTitle', 'Bachelor')

assert new_data == desired_data, 'Uh oh'
Ах, хороший знакомый код без излишней сложности, прекрасно. Но что будет когда придет полная спецификация? Видимо вернемся ко второму варианту, добавим к нему проверку данных, хорошие сообщения об ошибках, упакуем его в библиотеку и будем использовать.
Хм, но уже есть такая библиотека, смотрите:
import trafaret as t

hash_md5 = lambda d: hashlib.md5(d).hexdigest()
comma_to_list = lambda d: [s.strip() for s in d.split(',')]

converter = t.Dict({
    t.Key('userNameFirst') >> 'name': t.String,
    t.Key('userNameSecond') >> 'second_name': t.String,
    t.Key('userPassword') >> 'password': hash_md5,
    t.Key('userEmail', optional=True) >> 'email': t.Email,
    t.Key('userTitle', default='Bachelor') >> 'title': t.String,
    t.Key('userRoles') >> 'roles': comma_to_list,
})

assert converter.check(sample_data) == desired_data

Это чертовски удобно, когда можно только в одном месте записать соответствие полей.

2012-06-23

Тренажер для управленца

Прочел вчера весьма примечательный пост otecsergiy.livejournal.com/516120

Владелец бизнеса проводит параллели, рассматривая свое дело (рынок) как модель государства. Можно найти много недостатков и натяжек в таком сравнении, но это не так интересно. Лично меня зацепило фундаментальное свойство рассматриваемых систем, а именно — социум и необходимость компромиссов, что при рулении рынком, что при рулении государством. Нельзя игнорировать существующий социум.

Мне кажется, было бы полезно (для всех) запустить серию компьютерных игр — по типу Цивилизации скрещенной с Симсами. Игра первая — имитатор управления рынком, консультант разработки - отецсергий. В исходных (стартовых) данных для игры, помимо всяких активов и пассивов бизнеса, участвуют персонажи арендаторов, генерируемые случайно на основе весов ключевых характеристик — жадность-щедрость, тупость-мозговитость, лень-оборотистость, и так далее.

Имитатор управления автосервисом.

Строительно-ремонтной компанией (консультант Мастер).

И так вплоть до управления государством. Ведь странное дело, пилотов гоняют на тренажерах и имитаторах полета, хотя цена ошибки редко превышает сотни жизней. а управлять компанией и даже государством может кухарка? А ведь цена ошибки - миллионы жизней.

Думаю, такие игры принесли бы немало пользы. Например, при повышении градуса обсуждении «этойстраны» на кухне, можно было бы спросить о достижениях спорящих в том или ином имитаторе. Типа, если ты не дошел даже до 3-го уровня в управлении универмагом, о чем с тобой можно говорить, ты же нихрена не понимаешь в вопросе.

Во всяком случае, лично я, играя в тактические симуляторы типа SWAT, стал гораздо лучше понимать, почему ОМОН-овцы и прочие силовики поступают так а не иначе.

2012-06-22

Javascript FSM


Поскольку мне не подошла готовая библиотека, реализующая подход FSM, я решил делать свой вариант реализации. Простой, ничего лишнего, без блекджека-и-шлюх. В моем варианте реакция на событие определяется однозначно через текущее состояние и само событие. Фактически, строка «currentState_eventName» дает ссылку на функцию для исполнения.

Законченный пример (без HTML кода):
$(document).ready(onDocReady());

function onDocReady() {
 try {
  $(document).stopTime('doWork');
  $(document).everyTime(999, 'doWork', onTimerWork);
 } catch(ex) {
  console.log('Error in function onDocReady: ' + ex.description, true);
 }
} // function onDocReady()


function onTimerWork(i) { // work cycle
 try {
  // app ready?
  if (VSClient.isBusy) return;
  if (VSClient.currentState == '') { 
   VSClient.configure();
   return;
  }

  VSClient.procEvent('timer');

 } catch(ex) {
  $(document).stopTime('doWork');
  VSClient.procEvent('error');
  log('Error in function onTimerWork: ' + ex.description, true);
 }
} // function onTimerWork(i)


function onSelectFile() { // onClick button 'SelectFile...'
 try {
  VSClient.procEvent('selectFile');
 } catch (ex) {
  log('Error in function onSelectFile: ' + ex.description, true);
  VSClient.procEvent('error');
 }
 return true;
} // function onSelectFile()


VSClient = { // Object with app data
 sl: '' // Silverlight object
 , isBusy: false
 , fsmTable: [
  { from: '', event: 'configure', to: 'ready', action: 'configure' }
  , { from: 'ready', event: 'selectFile', to: 'wait4FileName', action: 'selectFile' }
  , { from: 'wait4FileName', event: 'timer', to: 'haveFileName', action: 'getFileName' }
  , { from: '*', event: 'error', to: 'error', action: 'stopOnError' }
 ]
 , currentState: ''
 , fsmArray: {}

 , configure: function() { // on page loaded, init app data
  log('VSClient.configure');
  this.isBusy = false;
  this.sl = $('#silverlightObject')[0].content.vcuSL;
  this.currentState = 'ready';
  for(var n = 0; n < this.fsmTable.length; n++) {
   var itm = this.fsmTable[n];
   this.fsmArray[itm.from + '_' + itm.event] = itm;
  }
  try {
   var res = this.sl.procMessage('test', '');
  } catch (ex) {
   this.sl = '';
   this.currentState = '';
  }
  log('VSClient curr.state: ' + this.currentState);
 } // configure: function(){}
}; // VSClient


VSClient.procEvent = function(evt) {
 var llog = log;
 if (evt == 'timer') llog = function(){;}
 var ind = this.currentState + '_' + evt;
 var itm = this.fsmArray[ind] || this.fsmArray['*_' + evt] || '';
 if (itm == '') {
  llog('VSClient.procEvent, no such transition: ' + ind);
  return;
 }
 llog('VSClient.procEvent, tableIndex: ' + ind);
 this.currentState = itm.to;
 llog('VSClient curr.state: ' + this.currentState);
 if (itm.action == '') {}
 else {
  this[itm.action].call(this);
 }
} // VSClient.procEvent = function(evt)


// { from: 'ready', event: 'selectFile', to: 'wait4FileName', action: this.selectFile }
VSClient.selectFile = function() { // fsm action
 log('VSClient.selectFile');
 var res = this.sl.procMessage('selectFile', '');
 if(res == '' || res.indexOf('fail') == 0) {
  // error
  log('VSClient.selectFile, error: ' + res, true);
 } else {
  // wait for SL, user must pick the file
  $('#inpFileName').val('');
 }
} // VSClient.selectFile = function()


// { from: 'wait4FileName', event: 'timer', to: 'haveFileName', action: this.getFileName }
VSClient.getFileName = function() { // fsm action
// check if user pick the file
 var res = VSClient.sl.procMessage('selectFile', 'getName');
 if(res == '') {
  // file not selected
  this.currentState = 'wait4FileName';
  return;
 }
 log('VSClient.getFileName, res: [' + res + ']');
 log('VSClient curr.state: ' + this.currentState);
 $('#inpFileName').val(res);
} // VSClient.getFileName = function()


// { from: '*', event: 'error', to: 'error', action: this.stopOnError }
VSClient.stopOnError = function() { // fsm action
 log('VSClient.stopOnError');
} // VSClient.stopOnError = function()

По сравнению с тем, что было (нагромождение if-else), как небо и земля. Теперь код понятнее изрядно.


Вообще, это я все терзаю задачу «resumable http chunked file upload». Просто эталонная задача для прокачки скиллов. Вот, к примеру, на хабре попалось:

Если вы когда-либо загружали видеофайл на сайт, то знаете это чувство когда загрузилось 90% и вы случайно обновляете страницу.
В этом учебном руководстве я покажу, как сделать видео загрузчик для сайта, который может возобновить прерванную загрузку, и генерировать обложку после завершения.
...
Чтобы сделать этот загрузчик, сервер должен отслеживать процесс загрузки, что бы была возможность восстановить его при обрыве. Чтобы выполнить эту задачу... с помощью Node.js...
мы будем использовать Socket.io...


Это перевод вот этой статьи. Ужасный перевод, но суть уловить можно. Как совершенно верно заметили в каментах:

Стрёмный перевод.
Но есть два полезных момента в статье: из неё вы узнаете, что Socket.io (посредством WebSocket) умеет передавать двоичные данные, и что файл на клиенте можно разбивать на куски с помощью slice api.

Пойду, попробую решить задачу возобновляемой отгрузки файлов на связке HTML5 FileAPI + Node.js + Socket.io
Надо же проверить, где грабли лежат. А вдруг многогигабайтные файлы невозможно обработать? Или — а нахрена тут Node.js, что, без него нельзя?

2012-06-21

Мастер

С подачи Гоблина открыл для себя еще одного толкового человека:

Алексей Земсков

Высшая степень уважения к заказчику — сделать свою работу как следует, а не как он хочет.


Если вам предстоит делать ремонт квартиры/дома или строить — загляните к Алексею, не пожалеете. А какие у него видео ролики — пестня!
Подходом к делу напоминает Артемия нашего Лебедева.

FSM

Захотелось мне странного намедни. Хочу код яваскриптовый попроще и попонятнее сделать в одном из проектов. А для этого его надо переписать в виде FSM. Как всегда, первый вопрос — самому делать велосипед или готовый взять? Решил попробовать готовый

Уже начал все переписывать и чую, не идет. Долго думал, почему. Потом понял — банально неудобно.

Как выглядит в библиотечке создание матрицы состояний:
var fsm = StateMachine.create({
  initial: 'green',
  events: [
    { name: 'warn',  from: 'green',  to: 'yellow' },
    { name: 'panic', from: 'yellow', to: 'red'    },
    { name: 'calm',  from: 'red',    to: 'yellow' },
    { name: 'clear', from: 'yellow', to: 'green'  }
]});
Где тут actions? Нету. Действия задаются как реализация колбеков:
events: [
    { name: 'warn',  from: 'green',  to: 'yellow' },
    { name: 'panic', from: 'yellow', to: 'red'    },
    { name: 'calm',  from: 'red',    to: 'yellow' },
    { name: 'clear', from: 'yellow', to: 'green'  }
  ],
  callbacks: {
    onpanic:  function(event, from, to, msg) { alert('panic! ' + msg);               },
    onclear:  function(event, from, to, msg) { alert('thanks to ' + msg);            },
    ongreen:  function(event, from, to)      { document.body.className = 'green';    },
    onyellow: function(event, from, to)      { document.body.className = 'yellow';   },
    onred:    function(event, from, to)      { document.body.className = 'red';      },
}
//    onbeforeevent - fired before the event
//    onleavestate - fired when leaving the old state
//    onenterstate - fired when entering the new state
//    onafterevent - fired after the event
И нет способа связать действие с парой Состояние_Событие. Есть отдельно на состояние и отдельно на событие, что размазывает код на дополниетельные if-else. Неудобно и некрасиво выходит.


А удобно было бы так (с форума python.su):
        self.table = {
            ('START', 'EOL'):       ('EXIT', None),
            ('START', 'GOODDIGIT'): ('GOOD', self.add_to_result),
            ('START', 'DELIM'):     ('START', None),
            ('GOOD', 'GOODDIGIT'):  ('GOOD', self.add_to_result),
            ('GOOD', 'BADDIGIT'):   ('BAD', self.clean_result),
…
    def parse(self, inp, state='START'):
        while state != 'EXIT':
            event = self.get_event(inp)
            state, action = self.table[(state, event)]
            if action:
                action()
На входе текущее состояние и событие, на выходе следующее состояние и функция для запуска. Удобно.

Так что для моей задачи готовый велосипед не подошел. Придется делать свой.

2012-06-19

Cancer

Уже не помню, откуда я утянул эту инфографику


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

2012-06-18

Альтернатива

Я уже отмечал историю Мартина Химаера, великий человек. Не побоялся развязать небольшую войнушку, отстаивая свои интересы.

А вот альтернативный взгляд на те давние события:

Есть в штате Колорадо город Грэнби, весьма небогатый и захолустный. Штат сам богатством не блещет, а округ Грэнд к процветающим не относится. И вот в этом городе решили построить цементный завод. В штате начинался строительный бум, ...
А где стройка - там цемент нужен.
А что такое завод в городишке, который живет с того бизнеса, что есть только при дороге? Это как манна небесная. Налоги, рабочие места, инфраструктура.
Но жил в городе некий мужик, который зарабатывал на жизнь тем, что варил глушаки грузовикам. Был у него кусок земли, на нем мастерская, при этом ни жены, ни детей - дожил до седых мудей, а все бобыль бобылем. Звали его, как уже все догадались, Марвин Химейер. Ага.
Чего он такой радостный на фото? А он просто понял, что сорвал банк, потому что его земля была как раз на пути коммуникаций к заводу и получить с этого он решил по-максимуму. Ему предложили землю продать, давая рыночную цену. Это на выезде из города, там много таких мастерских-халабуд, но он отказался и выдвинул контр-предложение. Дом на земле в этом месте стоит примерно 50 тыс., но Марвин запросил, насколько я помню, сперва 270 тыс. Цементная компания согласилась, но слишком легко, герой Марвин понял, что наебался и захотел уже полмиллиона. И они опять согласились, хоть компания и не крупная. Все уже подписали, и тут Марвин понял - опять наебался! Они бы и больше дали! И затребовал он уже миллион.
И вот тут нашла коса на камень, покупатель решил такой разводки уже не терпеть. А заодно на Марвина ополчились сограждане, которые из-за его жлобства не могли получить желанную работу.


Особенно доставляют каменты.
Ну что тут скажешь? У судей очень тяжелая работа — послушать обвинение, так ответчик жадина, мудак и уголовник. Послушать защиту, так ответчик просто скромный американский герой. Истина, как всегда, где-то между. Только факты, документы и сопоставление свидетельских показаний поможет нарисовать более-менее реалистичную картину.

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

2012-06-15

Экономия должна быть экономной

Самостоятельным людям в помощь. Как сэкономить при заказе отеля.

Самые известные такие сайты - Expedia, Priceline, Hotels.com, Kayak, и ещё вагон и маленькая тележка. Но нас в данный момент отдельно интересует Priceline. Точнее, его особая специательная фича. Она называется "Name your own price".

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

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

от собссно duginov: пошаговая инструкция на предмет "как не просто сократить элемент риска на Priceline, а вообще получить именно то, что хочется" :).


HiLoad в реальной жизни

Или хроники одного сайта. Завалялось у меня тут в букмарках, история про то как немного денег заработать и много фана получить.

Очень занимательная история

3 марта я максимально закешировал средставми CMS (MODx) все страницы сайта (кстати, браузерное кэширование у меня было к тому моменту уже давно настроено), увеличил оперативку до 3Гб, а так же вынес статический контент на отдельный сервер и спокойно ушёл спать. Проснувшись в 11 утра (MSK+3) я обнаружил, что серверы уже на пределе и нужно что-то делать. Благо, сайт был на облаке и у меня были широкие возможности для маневров.
Первым делом выделил БД на отдельный сервер. Помогло, но не надолго.
Увеличил оперативку до 8Гб и число ядер процессоров до 8. Ещё час жизни сайта был выигран.
И тут проснулась Москва! Стало страшно
Итоговые результаты проекта:

За период активного голосования (3 месяца) сайт посетило около 2млн. человек
Из них около 500 тыс. — за сутки с 8:00 4 марта по 8:00 5 марта (MSK).
За эти же сутки было просмотрено около 1 200 000 страниц.
За эти же сутки исходящего трафика: 0,5 Тб.
Затрачено на разработку и поддержку около 60 000 руб. Из них: 33 000 руб. на разработку, 8 000 руб. на хостинг, 19 000 руб. на контентную и другую поддержку.
Получено от рекламы Google Adsense около 190 000 руб.
Приобретено опыта: бесценно.


Просто экшн какой-то.

2012-06-14

Хороший backup

Интересный струмент:

увидел свет первый стабильный релиз инструмента для организации резервного копирования данных - Obnam 1.0... Код программы написан на языке Python и распространяется в рамках лицензии GPLv3+. Готовые пакеты сформированы для Ubuntu (PPA) и Debian.
Резервные копии размещаются в специальном репозитории, данные в котором хранятся в оптимальном представлении с использованием дедупликации
Поддержка размещения репозитория для хранения резервных копий на локальном диске и на внешних серверах с использованием ... обычного SSH-доступа
Режим доступа к резервным копиям в форме снапшотов
Поддержка режимов работы push и pull. В режиме push программа obnam устанавливается на стороне клиента и сохранение резервных копий инициируется клиентом. В режиме pull программа obnam устанавливается на сервер для хранения резервных копий и процесс копирования данных инициируется сервером (данные передаются по SFTP). С точки зрения безопасности предпочтителен режим push, так как для создания полной резервной копии в режиме pull требуется открытие удалённого доступа к ФС клиента с правами root

Один минус — пока не ясно, будет ли работать под MS Windows?


we wanted to have a name that was short, reasonably pronounceable by major cultures, and did not have too many Google hits. After much trying, we failed to come up with anything good. However, without a name, you can't even start the project, because what will you call its source directory? So we chose the OBligatory NAMe.

Надо будет глянуть под мелкоскопом.

2012-06-12

Silverlight всё?

С полей сражений платформ «богатых» веб-клиентов (rich web client) доходят печальные вести. Бойцы гибнут один за другим.

Уже более года проект Moonlight находится в глубокой стагнации и не развивается. По словам Мигеля его команда не заинтересована в разработке данного проекта и прекратила его развитие.
В настоящее время все связанные с Moonlight наработки доступны в GitHub, при этом последний коммит был совершён 18 мая прошлого года.
Примечательно, что компания Microsoft также фактически отказалась от разработки Silverlight в пользу технологий HTML5, которые будут использоваться в Windows 8. Silverlight 5 был выпущен в конце прошлого года и будет официально поддерживаться до 2021 года, но это будет последним значительным релизом платформы, развитие которой приостановлено.


Сначала отвалился Flash, теперь Silverlight. Java апплеты так и не прижились толком. В гордом одиночестве остается недоделанный HTML5. И опять масса разработчиков на ходу осваивает новые технологии, борется с глюками и переделывает продукты, вместо того чтобы плавно наращивать полезность своих творений.

Конечно же, нас уверяют, что «поддержка продлится до...» , но кто в здравом уме будет вкладываться в технологию, которую похоронили авторы?

rsync 4 Windows

В мире юниксовом хорошо известна тулза rsync. Резервные копии, просто копирование, синхронизация и подобные ништяки. А что в MS Windows? А в ней, родимой, есть DeltaCopy

DeltaCopy uses the rsync as-is - meaning the binaries for rsync that are available on Linux/UNIX operating systems are recompiled on Windows using CYGWIN libraries
DeltaCopy uses the rsync protocol to transfer files. This protocol listens on TCP/IP port 873 by default.
rsync protocol by itself does not offer a secure channel of communication. However, when bundled with SSH, rsync can be performed securely with encryption.
Using DeltaCopy users can backup/restore files from an rsync daemon running on any platform. DeltaCopy server is typically used on Windows that runs rsync daemon in the background. Linux/UNIX comes built-in with rsync binaries and therefore, DeltaCopy is not required.
DeltaCopy uses CYGWIN libraries to run rsync on Windows. Windows ACL is not compatible with UNIX Posix permissions. Due to this compatibility users often run into permission issues on the server-end.


А еще есть cwRsync.

Жить можно.

2012-06-08

LLVM

В последнее время все чаще и чаще в новостях встречается аббревиатура LLVM, хотя проекту, стоящему за этими буквами уже больше 10 лет. Что же такого особенного в, казалось бы, еще одном компиляторе?

LLVM is an umbrella project that hosts and develops a set of close-knit, low-level toolchain components (assemblers, compilers, debuggers, etc.), which are designed to be compatible with existing tools typically used on UNIX systems. The name "LLVM" was once an acronym, but is now just a brand for the umbrella project. While LLVM provides some unique capabilities and is known for some of its widely used tools (the Clang compiler, a C/C++/Objective-C compiler that provides a number of benefits over the GCC compiler), the main thing that sets LLVM apart from other compilers is its internal architecture.
The most popular design for a traditional static compiler (like most C compilers) is the three phase design whose major components are the front end, the optimizer, and the back end
While the benefits of a three-phase design are compelling and well-documented in compiler textbooks, in practice, it is almost never fully realized
let's dive into LLVM: The most important aspect of its design is the LLVM Intermediate Representation (IR), which is the form it uses to represent code in the compiler. LLVM IR is designed to host mid-level analyses and transformations that you find in the optimizer section of a compiler. It was designed with many specific goals in mind, including supporting lightweight runtime optimizations, cross-function/interprocedural optimizations, whole program analysis, and aggressive restructuring transformations, etc. The most important aspect of it, though, is that it is itself defined as a first class language with well-defined semantics.
...
After all, the goal isn't to be perfect, it is to keep getting better over time.


Очень хорошо, доступно и внятно написанная статья. Весьма поучительно.

2012-06-07

Бюрократизмы

Я три раза читал текст:

С 1 января 2012 года вступили в действие изменения, внесенные в Кодекс РФ об административных правонарушениях, по которым можно привлекать к ответственности собственников транспортных средств за нарушение в области благоустройства территории. Для этого достаточно сфотографировать или снять на видео припаркованный на газоне автомобиль.
Ответственность за размещение автотранспорта на территории, занятой зелеными насаждениями, предусмотрена и статьей 4.41 КоАП г. Москвы, которая входит в раздел нарушений в области охраны окружающей среды и природопользования. Однако, по этой статье сложно привлекать к административной ответственности собственника транспортного средства, если его нет рядом с припаркованной машиной. В связи с этим, в настоящее время Департаментом природопользования и охраны окружающей среды Москвы подготовлен законопроект по внесению изменений в КоАП г. Москвы в части переноса данной статьи в раздел нарушений в области благоустройства города. Внесенные изменения позволят привлекать к административной ответственности собственников транспортных средств при помощи фото- и видео фиксации.


И только на третий раз понял, о чем идет речь. Перевожу на понятный.
Смысл в том, что на текущий момент «парковка на газоне» относится к нарушениям природоохраны и природопользования. За эти нарушения наказать можно только если поймать за руку негодяя. Поэтому грядет изменение в кодексе, после которого такие нарушения будут считаться нарушениями «благоустройства города», за которые можно наказать имея только фото или видео машины на газоне.
А пока по прежнему.

Будем надеяться, что говнища на дорогах станет меньше а газоны станут зеленее. «Авточмо» в помощь.

А с другой стороны, бестолковые градоустроители часто так организуют дворы, что машину поставить просто некуда. Или на тротуар или на газон. Вот тут автовладельцам придется туго. Так что, во многих случаях, большую часть «газонов» надо или закатать в асфальт или, что лучше, накрыть сверху бетонными решетками (чтобы и травка прорастала и колеса до говнозема не доставали). Хотя бы вокруг многоэтажек. Можно посовещаться с жилсоветами, можно пройтись вечерком (когда все приехали домой) по району и осмотреть площади — и сразу всё станет ясно — что закатывать в асфальт а что засаживать зеленью.


Пара цитат по теме:

Сейчас вернулся в Таллин. Лет 5-6 назад на газонах парковались все, кому не лень. Штрафовать за неправильную парковку поручили частной коммерческой организации, которая имеет процент со штрафов. Самих штрафующих почти никогда нигде не видно, но стоит только на секунду отвернуться от неправильно припаркованной машины - на лобовом стекле моментально материализуется квитанция на 30 евро. Паркующихся на газонах уже очень давно не видел


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


Демонизация

На днях я упоминал инсталляцию, в которой к базе данных Redis добавлен веб-интерфейс посредством Webdis. Весь винегрет прекрасно работает, но есть но. Webdis приходится запускать руками при перезапуске сервера (машины).

Раз запустил, два. На третий раз понял, что так не пойдет. И демонизировал вебдис. В смысле, интегрировал его в систему стартовых скриптов Debian.

В конфиге Webdis есть опция отвечающая за форк процесса в фон. Включил ее
nano /root/webdis/webdis.json
 "daemonize":    true,

Потом скрипт «сервиса»
cp /etc/init.d/redis-server /root/webdis/webdis-server
nano /root/webdis/webdis-server
cp /root/webdis/webdis-server /etc/init.d/webdis-server
update-rc.d webdis-server defaults
service webdis-server start

Получившийся скрипт /etc/init.d/webdis-server прост как валяный сапог
#! /bin/sh
### BEGIN INIT INFO
# Provides:             webdis-server
# Required-Start:       $syslog $remote_fs $redis-server
# Required-Stop:        $syslog $remote_fs $redis-server
# Should-Start:         $redis-server
# Should-Stop:          $redis-server
# Default-Start:        2 3 4 5
# Default-Stop:         0 1 6
# Short-Description:    webdis HTTP server
# Description:          Webdis - HTTP interface to Redis
### END INIT INFO


PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DAEMON=/root/webdis/webdis
DAEMON_ARGS=
NAME=webdis-server
DESC=webdis-server

RUNDIR=/root/webdis
PIDFILE=$RUNDIR/redis-server.pid

test -x $DAEMON || exit 0

if [ -r /etc/default/$NAME ]
then
        . /etc/default/$NAME
fi

set -e
cd $RUNDIR

case "$1" in
  start)
        echo -n "Starting $DESC: "
        mkdir -p $RUNDIR
        touch $PIDFILE
        chown root:root $RUNDIR $PIDFILE
        chmod 755 $RUNDIR

        if [ -n "$ULIMIT" ]
        then
                ulimit -n $ULIMIT
        fi

        if start-stop-daemon --start --quiet --umask 007 --pidfile $PIDFILE --chuid root:root --chdir $RUNDIR --exec $DAEMON -- $DAEMON_ARGS
        then
                echo "$NAME."
        else
                echo "failed"
        fi
        ;;
  stop)
        echo -n "Stopping $DESC: "
        if start-stop-daemon --stop --retry forever/TERM/1 --quiet --oknodo --pidfile $PIDFILE --exec $DAEMON
        then
                echo "$NAME."
        else
                echo "failed"
        fi
        rm -f $PIDFILE
        sleep 1
        ;;

  restart|force-reload)
        ${0} stop
        ${0} start
        ;;

  status)
        echo -n "$DESC is "
        if start-stop-daemon --stop --quiet --signal 0 --name ${NAME} --pidfile ${PIDFILE}
        then
                echo "running"
        else
                echo "not running"
                exit 1
        fi
        ;;

  *)
        echo "Usage: /etc/init.d/$NAME {start|stop|restart|force-reload}" >&2
        exit 1
        ;;
esac

exit 0

Ребутнул машину, дернул за веб-интерфейс — работает. Просто, да?

2012-06-06

Remote Git repository (HTTP/HTTPS)

Захотелось мне сделать сервер с репозиториями Git, и чтобы работать с ними через HTTP/HTTPS. Как сказал бы Бендер - Well, I gonna go build my own Git server with blackjack and hookers.

Пацан сказал — пацан сделал. Даю отчет о создании HTTP(S) Git сервера on Debian Wheezy.

Сначала надо поднять сервер Git (Gitolite)

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

на клиенте от имени valik
  ssh-keygen
  ssh-copy-id '-p 22 -i ~/.ssh/id_rsa valik@scmserv'
  ssh -p 22 -i ~/.ssh/id_rsa valik@scmserv
  cp ~/.ssh/authorized_keys /tmp/valik.pub
  exit
  sudo aptitude install git

на сервере от рута
  useradd -d /home/git/ -m git
  passwd git
  aptitude install gitolite
  chmod a+r /tmp/valik.pub
  su -l git
  gl-setup -q /tmp/valik.pub

обратно на клиенте
  git clone ssh://git@scmserv/gitolite-admin
  cd gitolite-admin/
  nano conf/gitolite.conf # добавил репозиторий test_td
  git add .
  git commit -m "new repo"
  git commit --amend --author='valik <vasnake@gmail.com>'
  git push
  ssh git@scmserv info

В итоге на сервере scmserv есть репозиторий test_td с которым уже можно работать через сеть, по каналу ssh.

Теперь клиент для винды

Тоже ничего сложного

Если вышеупомянутый при установке сервера ключ ssh зацепить за сессию ssh в PuTTY, проблем не будет. А для этого надо сконвертировать ключ SSH в ключ PuTTY при помощи тулзы puttygen.exe, скормив ей файл id_rsa

Еще я споткнулся об формирование урла к репозиторию. Правильный URL, на текущий момент, выглядит так
ssh://git@scmserv/test_td
хотя и есть соблазн написать
ssh://valik@scmserv/test_td.git
но его надо побороть, ибо имя пользователя гитолайт возьмет из ключа, который возьмется из сессии путти, а добавка .git тут в принципе не нужна.

В ЧерепахнутомГите (tortoisegit) все операции выполняются по правой кнопе мыши. Наигравшись, можно приступать к следующему шагу, благодяря которому можно и не заморачиваться с ключами для PuTTY, ибо далее мы получим доступ к хранилищу (можно я так буду называть repository?) через HTTP(S).

Git repository which can be pushed into and pulled from over HTTP

Оказывается, все что нужно, это обеспечить доступ к папке с хранилищем через WebDAV.
По сцылкам приведены инструкции, которыми я пользовался. Как всегда, жизнь потребовала корректив. Вот ниже и зафиксирована моя персональная версия инструкции по созданию HTTP(s) канала к Git repository.

Apache web server, SSL


Понятно, первым делом
aptitude install apache2 openssl

Потом надо озаботится ключом и сертификатом для SSL
root@scmserv:~# mkdir /etc/ssl/localcerts
root@scmserv:~# openssl req -new -x509 -days 3650 -nodes -out /etc/ssl/localcerts/apache.pem -keyout /etc/ssl/localcerts/apache.key
 Country Name (2 letter code) [AU]:RU
 State or Province Name (full name) [Some-State]:Moscow
 Locality Name (eg, city) []:Moscow
 Organization Name (eg, company) [Internet Widgits Pty Ltd]:ALGIS LLC
 Organizational Unit Name (eg, section) []:IT dept
 Common Name (e.g. server FQDN or YOUR name) []:scmserv.algis.com
 Email Address []:valik@algis.com
root@scmserv:~# l /etc/ssl/localcerts/apache*
-rw------- 1 root root  916 Jun  5 17:22 /etc/ssl/localcerts/apache.key
-rw------- 1 root root 1086 Jun  5 17:22 /etc/ssl/localcerts/apache.pem
Получили самоподписанный сертификат.

chmod 600 /etc/ssl/localcerts/apache*
chown www-data /etc/ssl/localcerts/apache*
a2enmod ssl

Конфиги сайта
nano /etc/apache2/sites-available/default
 <VirtualHost *:80>
   ServerAdmin webmaster@localhost
   DocumentRoot /var/www
   <Directory />
     Options FollowSymLinks
     AllowOverride None
   </Directory>
   <Directory /var/www/>
     Options Indexes FollowSymLinks MultiViews
     AllowOverride None
     Order allow,deny
     allow from all
   </Directory>
   ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
   <Directory "/usr/lib/cgi-bin">
     AllowOverride None
     Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
     Order allow,deny
     Allow from all
   </Directory>
   ErrorLog ${APACHE_LOG_DIR}/error.log
   LogLevel warn
   CustomLog ${APACHE_LOG_DIR}/access.log combined
 </VirtualHost>

nano /etc/apache2/sites-available/default-ssl
 <IfModule mod_ssl.c>
 <VirtualHost _default_:443>
   ServerAdmin webmaster@localhost
   DocumentRoot /var/www
   <Directory />
     Options FollowSymLinks
     AllowOverride None
   </Directory>
   <Directory /var/www/>
     Options Indexes FollowSymLinks MultiViews
     AllowOverride None
     Order allow,deny
     allow from all
   </Directory>
   ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
   <Directory "/usr/lib/cgi-bin">
     AllowOverride None
     Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
     Order allow,deny
     Allow from all
   </Directory>
   ErrorLog ${APACHE_LOG_DIR}/error.log
   LogLevel warn
   CustomLog ${APACHE_LOG_DIR}/ssl_access.log combined
   SSLEngine on
   SSLCertificateFile    /etc/ssl/localcerts/apache.pem
   SSLCertificateKeyFile /etc/ssl/localcerts/apache.key
   <FilesMatch "\.(cgi|shtml|phtml|php)$">
     SSLOptions +StdEnvVars
   </FilesMatch>
   <Directory /usr/lib/cgi-bin>
     SSLOptions +StdEnvVars
   </Directory>
   BrowserMatch "MSIE [2-6]" \
     nokeepalive ssl-unclean-shutdown \
     downgrade-1.0 force-response-1.0
   BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown
 </VirtualHost>
 </IfModule>

nano /etc/apache2/ports.conf
 NameVirtualHost *:80
 Listen 80
 <IfModule mod_ssl.c>
  Listen 443
 </IfModule>
 <IfModule mod_gnutls.c>
  Listen 443
 </IfModule>

a2ensite default
a2ensite default-ssl
service apache2 stop
service apache2 start
Получили рабочий HTTP(S) сервер.

Web Git repository

Теперь я хочу видеть по урл
http://scmserv.algis.com/test_td.git/
свое хранилище
/home/git/repositories/test_td.git

для чего я его залинковал в подпапку вебсервера, ну и права доступа к файлам и каталогам пришлось докручивать
root@scmserv:~# pushd /home/git/repositories/test_td.git
chown -R www-data.git .
chmod -R ug+rw .
chmod -R a+r /home/git
chmod a+x /home/git
find /home/git -type d -exec chmod a+x {} \;
ln -s /home/git/repositories/test_td.git /var/www/test_td.git
Теперь в браузере я могу видеть содержимое хранилища.

DAV

Последние штрихи, прикручивание DAV к апачевскому конфигу
a2enmod dav*
nano /etc/apache2/conf.d/git.conf
 <Location /test_td.git>
  DAV on
  Allow from all
  Order allow,deny
  Require valid-user
  AuthType Basic
  AuthName "Git test_td repo"
  AuthUserFile /etc/apache2/git.passwd
  AuthGroupFile /etc/apache2/git.groups
 </Location>

htpasswd -c /etc/apache2/git.passwd valik
nano /etc/apache2/git.groups
 writers: valik git
 readers: guest
service apache2 restart
Теперь вебсервер обслуживает хранилище Git через протокол DAV. В общем, готово.
Правда, пришлось еще кое-что подкрутить
git@scmserv:/home/git/repositories/test_td.git$ git update-server-info
mv hooks/post-update.sample hooks/post-update
chmod +x hooks/post-update
Думаю, из этих трех команд две последние — лишние. Но не мешают и ладно.

На клиенте

На клиентской стороне полезно и даже, может, необходимо отконфигурять пару параметров.
Командой
$ git config remote.httptestrepo.url http://valik@scmserv.algis.com/test_td.git
В локальный реп гита вносится такая настройка (в файле .git\config)
[remote "httptestrepo"]
 url = http://valik@scmserv.algis.com/test_td.git
После чего можно ссылаться на сетевое хранилище вот так
$ git push httptestrepo master

Если же хочется задействовать https, то помимо задания секурного урл
$ git config remote.httptestrepo.url https://valik@scmserv.algis.com/test_td.git
придется еще отключить проверку сертификатов (у нас самоподписанный, чоужтам)
$ git config http.sslVerify false
Что дает в файле конфига такое
[http]
 sslVerify = false

Вот теперь можно пушить и пуллить из сетевого репозитория Git, по HTTP(S) каналу.

2012-06-05

Dropbox + encfs

Толковый рецепт. Просто, надежно и изящно

Организация шифрованного бэкапа с помощью rdiff-backup, encfs и Dropbox
я создаю с помощью encfs внутри ~/Dropbox отдельный каталог для
бэкапов, и монтирую его куда-нибудь за пределы каталога ~/Dropbox:

mkdir ~/Dropbox/encfs/backup
mkdir ~/backup
encfs ~/Dropbox/encfs/backup ~/backup


Безопасно — в Дропбоксе зашифрованный бекап.
Удобно — данные автоматом реплицируются на любое количество компов.
Но небесплатно.

2012-06-04

Debian APT problem

Внезапно на одной из машин (Debian) поломался пакетный менеджер. Перестал обновлять систему и сыпет ошибками типа

Package is in a very bad inconsistent state
This might mean you need to manually fix this package

Уперся, как в стену, в ошибку, сопровождаемую сообщениями
dpkg: error processing acpid (--configure): Package is in a very bad inconsistent state - you should reinstall it before attempting configuration.
E: I wasn't able to locate a file for the acpid package. This might mean you need to manually fix this package.

Тырк-пырк, туда-сюда — нефейхоа. Пошел гуглить и довольно быстро нагуглил решение. Делюсь:
после заклинания
dpkg --force-remove-reinstreq --remove
проблемный пакет удалился и ошибка ушла в небытие.


2012-06-01

Как клонировать виртмашину с Linux

Если пользоваться virtualbox, то очень просто.
В GUI-евой обочке, в меню выбрать «клонировать» и выбранная виртмашина будет клонирована. Через некоторое время, зависящее от размера файлов этой машинки. Дальше уже не так просто.

Надо обьяснить клону, что он не клон а вполне самостоятельный комп. Для этого надо поправить два-три файла внутри. Старотовать клон, зайти рутом, поправить файлы
nano /etc/hostname
nano /etc/udev/rules.d/70-persistent-net.rules
и, возможно, этот:
nano /etc/dhcp/dhclient.conf

Надо быть готовым к тому, что пока эти файлы поправлены не будут, машинка в сеть не попадет.
Клонировал Debian Wheezy, успешно.

Фишка, ради которой я все это написал, заключается в
/etc/udev/rules.d/70-persistent-net.rules
Ибо все остальное вполне очевидно. А вот содержимое этого файла отвечает за то, что сетевой интерфейс сам подняться не может после смены MAC-адреса. А этот самый мак-адрес менять надо, иначе в одной сети клон и оригинал не выживут.

Навеяно

Архив блога

Ярлыки

linux (241) python (191) citation (186) web-develop (170) gov.ru (159) video (124) бытовуха (115) sysadm (100) GIS (97) Zope(Plone) (88) бурчалки (84) Book (83) programming (82) грабли (77) Fun (76) development (73) windsurfing (72) Microsoft (64) hiload (62) internet provider (57) opensource (57) security (57) опыт (55) movie (52) Wisdom (51) ML (47) driving (45) hardware (45) language (45) money (42) JS (41) curse (40) bigdata (39) DBMS (38) ArcGIS (34) history (31) PDA (30) howto (30) holyday (29) Google (27) Oracle (27) tourism (27) virtbox (27) health (26) vacation (24) AI (23) Autodesk (23) SQL (23) Java (22) humor (22) knowledge (22) translate (20) CSS (19) cheatsheet (19) hack (19) Apache (16) Manager (15) web-browser (15) Никонов (15) functional programming (14) happiness (14) music (14) todo (14) PHP (13) course (13) scala (13) weapon (13) HTTP. Apache (12) Klaipeda (12) SSH (12) frameworks (12) hero (12) im (12) settings (12) HTML (11) SciTE (11) USA (11) crypto (11) game (11) map (11) HTTPD (9) ODF (9) купи/продай (9) Photo (8) benchmark (8) documentation (8) 3D (7) CS (7) DNS (7) NoSQL (7) cloud (7) django (7) gun (7) matroska (7) telephony (7) Microsoft Office (6) VCS (6) bluetooth (6) pidgin (6) proxy (6) Donald Knuth (5) ETL (5) NVIDIA (5) Palanga (5) REST (5) bash (5) flash (5) keyboard (5) price (5) samba (5) CGI (4) LISP (4) RoR (4) cache (4) car (4) display (4) holywar (4) nginx (4) pistol (4) spark (4) xml (4) Лебедев (4) IDE (3) IE8 (3) J2EE (3) NTFS (3) RDP (3) holiday (3) mount (3) Гоблин (3) кухня (3) урюк (3) AMQP (2) ERP (2) IE7 (2) NAS (2) Naudoc (2) PDF (2) address (2) air (2) british (2) coffee (2) fitness (2) font (2) ftp (2) fuckup (2) messaging (2) notify (2) sharepoint (2) ssl/tls (2) stardict (2) tests (2) tunnel (2) udev (2) APT (1) CRUD (1) Canyonlands (1) Cyprus (1) DVDShrink (1) Jabber (1) K9Copy (1) Matlab (1) Portugal (1) VBA (1) WD My Book (1) autoit (1) bike (1) cannabis (1) chat (1) concurrent (1) dbf (1) ext4 (1) idioten (1) join (1) krusader (1) license (1) life (1) migration (1) mindmap (1) navitel (1) pneumatic weapon (1) quiz (1) regexp (1) robot (1) science (1) serialization (1) spatial (1) tie (1) vim (1) Науру (1) крысы (1) налоги (1) пианино (1)