Import dużych liczb z pliku CSV

Od czasu do czasu jestem proszony o wyciąganie z bazy różnych raportów. Prosta sprawa: zapytanie w phpMyAdminie, eksport wyniku do pliku CSV i wysyłamy… o ile w pliku nie ma kolumny zawierającej dużą liczbę. W takim wypadku zarówno LibreOffice Calc jak i Excel mają nieprzyjemny zwyczaj zaokrąglania jej. Przykładowo w 16 cyfrowym numerze karty po którym identyfikujemy klienta i w którym ostatnia cyfra jest sumą kontrolną dostajemy na końcu zawsze 0, co oczywiście skutecznie niweczy zarówno sprawdzanie sumy kontrolnej jak i identyfikację klienta.

Na szczęście w Calcu można to naprawić. W Excelu zapewne też, ale odbiorcy moich raportów nie wiedzą jak, a ja nie mam Excela, żeby eksperymentować. Dlatego przed wysłaniem raportu do działu marketingu importuję plik do Calca. W okienku z opcjami importu należy zaznaczyć kolumnę zawierającą dużą liczbę i zmienić jej typ na Tekst.

Import pliku CSV

Potem należy zapisać arkusz w formacie Excela. Taki plik można dostarczyć do marketingu.

Podpisywanie dokumentów profilem zaufanym w PUE ZUS

Ilekroć próbuję cokolwiek załatwić przez Platformę Usług Elektronicznych Zakładu Ubezpieczeń Społecznych czuję się jakbym próbował wbić gwoździa starą skarpetą – może i da się to zrobić, ale na pewno nie jest to łatwe ani wygodne. Zupełnie jakby ZUS chciał odstraszyć ludzi od załatwiania spraw online.

Teraz próbowałem złożyć deklaracje ZUS ZWUA i ZUS ZUA. O ile wypełnienie deklaracji przez kreatora było w miarę łatwe (oczywiście przez Firefoksa, bo wbudowana w Chrome’a wtyczka Flash jest nieobsługiwana), o tyle próba wysłania deklaracji kończyła się fiaskiem. Na ekranie pojawiał się kręcioł, Java odpalała jakiś SZAFIR SDK instalator i mogiła. W akcie desperacji spróbowałem całą operację przeprowadzić z komputera z Windowsem. Tutaj sytuacja była o tyle lepsza, że wyskoczyło okno podpisywania dokumentów podpisem elektronicznym. To naprowadziło mnie na właściwy trop: mimo, że do PUE loguję się przez profil zaufany ePUAP i mimo, że jako ubezpieczony podpisuję dokumenty rzeczonym profilem zaufanym to w ePłatniku domyślnie włączone jest podpisywanie dokumentów podpisem elektronicznym. Żeby to zmienić należy w zakładce Płatnik wybrać w menu po lewej Ustawienia, a następnie Ustawienia konta. Tam należy wybrać „Sposób podpisywania dokumentów w aplikacji ePłatnik” jako „Podpis profilem zaufanym ePuap”.

ZUS PUE podpis

Tworzenie pliku CSV w pamięci

Nie tak dawno pisałem o funkcjach fgetcsv() i fputcsv() w kontekście nakładania filtrów na strumienie PHP. Również z pomocą strumieni można rozwiązać problem tworzenia plików CSV w pamięci. Wystarczy otworzyć taki niby-plik w pamięci:

$fh = fopen('php://temp', 'rw');

i już można do niego zapisywać dane przez fputcsv():

fputcsv($fh, ["Imię", "Nazwisko", "Login", "PESEL"]);

Na koniec wystarczy tylko przewinąć wskaźnik pozycji w pliku na początek i pobrać jego zawartość:

rewind($fh);
$csv = stream_get_contents($fh);
fclose($fh);

Katalogi przed plikami w Gnome Shell

Jestem otwarty na nowe rozwiązania w dziedzinie interfejsu graficznego aplikacji, ale wyświetlanie katalogów przed plikami w managerze plików jest dla mnie aksjomatem. Tymczasem po którejś aktualizacji Debiana Nautilus zaczął pokazywać pliki wymieszane z katalogami (w kolejności alfabetycznej). Co gorsza, nie dało się tego zmienić przez wyklikanie bezpośrednio w Nautilusie. Musiałem uciec się do googlania, który podpowiedział w użycie dconf-editor i kluczu org.gnome.nautilus.preferences zaznaczenie opcji sort-directories-first.

dconf

Checkbox kontra NOT NULL

W encji mamy pole legal1 typu boolean, które nie może być NULL-em.

    /**
     * @var boolean
     *
     * @ORM\Column(name="legal1", type="boolean", columnDefinition="TINYINT(1)")
     */
    private $legal1;

W formularzu pole jest prezentowane jako checkbox. Niby wszystko powinno działać, a jednak jeśli wyślemy formularz bez zaznaczania checkboxa to na encji nie jest wywoływana metoda setLegal1(), a w rezultacie Doctrine próbuje zapisać NULL jako wartość pola legal1, co oczywiście kończy się wyjątkiem.

Można kombinować na samej encji (np. zwracać z getLegal1() 0 jeśli wartość to NULL), ale chyba najbardziej eleganckim rozwiązaniem będzie zastosowanie transformera, który będzie tłumaczył wartości pomiędzy encją a formularzem. Prościutki transformer realizujący takie zadanie może wyglądać tak.

namespace MDurys\CommonBundle\Lib\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;

class BoolTransformer implements DataTransformerInterface
{
    public function transform($input)
    {
        return boolval($input);
    }

    public function reverseTransform($input)
    {
        return intval($input);
    }
}

W formularzu należy go zastosować jako model transfomer:

    $builder
        ->add(
            $builder->create('legal1', 'checkbox')->addModelTransformer(new BoolTransformer())
            )

Jego działanie polega na tym, że podczas tworzenia formularza zostanie wywołana metoda transform(), która wartość z bazy danych (0 lub 1) rzutuje na wartość boolowską. Z kolei podczas zapisywania formularza zostanie wywołana metoda reverseTransform(), która rzutuje wartość z formularza na liczbę całkowitą. Tym sposobem z NULL-a zrobi się 0.

Dekodowanie daty urodzenia i płci z PESEL-u

Wczoraj poprawiałem walidację numeru PESEL w aktualnym projekcie. PESEL zawiera informacje o płci i dacie urodzenia osoby, do wyciągnięcia tych danych napisałem sobie taką klasę.

class Pesel
{
    public static function extractSex($pesel)
    {
        return intval(substr($pesel, 9, 1)) % 2 == 0 ? 1 : 2;
    }

    public static function extractDate($pesel)
    {
        list($year, $month, $day) = sscanf($pesel, '%02s%02s%02s');
        switch (substr($month, 0, 1)) {
            case 2:
            case 3:
                $month -= 20;
                $year += 2000;
                break;
            case 4:
            case 5:
                $month -= 40;
                $year += 2100;
            case 6:
            case 7:
                $month -= 60;
                $year += 2200;
                break;
            case 8:
            case 9:
                $month -= 80;
                $year += 1800;
                break;
            default:
                $year += 1900;
                break;
        }
        return checkdate($month, $day, $year)
            ? new \DateTime("$year/$month/$day")
            : null;
    }
}

Metoda extractSex() zwraca 1 jeśli numer PESEL należy do kobiety lub 2 jeśli do mężczyzny. Metoda extractDate() zwraca obiekt DateTime z datą urodzenia z PESEL-u.

W klasie brakuje walidacji poprawności PESEL-u, ale w moim projekcie załatwia to osobny walidator.

aptitude: Błędna suma kontrolna

Kilka dni temu aptitude po aktualizacji dostępnych pakietów powiedział mi:

W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/main/source/SourcesIndex: Błędna suma kontrolna
W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/contrib/source/SourcesIndex: Błędna suma kontrolna
W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/non-free/source/SourcesIndex: Błędna suma kontrolna
W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/main/binary-amd64/PackagesIndex: Błędna suma kontrolna
W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/contrib/binary-amd64/PackagesIndex: Błędna suma kontrolna
W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/non-free/binary-amd64/PackagesIndex: Błędna suma kontrolna
W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/main/binary-i386/PackagesIndex: Błędna suma kontrolna
W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/contrib/binary-i386/PackagesIndex: Błędna suma kontrolna
W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/non-free/binary-i386/PackagesIndex: Błędna suma kontrolna
W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/contrib/i18n/Translation-enIndex: Błędna suma kontrolna
W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/main/i18n/Translation-plIndex: Błędna suma kontrolna
W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/main/i18n/Translation-enIndex: Błędna suma kontrolna
W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/non-free/i18n/Translation-enIndex: Błędna suma kontrolna
E: Nie udało się pobrać niektórych plików indeksu, zostały one zignorowane lub użyto ich starszej wersji.
E: Nie można przebudować informacji o pakietach

Poproszony o upgrade odparł zaś, że ma milion niespełnionych zależności i jeśli chcę to mogę go uruchomić z opcją --full-resolver. Na takie dictum acerbum chwilowo skapitulowałem, wszak nie muszę codziennie mieć najświeższych pakietów. Zakładałem, że coś się skopało na debianowym serwerze i zaraz to naprawią. Jednak i dzień później i w kolejne dni aptitude twardo obstawał przy swoim. Tego już było za wiele. Trzeba było pokazać, kto tu rządzi:

aptitude update -o Acquire::Pdiffs=false

Powyższa opcja wymusza pobranie pełnych list pakietów, a nie tylko diffów. Pomogło.

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