Бекап на Amazon S3 больших файлов

Очередной подход к бекапу больших файлов на Amazon S3. Когда возникла необходимость сделать зеркало бекапов на Amazon S3 я обнаружил что s3cmd все еще не знает как заливать файлы свыше 5GB на Amazon S3, хотя API начал поддерживать объекты размером до 5TB.

Для меня загадка почему в интернете почти нету готовых bash-скриптов для бекапа файлов свыше 5GB на Amazon S3 используя s3cmd. Либо все заливают бекапы используя duplicity или аналоги, либо у всех маленькие бекапы. Пришлось писать самому. Скрипт довольно простой и требует только наличия s3cmd.

#!/usr/local/bin/bash

FILES_DIR="/usr/backup/files_month" # Директория с бекапами
S3_TMP="/usr/backup/to_s3" # Временная директория
S3_BUCKET="my.bucket" # S3 Бакет

if [ ! -d "$FILES_DIR" ]; then
  echo "Path '$FILES_DIR' is incorrect."
  exit 1
fi

if [ ! -d "$S3_TMP" ]; then
  echo "Path '$S3_TMP' is incorrect."
  exit 1
fi

rm -rf "$S3_TMP/*"
cd "$FILES_DIR"

for i in `find . -type f -name "*.gz"`;
do
    FILE_NAME=$(basename "$i");
    FILE_SIZE=$(stat -f %z $i);

    DIR_NAME=$(dirname "$i");
    DESTINATION="$S3_TMP${DIR_NAME:1}/";

    mkdir -p "$DESTINATION"

    # Тут regexp для игнорирования некоторых папок/файлов
    # в моем случае это папки user_testing и tmp
    if [[ "$i" =~ (user_testing|tmp) ]]; then
        echo "[ Ignored ]: $i";
    elif [ $FILE_SIZE -gt 4200000000 ]; then
        echo "[ Split ]: $i"
        split -b 4G "$i" "${DESTINATION}${FILE_NAME}."
    else
        echo "[ Copy ]: $i -> ${DESTINATION}${FILE_NAME}"
        cp "$i" "$DESTINATION$FILE_NAME";
    fi
done

s3cmd sync \
    --skip-existing \
    --delete-removed \
    "$S3_TMP/" \
    "s3://$S3_BUCKET/"

Удаление файлов в CloudApp

Совершенно неожиданно мой аккаунт на CloudApp сообщил о том, что закончилось бесплатное место. Для удаления накопившегося мусора я полез в личный кабинет и каково было мое удивление когда я там не обнаружил, не только кнопки удалить все, но даже кнопки выделить все на странице, для последующего удаления. Не долго думая накидал UserScript, который добавляет кнопку Select All на страницу c файлами.

Готовую версию можно скачать на GIST, работает в Chrome и Firefox.

cloudapp.user.js

// ==UserScript==
// @name         CloupApp Select all
// @match        http://my.cl.ly/*
// @author       dotzero
// @description  Add Select All button to CloudApp web interface
// ==/UserScript==

function main() {
    $('#toolbar').prepend('<li><a class="button" href="#" id="selall">Select all</a></li>');
    $('#selall').on("click", function(event){
        event.preventDefault();
        $('#listing').find('input:checkbox').click();
    });
}

function insert(callback) {
    var script = document.createElement("script");
    script.textContent = "(" + callback.toString() + ")();";
    document.body.appendChild(script);
}

insert(main);

Результат

CloudApp Select All

Кодировка файла в плагинах Sublime Text 2

Сегодня вышел 2165 билд прекрасного редактора SublimeText 2, а с ним приехало несколько интересных API методов, среди которых меня прежде всего заинтересовал view.encoding(), который возвращает кодировку открытого файла. Если же файл не открыт а просто создан и еще не сохранен, то этот метод возвращает строку ‘Undefined’.

Для своих плагинов которых работают с текстом я написал небольшой хак который позволяет абстрагироваться от этого, беря также в учет новую настройку ‘default_encoding’ которая содержит кодировку по-умолчанию для новых файлов.

def enc(self):
    if self.view.encoding() == 'Undefined':
        return self.view.settings().get('default_encoding', 'UTF-8')
    else:
        return self.view.encoding()

Резервное копирование базы данных Bitrix

Конечно в самом Bitrix есть механизм резервного копирования, а кроме того есть такая статья Резервное копирование по расписанию. Но не одно из решений не будет эффективным если в базе данных хранятся не только таблицы Bitrix, а бекапить надо только их. В результате написал небольшой скрипт взяв за основу оригинальный скрипт резервного копирования и переписав в нем алгоритм копирования базы данных в результате будут бекапится только Битриксовые таблицы с префиксом b_ , а кроме того в моих конфигах Битрикса есть закомментированные переменные, поэтому поправил парсинг конфиг файла чтобы не было конфликтов.

#!/bin/sh
doc_root=$1

if [ -z $doc_root ]; then
    echo Usage: $0 /path/to/document/root
    exit
fi

dbconn=$doc_root/bitrix/php_interface/dbconn.php

readcfg() {
    grep "^\$$1" $dbconn | sed 's/.*"\(.*\)".*/\1/'
}

host=`readcfg DBHost`
username=`readcfg DBLogin`
password=`readcfg DBPassword`
database=`readcfg DBName`

utf=`grep 'BX_UTF' $dbconn | grep true`

if [ -z "$utf" ]; then
   charset=cp1251
else
   charset=utf8
fi

mysql -h$host -u$username -p$password -N information_schema -e "SELECT TABLE_NAME FROM TABLES WHERE TABLE_SCHEMA = '$database' AND TABLE_NAME LIKE 'b\_%'" > tables.txt
mysqldump -h$host -u$username -p$password --default-character-set=$charset $database `cat tables.txt` > dump.sql
tar czf dump_`date +%Y%m%d`.tgz dump.sql
rm -f tables.txt dump.sql

Множественное число существительных

Самый лаконичный вариант функции для выбора правильного склонения множественного числа на php.

$w1 = 'товар'; /* 1 */
$w2 = 'товара'; /* 2 */
$w5 = 'товаров'; /* 5 */

function plural_form($x, $w1, $w2, $w5)
{
    $w = array($w1, $w2, $w5);
    $d = ($p = $x % 100) % 10;

    return $x . ' ' . $w[$p == 11 || $d == 0 || ($p >= 10 && $p <= 20) || ($d >= 5 && $d <= 9) ? 2 : ($d == 1 ? 0 : 1)];
}

Несколько моих плагинов для Sublime Text 2

Почти месяц назад написал несколько плагинов для прекрасного редактора SublimeText 2, который я активно пользую каждый день, а написать о них сюда все никак не получалось. Плагины все написаны на Python и выложены на Github, там же можно найти подробную инструкцию по установке, а здесь я просто напишу пару слов о каждом.

ST2-Converter

Идею для данного плагина я позаимствовал у AnyEdit который использую в Eclipse. Плагин добавляет дополнительную функциональность в контекстное меню редактора и реализует некоторые рутинные операции со стоками. Краткий обзор некоторой функциональности:

# Преобразование символов в соответствующие HTML-сущности и обратно
<html> преобразуется в &lt;html&gt;

# Преобразование из CamelCase в under_scores и обратно
someFunctionName преобразуется в some_function_name

# Преобразование символов в Unicode обозначения
Unicode преобразуется в \u0055\u006E\u0069\u0063\u006F\u0064\u0065

# Кодирование и декодирование Base64
Test (Base64: VGVzdA==)

# Подсчет MD5 хешей
Test (MD5: 0cbc6611f5540bd0809a388dc95a615b)

# Преобразование меток времени из формата, принятого в Unix,
# в понятный человеку формат и обратно
2011-11-15 12:52 преобразуется в 1321361520.0

ST2-Brainfuck

Полнофункциональный интерпретатор эзотерического языка Brainfuck внутри Sublime Text 2. Поддерживает все команды языка включая ввод данных извне. К плагину прилагается папка с несколькими примерами кода.

ST2-WhatTheCommit

Давно хотелось использовать рандомные сообщения с сайта whatthecommit.com и вот результатом стал этот плагин. Функциональность минимально, просто делать коммиты в существующий GIT репозиторий, используя в качестве комментариев сообщения с данного сайта.

Raw GSM 6.10 Audio Stream проигрывание online

Столкнувшись с форматом Raw GSM 6.10 audio stream (.gsm) первый раз я решил пойти по пути наименьшего сопротивления и найти готовое решение для проигрывания таких файлов онлайн.

Наткнувшись на github на единственный вменяемый плеер под названием WavPlayer, поддерживающий формат Raw GSM 6.10 Audio Stream, я ужаснулся от его интерфейса да и написан он был на языке haXe. К счастью есть такой замечательный проект audio.js который представляет из себя симпатичный frontend JS плеер с backend кодеком на Flash.

Довольно быстро возникло желание совместить эти два проекта. Бегло осмотрев архитектуру плеера и api кодека, я отбросил всякое желание менять JS код плеера для совместимости с WavPlayer. Остался вариант переписать кодек, этим я и занялся.

До знакомства с WavPlayer я и понятия не имел о языке haXe на котором он написан. Посмотрев исходники кодека и почитав пару заметок на сайте языка haXe я довольно быстро освоил синтаксис который сильно напоминает ActionScript. Посидев несколько вечеров я закончил переписывание api, попутно исправив пару багов на которые автор явно забил, а может они проявляются только при проигрывании файлов gsm. Результатом всех усилий стал форкнутый проект, который можно посмотреть на github.

Сборка

Для самостоятельной сборки кодека необходимо установить haXe, скачать исходники кодека с github и скомпилировать командой:

haxe audiojs.hxml

В результате получится файл с названием audiojs.swf реализующий полный набор api методов которые необходимы для совместной работы с audio.js.

Готовый файл можно сказу скачать по ссылке.

Установка

  1. Устанавливаем скрипт audio.js
  2. Заменяем оригинальный файл audiojs.swf на новый
  3. А также в при инициализации скрипта необходимо указать в настройках useFlash: true.

Это необходимо чтобы флеш кодек использовался даже тогда когда браузер поддерживает формат audio тегов.

audiojs.events.ready(function() {
    audiojs.createAll({
      'useFlash': true // всегда использовать флеш
    });
});

Теперь для прослушивания online любой записи в формате .gsm достаточно вставить тег указывающий на файл. Например:

<audio src="test.gsm" />

Как поменять местами значения в колонках таблицы

Условия задачи: есть MySQL таблица table1. Используя только язык SQL запросов необходимо поменять местами значения из колонок value1 и value2. Структура таблицы имеет следующий вид:

CREATE TABLE `table1` (
  `id` INT NOT NULL AUTO_INCREMENT ,
  `value1` VARCHAR(50) NOT NULL DEFAULT '' ,
  `value2` VARCHAR(50) NOT NULL DEFAULT '' ,
  PRIMARY KEY (`id`) )
ENGINE = MyISAM;

Решение “в лоб”

Самым простым способом является явное переименование колонок таблицы. Данное решение самое простое в реализации и самое быстрое по результатам моих тестов.

ALTER TABLE  `table1`
CHANGE `value1` `value2` VARCHAR(50) NOT NULL DEFAULT '' ,
CHANGE `value2` `value1` VARCHAR(50) NOT NULL DEFAULT '';

Решение с временной переменной

Изящное решение с использованием временной переменной для хранения промежуточного результата. По скорости оно уступает первому решению тем не менее показывает дополнительные скилзы в SQL.

UPDATE `table1` SET `value1`=(@temp:=`value1`), `value1` = `value2`, `value2` = @temp;

Решение с подзапросом

Поскольку MySQL не даст сделать простой UPDATE с выборкой из этой же таблицы, поэтому будем извращаться с ON DUPLICATE KEY UPDATE. Самое медленное из предложенных решений, но данный синтаксис предоставляет определенные преимущества в некоторых ситуациях.

INSERT INTO `table1`
SELECT * FROM `table1` `t2` ON DUPLICATE
KEY UPDATE `value1` = `t2`.`value2`, `value2` = `t2`.`value1`;

Расшифровка купона на скидку при покупке Parallels

Послушав Радио-Т номер 254, захотелось расшифровать код на 10% скидку на продукты от Parallels.

Воспользовавшись подсказкой о числе 13 можно после некоторых поисков найти прийти к выводу об использовании алгоритма ROT13 (подробнее о нем можно почитать в википедии). По сути это элементарный сдвиг на 13 позиций в алфавите. Пишем простую функцию на Python 3.0.

import string

def rot13(text):
    rot13_trans = string.maketrans( \
        "ABCDEFGHIJKLMabcdefghijklmNOPQRSTUVWXYZnopqrstuvwxyz", \
        "NOPQRSTUVWXYZnopqrstuvwxyzABCDEFGHIJKLMabcdefghijklm")

    return string.translate(text, rot13_trans)

Первый этап пройден. Полученную в результате строку необходимо разбить на подстроки, затем сохранить в файл и пропустить через утилиту uudecode, которая есть в любом дистрибутиве linux (на mac’е тоже она есть). Но мне было лень заморачиваться с этим и я написал аналог также на питоне.

import base64, re

# s - это декодированная с помощью rot13 строка
matches = re.split('~', s)
print base64.b64decode(matches[1] + matches[2])

Описывать подробно смысла особого нет. Делим на подстроки по маске ‘~’, соединяем 2 и 3 строки и декодируем через base64. Вот и все. Финальный результат публиковать не стану, кому надо тот сам все сделает.

На Python 2.x можно написать через .decode(‘rot13’) и не морочить себе голову, как это сделал я.

import base64, re
matches = re.split('~', 'rot13 string'.decode('rot13'))
print base64.b64decode(matches[1] + matches[2])

Bitrix 11 + Basic Auth = 💩

В Битриксе разработчики или маркетологи видимо не в курсе что данные которые используются для HTTP-аутентификации не обязательно будут совпадать с теми данными которые используются для доступа в Административный раздел Битрикса. А потому навесив собственную Basic Authentication над Битриксом 11 получаем полную невозможность входа в административный раздел в случаях когда логин и пароль от HTTP-аутентификации не совпадают с теми под которыми можно войти в Административный раздел.

Файл отвечающий на эту несправедливость называется ./bitrix/modules/main/tools.php в котором есть идиотский метод ParseAuthRequest().

public static function ParseAuthRequest()
{
    $sDigest = '';

    if(isset($_SERVER['PHP_AUTH_USER']) && $_SERVER['PHP_AUTH_USER'] <> '')
    {
        // Basic Authorization PHP module
        return array("basic"=>array(
            "username"=>$_SERVER['PHP_AUTH_USER'],
            "password"=>$_SERVER['PHP_AUTH_PW'],
        ));
    }
    elseif(isset($_SERVER['PHP_AUTH_DIGEST']) && $_SERVER['PHP_AUTH_DIGEST'] <> '')
    {

    // Дальше нас не интересует

Единственный легальный способ который я сейчас вижу, это добавить в файл ./bitrix/php_interface/init.php такой хак:

if(isset($_SERVER['PHP_AUTH_USER']) && preg_match('#^/bitrix/admin/#ui', $_SERVER['REQUEST_URI'])) {
    $_SERVER['PHP_AUTH_USER'] = '';
}