Skip to content

Commit

Permalink
Revert "getting-started/bookshelf: initial rewrite (#951)"
Browse files Browse the repository at this point in the history
This reverts commit 0e0de93.
  • Loading branch information
tbpg committed Sep 11, 2019
1 parent 0e0de93 commit 1ec9d82
Show file tree
Hide file tree
Showing 34 changed files with 2,241 additions and 658 deletions.
Expand Up @@ -12,31 +12,34 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// The bookshelf command starts the bookshelf server, a sample app
// demonstrating several Google Cloud APIs, including App Engine, Firestore, and
// Cloud Storage.
// See https://cloud.google.com/go/getting-started/tutorial-app.
// Sample bookshelf is a fully-featured app demonstrating several Google Cloud APIs, including Datastore, Cloud SQL, Cloud Storage.
// See https://cloud.google.com/go/getting-started/tutorial-app
package main

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"strconv"

"cloud.google.com/go/firestore"
"cloud.google.com/go/pubsub"
"cloud.google.com/go/storage"
"github.com/gofrs/uuid"

uuid "github.com/gofrs/uuid"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"

"github.com/GoogleCloudPlatform/golang-samples/getting-started/bookshelf"
)

var (
// See template.go.
// See template.go
listTmpl = parseTemplate("list.html")
editTmpl = parseTemplate("edit.html")
detailTmpl = parseTemplate("detail.html")
Expand All @@ -47,55 +50,43 @@ func main() {
if port == "" {
port = "8080"
}
projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
if projectID == "" {
log.Fatal("GOOGLE_CLOUD_PROJECT must be set")
}

ctx := context.Background()

client, err := firestore.NewClient(ctx, projectID)
if err != nil {
log.Fatalf("firestore.NewClient: %v", err)
}
db, err := newFirestoreDB(client)
if err != nil {
log.Fatalf("newFirestoreDB: %v", err)
}

shelf, err := NewBookshelf(projectID, db)
if err != nil {
log.Fatalf("NewBookshelf: %v", err)
}

shelf.registerHandlers()

log.Printf("Listening on localhost:%s", port)
registerHandlers()
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}

func (b *Bookshelf) registerHandlers() {
func registerHandlers() {
// Use gorilla/mux for rich routing.
// See https://www.gorillatoolkit.org/pkg/mux.
// See http://www.gorillatoolkit.org/pkg/mux
r := mux.NewRouter()

r.Handle("/", http.RedirectHandler("/books", http.StatusFound))

r.Methods("GET").Path("/books").
Handler(appHandler(b.listHandler))
Handler(appHandler(listHandler))
r.Methods("GET").Path("/books/mine").
Handler(appHandler(listMineHandler))
r.Methods("GET").Path("/books/{id:[0-9]+}").
Handler(appHandler(detailHandler))
r.Methods("GET").Path("/books/add").
Handler(appHandler(b.addFormHandler))
r.Methods("GET").Path("/books/{id:[0-9a-zA-Z]+}").
Handler(appHandler(b.detailHandler))
r.Methods("GET").Path("/books/{id:[0-9a-zA-Z]+}/edit").
Handler(appHandler(b.editFormHandler))
Handler(appHandler(addFormHandler))
r.Methods("GET").Path("/books/{id:[0-9]+}/edit").
Handler(appHandler(editFormHandler))

r.Methods("POST").Path("/books").
Handler(appHandler(b.createHandler))
r.Methods("POST", "PUT").Path("/books/{id:[0-9a-zA-Z]+}").
Handler(appHandler(b.updateHandler))
r.Methods("POST").Path("/books/{id:[0-9a-zA-Z]+}:delete").
Handler(appHandler(b.deleteHandler)).Name("delete")
Handler(appHandler(createHandler))
r.Methods("POST", "PUT").Path("/books/{id:[0-9]+}").
Handler(appHandler(updateHandler))
r.Methods("POST").Path("/books/{id:[0-9]+}:delete").
Handler(appHandler(deleteHandler)).Name("delete")

// The following handlers are defined in auth.go and used in the
// "Authenticating Users" part of the Getting Started guide.
r.Methods("GET").Path("/login").
Handler(appHandler(loginHandler))
r.Methods("POST").Path("/logout").
Handler(appHandler(logoutHandler))
r.Methods("GET").Path("/oauth2callback").
Handler(appHandler(oauthCallbackHandler))

// Respond to App Engine and Compute Engine health checks.
// Indicate the server is healthy.
Expand All @@ -107,14 +98,30 @@ func (b *Bookshelf) registerHandlers() {
// [START request_logging]
// Delegate all of the HTTP routing and serving to the gorilla/mux router.
// Log all requests using the standard Apache format.
http.Handle("/", handlers.CombinedLoggingHandler(b.logWriter, r))
http.Handle("/", handlers.CombinedLoggingHandler(os.Stderr, r))
// [END request_logging]
}

// listHandler displays a list with summaries of books in the database.
func (b *Bookshelf) listHandler(w http.ResponseWriter, r *http.Request) *appError {
ctx := r.Context()
books, err := b.DB.ListBooks(ctx)
func listHandler(w http.ResponseWriter, r *http.Request) *appError {
books, err := bookshelf.DB.ListBooks()
if err != nil {
return appErrorf(err, "could not list books: %v", err)
}

return listTmpl.Execute(w, r, books)
}

// listMineHandler displays a list of books created by the currently
// authenticated user.
func listMineHandler(w http.ResponseWriter, r *http.Request) *appError {
user := profileFromSession(r)
if user == nil {
http.Redirect(w, r, "/login?redirect=/books/mine", http.StatusFound)
return nil
}

books, err := bookshelf.DB.ListBooksCreatedBy(user.ID)
if err != nil {
return appErrorf(err, "could not list books: %v", err)
}
Expand All @@ -124,22 +131,21 @@ func (b *Bookshelf) listHandler(w http.ResponseWriter, r *http.Request) *appErro

// bookFromRequest retrieves a book from the database given a book ID in the
// URL's path.
func (b *Bookshelf) bookFromRequest(r *http.Request) (*Book, error) {
ctx := r.Context()
id := mux.Vars(r)["id"]
if id == "" {
return nil, errors.New("no book with empty ID")
func bookFromRequest(r *http.Request) (*bookshelf.Book, error) {
id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
if err != nil {
return nil, fmt.Errorf("bad book id: %v", err)
}
book, err := b.DB.GetBook(ctx, id)
book, err := bookshelf.DB.GetBook(id)
if err != nil {
return nil, fmt.Errorf("could not find book: %v", err)
}
return book, nil
}

// detailHandler displays the details of a given book.
func (b *Bookshelf) detailHandler(w http.ResponseWriter, r *http.Request) *appError {
book, err := b.bookFromRequest(r)
func detailHandler(w http.ResponseWriter, r *http.Request) *appError {
book, err := bookFromRequest(r)
if err != nil {
return appErrorf(err, "%v", err)
}
Expand All @@ -149,14 +155,14 @@ func (b *Bookshelf) detailHandler(w http.ResponseWriter, r *http.Request) *appEr

// addFormHandler displays a form that captures details of a new book to add to
// the database.
func (b *Bookshelf) addFormHandler(w http.ResponseWriter, r *http.Request) *appError {
func addFormHandler(w http.ResponseWriter, r *http.Request) *appError {
return editTmpl.Execute(w, r, nil)
}

// editFormHandler displays a form that allows the user to edit the details of
// a given book.
func (b *Bookshelf) editFormHandler(w http.ResponseWriter, r *http.Request) *appError {
book, err := b.bookFromRequest(r)
func editFormHandler(w http.ResponseWriter, r *http.Request) *appError {
book, err := bookFromRequest(r)
if err != nil {
return appErrorf(err, "%v", err)
}
Expand All @@ -166,29 +172,44 @@ func (b *Bookshelf) editFormHandler(w http.ResponseWriter, r *http.Request) *app

// bookFromForm populates the fields of a Book from form values
// (see templates/edit.html).
func (b *Bookshelf) bookFromForm(r *http.Request) (*Book, error) {
ctx := r.Context()
imageURL, err := b.uploadFileFromForm(ctx, r)
func bookFromForm(r *http.Request) (*bookshelf.Book, error) {
imageURL, err := uploadFileFromForm(r)
if err != nil {
return nil, fmt.Errorf("could not upload file: %v", err)
}
if imageURL == "" {
imageURL = r.FormValue("imageURL")
}

book := &Book{
book := &bookshelf.Book{
Title: r.FormValue("title"),
Author: r.FormValue("author"),
PublishedDate: r.FormValue("publishedDate"),
ImageURL: imageURL,
Description: r.FormValue("description"),
CreatedBy: r.FormValue("createdBy"),
CreatedByID: r.FormValue("createdByID"),
}

// If the form didn't carry the user information for the creator, populate it
// from the currently logged in user (or mark as anonymous).
if book.CreatedByID == "" {
user := profileFromSession(r)
if user != nil {
// Logged in.
book.CreatedBy = user.DisplayName
book.CreatedByID = user.ID
} else {
// Not logged in.
book.SetCreatorAnonymous()
}
}

return book, nil
}

// uploadFileFromForm uploads a file if it's present in the "image" form field.
func (b *Bookshelf) uploadFileFromForm(ctx context.Context, r *http.Request) (url string, err error) {
func uploadFileFromForm(r *http.Request) (url string, err error) {
f, fh, err := r.FormFile("image")
if err == http.ErrMissingFile {
return "", nil
Expand All @@ -197,20 +218,15 @@ func (b *Bookshelf) uploadFileFromForm(ctx context.Context, r *http.Request) (ur
return "", err
}

if b.StorageBucket == nil {
return "", errors.New("storage bucket is missing: check config.go")
}
if _, err := b.StorageBucket.Attrs(ctx); err != nil {
if err == storage.ErrBucketNotExist {
return "", fmt.Errorf("bucket %q does not exist: check config.go", b.StorageBucketName)
}
return "", fmt.Errorf("could not get bucket: %v", err)
if bookshelf.StorageBucket == nil {
return "", errors.New("storage bucket is missing - check config.go")
}

// random filename, retaining existing extension.
name := uuid.Must(uuid.NewV4()).String() + path.Ext(fh.Filename)

w := b.StorageBucket.Object(name).NewWriter(ctx)
ctx := context.Background()
w := bookshelf.StorageBucket.Object(name).NewWriter(ctx)

// Warning: storage.AllUsers gives public read access to anyone.
w.ACL = []storage.ACLRule{{Entity: storage.AllUsers, Role: storage.RoleReader}}
Expand All @@ -227,56 +243,79 @@ func (b *Bookshelf) uploadFileFromForm(ctx context.Context, r *http.Request) (ur
}

const publicURL = "https://storage.googleapis.com/%s/%s"
return fmt.Sprintf(publicURL, b.StorageBucketName, name), nil
return fmt.Sprintf(publicURL, bookshelf.StorageBucketName, name), nil
}

// createHandler adds a book to the database.
func (b *Bookshelf) createHandler(w http.ResponseWriter, r *http.Request) *appError {
ctx := r.Context()
book, err := b.bookFromForm(r)
func createHandler(w http.ResponseWriter, r *http.Request) *appError {
book, err := bookFromForm(r)
if err != nil {
return appErrorf(err, "could not parse book from form: %v", err)
}
id, err := b.DB.AddBook(ctx, book)
id, err := bookshelf.DB.AddBook(book)
if err != nil {
return appErrorf(err, "could not save book: %v", err)
}
http.Redirect(w, r, fmt.Sprintf("/books/%s", id), http.StatusFound)
go publishUpdate(id)
http.Redirect(w, r, fmt.Sprintf("/books/%d", id), http.StatusFound)
return nil
}

// updateHandler updates the details of a given book.
func (b *Bookshelf) updateHandler(w http.ResponseWriter, r *http.Request) *appError {
ctx := r.Context()
id := mux.Vars(r)["id"]
if id == "" {
return appErrorf(errors.New("no book with empty ID"), "no book with empty ID")
func updateHandler(w http.ResponseWriter, r *http.Request) *appError {
id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
if err != nil {
return appErrorf(err, "bad book id: %v", err)
}
book, err := b.bookFromForm(r)

book, err := bookFromForm(r)
if err != nil {
return appErrorf(err, "could not parse book from form: %v", err)
}
book.ID = id

if err := b.DB.UpdateBook(ctx, book); err != nil {
return appErrorf(err, "UpdateBook: %v", err)
err = bookshelf.DB.UpdateBook(book)
if err != nil {
return appErrorf(err, "could not save book: %v", err)
}
http.Redirect(w, r, fmt.Sprintf("/books/%s", book.ID), http.StatusFound)
go publishUpdate(book.ID)
http.Redirect(w, r, fmt.Sprintf("/books/%d", book.ID), http.StatusFound)
return nil
}

// deleteHandler deletes a given book.
func (b *Bookshelf) deleteHandler(w http.ResponseWriter, r *http.Request) *appError {
ctx := r.Context()
id := mux.Vars(r)["id"]
if err := b.DB.DeleteBook(ctx, id); err != nil {
return appErrorf(err, "DeleteBook: %v", err)
func deleteHandler(w http.ResponseWriter, r *http.Request) *appError {
id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
if err != nil {
return appErrorf(err, "bad book id: %v", err)
}
err = bookshelf.DB.DeleteBook(id)
if err != nil {
return appErrorf(err, "could not delete book: %v", err)
}
http.Redirect(w, r, "/books", http.StatusFound)
return nil
}

// https://blog.golang.org/error-handling-and-go
// publishUpdate notifies Pub/Sub subscribers that the book identified with
// the given ID has been added/modified.
func publishUpdate(bookID int64) {
if bookshelf.PubsubClient == nil {
return
}

ctx := context.Background()

b, err := json.Marshal(bookID)
if err != nil {
return
}
topic := bookshelf.PubsubClient.Topic(bookshelf.PubsubTopicID)
_, err = topic.Publish(ctx, &pubsub.Message{Data: b}).Get(ctx)
log.Printf("Published update to Pub/Sub for Book ID %d: %v", bookID, err)
}

// http://blog.golang.org/error-handling-and-go
type appHandler func(http.ResponseWriter, *http.Request) *appError

type appError struct {
Expand All @@ -287,7 +326,9 @@ type appError struct {

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if e := fn(w, r); e != nil { // e is *appError, not os.Error.
log.Printf("Handler error: status code: %d, message: %s, underlying err: %#v", e.Code, e.Message, e.Error)
log.Printf("Handler error: status code: %d, message: %s, underlying err: %#v",
e.Code, e.Message, e.Error)

http.Error(w, e.Message, e.Code)
}
}
Expand Down

0 comments on commit 1ec9d82

Please sign in to comment.