poniedziałek, października 18, 2010

Wysyłamy pliki do Google Docs przy pomocy Go :-)

W ramach szukania pomysłów na wpis do bloga, a i nauki Go chciałem napisać sobie uploadowarkę plików do Google Docs przy pomocy Go........
I częściowo mi się to udało ;-)

Częściowo, bo działa tylko pod Linuskem, pod portem dla Windows nie działa.

Błąd na Windows pojawia się w trakcie próby zalogowania się do Google Docs, a dokładniej do Writely bo tak nazywa się serwis związany z Google Docs [bez arkuszy] dostaję błąd "could not find root certificate for chain", a przez to reszta zabawy nie ma sensu :-(

Na Linuksie jednak wszystko działa :-) i cały program wygląda tak:

package main

import ("fmt";"http";"os";"strings";"io";"net";"bufio")

type nopCloser struct {
io.Reader
}

func (nopCloser) Close() os.Error { return nil }

type readClose struct {
io.Reader
io.Closer
}


func main() {
var user string
var password string
if len(os.Args)==3 {
user = os.Args[1]
password = os.Args[2]
} else {
fmt.Println("Sorry, you need to provide your Google Account credentials")
return
}
fmt.Println(password)
fmt.Println(user)

// OK, tutaj zaczyna się logowanie do serwisów Google, tym razem do "writely" czyli składowiska dla dokumentów, prezentacji i obrazków Google Docs

bodyType:="application/x-www-form-urlencoded"
appName:="RMKGOTest"
appVersion:="0.01"
service:="writely"
var loginRequestBody = "accountType=HOSTED_OR_GOOGLE&Email="+user+"&Passwd="+password+"&service="+service+"&source="+appName+"-"+appVersion;
r,err:=http.Post("https://www.google.com/accounts/ClientLogin",bodyType,strings.NewReader(loginRequestBody))
fmt.Println(err)
reader := r.Body
var buf []byte = make([]byte,r.ContentLength)
reader.Read(buf)
fmt.Println(string(buf))
elems:=strings.Split(string(buf),"\n",-1)
fmt.Println(len(elems))

// OK, już jesteśmy zalogowani, teraz musimy pobrać nagłówki autoryzacji

var authHeader string
for i:=range elems {
if strings.Index(elems[i],"Auth=")==0 {
authHeader=strings.Replace(elems[i],"Auth=","GoogleLogin auth=",1)
}
}

// Teraz przygotowujemy się do wysłania naszego "pliku" do Google Docs :-)

// treść pliku
documentBody:="This is only a test\nSend from Go to Google Docs :-)"
// MIME type
bodyType="text/plain"

// Tworzymy requesta

req:=new(http.Request)
req.Method="POST"
req.RawURL="https://docs.google.com/feeds/default/private/full"
req.ProtoMajor = 1
req.ProtoMinor = 1
req.Host="docs.google.com"
req.Close = true
req.Body=nopCloser{strings.NewReader(documentBody)}
req.Header = make(map[string]string)
req.Header["Content-Type"]=bodyType
req.Header["Authorization"]=authHeader
req.Header["Slug"]="toster.txt"
req.Header["GData-Version"]="3.0"
req.ContentLength=int64(len(documentBody))
c,errNet:=net.Dial("tcp","","docs.google.com:80")

fmt.Println(errNet)
fmt.Println(c)

err2:=req.Write(c)
fmt.Println(err2)
reader2 := bufio.NewReader(c)
resp, err3 := http.ReadResponse(reader2, req.Method)
if err3 != nil {
c.Close()
fmt.Println(err3)
return
}

resp.Body = readClose{resp.Body, c}

fmt.Println(resp.Body)
fmt.Println(resp.ContentLength)
cLen:=resp.ContentLength
if cLen==-1 {
cLen = 8192
}
var buf2 []byte = make([]byte,cLen)
reader2.Read(buf2)
fmt.Println(string(buf2))

c.Close()

}


W celu jego uruchomienia kompilujemy go standardem:
8g httpTest2.go
8l httpTest2.8

i uruchamiamy:
./8.out eMail HasłoDoKontaGoogle

Gdzie eMail to adres którego używamy w Google Docs, a HasłoDoKontaGoogle to hasło do konta związanego z tym mailem.
Ta wersja programu wysyła plik do Google Docs przy pomocy HTTP, jeśli chcemy użyć HTTPS, to trzeba w sekcji import zmienić "net" w "crypto/tls", a w linii z net.Dial trzeba zmienić to net.Dial na tls.Dial, do tego trzeba zmienić docs.google.code:80 na docs.google.code:443.
Po skompilowaniu działa :-)

Efektem działania programu jest pojawienie się nowego pliku toster.txt w Google Docs :-)

Wszystko działa także z kontami w Google Apps.

Teraz parę słów krytyki ;-)
Po pierwsze zwracanie wielu wartości z funkcji jest super, ale tylko do czasu. Bo zawsze trzeba pobrać wszystkie wartości, a że Go chce być pomocne i uznaje za błąd istnienie niewykorzystanej zmiennej to trzeba też używać tych zwracanych wartości. Ja oszukałem bo błędy w większości przypadków tylko wypisuję, w rzeczywistości za każdym razem powinienem stosować taki "idiom":
coś, err = cośIo
if err!=nil {
// obsłuż błąd
return
}

Który jest jak najbardziej OK, ale to, że język do tego zmusza to przeszkadzajka. W ogólnym rozrachunku nie jest to może takie złe, bo w kodzie produkcyjnym trzeba się upewniać czy wszystko poszło OK, ale to męczy. Zresztą często w takich sytuacjach stosuje się sterowanie przepływem przy pomocy wyjątków, co nie jest ładne, ale jest szybsze w pisaniu, czyli robi się serie rzeczy, które mogą spowodować wyjątek, i jeśli wyjątek nie poleci to sterowanie przechodzi do następnej linii, aż wystąpi wyjątek, albo wszystko się uda zrobić.

Po drugie, pomieszanie światów. Raz używając http można stworzyć cały request POST i go wysłać, drugim razem trzeba zniżyć się do net albo crypto/tls czyli do poziomu socketów bo zechcieliśmy wykonać jedną nietypową rzecz, czyli ustawić nagłówki requesta.
W tym miejscu może nawet więcej sensu miałoby użycie tylko net albo crytpo/tls bez bawienia się w requesty.

Po trzecie, to wkurzające wymaganie jawnej konwersji typów. Java robi to milej. Przez tą konwersję trzeba ciągle kombinować. Przez co mam taką linię:
req.ContentLength=int64(len(documentBody))
Bo oczywiście do ContentLength trzeba włożyć int64, a len zwraca "tylko" int.
Strasznie to upierdliwe.

Po czwarte, nadużywanie struktur. Jak widać w źródle, musiałem ukraść ze źródeł Go 2 struktury, nopCloser i readClose, bo komuś się ubzdurało, żeby strumień i jego zamykanie były oddzielnymi bytami, ale akurat do wielu bibliotek trzeba dostarczać strumienie, które są jednocześnie czytalne czy zapisywalne i zamykalne......

Po piątek, zmiany w kompilatorze. To jest na razie język eksperymentalny, choć Google podobno pisze w nim niektóre swoje wewnętrzne narzędzia.
Np. to, że teraz nie używa się średnika bo kompilator sam sobie go potrafi włożyć, nie działa z kompilatorem sprzed pół roku czy jakoś tak. Biblioteki też się zmieniają.

Po szóste, port dla Windows. Nie ma oficjalnego, jest taki mniej oficjalny, który ma np. problemy z HTTPSem.

Dla ciekawskich, ten program wyżej to dziecko jakichś 4 godzin, czyli dość długo powstawał.


Podobne postybeta
Zmienne Go ;-)
Chrome OS, Chrome2Chrome i w ogóle Chrome ;-)
Toperz ;-) czyli OCR + Android odsłona 2 albo któraś tam
Go dla Java'owca ;-) odcinek 1 "klasy"
Wredne Google Docs

4 komentarze:

  1. Skoro mowa o zwracaniu wielu wartości to można napisać tak:

    jeden,_ = funkcja()

    Taki miły idiom, znaczący "zignoruj tą wartość".


    Z klienta sieciowego używałem jedynie http.Get(), działa bez problemów, ale tylko w banalnych przypadkach, nawet ustawienie cookie nie wchodzi w rachubę.
    Problem jest znany i zgłoszony jako błąd. Puki co pozostaje jedynie korzystanie z innych bibliotek (są takie).

    W Go istnieją typy io.ReadCloser i io.WriteCloser, czy o to Ci chodziło?

    OdpowiedzUsuń
  2. Tego _ nie znałem :-)
    Widziałem w przykładach, ale nie zwracałem uwagi na to.

    Co do io.ReadCloser i io.WriteCloser to to są interfejsy, nie konkretne "klasy", nie można ich więc użyć w tym sensie, to one przez ten ducktyping "dopasują" do siebie te, nazwijmy moje nopCloser/readClose.
    Mówiąc inaczej to req.Body w linii:
    req.Body=nopCloser{strings.NewReader(documentBody)}
    ma typ io.writeCloser (chyba) i żeby móc tam coś dopisać to muszę tam przypisać konkretny obiekt stworzony z konkretnej klasy.

    Co do samych bibliotek to na Linuksie te mi wystarczyły :-) wszystko zadziałało tak jak powinno. Na Windowsie nie działa.

    OdpowiedzUsuń
  3. Aaha, teraz rozumiem czemu wprowadziłeś nopCloser.

    Samemu otarłem się o problem, ale przy moich eksperymentach nie był na tyle dotkliwy bym zwrócił nań uwagę. Wiele obiektów implementuje jedynie interfejs io.Reader, co zazwyczaj wystarcza, ale nie zawsze.

    Tak swoją drogą, ciekaw jestem Twojej opinii na temat biblioteki io w porównaniu z Java. Dla mnie ta z Go, wydaje się wygodniejsza w użyciu, ale może to być spowodowane tym że w Java piszę od przypadku do przypadku.

    OdpowiedzUsuń
  4. Jak na razie mi się nie podobają. W Java'ie byłoby prościej. Gdybym potrzebował strumienia, a miałbym tablicę bajtów to użyłbym ByteArrayInputStream albo ByteArrayOutputStream, co prawda mało oszczędne jeśli chodzi o pamięć, ale na PC działa spoko [w mobile na Androidzie może robić problemy bo na takim G1 sterta to chyba max 12 MB].

    Zrobienie tego POSTa w Java'ie to by było coś w stylu:
    ByteArrayOutputStream baos = new ByteArrayOutputStream("this is a test...".getBytes());
    URL url = new URL("https://docs.google.com/feeds/default/private/full");
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setRequestMethod("POST");
    conn.setRequestProperty("Authorization", "GoogleLogin auth="+auth);conn.setRequestProperty("GData-Version", "3.0");
    conn.setRequestProperty("Content-Type", "text/plain");
    conn.setRequestProperty("Slug", "toster.txt");
    long contentLength = baos.size(); conn.setRequestProperty("Content-Length", ""+contentLength); conn.setDoOutput(true);
    conn.connect(); conn.getOutputStream().write(baos.toByteArray());

    bez tworzenia dodatkowych typów.
    Ale tutaj może przemawiać przeze mnie stary myśliwy [ulubiona anegdotka trenerów TDD u mnie w firmie ;-) "Gdy staremu indiańskiemu myśliwemu, który upolował z łuku już wszystko co się da pokazali strzelbę, obejrzał, strzelił, ustrzelił coś i przyznał, że fajne. Ale gdy zaczął biec na niego niedźwiedź sięgnął po znane narzędzie czyli łuk".]

    OdpowiedzUsuń