Aus Linux-Magazin 01/2012

API des Productivity-Tools Evernote ausreizen

© Benis Arapovic, 123RF.com

Ein kräftiger Windstoß genügt, um das System der Notizzettel vom Schreibtisch zu fegen. Evernote, eine Art hochstrukturierter digitaler Notizblock, zeigt dem User seine Aufzeichnungen unabhängig von Ort und Endgerät. Ein API erlaubt programmatischen Zugriff mit Facebooks Thrift-Library.

Ideen entstehen oft an ungewöhnlichen Orten. Erfahrungsgemäß verschwinden sie aber auch schnell wieder, wenn man sie nicht sofort zu Papier bringt. Statt auf Notizzettel vertraut der Kreative heute aufs Internet, dort sind Daten haltbarer als auf losen Zetteln und ein Dokumentenhaufen ist mit maschineller Hilfe in Sekunden durchsucht.

Nadel im Heuhaufen

Die kommerzielle Applikation Evernote, in der für Normalverbraucher ausreichenden Basisversion kostenlos [2], bietet ein Browserinterface und Apps für mobile Endgeräte wie iPhone oder iPad für virtuelle Zettelkästen. Die so genannten Notes – formatierter Text mit Bildern, Audiodateien oder per Screenshot oder Cut&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 Endgeräten. Eine auf dem PC mit dem Browser vorgenommene Änderung erscheint binnen Sekunden im Browser auf dem Laptop, wenn der im Evernote-Konto eingeloggt ist. Auf Endgeräten wie iPad oder Macbook speichern die Evernote-Apps die Daten sogar lokal, was den Offlinebetrieb zulässt.

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

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

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

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

API statt GUI

Doch nicht immer hat man ein GUI parat, wenn Ideen auftauchen, und deswegen suchte ich nach einem Kommandozeilen-Tool. Evernote bietet zum Glück ein API 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 Performancegründen, denn das Binärprotokoll ist schlanker als die Kommunikation mit XML-Objekten.

Schlank mit Thrift

Entwickler beschreiben 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 (Abbildung 2), angefangen bei C++ und Java, aber auch für Skriptsprachen wie Perl, Ruby, Python, PHP, Javascript und sogar Exoten wie Erlang. Ziel dieser Zwischenschicht ist es, den Applikationsprogrammierer gänzlich von der ätzenden Fummelei bei der plattformübergreifenden Datenübertragung abzuschirmen.

Abbildung 2: Der Thrift-Compiler generiert aus der Thrift-Definition Perl-Code.

Abbildung 2: Der Thrift-Compiler generiert aus der Thrift-Definition Perl-Code.

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, der 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.

Listing 1

image_process.thrift

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 }

Klasse statt Masse

Thrift unterstützt wenige, aber mächtige und portable Datentypen. Neben einfachen 32- oder 64-Bit-Integern darf der User Daten in Structs verpacken oder Maps nutzen, die Perls Hashtypen ähneln. So definiert Listing 1 eine Struktur »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 nummerierten ersten Parameter eine »Rotation« -Struktur entgegennimmt und einen String mit den veränderten Bilddaten zum Client gibt.

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 verpatzte Aktion bereithält. Wer die Thrift-Distribution von Apache.org [6] herunterlädt und diese mit »sh ./configure« und »make« kompiliert, 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 nötigen 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. Er 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 von »localhost« , auf dem der Server später lauscht. Die Zeile 22 instanziert mit dem vorher definierten Binärprotokoll ein »RotatorClient« -Objekt, dessen Perl-Code »thrift« ebenfalls autogeneriert hat.

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 }

Thrift wirft Exceptions

Der Eval-Block ab Zeile 28 fängt etwaige Exceptions ab, die nachfolgende Prüfung im If-Konstrukt ab Zeile 47 druckt vom Server stammende Exceptions-Objekte mit 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 instanzierte 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ückgelieferten String aufzuschnappen. Die Funktion »blurt()« , auch aus dem Modul Sysadm::Install vom CPAN, schreibt die rotierten Imagedaten in eine neue Datei, deren Name mit »rotated-*« beginnt.

Der zugehörige Server in Listing 3 definiert das Package »RotateHandler« zum Abarbeiten der Clientrequests, 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 36 abgesetzte »tap« -Befehl ruft die Imagemagick-Utility »convert« mit der Option »-rotate« auf, was die rotierte Ergebnisdatei in die zweite temporäre Datei schreibt.

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();

Schlägt dies fehl, bastelt Zeile 40 ein Exception-Objekt, das Zeile 42 dann auswirft. 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 schickt, ohne dass die Applikationslogik einen Finger rühren muss.

Das Hauptprogramm ab Zeile 49 nutzt lediglich das vordefinierte Thrift-Modul und fährt mit »Thrift::ForkingServer« einen Server hoch, der auf Port 9001 auf Clientanfragen lauscht und jedes Mal einen Parallelprozess startet, um eingehende Requests abzuarbeiten. Nach dem Starten des Servers in einem anderen Terminal ruft der User auf 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 das erwähnte Evernote-API mit dem Thrift-Framework anzusprechen, muss der Entwickler von [2] zunächst einen API-Key abholen. Da es sich beim neuen Utility um ein Kommandozeilenskript und keine Webapplikation handelt, ist in Abbildung 3 »Client Application« zu wählen. Der Entwickler erhält dann einen »Consumer Key« und ein »Consumer Secret« , mit dem er zunächst auf http://www.sandbox.evernote.com herumspielen kann, bevor es nach abgeschlossener Testphase auf http://www.evernote.com]in die Vollen geht.

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

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

Auf der Developer-Seite findet sich ein Link zu einem SDK im Zip-Format, das weitere Sprachanbindungen und vorkompilierte Thrift-Wrapper für Perl enthält. Um die Thrift-Definitionen mit »thrift« in Perl-Code umzuwandeln, ruft der Zettelkasten-Programmierer

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 ist. Anschließend liegen die ».pm« -Dateien unter »gen-perl« .

Aus Alt mach Neu

Leider nutzt Thrift beim Erzeugen des Perl-Codes die veraltete Notation »new Class()« , was Perl-5.10.1 nicht verdaut. 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 alte Syntax durch »Class->new()« . Nun sollte das Skript in Listing 4 ohne Murren laufen.

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 );

In den Zeilen 14 bis 17 stehen die für den Zugriff nötigen Credentials. Produktionsskripte sollten diese aus Sicherheitsgründen natürlich auslagern, am besten in einem 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 (Abbildung 4). Anders als der zuvor erzeugte Testclient nutzt das Skript »Thrift::HttpClient« , der mit der Evernote-Website mittels HTTP kommuniziert. In dem Thrift-Kleister ist »EDAMUserStore::UserStoreClient« definiert, und Listing 4 instanziert dieses Clientobjekt für die User-Authentisierung auf Evernote in Zeile 34.

Abbildung 4: Per Perl-Skript eingeschleuste Notiz im »Inbox«-Ordner.

Abbildung 4: Per Perl-Skript eingeschleuste Notiz im »Inbox«-Ordner.

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 SDK 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 über 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 Benutzer fällt und deren Kürzel bei Requests an die Basis-URL des Web-API anzuhängen ist. Die Methode »authenticationToken()« gibt den Token an, den die Applikation den folgenden Requests beilegen muss.

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 Ordnernamen und mit »guid()« eine eindeutige ID an, mit der sich eine neu erzeugte Note in den Ordner verfrachten lässt. Zeile 75 prüft nun bei jedem aufgelisteten Notebook, ob es sich um »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« -Objekts schickt die neue Note in den Zeilen 90 und 91 samt 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 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 dem Evernote-API lassen sich weitere praktische Applikationen zaubern. Da die Applikation den User nur einzelne Notebooks exportieren lässt, böte sich ein Backupskript an, das sich durch alle Notebooks hangelt, den Inhalt der Notes extrahiert und in einem XML-Format in einer Backupdatei speichert. Wer Evernote täglich nutzt, weiß zusätzliche Maßnahmen zur Sicherung brillanter Ideen sicher zu schätzen.

Online PLUS

In einem Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/plus/2012/01

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. Thrift, Scalable Cross-Language Services Implementation: http://thrift.apache.org/static/thrift-20070401.pdf
  6. Thrift-Projekt: http://thrift.apache.org

Der Autor

Michael Schilli arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. Er hat “Goto Perl 5” (auf Deutsch) und “Perl Power” (auf Englisch) für Addison-Wesley geschrieben und ist unter mailto:mschilli@perlmeister.com zu erreichen. Seine Homepage ist http://perlmeister.com.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben