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. |
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.
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.
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. |
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.
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.
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.
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.
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.
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.
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.
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. |
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.
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.
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.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2012/01/Perl
Evernote: http://evernote.com
David Pierce, "9 Ways I Use Evernote", http://www.digitizd.com/2009/04/23/9-ways-i-use-evernote/
David Allen, "Getting Things Done: The Art of Stress-Free Productivity", http://www.amazon.com/dp/0142000280
Mark Slee, Aditya Agarwal and Marc Kwiatkowski, "Thrift: Scalable Cross-Language Services Implementation", http://thrift.apache.org/static/thrift-20070401.pdf
Thrift Project Page, http://thrift.apache.org/
Michael Schilliarbeitet 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. |