mirror of
https://github.com/KevinMidboe/planetposen-images.git
synced 2025-10-29 13:20:11 +00:00
App for fetching and uploading images to cloud bucket storage
This commit is contained in:
24
server/handler/error.go
Normal file
24
server/handler/error.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// handleError - Logs the error (if shouldLog is true), and outputs the error message (msg)
|
||||
func handleError(w http.ResponseWriter, err error, msg string, statusCode int, shouldLog bool) {
|
||||
if shouldLog {
|
||||
log.WithField("err", err).Error(msg)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
errorJSON, _ := json.Marshal(struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: msg,
|
||||
})
|
||||
w.Write(errorJSON)
|
||||
}
|
||||
11
server/handler/healthz.go
Normal file
11
server/handler/healthz.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package handler
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Healthz is used for our readiness and liveness probes.
|
||||
// GET /_healthz
|
||||
// Responds: 200
|
||||
func Healthz(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(http.StatusText(http.StatusOK)))
|
||||
}
|
||||
119
server/handler/images.go
Normal file
119
server/handler/images.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/kevinmidboe/planetposen-images/util"
|
||||
"strings"
|
||||
|
||||
// "github.com/sirupsen/logrus"
|
||||
// "encoding/json"
|
||||
"fmt"
|
||||
"github.com/kevinmidboe/planetposen-images/clients/gcs"
|
||||
"github.com/kevinmidboe/planetposen-images/image"
|
||||
// "github.com/dbmedialab/dearheart/event"
|
||||
// "github.com/dbmedialab/dearheart/server/internal/serverutils"
|
||||
"github.com/gorilla/mux"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
// "strconv"
|
||||
// "strings"
|
||||
)
|
||||
|
||||
// UploadImages takes a request with file form and uploads the content to GCS
|
||||
func UploadImages(hostname string, gcsClient gcs.Client) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get initial protocol data
|
||||
ctx := r.Context()
|
||||
|
||||
file, fileHeader, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
handleError(w, err, "unable to handle file", http.StatusBadRequest, true)
|
||||
return
|
||||
}
|
||||
var maxSize int64 = 10 * 1024 * 1024 * 1024
|
||||
if fileHeader.Size > maxSize {
|
||||
handleError(w, nil, "File sized %d, larger than the max of %d\", fileHeader.Size, maxSize", http.StatusBadRequest, true)
|
||||
return
|
||||
}
|
||||
|
||||
filename := strings.ReplaceAll(fileHeader.Filename, "/", "-")
|
||||
defer file.Close()
|
||||
|
||||
writer, path, err := gcsClient.FileWriter(ctx, filename)
|
||||
if err != nil {
|
||||
handleError(w, err, "File unable to write file to gcs", http.StatusServiceUnavailable, true)
|
||||
return
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
_, err = io.Copy(writer, file)
|
||||
if err != nil {
|
||||
handleError(w, err, "Error copying file to GCS", http.StatusInternalServerError, true)
|
||||
}
|
||||
|
||||
finalURL := util.ImageURL(hostname, string(path))
|
||||
responseStruct := image.Image{
|
||||
Path: string(path),
|
||||
URL: finalURL,
|
||||
}
|
||||
|
||||
responseData, _ := json.Marshal(responseStruct)
|
||||
_, _ = w.Write(responseData)
|
||||
}
|
||||
}
|
||||
|
||||
// FetchImage gets a single image from GCS
|
||||
func FetchImage(gcsClient gcs.Client) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
path := gcs.EncodedPath(mux.Vars(r)["path"])
|
||||
if path == "" {
|
||||
handleError(w, nil, "missing image path ", http.StatusBadRequest, true)
|
||||
return
|
||||
}
|
||||
|
||||
reader, err := gcsClient.FileReader(ctx, path)
|
||||
if err != nil {
|
||||
handleError(w, err, "error from gcs file reader ", http.StatusBadRequest, true)
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// we can ignore the error, because we've already verified the path decodes
|
||||
filename, _ := path.Decode()
|
||||
extension := filepath.Ext(string(filename))
|
||||
if extension != "" {
|
||||
w.Header().Set("Content-Type", fmt.Sprintf("image/%s", extension[1:]))
|
||||
}
|
||||
|
||||
_, err = io.Copy(w, reader)
|
||||
if err != nil {
|
||||
handleError(w, err, "Couldn't copy the file from GCS ", http.StatusInternalServerError, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ListImages(gcsClient gcs.Client) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
files, err := gcsClient.FileLister(ctx)
|
||||
if err != nil {
|
||||
handleError(w, err, "error from gcs file lister ", http.StatusBadRequest, true)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
responseJSON, _ := json.Marshal(struct {
|
||||
Message string `json:"message"`
|
||||
Success bool `json:"success"`
|
||||
Files []string `json:"files"`
|
||||
}{
|
||||
Message: "Google storage bucket contents",
|
||||
Success: true,
|
||||
Files: files,
|
||||
})
|
||||
w.Write(responseJSON)
|
||||
}
|
||||
}
|
||||
19
server/router.go
Normal file
19
server/router.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/kevinmidboe/planetposen-images/server/handler"
|
||||
)
|
||||
|
||||
const v1API string = "/api/v1"
|
||||
|
||||
func (s *Server) setupRoutes() {
|
||||
s.Router.HandleFunc("/_healthz", handler.Healthz).Methods("GET").Name("Health")
|
||||
|
||||
api := s.Router.PathPrefix(v1API).Subrouter()
|
||||
api.HandleFunc("/images", handler.UploadImages(s.Config.Hostname, s.GCSClient)).Methods("POST").Name("UploadImages")
|
||||
api.HandleFunc("/images", handler.ListImages(s.GCSClient)).Methods("GET").Name("ListImages")
|
||||
|
||||
// Raw image fetcher
|
||||
api.HandleFunc("/images/{path}", handler.FetchImage(s.GCSClient)).Methods("GET").Name("FetchImage")
|
||||
|
||||
}
|
||||
85
server/server.go
Normal file
85
server/server.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Package server provides functionality to easily set up an HTTTP server.
|
||||
//
|
||||
// Clients:
|
||||
// Database
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/kevinmidboe/planetposen-images/clients/gcs"
|
||||
"github.com/kevinmidboe/planetposen-images/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Server holds the HTTP server, router, config and all clients.
|
||||
type Server struct {
|
||||
Config *config.Config
|
||||
HTTP *http.Server
|
||||
Router *mux.Router
|
||||
GCSClient gcs.Client
|
||||
}
|
||||
|
||||
// Create sets up the HTTP server, router and all clients.
|
||||
// Returns an error if an error occurs.
|
||||
func (s *Server) Create(ctx context.Context, config *config.Config) error {
|
||||
// metrics.RegisterPrometheusCollectors()
|
||||
|
||||
s.Config = config
|
||||
s.Router = mux.NewRouter()
|
||||
s.HTTP = &http.Server{
|
||||
Addr: fmt.Sprintf(":%s", s.Config.Port),
|
||||
Handler: s.Router,
|
||||
}
|
||||
|
||||
gcsClient, err := gcs.NewClient(ctx, config.GCSBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.GCSClient = gcsClient
|
||||
|
||||
s.setupRoutes()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Serve tells the server to start listening and serve HTTP requests.
|
||||
// It also makes sure that the server gracefully shuts down on exit.
|
||||
// Returns an error if an error occurs.
|
||||
func (s *Server) Serve(ctx context.Context) error {
|
||||
// closer, err := trace.InitGlobalTracer(s.Config)
|
||||
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// defer closer.Close()
|
||||
|
||||
go func(ctx context.Context, s *Server) {
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
<-stop
|
||||
|
||||
log.Info("Shutdown signal received")
|
||||
|
||||
if err := s.HTTP.Shutdown(ctx); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}(ctx, s)
|
||||
|
||||
log.Infof("Ready at: %s", s.Config.Port)
|
||||
|
||||
if err := s.HTTP.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user