Ubuntu One - filesharing dla początkujących
02.02.2013 17:21
Parafrazując klasykę: W czasie sesji studenci się nudzą. To ogólnie znana rzecz. Zamiast chłonąć wiedzę niezbędną do zaliczenia mniej lub bardziej ważnych egzaminów, jedni oglądają seriale, inni kontemplują anatomię krążącej po pokoju muchy, a ja... Ja postanowiłem napisać zbędną aplikacyjkę automatyzującą trywialną i niezbyt czasochłonną czynność. Prawda, że szczytny cel?
Jedną z ważniejszych funkcjonalności mojej YetAnotherUselessApplication miała być publikacja wynikowego pliku tekstowego i udostępnienie odnośnika wyszczególnionej osobie. Mam kawalątek serwera FTP, więc udostępnienie jednego katalogu osobie trzeciej nie stanowi problemu. Rozwiązanie banalne.
Zbyt banalne...
Może to przez wyrzuty sumienia (gdzieś na krawędzi pola wzroku ciągle miga czerwone jak cegła i rozgrzane jak piec przypomnienie o zbliżającym się dniu sądu), ale postanowiłem spróbować czegoś nowego i być może nawet czegoś się nauczyć. Odpaliłem więc Google'a i sprawdziłem, które popularne hostingi plikowe otwarte są na komunikację nie tylko przez przeglądarkę internetową. Jak się okazało, jest takowych całkiem sporo. Moją uwagę przyciągnęło Ubuntu One. Niby mój sprzęt nie lubi się z debianowcami, a działaniom Canonical przyglądam się z lekkim politowaniem, ale... Czemu by nie spróbować? Zwłaszcza że znalazłem informacje o javowym API. Nastawionym co prawda na Androida, ale z powodzeniem działającym na "zwykłej" Javie.
API okazało się być nie dziełem Canonical, a tworem sympatyka idei U1. Samo w sobie nic złego - proza życia linuksowego światka. Trochę gorzej, że dokumentacja do wybitnie wyczerpujących nie należy i parę średnio oczywistych spraw wyjaśnia średnio oczywistymi sposobami. Przykładów wykorzystania można w internecie ze świecą szukać, a i tak na chwilę obecną znajdzie się tylko kod aplikacji popełnionej przez autora. Po paru dniach zabawy i kilku zagwozdkach (z kategorii tych, po których człowiek puka się w czoło i mamrocze "To takie proste, dlaczego wcześniej na to nie wpadłem?") przyszło mi tłumaczyć zagadnienie znajomemu. A jak już raz poszło, może skrobnę jeszcze co nieco tutaj. A nuż ktoś, tak samo jak ja, będzie szukał króciutkiego i banalnego przykładu działania U1FileAPI ;‑]
Disclaimer
Na samym wstępie - to podstawy podstaw, w żadnym wypadku coś, co można by w niezmienionej formie wykorzystać w najprostszej nawet aplikacji traktowanej "na poważnie". Starałem się, żeby było prosto i czytelnie. Żeby kod w ogóle zadziałał, niezbędne będzie obsłużenie paru wyjątków (IDE powie wam jakich) - Internet i serwery bywają kapryśne. Poza tym do działania trzeba dorzucić sporo zewnętrznych bibliotek (lista na końcu). No i przy czymś większym niż kilkadziesiąt linijek warto popakować to w klasy i metody.
Żeby wiedzieli, że ja to ja
Ubuntu One korzysta z dobrodziejstw OAuth. Można powiedzieć, że to mechanizm zbliżony do znanego i uznanego OpenID. Na podstawie loginu (w postaci adresu email), hasła i identyfikatora aplikacji serwer autoryzujący przyznaje nam token - klucz, który ma nas jednoznacznie identyfikować. To właśnie on otwiera nam podwoje do Ubuntu One i paru innych usług oferowanych przez Canonical. Autor javowego U1FileApi udostępnia również bibliotekę mocno upraszczającą i ten aspekt. Autoryzacja wygląda więc tak:
BasicAuthorizer basicAuthorizer = new BasicAuthorizer(login, password); U1AuthAPI authAPI = new U1AuthAPI( packageName, version, httpClient, basicAuthorizer); AuthenticateResponse authenticateResponse = authAPI.authenticate("Ubuntu One @ Moja Aplikacja"); OAuthAuthorizer oAuthAuthorizer = new OAuthAuthorizer(authenticateResponse); authAPI.setAuthorizer(oAuthAuthorizer);
Jak widzimy, autoryzacja przebiega w kilku etapach.
Do wstępnego potrzebny nam login i hasło naszego konta. Przy okazji kolejnego wymaga się od nas nazwy pakietu i wersji aplikacji (zapewne do celów statystycznych, bo zawartość obu zmiennych nie musi pokrywać się z faktami), dalej httpClient - instancja DefaultHttpClienta z ThreadSafeClientConnManagerem ze znanej i lubianej biblioteki fundacji Apache i wcześniej utworzony BasicAuthorizer. Tak przygotowany obiekt klasy U1AuthAPI jest właśnie tym, na czym nam najbardziej zależało, ale dopiero w ostatniej linijce przedstawionego kodu zyskuje swoje magiczne funkcjonalności. Wcześniej musimy przekazać serwerowi OAuth login, hasło i identyfikator aplikacji (linia 3), z odpowiedzi przygotować ostateczny uwierzytelnieniacz i zaaplikować go do naszego authApi. Koniec, jesteśmy zalogowani.
Ufff...
Jeszcze mały komentarz odnośnie "Ubuntu One @ Moja Aplikacja". Twórca API nalega, żeby trzymać się takiej konwencji zapisu, włącznie ze spacjami. Po @ oczywiście możemy wpisać, co nam się podoba, powinniśmy jednak pamiętać, że właśnie ten identyfikator wraz z datą logowania pojawi się na naszym profilu Ubuntu One w sekcji Devices. Warto rozważyć treść pod kątem "A co po przeczytaniu powiedziałaby moja matka?".
I od razu z rozpędu kolejny komentarz. Token, który utrzymujemy z serwera OAuth, wystarczy nam na wiele logowań. Teoretycznie powinien mieć jakiś czas życia, ale z tego, co zaobserwowałem, to zależy od możnowładców u sterów danej usługi. Facebook wymaga odnowienia co 60 dni. Do Twittera można używać tego samego do końca świata i jeden dzień dłużej. Na razie nie dostałem odpowiedzi od na pytanie, jak się na to zapatruje Ubuntu, ale w celach zabawowych można sobie troszkę przyspieszyć proces. Wystarczy zapisać zawartość tokena (wrzucić do pliku albo nawet wypisać na ekran, najnormalniej w świecie skopiować i na stałe przypisać do zmiennej) i już więcej nie łączyć się z serwerem oAuth.
Do tokena dobieramy się tak:
String token = authenticateResponse.getSerialized();
I wtedy zamiast linijek 3 i 4 stosujemy
OAuthAuthorizer oAuthAuthorizer = OAuthAuthorizer.getWithTokens( token, new PlainTextMessageSigner());
Dokumentacja wspomina też o opcjonalnej synchronizacji czasowej naszego autentykatora i serwera uwierzytenień, ale w moim przypadku wydaje się to być zbędne. Mimo wszystko, gdyby ktoś z nieznanego powodu nie mógł otrzymać dostępu, może wypróbować:
OAuthAuthorizer.syncTimeWithSSO(httpClient); OAuthAuthorizer.syncTimeWithU1(httpClient);
Kolejna (i ostatnia, obiecuję) sprawa pośrednio związana z uwierzytelnieniem. Trochę to trwa, ale podobno w dobrym tonie jest pingnąć nasze konto U1. Z tego, co zaobserwowałem, musimy tak zrobić, jeżeli pobieramy token z serwera (inaczej, mimo poprawnej autoryzacji, nie zostaniemy dopuszczeni do plików). Jeżeli idziemy na skróty i uwierzytelniamy się gotowcem, zazwyczaj można ten krok pominąć. Doświadczenie wynikające z x‑godzinnej zabawy wskazuje, że jeżeli wykonujemy parę niezbyt oddalonych od siebie czasowo czynności, warto pingnąć za pierwszym razem, a potem już się nie przejmować.
authAPI.pingUbuntuOne(login);
Jeżeli w wyniku dostaniemy INFO: Ping OK: HTTP/1.1 200 OK, to OK.
To, co tygryski lubią najbardziej
Po wszystkich tych obrzędach wstępnych tak naprawdę nie zostało już wiele do wyjaśniania. Pełnię władzy nad zamieszczonymi plikami daje nam obiekt klasy U1FileAPI. Do stworzenia go ponownie wymaga się od nas nazwy pakietu i wersji aplikacji, httpClienta i, co najważniejsze, naszego OAuthAuthorizera.
final U1FileAPI fileAPI = new U1FileAPI( packageName, version,httpClient, oAuthAuthorizer);
Słówko o strukturze katalogowej U1
U podstawy stoją woluminy. Domniemuję, że są to katalogi, z którymi synchronizują się poszczególne urządzenia użytkownika. Domyślnie posiadamy jeden (niewidoczny z poziomu przeglądarki) wolumin o nazwie ~/Ubuntu One, ale nic nie stoi na przeszkodzie, żeby stworzyć ich więcej.
Dalej mamy byty zwane węzłami. To ogólny typ zawierający metody wspólne dla katalogów i plików. Jeżeli chcemy wywołać metodę któregoś z podtypów, stosujemy najnormalniejsze w świecie rzutowanie. Oczywiście musimy uważać, żeby nie potraktować katalogu jak pliku i odwrotnie. Podtyp węzła poznajemy dzięki metodzie getKind().
if (u1Node.getKind() == U1NodeKind.FILE) System.out.println(u1Node.getName() + " jest plikiem");
Magia właściwa
Najprostsze i najbardziej standardowe "zrobienie czegoś" wygląda tak:
final String rootNode = "~/Ubuntu One"; fileAPI.listDirectory(rootNode, new U1NodeListener() { @Override public void onSuccess(U1Node u1Node) { System.out.println(u1Node.getName()); } });
Konstrukcja nie powinna dziwić nikogo, kto wykroczył poza javowe HelloWorld. Serwerowi przesyłamy polecenie i nasłuchujemy odpowiedzi, a ta inicjuje odpowiednie zdarzenie. W powyższym przykładzie wykorzystano onSuccess, które startuje po powodzeniu każdej czynności cząstkowej. W przypadku listowania katalogu taką czynnością jest wykrycie węzła (katalogu lub pliku). Zwracany jest nam też obiekt zawierający dane dotyczące węzła. Ścieżkę, nazwę, czy jest publiczny itd.
Oprócz onSuccess możemy skorzystać z onStart, onUbuntuOneFailure (błąd po stronie U1), onFailure (błąd nasz lub między nami i U1) i onFinish.
Prawie każde polecenie wymaga od nas podania ścieżki do zasobu. Ta, jak widać na przykładzie wyżej, jest zwykłym łańcuchem tekstowym. Możemy zażądać np. "~/Ubuntu One/testFolder/log12-03.txt" albo (przy wykorzystaniu utworzonej zmiennej) rootNode+"/testFolder/log12-03.txt". lub jeszcze lepiej wylistować katalog, zapisać ścieżkę konkretnego węzła do zmiennej i przekazać ją do kolejnego żądania.
Innym powszechnym składnikiem jest adekwatny Listener. Tych mamy sporo: dedykowane dla każdego rodzaju obiektu i uniwersalne, bardziej ogólne. W praktyce wygląda to tak, że IDE samo doradzi nam, którego użyć w danym miejscu (dzięki ci, Najwyższy, za kombinację CTRL + spacja). Chętnych zachęcam do rzucenia okiem na dokumentację.
Ostatni komentarz tyczy się ścieżki zapisu pliku pobieranego/uploadowanego. Wskazujemy nie tylko na katalog (w U1 lub na dysku), ale zawieramy też nazwę pliku. Inaczej odpowiednie metody mogą stwierdzić, że wszystko jest ok, a my efektu nie zobaczymy.
Teraz, kiedy wszystko jest już jasne, brakuje tylko listy niezbędnych do działania jarów i przykładowego kawałka kodu (przypominam - obsługa wyjątków i niepowodzeń transferów to naprawdę ważna rzecz). Nagłówki części innych metod wskaże wam dokumentacja, resztę podpowie IDE.
Prosty przykład
Biblioteki do dołączenia:
- commons-codec
- commons-logging
- httpclient
- httpcore
- jackson-mini
- json-org
- libUbuntuOneFiles
- libUbuntuSSO
- signpost-commonshttp
- signpost-core
W nieco czytelniejszej formie.
Boolean isSyncingModeOn = false; Boolean isPingModeOn = false; // Oczywiście zamiast UbuntuOneTest każdy wstawia własną klasę final String packageName = UbuntuOneTest.class.getPackage().getName(); final String version = "0.1"; final HttpClient httpClient = new DefaultHttpClient( new ThreadSafeClientConnManager()); final String login = "mail@gmail.com"; final String password = "moje_hasło"; final String rootNode = "~/Ubuntu One"; String token = "140 dziwnych znaczków"; // Jeżeli nie mamy zapisanego tokena: //String token = null; BasicAuthorizer basicAuthorizer = new BasicAuthorizer(login, password); U1AuthAPI authAPI = new U1AuthAPI( packageName, version, httpClient, basicAuthorizer); OAuthAuthorizer oAuthAuthorizer; if (token == null) { AuthenticateResponse authenticateResponse = authAPI.authenticate("Ubuntu One @ Moja Aplikacja"); oAuthAuthorizer = new OAuthAuthorizer(authenticateResponse); token = authenticateResponse.getSerialized(); } else { oAuthAuthorizer = OAuthAuthorizer.getWithTokens( token, new PlainTextMessageSigner()); } authAPI.setAuthorizer(oAuthAuthorizer); if (isSyncingModeOn) { OAuthAuthorizer.syncTimeWithSSO(httpClient); OAuthAuthorizer.syncTimeWithU1(httpClient); } if (isPingModeOn) { authAPI.pingUbuntuOne(login); } final U1FileAPI fileAPI = new U1FileAPI( packageName, version,httpClient, oAuthAuthorizer); // Tworzy katalog /test/ w obrębie domyślnego woluminu fileAPI.makeDirectory( rootNode+"/test/", new U1NodeListener() { @Override public void onSuccess(U1Node u1Node) { System.out.println("Stworzyłem katalog "+u1Node.getName()); } } ); // Uploaduje tekstowy plik log1 do uprzednio utworzonego katalogu fileAPI.uploadFile( "/home/frank/log1", // ścieżka na dysku "text:plain", // zawartość pliku rootNode+"/test/log1", // ścieżka docelowa true, // czy serwer może zduplikować plik, // jeżeli posiada już identyczny false, // czy od razu upublicznić new U1UploadListener() { @Override public void onSuccess(U1Node u1Node) { System.out.println("Zuploadowałem plik: "+u1Node.getName()); } }, null // cancelTrigger ); // Pobiera utworzony plik, zapisuje go jako [new]log1 fileAPI.downloadFile( rootNode+"/test/log1", "/home/frank/[new]log1", new U1DownloadListener() { @Override public void onSuccess() { System.out.println("Pobieranie zakończone"); } }, null ); // Usuwa plik fileAPI.deleteNode( rootNode+"/test/log1", new U1NodeListener() { @Override public void onSuccess(U1Node u1Node) { System.out.println(u1Node.getName()+" usunięty"); } } ); // Wypisuje nazwy i linki do publicznych plików (nie węzłów) // z domyślnego katalogu domyślnego woluminu fileAPI.listDirectory( rootNode, new U1NodeListener() { @Override public void onSuccess(U1Node u1Node) { if (u1Node.getKind() == U1NodeKind.FILE) { U1File file = (U1File)u1Node; if (file.getIsPublic()) System.out.println( file.getName()+": "+file.getPublicUrl())); } } } ); // Wypisuje nazwy i adresy wszystkich publicznych plików // z _całej struktury_ konta U1 fileAPI.getPublicFiles(new U1NodeListener() { @Override public void onSuccess(U1Node u1Node) { U1File file = (U1File)u1Node; System.out.println(String.format( "%s: %s", file.getName(), file.getPublicUrl())); } });
Zapomniałbym...