From b0811da602a4d0836d3e24fc192fb075cda14085 Mon Sep 17 00:00:00 2001 From: Mihai Parparita Date: Tue, 29 Jul 2014 21:57:33 -0700 Subject: [PATCH] Break out accounts, digests, caching and session configuration out of githup.go. --- TODO | 3 +- app/account.go | 43 ++++++++++ app/caching_transport.go | 62 ++++++++++++++ app/digest.go | 63 ++++++++++++++ app/githop.go | 181 +-------------------------------------- app/session.go | 46 ++++++++++ 6 files changed, 217 insertions(+), 181 deletions(-) create mode 100644 app/account.go create mode 100644 app/caching_transport.go create mode 100644 app/digest.go create mode 100644 app/session.go diff --git a/TODO b/TODO index 2de1f7b..871e785 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,6 @@ TODO -- Break up github.go +- Break up indexHandler +- Single email sending button - Loop over registered accounts and send them email - Flash message and sign out when OAuth token has expired/is invalid - Handle pagination for user repository list diff --git a/app/account.go b/app/account.go new file mode 100644 index 0000000..621bf72 --- /dev/null +++ b/app/account.go @@ -0,0 +1,43 @@ +package githop + +import ( + "bytes" + "encoding/gob" + + "appengine" + "appengine/datastore" + + "code.google.com/p/goauth2/oauth" +) + +type Account struct { + GitHubUserId int `datastore:",noindex"` + // The datastore API doesn't store maps, and the token contains one. We + // thefore store a gob-serialized version instead. + OAuthTokenSerialized []byte + OAuthToken oauth.Token `datastore:"-,"` +} + +func GetAccount(c appengine.Context, gitHubUserId int) (*Account, error) { + key := datastore.NewKey(c, "Account", "", int64(gitHubUserId), nil) + account := new(Account) + err := datastore.Get(c, key, account) + if err != nil { + return nil, err + } + r := bytes.NewBuffer(account.OAuthTokenSerialized) + err = gob.NewDecoder(r).Decode(&account.OAuthToken) + return account, err +} + +func (account *Account) Put(c appengine.Context) error { + w := new(bytes.Buffer) + err := gob.NewEncoder(w).Encode(&account.OAuthToken) + if err != nil { + return err + } + account.OAuthTokenSerialized = w.Bytes() + key := datastore.NewKey(c, "Account", "", int64(account.GitHubUserId), nil) + _, err = datastore.Put(c, key, account) + return err +} diff --git a/app/caching_transport.go b/app/caching_transport.go new file mode 100644 index 0000000..8517193 --- /dev/null +++ b/app/caching_transport.go @@ -0,0 +1,62 @@ +package githop + +import ( + "bufio" + "bytes" + "net/http" + "net/http/httputil" + "strings" + + "appengine" + "appengine/memcache" +) + +// Simple http.RoundTripper implementation which wraps an existing transport and +// caches all responses for GET and HEAD requests. Meant to speed up the +// iteration cycle during development. +type CachingTransport struct { + Transport http.RoundTripper + Context appengine.Context +} + +func (t *CachingTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { + if req.Method != "GET" && req.Method != "HEAD" { + return t.Transport.RoundTrip(req) + } + cacheKey := "CachingTransport:" + req.URL.String() + "#" + authorizationHeaders, ok := req.Header["Authorization"] + if ok { + cacheKey += strings.Join(authorizationHeaders, "#") + } else { + cacheKey += "Unauthorized" + } + + cachedRespItem, err := memcache.Get(t.Context, cacheKey) + if err != nil && err != memcache.ErrCacheMiss { + t.Context.Errorf("Error getting cached response: %v", err) + return t.Transport.RoundTrip(req) + } + if err == nil { + cacheRespBuffer := bytes.NewBuffer(cachedRespItem.Value) + resp, err := http.ReadResponse(bufio.NewReader(cacheRespBuffer), req) + if err == nil { + return resp, nil + } else { + t.Context.Errorf("Error readings bytes for cached response: %v", err) + } + } + resp, err = t.Transport.RoundTrip(req) + if err != nil { + return + } + respBytes, err := httputil.DumpResponse(resp, true) + if err != nil { + t.Context.Errorf("Error dumping bytes for cached response: %v", err) + return resp, nil + } + err = memcache.Set(t.Context, &memcache.Item{Key: cacheKey, Value: respBytes}) + if err != nil { + t.Context.Errorf("Error setting cached response: %v", err) + } + return resp, nil +} diff --git a/app/digest.go b/app/digest.go new file mode 100644 index 0000000..029bdf7 --- /dev/null +++ b/app/digest.go @@ -0,0 +1,63 @@ +package githop + +import ( + "sort" + "time" + + "github.com/google/go-github/github" +) + +type RepoDigest struct { + Repo *github.Repository + Commits []github.RepositoryCommit +} + +// sort.Interface implementation for sorting RepoDigests. +type ByRepoFullName []*RepoDigest + +func (a ByRepoFullName) Len() int { return len(a) } +func (a ByRepoFullName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByRepoFullName) Less(i, j int) bool { return *a[i].Repo.FullName < *a[j].Repo.FullName } + +type Digest struct { + User *github.User + StartTime time.Time + EndTime time.Time + RepoDigests []*RepoDigest +} + +func (digest *Digest) Fetch(repos []github.Repository, githubClient *github.Client) error { + type RepoDigestResponse struct { + repoDigest *RepoDigest + err error + } + ch := make(chan *RepoDigestResponse) + for _, repo := range repos { + go func(repo github.Repository) { + commits, _, err := githubClient.Repositories.ListCommits( + *repo.Owner.Login, + *repo.Name, + &github.CommitsListOptions{ + Author: *digest.User.Login, + Since: digest.StartTime, + Until: digest.EndTime, + }) + if err != nil { + ch <- &RepoDigestResponse{nil, err} + } else { + ch <- &RepoDigestResponse{&RepoDigest{&repo, commits}, nil} + } + }(repo) + } + for i := 0; i < len(repos); i++ { + select { + case r := <-ch: + if r.err != nil { + return r.err + } + digest.RepoDigests = append(digest.RepoDigests, r.repoDigest) + } + } + sort.Sort(ByRepoFullName(digest.RepoDigests)) + return nil +} diff --git a/app/githop.go b/app/githop.go index 7d83224..23fd7a7 100644 --- a/app/githop.go +++ b/app/githop.go @@ -1,23 +1,14 @@ package githop import ( - "bufio" - "bytes" - "encoding/base64" - "encoding/gob" "encoding/json" "html/template" "io/ioutil" "log" "net/http" - "net/http/httputil" - "sort" - "strings" "time" "appengine" - "appengine/datastore" - "appengine/memcache" "appengine/urlfetch" "code.google.com/p/goauth2/oauth" @@ -31,126 +22,6 @@ var githubOauthConfig oauth.Config var sessionStore *sessions.CookieStore var sessionConfig SessionConfig -type SessionConfig struct { - AuthenticationKey string - EncryptionKey string - CookieName string - UserIdKey string -} - -type Account struct { - GitHubUserId int `datastore:",noindex"` - // The datastore API doesn't store maps, and the token contains one. We - // thefore store a gob-serialized version instead. - OAuthTokenSerialized []byte - OAuthToken oauth.Token `datastore:"-,"` -} - -func GetAccount(c appengine.Context, gitHubUserId int) (*Account, error) { - key := datastore.NewKey(c, "Account", "", int64(gitHubUserId), nil) - account := new(Account) - err := datastore.Get(c, key, account) - if err != nil { - return nil, err - } - r := bytes.NewBuffer(account.OAuthTokenSerialized) - err = gob.NewDecoder(r).Decode(&account.OAuthToken) - return account, err -} - -func (account *Account) Put(c appengine.Context) error { - w := new(bytes.Buffer) - err := gob.NewEncoder(w).Encode(&account.OAuthToken) - if err != nil { - return err - } - account.OAuthTokenSerialized = w.Bytes() - key := datastore.NewKey(c, "Account", "", int64(account.GitHubUserId), nil) - _, err = datastore.Put(c, key, account) - return err -} - -type RepoDigest struct { - Repo *github.Repository - Commits []github.RepositoryCommit -} - -// sort.Interface implementation for sorting RepoDigests. -type ByRepoFullName []*RepoDigest - -func (a ByRepoFullName) Len() int { return len(a) } -func (a ByRepoFullName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByRepoFullName) Less(i, j int) bool { return *a[i].Repo.FullName < *a[j].Repo.FullName } - -type Digest struct { - User *github.User - StartTime time.Time - EndTime time.Time - RepoDigests []*RepoDigest -} - -func (digest *Digest) Fetch(repos []github.Repository, githubClient *github.Client) error { - type RepoDigestResponse struct { - repoDigest *RepoDigest - err error - } - ch := make(chan *RepoDigestResponse) - for _, repo := range repos { - go func(repo github.Repository) { - commits, _, err := githubClient.Repositories.ListCommits( - *repo.Owner.Login, - *repo.Name, - &github.CommitsListOptions{ - Author: *digest.User.Login, - Since: digest.StartTime, - Until: digest.EndTime, - }) - if err != nil { - ch <- &RepoDigestResponse{nil, err} - } else { - ch <- &RepoDigestResponse{&RepoDigest{&repo, commits}, nil} - } - }(repo) - } - for i := 0; i < len(repos); i++ { - select { - case r := <-ch: - if r.err != nil { - return r.err - } - digest.RepoDigests = append(digest.RepoDigests, r.repoDigest) - } - } - sort.Sort(ByRepoFullName(digest.RepoDigests)) - return nil -} - -func initSessionConfig() { - configBytes, err := ioutil.ReadFile("config/session.json") - if err != nil { - log.Panicf("Could not read session config: %s", err.Error()) - } - err = json.Unmarshal(configBytes, &sessionConfig) - if err != nil { - log.Panicf("Could not parse session config %s: %s", configBytes, err.Error()) - } - - authenticationKey, err := base64.StdEncoding.DecodeString(sessionConfig.AuthenticationKey) - if err != nil { - log.Panicf("Could not decode session config authentication key %s: %s", sessionConfig.AuthenticationKey, err.Error()) - } - encryptionKey, err := base64.StdEncoding.DecodeString(sessionConfig.EncryptionKey) - if err != nil { - log.Panicf("Could not decode session config encryption key %s: %s", sessionConfig.EncryptionKey, err.Error()) - } - - sessionStore = sessions.NewCookieStore(authenticationKey, encryptionKey) - sessionStore.Options.Path = "/" - sessionStore.Options.MaxAge = 86400 * 30 - sessionStore.Options.HttpOnly = true - sessionStore.Options.Secure = !appengine.IsDevAppServer() -} - func initGithubOAuthConfig() { path := "config/github-oauth" if appengine.IsDevAppServer() { @@ -171,7 +42,7 @@ func initGithubOAuthConfig() { } func init() { - initSessionConfig() + sessionStore, sessionConfig = initSession() initGithubOAuthConfig() router = mux.NewRouter() @@ -335,53 +206,3 @@ func githubOAuthTransport(r *http.Request) *oauth.Transport { Transport: cachingTransport, } } - -// Simple http.RoundTripper implementation which wraps an existing transport and -// caches all responses for GET and HEAD requests. Meant to speed up the -// iteration cycle during development. -type CachingTransport struct { - Transport http.RoundTripper - Context appengine.Context -} - -func (t *CachingTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - if req.Method != "GET" && req.Method != "HEAD" { - return t.Transport.RoundTrip(req) - } - cacheKey := "CachingTransport:" + req.URL.String() + "#" - authorizationHeaders, ok := req.Header["Authorization"] - if ok { - cacheKey += strings.Join(authorizationHeaders, "#") - } else { - cacheKey += "Unauthorized" - } - - cachedRespItem, err := memcache.Get(t.Context, cacheKey) - if err != nil && err != memcache.ErrCacheMiss { - t.Context.Errorf("Error getting cached response: %v", err) - return t.Transport.RoundTrip(req) - } - if err == nil { - cacheRespBuffer := bytes.NewBuffer(cachedRespItem.Value) - resp, err := http.ReadResponse(bufio.NewReader(cacheRespBuffer), req) - if err == nil { - return resp, nil - } else { - t.Context.Errorf("Error readings bytes for cached response: %v", err) - } - } - resp, err = t.Transport.RoundTrip(req) - if err != nil { - return - } - respBytes, err := httputil.DumpResponse(resp, true) - if err != nil { - t.Context.Errorf("Error dumping bytes for cached response: %v", err) - return resp, nil - } - err = memcache.Set(t.Context, &memcache.Item{Key: cacheKey, Value: respBytes}) - if err != nil { - t.Context.Errorf("Error setting cached response: %v", err) - } - return resp, nil -} diff --git a/app/session.go b/app/session.go new file mode 100644 index 0000000..380e45f --- /dev/null +++ b/app/session.go @@ -0,0 +1,46 @@ +package githop + +import ( + "encoding/base64" + "encoding/json" + "io/ioutil" + "log" + + "appengine" + + "github.com/gorilla/sessions" +) + +type SessionConfig struct { + AuthenticationKey string + EncryptionKey string + CookieName string + UserIdKey string +} + +func initSession() (sessionStore *sessions.CookieStore, sessionConfig SessionConfig) { + configBytes, err := ioutil.ReadFile("config/session.json") + if err != nil { + log.Panicf("Could not read session config: %s", err.Error()) + } + err = json.Unmarshal(configBytes, &sessionConfig) + if err != nil { + log.Panicf("Could not parse session config %s: %s", configBytes, err.Error()) + } + + authenticationKey, err := base64.StdEncoding.DecodeString(sessionConfig.AuthenticationKey) + if err != nil { + log.Panicf("Could not decode session config authentication key %s: %s", sessionConfig.AuthenticationKey, err.Error()) + } + encryptionKey, err := base64.StdEncoding.DecodeString(sessionConfig.EncryptionKey) + if err != nil { + log.Panicf("Could not decode session config encryption key %s: %s", sessionConfig.EncryptionKey, err.Error()) + } + + sessionStore = sessions.NewCookieStore(authenticationKey, encryptionKey) + sessionStore.Options.Path = "/" + sessionStore.Options.MaxAge = 86400 * 30 + sessionStore.Options.HttpOnly = true + sessionStore.Options.Secure = !appengine.IsDevAppServer() + return +}