Google Drive mit seinen 5GB an kostenlosem Speicherplatz bietet sich als Online-Lager für eingescannter Papierbücher an. Clients für alle gängigen mobilen Tablets sowie eine umfassende API helfen beim Lesen von unterwegs und automatischem Einspeichern.
Noch zieren sich die deutschen Verlage etwas mit der Umstellung auf digitale Bücher, doch die Weichen sind längst gestellt. Wer hat schon Lust, ein paar Kilo Bücher mit in den Urlaub zu schleppen, wenn Übergepäck Unsummen kostet? Warum zuhause Regale mit staubfangenden Büchern zustellen, die eh keiner mehr herauszieht?
Allerdings habe ich bereits jahrelang Regale mit ungelesenen Papierbüchern zugestellt, und möchte nun nicht alle neu im Digitalformat kaufen, zumal die meisten älteren wohl nie digital erscheinen werden. Deshalb habe ich mir einen vierhundert Dollar teurer Wunderscanner von Fujitsu angeschafft, sowie ein 'Guillotine' genanntes Papiermesser (Abbildung 3), mit dem man Bücher bis zu etwa 600 Seiten von Umschlag und Bindung befreien kann. Mit einem Teppichmesser trenne ich dazu zunächst Hardcover vom Buchrumpf, die Guillotine aus chinesischer Produktion schneidet dann den geklebten oder gebundenen Buchrücken ab und die so enstandene Loseblattsammlung zieht anschließend der Fujitsu S1500 ein (Abbildungen 1-3), fährt eine OCR-Zeichenerkennung auf das Digitalformat und speichert sie als einzelnes PDF ab.
Abbildung 1: Mit einem Teppichmesser wird der Umschlagdeckel abgetrennt. |
Abbildung 2: Eine sogenannte Guillotine schneidet den geklebten Buchrücken ab. |
Abbildung 3: Der Fujitsu-Scanner S1500 zieht die losen Buchseiten ein und fährt einen OCR-Durchgang. |
Ein dicker Wälzer wie "Algorithmen in C++" schlägt so als PDF mit etwa 200-300 MB zu Buche. Damit der User das Nachschlagewerk auf allen Geräten, vom heimischen PC bis zum iPad im Urlaub, einsehen kann, bieten Firmen wie Dropbox Applikationen an, die einmal eingespeicherte Dateien magisch über das Netz verteilen, ohne dass der Anwender dies groß anordnen muss. Google ist mit "Google Drive" relativ neu in diesem Storage-Geschäft. Die für PC, Mac, iPad, Android-Geräte und Web-Browser verfügbaren Applikationen sind noch nicht so ausgereift wie die vom Platzhirschen Dropbox, aber durchaus funktionsfähig. Eine native Applikation für Linux fehlt noch, wird aber wahrscheinlich irgendwann nachgeliefert. Beide Konkurrenten stellen interessierten Bastlern hervorragen dokumentierte APIs zur Verfügung, damit diese nach Herzenslust selbst Applikationen zaubern können.
Abbildung 4: Im Web-Browser zeigt Google Drive die gespeicherten PDF-Dateien. |
Abbildung 4 zeigt die auf Google Drive hochgeladenen PDF-Dateien in einem Chrome-Browser, aber auch Firefox wird unterstützt. Ein Mausklick auf eine Datei löst einen Download aus, und einige Zeit später springt der PDF-Reader an, um das gescannte Buch anzuzeigen. Im iPad speichert die Applikation "Google Drive" die PDF-Daten zwischen, bis zu einer einstellbaren Grenze. Beim Download der App ist darauf zu achten, dass es sich um das Original von Google handelt, es sind einige minderwertige Klone von Drittfirmen im Umlauf.
Abbildung 5: Das gescannte Algorithmus-Buch im PDF-Reader "Goodreader" auf dem iPad. |
Abbildung 6: Die Google Drive-App auf dem iPad, die gescannte Papierbücher als PDFs anzeigt. |
Als PDF-Reader empfiehlt sich bei großen PDF-Dateien nicht das Original von Adobe, da es nicht für schwachbrüstige Mobilgeräte ausgelegt ist. Kostenpflichtige Apps wie Goodreader oder ezPDF auf einem Android-Telefon bieten da mehr Komfort und Performance. Das Hochladen der gescannten Bücher gestaltet sich im Browser recht einfach, allerdings kann es je nach Netzwerkverbindung eine Weile dauern. Statt dies jedes Mal manuell durchzuführen, empfiehlt sich der Einsatz einer handgeschriebenen Applikation. Dank der umfangreichen API mit hervorragender Dokumentation ([3]) ist dies leicht möglich. Leider liefert Google nur SDKs für Java, Ruby, PHP und Python, doch die REST-basierte Web-API lässt sich auch leicht von einem Perl-Skript nutzen.
Nur berechtigte User mit registrierten Applikationen dürfen programmatisch auf die unter Umständen sensiblen Daten auf dem Google Drive zugreifen. Google regelt die Berechtigung mit dem OAuth-Protokoll, mit dem Web-Applikationen von Drittanbietern Userdaten lesen und manipulieren können ohne dass der User dem Drittanbieter sein Passwort mitteilen muss.
Hierzu holt sich eine registrierte Applikation zunächst von Google einen Request-Token ab und leitet den User auf die Google-Login-Seite weiter. Dort trägt dieser seinen Usernamen und sein Passwort ein, falls er noch nicht eingeloggt ist, und erhält anschließend einen Dialog vorgesetzt, der nachfragt, ob er wirklich gewillt ist, dem Drittanbieter Zugriff auf seine Daten zu gewähren. Nach der Bestätigung leitet Google den Browser des Benutzers wieder zurück zur Applikation des Drittanbieters und schickt diesem einen Access-Token und einen Refresh-Token mit.
Der Access-Token gilt für eine bestimmte Zeitspanne (zum Beispiel eine Stunde) und erlaubt dem Drittanbieter, entsprechend dem vom User gewählten Berechtigungs-"Scope" auf dessen Daten herumzuorgeln. Manche Scopes erlauben nur das Lesen der Email-Adresse, andere das Lesen aller Dateien auf Google Drive und einer sogar das Lesen und Schreiben aller Daten. Erlischt die Gültigkeit des Access-Tokens nach der eingestellten Zeitspanne, schickt die Applikation den Refresh-Token zurück an Google und erhält postwendend einen neuen Access-Token, falls der User der Applikation auf Google noch nicht den Zugriff entzogen hat.
Auf der "API Console" ([4]) meldet der Experimentierfreudige Applikationen an, die Zugriff auf die Google-APIs brauchen. Neue Projekte dürfen aus einer Vielzahl von APIs wählen (Abbildung 5). Für Google Drive ist der Schalter "Google Drive API" umzulegen, der für das darunter stehende Google Drive SDK bleibt auf "off".
Abbildung 7: Registrierung der Applikation, die die Google-Drive-API nutzt, bei Google. |
Abbildung 8: Anschalten der Google Drive API (nicht des SDKs) für das Perl-Projekt. |
Nach dem Akzeptieren der allgemeinen Nutzungsbedingungen erscheinen zwei Schlüssel
als Hex-Strings: Die Client-ID und das "Client Secret". Da beide fest im Client
verdrahtet sind, handelt es sich bei letzterem wohl kaum um ein Geheimnis. Scripts
mit diesen beiden Werten dürfen von den Google-API-Servern nun Request-Tokens
verlangen, um User zu registrieren. Das Skript in Listing 1 startet einen
Mojolicious-Server auf Port 8082 des lokalen Rechners, mit dem sich der Browser
in Abbildung Y verbindet. Dieses Verfahren wurde im Perl-Snapshot
schon einmal in [2] mit der Dropbox-API besprochen
und dient dazu, Google den Request-Token
zu entlocken, weil es sich bei dem Skript zum Zugriff auf die Google-Drive-Dateien
nicht um eine Web-Applikation handelt. Klickt der User auf den dargestellten Link
"Login on Google Drive", springt der Browser zu Google, das den User authentifiziert
und fragt, ob er der Applikation "splitsync" (so der Name der Applikation)
Zugriff auf sein Drive gewährt (Abbildung Z). Nach dieser Bestätigung schickt
Google den Browser wieder zurück auf die Redirect-URL, in diesem Fall den
Mojolicious-Server auf http://localhost:8082, der die ebenfalls übermittelten
Access- und Refresh-Tokens in der YAML-Datei ~/.google-drive.yml
abspeichert und
die Erfolgsmeldung (Abbildung (A)) ausgibt.
Abbildung 9: (Y) Zum Einloggen wendet sich der User an den Mojolicious-Server. |
Abbildung 10: (Z) Google fragt den User, ob er der Perl-Applikation "splitsync" Zugriff auf seine Drive-Dateien gewährt. |
Abbildung 11: (A) Der Mojolicious-Server hat Access- und Refresh-Tokens von Google erhalten und speichert sie in einer YAML-Datei ab. |
Die bei der Projekt-Registrierung erhaltene Client-ID und das Client-Secret
verdrahtet google-drive-init
in den Zeilen 13 und 14. Als Scope gibt es
"www.googleapis.com/auth/drive" an und verlangt demnach Schreib-Lese-Zugang zum
Drive des Users. Entsprechende Rechte holt Google in Abbildung (Z) ein.
Eingehende Browseranfragen unter dem Pfad "/" landen im Handler ab Zeile 34,
der den Parameter login_url
für das Template am unteren Rand des Skripts
angibt. Später stellt der Browser einen Link darauf dar (Abbildung Y). Die
Rücksprungadresse auf dem Mojolicious-Server liegt unter dem Pfad /callback
.
Der zugehörige, ab Zeile 44 definierte Handler schnappt sich den von Google gesandten
Parameter code
und gibt ihn der Funktion tokens_get
. Sie schickt einen
POST-Request and die Accounts-API in Zeile 73, packt die Client-ID und das
Client-Secret bei, und bekommt einen Request- und einen Refresh-Token im JSON-Format
zurück.
Beide legt sie zusammen mit dem Verfallsdatum und den Client-Daten
in der YAML-Datei "~/.google-drive.yml" im Home-Verzeichnis des Users ab, damit
später Skripts wie das in Listing 2 mit den Drive-Daten des Users spielen dürfen.
Da das Verfallsdatum des Request-Tokens in relativ nach der aktuellen Zeit verstrichenen
Sekunden eintrifft, addiert Zeile 59 die aktuelle Zeit in Sekunden seit 1970 dazu
und speichert den Zeitstempel unter dem Schlüssel expires
ab. Später muss
ein Skript nur noch prüfen, ob die aktuelle Sekundenzeit den Zeitstempel
überschritten hat.
001 #!/usr/local/bin/perl -w 002 use strict; 003 use Mojolicious::Lite; 004 use YAML qw( DumpFile ); 005 use Log::Log4perl qw(:easy); 006 use HTTP::Request::Common; 007 use JSON qw( from_json ); 008 use URI; 009 010 my $client_id = "XXX"; 011 my $client_secret = "YYY"; 012 my $scope = 013 "https://www.googleapis.com/auth/drive"; 014 my $listen = "http://localhost:8082"; 015 my($home) = glob '~'; 016 my $CFG_FILE = 017 "$home/.google-drive.yml"; 018 my $redir_uri = "$listen/callback"; 019 020 my $login_uri = URI->new( 021 "https://accounts.google.com" . 022 "/o/oauth2/auth" ); 023 024 $login_uri->query_form ( 025 response_type => "code", 026 client_id => $client_id, 027 redirect_uri => $redir_uri, 028 scope => $scope, 029 ); 030 031 @ARGV = (qw(daemon --listen), $listen); 032 033 ########################################### 034 get '/' => sub { 035 ########################################### 036 my ( $self ) = @_; 037 038 $self->stash->{login_url} = 039 $login_uri->as_string(); 040 041 } => 'index'; 042 043 ########################################### 044 get '/callback' => sub { 045 ########################################### 046 my ( $self ) = @_; 047 048 my $code = $self->param( "code" ); 049 050 my( $access_token, $refresh_token, 051 $expires_in ) = 052 tokens_get( $self->param( "code" ) ); 053 054 DumpFile $CFG_FILE, { 055 access_token => $access_token, 056 refresh_token => $refresh_token, 057 client_id => $client_id, 058 client_secret => $client_secret, 059 expires => time() + $expires_in, 060 code => $code 061 }; 062 063 $self->render_text( "Tokens saved.", 064 layout => 'default' ); 065 }; 066 067 ########################################### 068 sub tokens_get { 069 ########################################### 070 my( $code ) = @_; 071 072 my $req = &HTTP::Request::Common::POST( 073 'https://accounts.google.com/o/' . 074 'oauth2/token', 075 [ 076 code => $code, 077 client_id => $client_id, 078 client_secret => $client_secret, 079 redirect_uri => $redir_uri, 080 grant_type => 'authorization_code', 081 ] 082 ); 083 084 my $ua = LWP::UserAgent->new(); 085 my $resp = $ua->request($req); 086 087 if( $resp->is_success() ) { 088 my $data = 089 from_json( $resp->content() ); 090 091 return ( $data->{ access_token }, 092 $data->{ refresh_token }, 093 $data->{ expires_in } ); 094 } 095 096 warn $resp->status_line(); 097 return undef; 098 } 099 100 app->start; 101 102 __DATA__ 103 ########################################### 104 @@ index.html.ep 105 % layout 'default'; 106 <a href="<%= $login_url %>" 107 >Login on Google Drive</a> 108 109 @@ layouts/default.html.ep 110 <!doctype html><html> 111 <head><title>Token Fetcher</title></head> 112 <body> 113 <pre> 114 <%== content %> 115 </pre> 116 </body> 117 </html>
Listing 2 implementiert eine praxisorientierte Applikation, die prüft, ob alle in einem lokalen Verzeichnis gespeicherten Dateien bereits auf das Google Drive hochgespielt wurden. Mittels der API schickt sie eine Anfrage an den Google Drive API server unter dem Pfad /files, der ein Listing aller unter dem Account des autentisierten Users liegenden Dateien anfordert.
01 #!/usr/local/bin/perl -w 02 use strict; 03 use LWP::UserAgent; 04 use HTTP::Request; 05 use HTTP::Headers; 06 use HTTP::Request::Common; 07 use File::Basename; 08 use YAML qw( LoadFile DumpFile ); 09 use JSON qw( from_json ); 10 11 my( $home ) = glob "~"; 12 my $cfg_file = "$home/.google-drive.yml"; 13 my $syncdir = "$home/books"; 14 my @books_local = 15 map { basename $_ } <$syncdir/*.pdf>; 16 17 my $cfg = LoadFile $cfg_file; 18 19 if( $cfg->{ expires } - 60 < time() ) { 20 warn "Token needs to be refreshed."; 21 token_refresh( $cfg ); 22 DumpFile( $cfg_file, $cfg ); 23 } 24 25 my $req = HTTP::Request->new( 26 GET => 'https://www.googleapis.com/' . 27 'drive/v2/files?maxResults=3000', 28 HTTP::Headers->new( Authorization => 29 "Bearer " . $cfg->{ access_token }) 30 ); 31 32 my $ua = LWP::UserAgent->new(); 33 my $resp = $ua->request( $req ); 34 35 if( ! $resp->is_success() ) { 36 die $resp->message(); 37 } 38 39 my $data = from_json( $resp->content() ); 40 41 my %books_remote = (); 42 43 for my $item ( @{ $data->{ items } } ) { 44 if( $item->{ kind } eq "drive#file" ) { 45 my $file = $item->{ originalFilename }; 46 next if !defined $file; 47 $books_remote{ $file } = 1; 48 } 49 } 50 51 for my $book ( @books_local ) { 52 if( !exists $books_remote{ $book } ) { 53 print "Book not saved yet: [$book]\n"; 54 } 55 } 56 57 ########################################### 58 sub token_refresh { 59 ########################################### 60 my( $cfg ) = @_; 61 62 my $req = &HTTP::Request::Common::POST( 63 'https://accounts.google.com/o' . 64 '/oauth2/token', 65 [ 66 refresh_token => 67 $cfg->{ refresh_token }, 68 client_id => 69 $cfg->{ client_id }, 70 client_secret => 71 $cfg->{ client_secret }, 72 grant_type => 'refresh_token', 73 ] 74 ); 75 76 my $ua = LWP::UserAgent->new(); 77 my $resp = $ua->request($req); 78 79 if ( $resp->is_success() ) { 80 my $data = 81 from_json( $resp->content() ); 82 $cfg->{ access_token } = 83 $data->{ access_token }; 84 $cfg->{ expires } = 85 time() + $data->{ expires_in }; 86 return 1; 87 } 88 89 warn $resp->status_line(); 90 return undef; 91 }
Liegt ein gültiger Request-Token vor, kommt von Google ein JSON-String nach Abbildung
C zurück, der alle auf dem Drive liegenden Dateien anzeigt, unabhängig davon, in welchem
Verzeichnis sie liegen. Zeile 44 prüft, ob der Parameter kind
auf
"drive#file" gesetzt ist und Zeile 45 extrahiert den Wert von originalFilename
,
den Namen der hochgeladenen Datei. Jeder Treffer wird im Hash %books_remote
abgespeichert, damit die For-Schleife ab Zeile 51, die über alle lokal im
vorgegebenen Verzeichnis ~/books
gefundenen Bücher läuft, blitzschnell
feststellen kann, welche Bücher noch nicht gesichert wurden.
Ist der in der YAML-Datei gesicherte Token abgelaufen, weil seit dem
Lauf von google-drive-init
mehr als eine Stunde vergangen ist, holt die
Funktion token_refresh
ab Zeile 58 einen frischen Access-Token vom Server.
Sie benötigt außer dem Refresh-Token nur noch die Client-Daten, aber keine
Eingaben seitens des Benutzers mehr. Dieser kann weitere Token-Auffrischer nur
verhindern, indem er auf Google der Applikation "splitsync" die Rechte hierzu
entzieht.
Abbildung 12: (C) JSON-Daten mit auf Google Drive gespeicherten Dokumenten, die die Google-API auf eine Datei-Listing-Anfrage zurückschickt. |
Abbildung 13: Volltextsuche auf den PDF-Dateien im Google-Drive. |
Eine schöner Nebeneffekt der Datenspeicherung auf Google Drive ist, dass Google den Inhalt der Dateien indiziert und eine Volltextsuche darauf erlaubt. Zu beachten ist allerdings, dass dies nicht mit 200MB-Monsterdateien funktioniert. Als weiteres Testprojekt habe ich deshalb ein Skript erstellt, das die PDF-Dateien an Seitengrenzen in Einzelstücke von etwa 25MB Größe aufteilt. Dies beschleunigt nicht nur das Herunterladen bestimmter Buchteile, sondern wirft auch Googles Indizierungsmechanismus an, so dass nun, wie in Abbildung X ersichtlich, eine Suche nach "Heuschnupfen" tatsächlich Treffer im ebenfalls eingescannten "Medizinischen Hausbuch" liefert.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2012/12/Perl
"Ab in die Kiste", Michael Schilli, Linux-Magazin 2011/07, http://www.linux-magazin.de/Heft-Abo/Ausgaben/2011/07/Perl-Snapshot
"Integrate your app with Google Drive", https://developers.google.com/drive/
"API Console", https://code.google.com/apis/console#access