code/app/main.go (190 lines of code) (raw):
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
var cs CloudStorage
func main() {
port := os.Getenv("PORT")
bucket := os.Getenv("BUCKET")
if port == "" {
port = "8080"
}
fmt.Printf("Port: %s\n", port)
var err error
cs, err = NewCloudStorage(bucket)
if err != nil {
log.Printf("failed to create client: %v", err)
return
}
defer cs.Close()
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/api/v1/image", listHandler).Methods(http.MethodGet, http.MethodOptions)
router.HandleFunc("/api/v1/image", createHandler).Methods(http.MethodPost)
router.HandleFunc("/api/v1/image/{id}", readHandler).Methods(http.MethodGet)
router.HandleFunc("/api/v1/image/{id}", deleteHandler).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/image/{id}", updateHandler).Methods(http.MethodPost, http.MethodPut)
router.PathPrefix("/").Handler(http.FileServer(http.Dir("./static/")))
headersOk := handlers.AllowedHeaders([]string{"X-Requested-With"})
originsOk := handlers.AllowedOrigins([]string{"*"})
methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS", "DELETE"})
log.Fatal(http.ListenAndServe(":"+port, handlers.CORS(originsOk, headersOk, methodsOk)(router)))
}
func listHandler(w http.ResponseWriter, r *http.Request) {
fs, err := cs.List()
if err != nil {
writeErrorMsg(w, fmt.Errorf("failed to list files: %v", err))
return
}
is, err := NewImages(fs)
if err != nil {
writeErrorMsg(w, fmt.Errorf("failed to convert files to images images: %v", err))
return
}
writeJSON(w, is, http.StatusOK)
return
}
func createHandler(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(10 << 20)
// FormFile returns the first file for the given key `myFile`
// it also returns the FileHeader so we can get the Filename,
// the Header and the size of the file
file, handler, err := r.FormFile("myFile")
if err != nil {
writeErrorMsg(w, fmt.Errorf("error retrieving file: %v", err))
return
}
defer file.Close()
mimemap := NewMimeMap([]string{"image/png", "image/jpeg", "image/gif"})
mimetype := handler.Header.Get("Content-Type")
if !mimemap.Valid(mimetype) {
mimelist := mimemap.List()
writeErrorMsg(w, fmt.Errorf("invalid image type, want one of %s got : %s", mimelist, mimetype))
return
}
if err := cs.Create(handler.Filename, file); err != nil {
writeErrorMsg(w, fmt.Errorf("image couldn't be created: %v", err))
return
}
writeResponse(w, http.StatusCreated, "")
return
}
type MimeMap map[string]bool
func NewMimeMap(s []string) MimeMap {
m := make(MimeMap)
for _, v := range s {
m[v] = true
}
return m
}
func (m MimeMap) Valid(mimetype string) bool {
_, ok := m[mimetype]
return ok
}
func (m MimeMap) List() string {
var sb strings.Builder
for i := range m {
sb.WriteString(i)
sb.WriteString(", ")
}
return strings.TrimRight(sb.String(), ", ")
}
func updateHandler(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
r.ParseMultipartForm(10 << 20)
// FormFile returns the first file for the given key `myFile`
// it also returns the FileHeader so we can get the Filename,
// the Header and the size of the file
file, handler, err := r.FormFile("myFile")
if err != nil {
writeErrorMsg(w, fmt.Errorf("error retrieving file: %v", err))
return
}
defer file.Close()
if err := cs.Delete(id); err != nil {
writeErrorMsg(w, fmt.Errorf("error replacing file: %s", err))
return
}
if err := cs.Create(handler.Filename, file); err != nil {
writeErrorMsg(w, fmt.Errorf("image couldn't be created: %v", err))
return
}
writeResponse(w, http.StatusOK, "")
return
}
func readHandler(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
fs, err := cs.Read(id)
if err != nil {
writeErrorMsg(w, fmt.Errorf("failed to read files %s: %v", id, err))
return
}
is, err := NewImages(fs)
if err != nil {
writeErrorMsg(w, fmt.Errorf("failed to convert files to images images: %v", err))
return
}
if len(is) < 1 {
writeResponse(w, http.StatusNoContent, "")
return
}
writeJSON(w, is[0], http.StatusOK)
}
func deleteHandler(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
if err := cs.Delete(id); err != nil {
writeErrorMsg(w, err)
return
}
msg := Message{"image deleted", fmt.Sprintf("image id: %s", id)}
writeJSON(w, msg, http.StatusNoContent)
}
// JSONProducer is an interface that spits out a JSON string version of itself
type JSONProducer interface {
JSON() (string, error)
JSONBytes() ([]byte, error)
}
func writeJSON(w http.ResponseWriter, j JSONProducer, status int) {
json, err := j.JSON()
if err != nil {
writeErrorMsg(w, err)
return
}
writeResponse(w, status, json)
return
}
func writeErrorMsg(w http.ResponseWriter, err error) {
s := fmt.Sprintf("{\"error\":\"%s\"}", err)
writeResponse(w, http.StatusInternalServerError, s)
return
}
func writeResponse(w http.ResponseWriter, status int, msg string) {
if status != http.StatusOK {
weblog(fmt.Sprintf(msg))
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type,access-control-allow-origin, access-control-allow-headers")
w.WriteHeader(status)
w.Write([]byte(msg))
return
}
func weblog(msg string) {
log.Printf("Webserver : %s", msg)
}
// Message is a structure for communicating additional data to API consumer.
type Message struct {
Text string `json:"text"`
Details string `json:"details"`
}
// JSON marshalls the content of a todo to json.
func (m Message) JSON() (string, error) {
bytes, err := json.Marshal(m)
if err != nil {
return "", fmt.Errorf("could not marshal json for response: %s", err)
}
return string(bytes), nil
}
// JSONBytes marshalls the content of a todo to json as a byte array.
func (m Message) JSONBytes() ([]byte, error) {
bytes, err := json.Marshal(m)
if err != nil {
return []byte{}, fmt.Errorf("could not marshal json for response: %s", err)
}
return bytes, nil
}