package server import ( "crypto/rand" "crypto/sha1" "fmt" "math/big" "net/http" "path/filepath" "strings" "time" "github.com/apex/log" "gitlab.com/unboundsoftware/s3uploader/storage" ) type Server struct { *http.ServeMux store storage.Storage returnUrl string logger log.Interface now func() time.Time } func (s *Server) HandlePut(w http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodOptions: s.cors(w) case http.MethodPut: path := strings.TrimPrefix(req.URL.Path, "/put") s.logger.Infof("uploading to %s", path) s.upload(path, w, req) default: w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte("This endpoint requires PUT")) } } func (s *Server) HandleUpload(w http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodOptions: s.cors(w) case http.MethodPut: prefix, err := randomString(64) if err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("An error occurred")) return } now := s.now() day := now.Format("20060102") prefixHash := hash(prefix) dayHash := hash(now.Format(time.RFC3339Nano)) s.logger.Infof("uploading to %s/%s%s", day, prefixHash, dayHash) s.upload(fmt.Sprintf("%s/%s%s", day, prefixHash, dayHash), w, req) default: w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte("This endpoint requires PUT")) } } func (s *Server) HandleHealth(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("OK")) } func (s *Server) upload(path string, w http.ResponseWriter, req *http.Request) { err := s.store.Store(path, req.Body) if err != nil { s.logger.WithError(err).Error("error storing object in bucket") w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("error storing object in bucket")) return } w.Header().Add("Access-Control-Expose-Headers", "X-File-URL") fileUrl := filepath.Join(s.returnUrl, path) w.Header().Add("X-File-URL", fileUrl) s.logger.Infof("success - file is available at %s", fileUrl) _, _ = w.Write([]byte("success")) } func (s *Server) cors(w http.ResponseWriter) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "PUT,OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With") w.Header().Set("Access-Control-Max-Age", "1728000") w.WriteHeader(http.StatusNoContent) } func New(store storage.Storage, url string, logger log.Interface) http.Handler { mux := &Server{ ServeMux: http.NewServeMux(), store: store, returnUrl: url, logger: logger, now: time.Now, } mux.HandleFunc("/put/", mux.HandlePut) mux.HandleFunc("/upload", mux.HandleUpload) mux.HandleFunc("/health", mux.HandleHealth) return mux } func hash(s string) string { hasher := sha1.New() hasher.Write([]byte(s)) return fmt.Sprintf("%x", hasher.Sum(nil)) } func randomString(n int) (string, error) { const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ret := make([]byte, n) for i := 0; i < n; i++ { num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) if err != nil { return "", err } ret[i] = letters[num.Int64()] } return string(ret), nil }