Filtry strumieni w PHP

Ostatnio pracowałem nad wymianą plików w formacie CSV. PHP ma funkcje fputcsv() i fgetcsv(), które dbają o właściwe umieszczanie/dekodowanie cudzysłowów, więc zadanie jest trywialne. Jedyny problem polegał na tym, że zdalny system zapisywał i łykał pliki z kodowaniem Windows-1250 i znacznikami końca linii CRLF. Oczywiście można traktować iconvem tablice przekazywane do fputcsv() i zwracane przez fgetcsv(), przy zapisie można po każdym fputcsv() zapisywać znacznik LF, ale to rozwiązanie niezbyt eleganckie.

Na szczęście architektura PHP 5 jest elastyczna, funkcje wyjścia/wejścia operują na strumieniach, a do strumieni można dodawać filtry. Powyższy problem można rozwiążać dodając do strumienia filtr, który dokona konwersji kodowania i znaczników końca linii. Wystarczy do tego malutka klasa:

namespace MDurys\DataBundle\StreamFilter;

class CP1250CRLFFilter extends \php_user_filter
{
    public function filter($in, $out, &$consumed, $closing)
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $bucket->data = str_replace("\n", "\r\n", iconv('UTF-8', 'CP1250', $bucket->data));
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

Plik otwieramy jak zwykle:

if (null === ($fh = fopen($path, 'w'))) {
        throw new FileException($path);
}

Potem wystarczy zarejestrować filtr i dodać go do uchwytu naszego pliku:

stream_filter_register('windows', '\MDurys\DataBundle\StreamFilter\CP1250CRLFFilter');
stream_filter_append($fh, 'windows');

Od tej pory wszystko co zapiszemy do pliku zostanie skonwertowane z UTF-8 na CP1250, a znacznki końca linii zamienione na CRLF.

Edycja boot menu UEFI w Debianie

Mój służbowy laptop (HP Pavilion G6) musiał powędrować do serwisu, więc admin przełożył dysk i pamięć z mojego komputera do identycznego modelu. Na chłopski rozum wszystko powinno ruszyć bez problemu, ale niestety po takiej operacji komputer przywitał mnie komunikatem „Hard Disk – (3F0) BootDevice not found”. Oczywiście opcje UEFI były identycznie ustawione w obydwu komputerach.

Z problemem biedziłem się kilka godzin, zastanawiałem się czy to problem z szyfrowaniem dysku (LUKS), flagą boot partycji, a może identyfikatorami partycji. Problem musiał być trywialny, bo po uruchomieniu systemu z LiveCD partycje można było podmontować, a dane były na miejscu. Na wszelkie wypadek dodam, że ponowna instalacja GRUB-a nie pomogła.

Odkryłem, że jeśli wystartuję komputer z LiveCD, ale zamiast uruchamiać Linuksa wyjdę z menu GRUB-a (Escape, a potem exit) to komputer pokazuje menu UEFI, z którego mogę wybrać urządzenie i plik uruchomieniowy. Na moim dysku komputer widział plik EFI/debian/grubx64.efi, po wybraniu którego system startował normalnie.

Z pomocą kolegi trafiłem na trop programu efibootmgr, który służy do edycji menu uruchomieniowego UEFI. Po kilku próbach doszedłem do polecenia, które postawiło mój system na nogi:

efibootmgr --create --label "Debian" --loader \\EFI\\debian\\grubx64.efi

Powyższe polecenia zakłada, że partycja EFI jest na /dev/sda i jest podmontowana jako /boot/efi. Jeżeli jest inaczej należy je wskazać odpowiednio przez --disk i --part. Użycie backslashy w parametrze --loader jest istotne.

Efekt działania można sprawdzić poleceniem efibootmgr --verbose. U mnie wygląda to tak:

BootCurrent: 0001
Timeout: 0 seconds
BootOrder: 3001,2001,2002,2003
Boot0001* Debian	HD(1,800,f3800,b865a07b-fdff-428d-9443-9fb85820b4f6)File(\EFI\debian\grubx64.efi)
Boot2001* USB Drive (UEFI)	RC
Boot2002* Internal CD/DVD ROM Drive (UEFI)	RC
Boot3001* Internal Hard Disk	RC

SSHFS – problemy i rozwiązania

Trochę wody w Wiśle upłynęło odkąd ostatnio używałem SSHFS, więc kiedy przyszło mi podmontować udział z serwera przez ten protokół nie obyło się bez problemów.

Instalacja

O ile wcześniej tego nie zrobiliśmy to SSHFS trzeba zainstalować:

aptitude install sshfs

Montowanie:

sshfs mdurys@10.0.0.2:/home/mdurys/projekt /home/joe/Projekty/projekt

Moja nazwa użytkownika na zdalnej maszynie jest inna niż na lokalnej, więc musiałem ją podać w ścieżce.

Tu może pojawić się pierwszy problem:

failed to open /dev/fuse: Permission denied

Rozwiązanie jest proste:

usermod -aG fuse joe

Następnie trzeba się przelogować, lub, jeśli ktoś lub starą windowsową szkołę, ponownie uruchomić komputer.

Dla pewności można sprawdzić czy użytkownik trafił do grupy fuse.

groups joe
joe : joe cdrom floppy audio dip video plugdev fuse scanner netdev bluetooth

Problem z właścicielem plików

Jeśli nazwy i identyfikatory użytkowników są różne na obu maszynach to wystąpią problemy z dostępem do plików ponieważ SSHFS w lokalnym systemie pokaże takiego właściciela jak na zdalnym systemie.

-rw-r--r-- 1 1001 1001   141 sty  9 11:13 AppCache.php
-rw-r--r-- 1 1001 1001  2395 sty  9 11:13 AppKernel.php
-rw-r--r-- 1 1001 1001   267 sty  9 11:13 autoload.php
-rwxr-xr-x 1 1001 1001   865 sty  9 11:13 console

Można temu zaradzić używając opcji idmap=user

sshfs -o idmap=user mdurys@10.0.0.2:/home/mdurys/projekt /home/joe/Projekty/projekt

Dzięki temu użytkownik dokonujący montowania zostanie zmapowany na użytkownika, na którego się łączymy.

-rw-r--r-- 1 joe 1001   141 sty  9 11:13 AppCache.php
-rw-r--r-- 1 joe 1001  2395 sty  9 11:13 AppKernel.php
-rw-r--r-- 1 joe 1001   267 sty  9 11:13 autoload.php
-rwxr-xr-x 1 joe 1001   865 sty  9 11:13 console

Problem z grupą plików

Jak widać na liście plików z poprzedniego akapitu, pliki mają poprawnego właściciela, ale grupa ciągle nie jest zmapowana. To może powodować problemy z dostępem, ale i na to jest remedium:

sshfs -o idmap=user -o uid=1000 -o gid=1000 mdurys@10.0.0.2:/home/mdurys/projekt /home/joe/Projekty/projekt

Po takim zabiegu również grupy są poprawne:

-rw-r--r-- 1 joe joe   141 sty  9 11:13 AppCache.php
-rw-r--r-- 1 joe joe  2395 sty  9 11:13 AppKernel.php
-rw-r--r-- 1 joe joe   267 sty  9 11:13 autoload.php
-rwxr-xr-x 1 joe joe   865 sty  9 11:13 console

SSHFS w /etc/fstab

Żeby nie wpisywać wszystkiego z palca z każdym razem można definicję zasobu wrzucić do /etc/fstab. W takim wypadku trzeba podać ścieżkę do klucza, który ma zostać użyty do autoryzacji.

sshfs#mdurys@10.0.0.2:/home/mdurys/projekt /home/joe/Projekty/projekt fuse defaults,IdentityFile=/home/joe/.ssh/id_rsa,uid=1000,gid=1000 0 0

Jeśli zasób możne być niedostępny podczas uruchamiania komputera to nie warto montować go automatycznie, lepiej umożliwić montowanie go przez użytkowników na żądanie.

sshfs#mdurys@10.0.0.2:/home/mdurys/projekt /home/joe/Projekty/projekt fuse defaults,noauto,user,uid=1000,gid=1000 0 0

bash: brak dostępu

Ostatni problem, na który się nartknąłem był związany z uruchamianiem skryptów:

app/console
bash: app/console: Brak dostępu

Dzieje się tak ponieważ parametr user w opcjach montowania implikuje noexec, co można łatwo sprawdzić poleceniem

mount -l

Oczywiście zamiast app/console można posługiwać się php app/console, ale jeśli chcemy korzystać z dobrodziejstw autouzupełniania poleceń albo po prostu oszczędzać klawiaturę to należy dodać parametr exec:

sshfs#mdurys@10.0.0.2:/home/mdurys/projekt /home/joe/Projekty/projekt fuse defaults,noauto,user,exec,uid=1000,gid=1000 0 0

Lektura dodatkowa

Dlaczego drukowanie dwustronne przestało działać?

Drukowanie dwustronne to obok majtek z gumką jeden z najprzydatniejszych wynalazków naszej ery. Zrozumiecie zatem moje zdumienie i zaniepokojenie, kiedy to po świeżej instalacji systemu stosowna opcja zniknęła z okienka ustawień wydruku.

Przyczyna problemu tkwi w wybranym sterowniku drukarki. Jak widzicie na obrazku, do mojego Lexmarka E450DN Debian proponuje trzy sterowniki. W tej chwili nie pamiętam czemu wybrałem lj5gray, jednak ten sterownik nie pozwala korzystać z dobrodziejstw druku dwustronnego. Po zmianie sterownika na Postscript znowu mogę drukować dwustronnie.

Wybór sterownika drukarki

Symfony2, Twig i SwiftMailer czyli rzecz o obrazkach w mailu

Ongiś pisanie maili w HTML-u było naruszeniem netykiety, ale obecnie zdecydowana większość odbiorców jak i nadawców woli otrzymywać maile w HTML-u. Generowanie i wysyłanie takowych w aplikacji opartej na Symfony2 jest banalnie proste dzięki Twigowi i Swift Mailerowi. Nieco mniej przyjemnie zaczyna się robić, kiedy w treści maili ma pojawić się grafika.

Możliwości są dwie i każda rodzi pewne problemy:

  1. Linkowanie do plików na zdalnym serwerze. Niestety funkcja url() w Twigu akceptuje tylko ścieżki zdefiniowane w routingu, nie można nią linkować do grafik. Z kolei znacznik image obsługiwany przez Assetica domyślnie generuje relatywne URL-e, a poza tym jest trochę nieporęczny w użyciu.
  2. Osadzenie plików w wiadomości. Tu problem natury programistycznej polega na tym, że żeby osadzić grafikę w wiadomości trzeba skorzystać z metody Swift_Message::embed(), do której nie mamy dostępu z poziomu Twiga.

Wszystkie powyższe problemy można rozwiązać tworząc rozszerzenie do Twiga.

namespace MDurys\MailBundle\Twig;

use Symfony\Component\DependencyInjection\ContainerInterface;

class MailExtension extends \Twig_Extension
{
    private $kernel;

    private $host;

    private $imageCache;

    private $messageCache;

    public function __construct(ContainerInterface $container)
    {
        $context = $container->get('router')->getContext();
        $this->host = $context->getScheme() . '://' . $context->getHost();
        $this->kernel = $container->get('kernel');
    }

    public function getFunctions()
    {
        return array(
            new \Twig_SimpleFunction('mail_embed', array($this, 'mailEmbed')),
            new \Twig_SimpleFunction('mail_url', array($this, 'mailUrl')),
        );
    }

    public function mailEmbed(\Swift_Message $message, $file)
    {
        if (!isset($this->messageCache[$message->getId()][$file]))
        {
            if (!isset($this->imageCache[$file]))
            {
                $this->imageCache[$file] = \Swift_Image::fromPath($this->kernel->locateResource($file));
            }
            $this->messageCache[$message->getId()][$file] = $message->embed($this->imageCache[$file]);
        }
        return $this->messageCache[$message->getId()][$file];
    }

    public function mailUrl($file)
    {
        return $this->host . $file;
    }

    public function getName()
    {
        return 'mdurys_mail_extension';
    }
}

Rozszerzenie udostępnia dwie funkcje Twiga: mail_embed() i mail_url(), które odpowiednio umożliwiają osadzenie obrazka w wiadomości i wygenerowanie abosultnego URL-a do zasobu. Osadzane obrazki są cacheowane, więc jeśli dany obrazek jest wykorzystywany kilka razy w danej wiadomości to zostanie osadzony tylko raz, a jeśli obrazek zostanie osadzony w kilku wiadomościach to proces konwersji na obiekt Swift_Image zostanie wykonany tylko raz.

Aby móc z tego rozszerzenia korzystać należy je uaktywnić w services.yml bundla. Ważne jest, żeby otagować usługę jako „twig.extension”.

services:
  mdurys.twig.mail_extension:
    class: MDurys\MailBundle\Twig\MailExtension
    arguments: [ "@service_container" ]
    tags:
      - { name: twig.extension }

Jeśli chcemy osadzić obrazek w wiadomości to podczas renderowania szablonu trzeba przekazać obiekt wiadomości.

$message = \Swift_Message::newInstance();
$htmlPart = $this->renderView(
    'MailBundle:Test:email.html.twig',
    array('message' => $message)
    );
$message
    ->setFrom('mail@example.com')
    ->setTo('test@fexample.com')
    ->setSubject('Mail z obrazkami')
    ->setBody($htmlPart, 'text/html');
$this->get('mailer')->send($message);

W szablonie Twiga wystarczy wywołać funkcję mail_embed() przekazując jej jako parametry obiekt wiadomości i lokalizację pliku.

<img src="{{ mail_embed(message, '@MDurysMailBundle/Resources/public/images/smiley.gif') }}" width="16" height="16" alt=":-)" />

Linkowanie do zewnętrznych zasobów jest prostsze i sprowadza się do wywołania funkcji mail_url() w szablonie:

<img src="{{ mail_url('/bundles/mdurysmail/images/big_logo.jpg') }}" width="600" height="240" alt="Big Logo" />

Klucz obcy jako klucz główny w Doctrine2

Jest to jak najbardziej możliwe i bardzo ładnie opisane w dokumentacji Doctrine2. Przykład poniżej.

namespace Acme\Bundle\ShopBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="customer_preferences")
 */
class CustomerPreferences
{
    /**
     * @ORM\Id
     * @ORM\OneToOne(targetEntity="Acme\Bundle\ShopBundle\Entity\Customer")
     * @ORM\JoinColumn(name="customer_id", referencedColumnName="id")
     */
    private $customer;

    // ...
}

W tabeli wygenerowanej z tej encji pole customer_id będzie zarówno kluczem obcym wskazującym na pole id w tabeli customer jak i kluczem głównym.

Z jednej strony wydaje się to oczywiste i intuicyjne, ale parę razy przekonałem się, że rozwiązania w Doctrine2 nie zawsze są oczywiste i intuicyjne. Na szczęście akurat w tym wypadku są.

Antyspam w O2.pl lubi linki

Tworzenie i wysyłanie maili z aplikacji tak, by nie wpadały do spamu to spory kawałek wiedzy. Być może kiedyś opiszę moje doświadczenia w tym temacie, dzisiaj skupię się na jednej ciekawostce związanej z omijaniem filtra antyspamowego w poczcie O2.pl.

Problem na pierwszy rzut oka nietypowy: niektóre z maili wysyłanych na konta O2.pl dochodzą, inne zaś wpadają do spamu. Oczywiście maile wysyłane w z jednej aplikacji, wysyłane w ten sam sposób i z grubsza w tym samym czasie, jedyną różnicą jest treść. Niestety nagłówki w O2.pl nie zawierają informacji co dokładnie powoduje zakwalifikowanie wiadomości jako spam. Na wyczucie próbowałem parafrazować i przeredagować treść problematycznych maili, ale to nic nie dało. W końcu zauważyłem, że odrzucane maile nie zawierają żadnych linków podczas gdy te poprawnie dostarczane przynajmniej jeden. Na próbę dodałem byle jaki link i jak za dotknięciem magicznej różdżki maile zaczęły lądować w skrzynce „Odebrane”.

Na moje wyczucie powinno być odwrotnie, to maile z linkami są bardziej podejrzane. Zgaduję, że może filtr antyspamowy O2.pl zakłąda, że wiadomość powinna zawierać link do wypisania się z listy, ale to byłoby dość dziwne założenie. Tak czy owak dodanie linka pomaga.

Przydatne gotowce ffmpeg

Mój aparat nagrywa filmy korzystając z kodeka MJPEG dla obrazu i PCM dla dźwięku, co gwarantuje dobrą jakość, ale kosztem niskiego stopnia kompresji. W większości wypadków nie potrzebuję takiej jakości, więc by zaoszczędzić miejsce na dysku konwertuję filmy do formatu MP4 z obrazem w H264 i dźwiękiem AAC. By nie musieć konwertować każdego pliku z osobna używam polecenia find w połączeniu z ffmpeg i taki duet konwertuje wszystkie filmy we wskazanym katalogu:

find /media/foto/wakacje_2013/ -type f -iname *.AVI -exec ffmpeg -i {} -c:v libx264 -preset slow -crf 22 -acodec libfaac -ac 2 -ar 44100 -ab 128k {}.mp4 \;

Jeśli dodatkowo zachodzi potrzeba obrotu obrazu można posłużyć się filtrem transpose z wartością 1 powodującym obrót o 90 stopni:

find /media/foto/chorwacja_2008/ -type f -iname *.AVI -exec ffmpeg -i {} -vf "transpose=1" -c:v libx264 -preset slow -crf 22 -acodec libfaac -ac 2 -ar 44100 -ab 128k {}.mp4 \;

Jak zwykle w wypadku polecenia find tylny ukośnik i średnik na końcu linii są niezbędne.

Jeździłem Bentleyem… wśród odkurzaczy

Gościłem prezentację odkurzacza urządzenia do filtracji powietrza i sprzątania Rainbow. Nigdy nie byłem na prezentacji kosmicznych garnków ani cudownych kołder, więc byłem ciekawy jak to wygląda. Spodziewałem się, że będzie to trwać godzinę do półtorej. Tymczasem pranie/odkurzanie mózgu trwało… trzy godziny, a prowadzący powiedział, że to krótko, bo zazwyczaj trwa ok. pięciu. W tym czasie dowiedziałem się, że mokry kurz nie lata, że w kanapie mam dwa kilo złuszczonego naskórka, że śpiąc krócej wstaję świeższy niż po długim śnie, bo wdycham mniej roztoczy, a kilka wilgotnych szmatek symulujących moje płuca zostało zatkanych syfem wyrzucanym przez zwykłe odkurzacze. Efektem ubocznym prezentacji było odkurzenie połowy salonu i dwóch psich dywaników.

 Rainbow E2

Żarty na bok. Samo urządzenie sprawia dobre wrażenie. Przede wszystkim znakomicie robi to co ma robić odkurzacz czyli ssie jak filmografia Milli Jovovich. Przy tym jest cichszy niż mój w miarę nowy Samsung, żre mniej prądu, a wylanie wody z pojemnika jest łatwiejsze i bardziej higieniczne niż opróżnienie worka. Jest solidnie zbudowany, co daje nadzieję, że faktycznie będzie działać te 20 lat. Jednocześnie ma podobne gabaryty co typowy odkurzacz i jest od niego niewiele cięższy. Ponadto podoba mi się, że jest produkowany w USA, a nie Chinach. A to wszystko za jedyne 9000 zł.

No właśnie, taka cena czyni Rainbowa Bentleyem wśród odkurzaczy. Super samochód, super osiągi, ale płaci się przede wszystkim za elitarność i prestiż marki, który spływa na nabywcę. W wypadku samochodu to działa. Snobować się odkurzaczem? Nie.

PS. Jakby ktoś chciał sprawdzić to na własnej skórze to chętnie polecę was akwizytorowi, a ja dostanę nawilżacz i jonizator powietrza RainMate warty 700 zł!

Żegnaj T-Mobile

Przedwczoraj pożegnałem się z T-Mobile. Z jednej strony nic wielkiego, telefonia komórkowa jest teraz usługą tak podstawową i powszechną jak higiena jamy ustnej, a nie rozwodziłbym się nad zmianą pasty do zębów. Z drugiej strony abonament w Erze, a później T-Mobile, miałem od ponad 11 lat. To dłużej niż używam jakiejkolwiek marki pasty do zebów, to nawet dłużej niż jestem z moją dziewyczną, a obecnie żoną. ;-) Zapewne dlatego tak po ludzku trochę mi żal, że T-Mobile rozstał się ze mną bez mrugnięcia okiem, jakby te 11 lat z hakiem nic dla niego nie znaczyło. Ani przed końcem umowy ani po złożeniu wniosku o przeniesienie numeru nie skontaktował się ze mną i nie zaproponował przedłużenia umowy, o próbie przebicie oferty konkurencji nawet nie wspominając. Nie dostałem nawet SMS-a „szkoda, że odszedłeś, ale mam nadzieję, że wrócisz”. Żal mi, bo wydaje mi się, że byłem dobrym klientem, od ponad 11 lat płaciłem abonament, niby niezbyt wysoki, ale w dobie malejącej liczby abonentów i ostrej konkurencji w segmencie usług przedpłaconych ponoć każdy regularnie płacący łoś jest cenny.

Jednocześnie odczuwam ulgę, bo już od dłuższego czasu związek z T-Mobile mi się nie układał. Przy każdym przedłużeniu czułem się jak klient drugiej kategorii, bo pomimo wieloletniego stażu (a właściwie przez ten wieloletni staż) dawano mi gorsze warunki niż nowym klientom (tak, wiem, że tak działa dużo firm). Nie mogliśmy znaleźć porozumienia co do wzajemnych oczekiwań – ja miałem niewielkie potrzeby i chciałem niewiele płacić, T-Mobile konsekwentnie kasował tanie taryfy i usilnie chciał mi wcisnąć usługi, których nie potrzebuję i słono za nie kasować. W związku z tym szczególnie nie spodobało mi się skasowanie programu Era Premia, dzięki któremu „za darmo” mogłem nieco dopasować taryfę do moich potrzeb. Irytowało mnie poczucie humoru T-Mobile przejawiające się przykładowo w taryfie multimedialnej dla smartfonów z pakietem 100 MB transferu. Dużą kroplą w czarze goryczy była żenująca reklama „najlepsi przechodzą do T-Mobile”.

Zatem żegnaj T-Mobile, tak chyba będzie lepiej dla nas obojga.