Multitalent (Linux-Magazin, Januar 2012)

Die Applikation Evernote, eine Art hochstrukturierter Notizblock, zeigt dem User seine aktuellen Aufzeichungen, egal wo er sich gerade aufhält oder welches Endgerät er momentan benutzt. Eine API erlaubt programmatischen Zugriff mittels Facebooks Thrift-Library.

Ideen entstehen oft an den ungewöhnlichsten Orten, ob im fahrenden Bus oder während einer längeren Toilettensitzung. Erfahrungsgemäß verschwinden sie aber auch sehr schnell wieder aus dem Gedächtnis, wenn man sie nicht sofort zu Papier bringt. Statt Notizzetteln vertraut der moderne User heutzutage aber aufs Internet, schließlich speichert es Daten permanenter als loses Zettelwerk und ein gesuchter Begriff im Dokumentenhaufen findet sich mit maschineller Hilfe in Sekunden.

Abbildung 1: In Evernote notebooks abgelegte Web-Clippings während der Entstehung dieses Artikels.

Nadel im Heuhaufen

Die kommerzielle Applikation Evernote ([2]), die in der für Normalverbraucher völlig ausreichenden Basisversion kostenlos ist [2], bietet nicht nur ein Browserinterface, sondern auch Apps für mobile Endgeräte wie iPhone oder iPad für ihren virtuellen Zettelkasten. Die "Notes", formatierter Text mit Bildern, Audiodateien oder per Screenshot oder Cut-and-Paste eingefangene Webseiten, fasst der User thematisch in "Notebooks" zusammen, die sich wiederum in Unterordnern ("Stacks") organisieren lassen.

Der Clou an Evernote ist die laufend und unauffällig stattfindende Synchronisierung zwischen den Endgeräten. Eine auf dem Desktop mit dem Browser vorgenommene Änderung erscheint binnen weniger Sekunden auf einem laufenden Browser auf dem Laptop, der ebenfalls in das Evernote-Konto des Users eingeloggt ist. Auf mobilen Endgeräten wie dem iPad oder einem Macbook speichern die Evernote-Apps die Daten sogar lokal, sodass der User auch im Offline-Betrieb auf sie zugreifen kann.

Das simple Strukturierungsmodell der Notes in Evernotes lädt zu kreativen Basteleien ein. Aus den rudimentären Gestaltungselementen zimmert sich der User ein maßgeschneidertes Produktivitäts-Tool zusammen. Manche User berichten auf ihren Blogs ([3]), dass sie sich Kalenderfunktionen eingerichtet haben und ihren Tagesablauf im GTD-("Getting Things Done")-Verfahren ([4]) organisieren.

Abbildung 2: Evernote synchronisiert die gespeicherten Informationen nahtlos zwischen verschiedenen Endgeräten, hier auf einem I-Pad.

Abbildung 1 zeigt zum Beispiel meine Evernote-Notizen zur Entstehung dieses Artikels. Auf Stackoverflow.com stieß ich zum Beispiel auf ein Anwendungsbeispiel für die Evernote-API mit dem Thrift-Framework, und archivierte dieses flugs mit Evernotes Web-Clipper als Einstiegspunkt für spätere Recherchen. Weiter fand ich ein PDF mit dem Whitepaper des Thrift-Frameworks sowie einige Perl-Beispiele auf apache.org. Mit dieser Sammlung bewaffnet war es später ein Leichtes, offene Fragen mit den geclippten Texten oder mit Hilfe der ebenfalls gespeicherten Web-Links zu beantworten.

API statt GUI

Doch nicht immer hat man eine GUI parat, wenn man frische Ideen aufschreiben möchte, und deswegen suchte ich nach einem Kommandozeilen-Tool. Evernote bietet zum Glück eine API zum Service an und hat für die Kommunikation zwischen Clients und dem Server das von Facebook erfundene Thrift-Protokoll ([5]) gewählt. Die Entscheidung fiel wohl aus Performance-Gründen, denn das Binärprotokoll ist schlanker als Kommunikation mit XML-Objekten.

Schlank mit Thrift

Wie in Abbildung 3 illustriert, beschreiben Entwickler die zwischen Client und Server ausgetauschten Datenstrukturen in einer Datei mit der Endung .thrift im leicht lesbaren Thrift-Format. Der Thrift-Compiler erzeugt daraus Bibliotheksfunktionen für eine Vielzahl von Programmiersprachen, angefangen bei C++ und Java, aber auch Scriptsprachen wie Perl, Ruby, Python, PHP, JavaScript und schließlich sogar Exoten wie Erlang. Ziel dieser Zwischenschicht ist es, den Applikationsprogrammierer gänzlich von der ätzenden Fummelei bei der plattformübergreifenden Datenübertragung abzuschirmen.

Als Beispiel zeigt Listing 1 mit der Datei image_process.thrift die Datenstrukturen und Servicedefinitionen für einen Server, der Bilddateien um 90 Grad rotiert. Der Client schickt die Binärdaten einer JPG-Datei an den Server, letzterer nutzt das Tool convert des ImageMagick-Pakets, führt die Rotation durch und schickt das Ergebnis, ebenfalls im Binärformat an den Client zurück. Dieser speichert die JPG-Daten dann auf der Festplatte ab und meldet dem User die erfolgreiche Konvertierung oder druckt eine Fehlermeldung aus.

Abbildung 3: Der thrift-Compiler generiert aus der Thrift-Definition Perl-Code, den wiederum sowohl Client als auch Server der Applikation nutzen.

Klasse statt Masse

Thrift unterstützt nur wenige aber mächtige und portable Datentypen. Neben einfachen 32- oder 64-bit Integern darf der User Daten in Structs verpacken, oder Maps benutzen, die Perls Hashtypen ähneln. So definiert Listing 1 eine Structur Rotation, die einen Integer mit dem gewünschten Rotationswinkel und einen String mit den Binärdaten des zu drehenden Bildes aufnimmt. Der ab Zeile 12 definierte Service Rotator definiert die Funktion rotate(), die als numerierten ersten Parameter eine oben beschriebene Rotation-Struktur entgegennimmt und einen String mit den veränderten Bilddaten zum Client zurückgibt.

Listing 1: image_process.thrift

    01 namespace perl image_process
    02 
    03 struct Rotation {
    04     1: i32    angle,
    05     2: string image,
    06 }
    07 
    08 exception Failed {
    09     1: string why
    10 }
    11 
    12 service Rotator {
    13     string rotate( 1:Rotation r) 
    14       throws ( 1:Failed oops )
    15 }

Thrift wirft Exceptions, falls etwas schiefgeht, und Zeile 8 definiert eine Exception vom Typ Failed, die einen String why mit einer Erklärung für die schiefgegangene Aktion bereithält. Wer die Thrift-Distribution von apache.org ([6]) herunterlädt, und diese mit sh ./configure und make compiliert, erhält ein Executable namens thrift. Falls der Build fehlschlägt, weil Pakete für exotische Sprachen fehlen, kann man diese mit der configure-Option --disable-xxx eliminierten. Der Aufruf

    thrift -r --gen perl image_process.thrift

erzeugt den notwendigen Perl-Kleister für den reibungslosen Datenaustausch zwischen Client und Server im Unterverzeichnis gen-perl/image_process. Der Client in Listing 2 holt sich die so generierten Thrift-Wrapper in Zeile 10 herein und definiert mit Thrift::Socket, Thrift::BufferedTransport und Thrift::BinaryProtocol, die der Thrift-Distribution im Verzeichnis perl/lib beiliegen, einen Kommunikationskanal über einen Unix-Socket auf Port 9001 des localhost, auf dem der Server später lauscht. Zeile 22 instanziiert mit dem vorher definierten Binärprotokoll ein RotatorClient-Objekt, dessen Perl-Code ebenfalls von thrift autogeneriert wurde.

Thrift wirft Exceptions

Der Eval-Block ab Zeile 28 fängt etwaige Exceptions ab und die nachfolgende Prüfung im if-Konstrukt ab Zeile 47 druckt eventuell vom Server stammende Exceptions-Objekte mitsamt den in ihnen schlummernden Fehlertexten aus. Nach dem Öffnen des Transports in Zeile 29 liest der Client die auf der Kommandozeile angegebene Bilddatei mit der Funktion slurp aus dem CPAN-Modul Sysadm::Install von der Festplatte. Das in Zeile 34 instantiierte Objekt vom Typ image_process::Rotation baut mit den Methoden image() und angle() die zu übertragende Datenstruktur zusammen. Fehlt nur noch, in Zeile 39 die Methode rotate mit der Struktur aufzurufen und das Ergebnis im zurückkommenden String aufzuschnappen. Die Funktion blurt(), ebenfalls aus dem Modul Sysadm::Install vom CPAN, schreibt die rotierten Image-Daten dann in eine neue Datei, deren Name mit rotated-* beginnt.

Listing 2: rotate-client

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Sysadm::Install qw(slurp blurt);
    04 use Thrift;
    05 use Thrift::BinaryProtocol;
    06 use Thrift::Socket;
    07 use Thrift::BufferedTransport;
    08 
    09 use lib 'gen-perl';
    10 use image_process::Rotator;
    11 
    12 my $socket =
    13   Thrift::Socket->new( "localhost", 9001 );
    14 
    15 my $transport =
    16   Thrift::BufferedTransport->new( $socket,
    17   1024, 1024 );
    18 
    19 my $protocol =
    20   Thrift::BinaryProtocol->new($transport);
    21 my $client =
    22   image_process::RotatorClient->new(
    23   $protocol);
    24 
    25 my ($image) = @ARGV;
    26 die "usage: $0 image" if !defined $image;
    27 
    28 eval {
    29   $transport->open();
    30 
    31   my $image_data = slurp $image;
    32 
    33   my $action =
    34     image_process::Rotation->new();
    35   $action->image($image_data);
    36   $action->angle(90);
    37 
    38   my $rotated_image_data =
    39     $client->rotate($action);
    40 
    41   blurt $rotated_image_data,
    42     "rotated-$image";
    43 
    44   $transport->close();
    45 };
    46 
    47 if ($@ =~ m/image_process/ and 
    48     exists $@->{why}) {
    49   die $@->{why};
    50 } elsif( $@ ) {
    51   die $@;
    52 }

Der zugehörige Server in Listing 3 definiert das Package RotateHandler zum Abarbeiten der Client-Requests, das auf der autogenerierten Klasse image_process::RotatorIf fußt. In seiner Methode rotate() die der Server dank Thrift-Magie anspringt, falls der Client mit rotate() einen Request abgesetzt hat, legt er zwei temporäre Dateien an, extrahiert die Bilddaten aus dem hereingereichte Rotation-Objekt, und pumpt sie in die erste Datei. Der in Zeile 38 abgesetzte tap-Befehl ruft die ImageMagick-Utility convert mit der Option -rotate auf, die die rotierte Ergebnisdatei in die zweite temporäre Datei schreibt. Schlägt dies fehl, zimmert Zeile 40 ein Exception-Objekt zusammen, das Zeile 42 dann wirft. Thrift-Magie fängt die Exception ab und überträgt sie zum Client, der sie wiederum wirft. rotate() gibt in Zeile 45 die Bilddaten zurück, die der Thrift-Layer aufschnappt, eintütet und dem Client übermittelt, ohne dass die Applikationslogik auch nur einen Finger rühren muss.

Listing 3: rotate-server

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Thrift::Socket;
    04 use Thrift::Server;
    05 
    06 use lib 'gen-perl';
    07 use image_process::Rotator;
    08 
    09 ###########################################
    10 package RotateHandler;
    11 ###########################################
    12 use base qw(image_process::RotatorIf);
    13 use Sysadm::Install qw(slurp blurt tap);
    14 use File::Temp qw(tempfile);
    15 
    16 ###########################################
    17 sub new {
    18 ###########################################
    19   my( $class ) = @_;
    20 
    21   return bless {}, $class;
    22 }
    23 
    24 ###########################################
    25 sub rotate {
    26 ###########################################
    27   my ( $self, $rotation ) = @_;
    28 
    29   my ( $fh1, $infile ) =
    30     tempfile( UNLINK => 1 );
    31   my ( $fh2, $outfile ) =
    32     tempfile( UNLINK => 1 );
    33 
    34   blurt $rotation->{image}, $infile;
    35   my ( $stdout, $stderr, $rc ) =
    36     tap "convert", "-rotate",
    37     $rotation->{angle}, $infile, $outfile;
    38 
    39   if ( $rc != 0 ) {
    40     my $x = image_process::Failed->new();
    41     $x->why($stderr);
    42     die $x;
    43   }
    44 
    45   return slurp $outfile;
    46 }
    47 
    48 ###########################################
    49 package main;
    50 ###########################################
    51 use Log::Log4perl qw(:easy);
    52 Log::Log4perl->easy_init($DEBUG);
    53 
    54 my $port    = 9001;
    55 my $handler = RotateHandler->new();
    56 my $processor =
    57   image_process::RotatorProcessor->new(
    58   $handler);
    59 my $serversocket =
    60   Thrift::ServerSocket->new($port);
    61 my $forkingserver =
    62   Thrift::ForkingServer->new( $processor,
    63   $serversocket );
    64 
    65 DEBUG "Server starting on port $port";
    66 $forkingserver->serve();

Das Hauptprogramm ab Zeile 49 nutzt lediglich vordefinierte Thrift::Module und fährt mit Thrift::ForkingServer einen Server hoch, der auf Port 9001 auf Client-Anfragen lauscht und jedesmal einen Parallelprozess hochfährt um den gerade eingehenden Request abzuarbeiten. Nach dem Starten des Servers in einem anderen Terminal ruft der User auf der Client-Seite

    ./rotate-client image.jpg

auf, worauf das Kommando nach kurzer Verzögerung zurückkehrt und die Datei rotated-image.jpg im aktuellen Verzeichnis zurücklässt.

API zum Zettelkasten

Um nun die eingangs erwähnte Evernote-API mit dem Thrift-Framework anzusprechen, muss der Entwickler von [2] zunächst einen API-Key abholen. Da es sich bei der neuen Utility um ein Kommandozeilenskript und keine Web-Applikation handelt, ist in Abbildung 4 "Client Application" auszuwählen. Der Entwickler erhält dann einen "Consumer Key" und ein "Consumer Secret", mit dem er zunächst auf sandbox.evernote.com herumspielen kann, bevor es nach abgeschlossener Testphase auf evernote.com in die Vollen geht.

Abbildung 4: Auf der Evernote Developer-Seite erhalten Entwickler den erforderlichen API-Key.

Auf der Evernotes-Developer-Seite findet sich ebenfalls ein Link zu einem SDK im Zip-Format, das neben vielen weiteren Sprachanbindungen auch schon die vorkompilierten Thrift-Wrapper für Perl enthält. Um die thrift-Definitionen nochmals mit thrift in Perl-Code umzuwandeln, ruft man

    thrift -r --gen perl evernote-api-1.19/thrift/UserStore.thrift
    thrift -r --gen perl evernote-api-1.19/thrift/NoteStore.thrift

auf, wenn das SDK in evernote-api-1.19 entpackt wurde. Anschließend liegen dann die autogenerierten .pm-Dateien unter gen-perl im aktuellen Verzeichnis.

Aus Alt mach Neu

Leider nutzt Thrift beim Generieren des Perl-Codes die veraltete Notation new Class(), was das auf dem Testrechner verwendete perl-5.10.1 nicht verdaute. Ein beherzt aufgerufenes

    find gen-perl -name '*.pm' \
      -exec perl -p -i -e \
      's/\bnew (.*?)\(/$1->new(/g;' {} \;

durchstöbert alle autogenerierten .pm-Dateien und ersetzt die veraltete Syntax durch die neue im Format Class->new(). Nun sollte das Skript in Listing 4 ohne Murren laufen. In den Zeilen 14 bis 17 stehen die für den Zugriff notwendigen Credentials, die Produktions-Skripts aus Sicherheitsgründen auslagern sollten, am besten in einen verschlüsselten Passwort-Safe.

Noch kompatibel?

Zeile 19 nimmt ein einziges Kommandozeilenelement als Note-Titel entgegen, für Merkzettel mit mehreren Worten verwendet man Anführungszeichen:

    evernote-add "Milch einkaufen"

Das Kommando kontaktiert den Evernote-Server, legt eine neue Note mit dem Titel "Milch einkaufen" an, lässt den Body leer und fügt sie in ein vorher angelegtes Notebook namens "Inbox" ein. Anders als der vorher erzeugte Test-Client nutzt das Skript Thrift::HttpClient, der mit der Evernote-Website mittels HTTP kommuniziert. Im Thrift-Kleister ist EDAMUserStore::UserStoreClient definiert, und Listing 4 instantiiert dieses Client-Objekt für die User-Authentisierung auf Evernote in Zeile 34. Die in Zeile 38 aufgerufene Methode checkVersion prüft mit den aus dem autogenerierten Code hervorgeholten Konstanten EDAM_VERSION_MAJOR und EDAM_VERSION_MINOR, ob die Version des SDKs noch mit der Evernote-Website kompatibel ist.

EDAM steht für "Evernote Data Access and Management" und stellt zwei verschiedene Kommunikationsklassen für die Interaktion mit dem Evernote-Service bereit. EDAMUserStore::UserStoreClient hilft bei der Authentisierung des Users mit seinem Kürzel, dem Passwort, dem Consumer Key und dem Consumer Secret. Akzeptiert der Evernote-Server die Kombination, schickt er einen Authorisierungstoken zurück, den die Applikation für einen begrenzten Zeitraum für Requests mit dem EDAMNoteStore::NoteStoreClient verwenden darf. Letzterer dient zum Herumorgeln auf dem Evernote-Notizblock des Users.

Die in Zeile 48 aufgerufene Methode authenticate() liefert im Erfolgsfall ein Objekt zurück, dessen Methode user() ein User-Objekt bereitstellt. Dessen Methode shardId() gibt die User-Partition auf Evernote zurück, in die der User fällt und deren Kürzel bei Requests an die Basis-URL der Web-API anzuhängen ist. Weiter gibt die Methode authenticationToken() den Token an, den die Applikation den folgenden Requests beilegen muss.

Abbildung 5: Per Perlskript eingeschleuste Notiz im "Inbox"-Ordner.

Orgeln durch Notizen

So setzt Zeile 69 mit listNotebooks() den Befehl zum Auslesen aller Notebook-Ordner des Users auf dem Evernote-Server ab. Zurück kommt eine Referenz auf einen Perl-Array, über den die for-Schleife ab Zeile 74 iteriert. Jedes Notebook-Objekt gibt mit der Methode name() den Namen des Ordners und mit guid() eine eindeutige ID an, mit der sich später eine neu erzeugte Note in den Ordner verfrachten lässt.

Listing 4: evernote-add

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Thrift;
    04 use Thrift::HttpClient;
    05 use Thrift::BinaryProtocol;
    06 
    07 use lib 'gen-perl';
    08 use EDAMUserStore::Constants;
    09 use EDAMUserStore::UserStore;
    10 use EDAMNoteStore::NoteStore;
    11 use EDAMErrors::Types;
    12 use EDAMTypes::Types;
    13 
    14 my $username        = "perlsnapshot";
    15 my $password        = "*******";
    16 my $consumer_key    = "perlsnapshot";
    17 my $consumer_secret = "****************";
    18 
    19 my( $message ) = @ARGV;
    20 die "usage: $0 note" if !defined $message;
    21 
    22 my $evernote_host = "evernote.com";
    23 my $user_store_uri =
    24     "https://$evernote_host/edam/user";
    25 my $note_store_uri_base =
    26     "https://$evernote_host/edam/note/";
    27 
    28 my $http_client =
    29   Thrift::HttpClient->new($user_store_uri);
    30 my $protocol = Thrift::BinaryProtocol->new(
    31   $http_client);
    32 
    33 my $client =
    34   EDAMUserStore::UserStoreClient->new(
    35   $protocol);
    36 
    37 my $version_ok =
    38   $client->checkVersion( "perlsnapshot",
    39 EDAMUserStore::Constants::EDAM_VERSION_MAJOR,
    40 EDAMUserStore::Constants::EDAM_VERSION_MINOR,
    41   );
    42 
    43 if ( !$version_ok ) {
    44   die "Version not ok";
    45 }
    46 
    47 my $result =
    48   $client->authenticate( $username,
    49   $password, $consumer_key,
    50   $consumer_secret );
    51 
    52 my $user = $result->user();
    53 
    54 my $note_store_uri =
    55   $note_store_uri_base . $user->shardId();
    56 
    57 my $note_store_client =
    58   Thrift::HttpClient->new($note_store_uri);
    59 
    60 my $note_store_protocol =
    61   Thrift::BinaryProtocol->new(
    62     $note_store_client);
    63 
    64 my $note_store =
    65   EDAMNoteStore::NoteStoreClient->new(
    66     $note_store_protocol);
    67 
    68 my $notebooks =
    69   $note_store->listNotebooks(
    70     $result->authenticationToken() );
    71 
    72 my $inbox_guid;
    73 
    74 for my $notebook (@$notebooks) {
    75   if ( $notebook->name() eq "Inbox" ) {
    76     $inbox_guid = $notebook->guid();
    77     last;
    78   }
    79 }
    80 
    81 if ( !defined $inbox_guid ) {
    82   die "No Inbox notebook found";
    83 }
    84 
    85 my $note = EDAMTypes::Note->new();
    86 $note->title( $message );
    87 $note->content();
    88 
    89 my $created =
    90   $note_store->createNote(
    91   $result->authenticationToken(), $note );
    92 
    93   # move new note to "Inbox"
    94 $note_store->copyNote(
    95   $result->authenticationToken(),
    96   $created->guid(), $inbox_guid );

Zeile 75 prüft nun bei jedem aufgelisteten Notebook, ob es sich um das Notebook "Inbox" handelt und bricht die Schleife ab, nachdem es dessen GUID in der Variablen $inbox_guid abgelegt hat. Ein neues Note-Objekt mit dem auf der Kommandozeile hereingereichten Titel legt Zeile 85 mit dem Konstruktor der Klasse EDAMTypes::Note an. Der Aufruf von content() lässt den Inhalt der Note bewusst leer. Die Methode createNote() des NoteStoreClient-Objektes schickt die neue Note in Zeile 90 samt dem Auth-Token an den Server, der im Erfolgsfall in der Variablen $created ein Note-Objekt zurückgibt, dessen Methode guid() die GUID der neu angelegten Note ausgibt. Damit die neue Note wie in Abbildung 5 gezeigt im Notebook "Inbox" landet, muss Zeile 94 sie mit der Methode copyNote() unter Angabe des Auth-Tokens, der aktuellen GUID der neuen Note und der GUID des vorher ermittelten "Inbox"-Notebooks dorthin verfrachten.

Grenzenlose Vielfalt

Mit der Evernote-API lassen sich noch weitere praktische Applikationen zaubern. Da die Evernote-Applikation den User nur einzelne Notebooks exportieren lässt, böte sich ein Backup-Skript an, das sich durch alle Notebooks hangelt, den Inhalt der Notes extrahiert und in einem XML-Format in einer Backup-Datei speichert. Wer Evernote täglich nutzt, weiß zusätzliche Maßnahmen zur Sicherung brillanter Ideen sicher zu schätzen.

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2012/01/Perl

[2]

Evernote: http://evernote.com

[3]

David Pierce, "9 Ways I Use Evernote", http://www.digitizd.com/2009/04/23/9-ways-i-use-evernote/

[4]

David Allen, "Getting Things Done: The Art of Stress-Free Productivity", http://www.amazon.com/dp/0142000280

[5]

Mark Slee, Aditya Agarwal and Marc Kwiatkowski, "Thrift: Scalable Cross-Language Services Implementation", http://thrift.apache.org/static/thrift-20070401.pdf

[6]

Thrift Project Page, http://thrift.apache.org/

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.