diff --git a/adapters.go b/adapters/adapter.go similarity index 94% rename from adapters.go rename to adapters/adapter.go index 91475c6..f87d454 100644 --- a/adapters.go +++ b/adapters/adapter.go @@ -1,4 +1,4 @@ -package main +package adapters import "net/http" diff --git a/logging.go b/adapters/logging.go similarity index 96% rename from logging.go rename to adapters/logging.go index ff7ed46..1e6d2a3 100644 --- a/logging.go +++ b/adapters/logging.go @@ -1,4 +1,4 @@ -package main +package adapters import ( "log" diff --git a/api.go b/api.go deleted file mode 100644 index d71db29..0000000 --- a/api.go +++ /dev/null @@ -1,168 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/gorilla/mux" - "github.com/minio/minio-go" -) - -// CopyObjectInfo is the information about an object to copy -type CopyObjectInfo struct { - BucketName string `json:"bucketName"` - ObjectName string `json:"objectName"` - SourceBucketName string `json:"sourceBucketName"` - SourceObjectName string `json:"sourceObjectName"` -} - -// CreateBucketHandler creates a new bucket -func (s *Server) CreateBucketHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var bucket minio.BucketInfo - - err := json.NewDecoder(r.Body).Decode(&bucket) - if err != nil { - msg := "error decoding json" - handleHTTPError(w, msg, err, http.StatusUnprocessableEntity) - return - } - - err = s.S3.MakeBucket(bucket.Name, "") - if err != nil { - msg := "error making bucket" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusCreated) - - err = json.NewEncoder(w).Encode(bucket) - if err != nil { - msg := "error encoding json" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - }) -} - -// CreateObjectHandler allows to upload a new object -func (s *Server) CreateObjectHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - if r.Header.Get("Content-Type") == "application/json" { - var copy CopyObjectInfo - - err := json.NewDecoder(r.Body).Decode(©) - if err != nil { - msg := "error decoding json" - handleHTTPError(w, msg, err, http.StatusUnprocessableEntity) - return - } - - var copyConds = minio.NewCopyConditions() - objectSource := fmt.Sprintf("/%s/%s", copy.SourceBucketName, copy.SourceObjectName) - err = s.S3.CopyObject(copy.BucketName, copy.ObjectName, objectSource, copyConds) - if err != nil { - msg := "error copying object" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusCreated) - - err = json.NewEncoder(w).Encode(copy) - if err != nil { - msg := "error encoding json" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - } else { - err := r.ParseMultipartForm(32 << 20) - if err != nil { - msg := "error parsing form" - handleHTTPError(w, msg, err, http.StatusUnprocessableEntity) - return - } - - file, handler, err := r.FormFile("file") - if err != nil { - msg := "error getting form file" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - defer file.Close() - - _, err = s.S3.PutObject(vars["bucketName"], handler.Filename, file, "application/octet-stream") - if err != nil { - msg := "error putting object" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusCreated) - } - }) -} - -// DeleteBucketHandler deletes a bucket -func (s *Server) DeleteBucketHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - err := s.S3.RemoveBucket(vars["bucketName"]) - if err != nil { - msg := "error removing bucket" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) - }) -} - -// DeleteObjectHandler deletes an object -func (s *Server) DeleteObjectHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - err := s.S3.RemoveObject(vars["bucketName"], vars["objectName"]) - if err != nil { - msg := "error removing object" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - }) -} - -// GetObjectHandler downloads an object to the client -func (s *Server) GetObjectHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - objectName := vars["objectName"] - - object, err := s.S3.GetObject(vars["bucketName"], objectName) - if err != nil { - msg := "error getting object" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", objectName)) - w.Header().Set("Content-Type", "application/octet-stream") - - _, err = io.Copy(w, object) - if err != nil { - msg := "error copying object" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - }) -} diff --git a/buckets/create-handler.go b/buckets/create-handler.go new file mode 100644 index 0000000..6577436 --- /dev/null +++ b/buckets/create-handler.go @@ -0,0 +1,40 @@ +package buckets + +import ( + "encoding/json" + "net/http" + + "github.com/mastertinner/s3-manager/web" + minio "github.com/minio/minio-go" +) + +// CreateHandler creates a new bucket +func CreateHandler(s3 *minio.Client) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var bucket minio.BucketInfo + + err := json.NewDecoder(r.Body).Decode(&bucket) + if err != nil { + msg := "error decoding json" + web.HandleHTTPError(w, msg, err, http.StatusUnprocessableEntity) + return + } + + err = s3.MakeBucket(bucket.Name, "") + if err != nil { + msg := "error making bucket" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusCreated) + + err = json.NewEncoder(w).Encode(bucket) + if err != nil { + msg := "error encoding json" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + }) +} diff --git a/buckets/delete-handler.go b/buckets/delete-handler.go new file mode 100644 index 0000000..decbf68 --- /dev/null +++ b/buckets/delete-handler.go @@ -0,0 +1,25 @@ +package buckets + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/mastertinner/s3-manager/web" + minio "github.com/minio/minio-go" +) + +// DeleteHandler deletes a bucket +func DeleteHandler(s3 *minio.Client) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + err := s3.RemoveBucket(vars["bucketName"]) + if err != nil { + msg := "error removing bucket" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/minio.go b/datasources/minio.go similarity index 97% rename from minio.go rename to datasources/minio.go index cbd1b27..a6e139d 100644 --- a/minio.go +++ b/datasources/minio.go @@ -1,4 +1,4 @@ -package main +package datasources import ( "log" diff --git a/main.go b/main.go index 7e50ed5..dcef3be 100644 --- a/main.go +++ b/main.go @@ -6,18 +6,15 @@ import ( "os" "github.com/gorilla/mux" - "github.com/minio/minio-go" + "github.com/mastertinner/s3-manager/adapters" + "github.com/mastertinner/s3-manager/buckets" + "github.com/mastertinner/s3-manager/datasources" + "github.com/mastertinner/s3-manager/objects" + "github.com/mastertinner/s3-manager/views" ) -// Server is a server containing a minio client -type Server struct { - S3 *minio.Client -} - func main() { - s := &Server{ - S3: NewMinioClient(), - } + s3 := datasources.NewMinioClient() logger := log.New(os.Stdout, "request: ", log.Lshortfile) router := mux.NewRouter() @@ -25,39 +22,63 @@ func main() { router. Methods("GET"). Path("/"). - Handler(Adapt(IndexHandler(), Logging(logger))) + Handler(adapters.Adapt( + views.IndexHandler(), + adapters.Logging(logger), + )) router. Methods("GET"). Path("/buckets"). - Handler(Adapt(s.BucketsPageHandler(), Logging(logger))) + Handler(adapters.Adapt( + views.BucketsHandler(s3), + adapters.Logging(logger), + )) router. Methods("GET"). Path("/buckets/{bucketName}"). - Handler(Adapt(s.BucketPageHandler(), Logging(logger))) + Handler(adapters.Adapt( + views.BucketHandler(s3), + adapters.Logging(logger), + )) api := router.PathPrefix("/api").Subrouter() - buckets := api.PathPrefix("/buckets").Subrouter() - buckets. + br := api.PathPrefix("/buckets").Subrouter() + br. Methods("POST"). Path(""). - Handler(Adapt(s.CreateBucketHandler(), Logging(logger))) - buckets. + Handler(adapters.Adapt( + buckets.CreateHandler(s3), + adapters.Logging(logger), + )) + br. Methods("DELETE"). Path("/{bucketName}"). - Handler(Adapt(s.DeleteBucketHandler(), Logging(logger))) - buckets. + Handler(adapters.Adapt( + buckets.DeleteHandler(s3), + adapters.Logging(logger), + )) + br. Methods("POST"). Path("/{bucketName}/objects"). - Handler(Adapt(s.CreateObjectHandler(), Logging(logger))) - buckets. + Handler(adapters.Adapt( + objects.CreateHandler(s3), + adapters.Logging(logger), + )) + br. Methods("GET"). Path("/{bucketName}/objects/{objectName}"). - Handler(Adapt(s.GetObjectHandler(), Logging(logger))) - buckets. + Handler(adapters.Adapt( + objects.GetHandler(s3), + adapters.Logging(logger), + )) + br. Methods("DELETE"). Path("/{bucketName}/objects/{objectName}"). - Handler(Adapt(s.DeleteObjectHandler(), Logging(logger))) + Handler(adapters.Adapt( + objects.DeleteHandler(s3), + adapters.Logging(logger), + )) port := os.Getenv("PORT") if len(port) == 0 { diff --git a/objects/create-handler.go b/objects/create-handler.go new file mode 100644 index 0000000..42b8d1c --- /dev/null +++ b/objects/create-handler.go @@ -0,0 +1,80 @@ +package objects + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/gorilla/mux" + "github.com/mastertinner/s3-manager/web" + minio "github.com/minio/minio-go" +) + +// CopyObjectInfo is the information about an object to copy +type CopyObjectInfo struct { + BucketName string `json:"bucketName"` + ObjectName string `json:"objectName"` + SourceBucketName string `json:"sourceBucketName"` + SourceObjectName string `json:"sourceObjectName"` +} + +// CreateHandler allows to upload a new object +func CreateHandler(s3 *minio.Client) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + if r.Header.Get("Content-Type") == "application/json" { + var copy CopyObjectInfo + + err := json.NewDecoder(r.Body).Decode(©) + if err != nil { + msg := "error decoding json" + web.HandleHTTPError(w, msg, err, http.StatusUnprocessableEntity) + return + } + + var copyConds = minio.NewCopyConditions() + objectSource := fmt.Sprintf("/%s/%s", copy.SourceBucketName, copy.SourceObjectName) + err = s3.CopyObject(copy.BucketName, copy.ObjectName, objectSource, copyConds) + if err != nil { + msg := "error copying object" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusCreated) + + err = json.NewEncoder(w).Encode(copy) + if err != nil { + msg := "error encoding json" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + } else { + err := r.ParseMultipartForm(32 << 20) + if err != nil { + msg := "error parsing form" + web.HandleHTTPError(w, msg, err, http.StatusUnprocessableEntity) + return + } + + file, handler, err := r.FormFile("file") + if err != nil { + msg := "error getting form file" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + defer file.Close() + + _, err = s3.PutObject(vars["bucketName"], handler.Filename, file, "application/octet-stream") + if err != nil { + msg := "error putting object" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) + } + }) +} diff --git a/objects/delete-handler.go b/objects/delete-handler.go new file mode 100644 index 0000000..7842eef --- /dev/null +++ b/objects/delete-handler.go @@ -0,0 +1,25 @@ +package objects + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/mastertinner/s3-manager/web" + minio "github.com/minio/minio-go" +) + +// DeleteHandler deletes an object +func DeleteHandler(s3 *minio.Client) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + err := s3.RemoveObject(vars["bucketName"], vars["objectName"]) + if err != nil { + msg := "error removing object" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + }) +} diff --git a/objects/get-handler.go b/objects/get-handler.go new file mode 100644 index 0000000..eb5810d --- /dev/null +++ b/objects/get-handler.go @@ -0,0 +1,36 @@ +package objects + +import ( + "fmt" + "io" + "net/http" + + "github.com/gorilla/mux" + "github.com/mastertinner/s3-manager/web" + minio "github.com/minio/minio-go" +) + +// GetHandler downloads an object to the client +func GetHandler(s3 *minio.Client) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + objectName := vars["objectName"] + + object, err := s3.GetObject(vars["bucketName"], objectName) + if err != nil { + msg := "error getting object" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", objectName)) + w.Header().Set("Content-Type", "application/octet-stream") + + _, err = io.Copy(w, object) + if err != nil { + msg := "error copying object" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + }) +} diff --git a/objects/with-icon.go b/objects/with-icon.go new file mode 100644 index 0000000..8e68f6d --- /dev/null +++ b/objects/with-icon.go @@ -0,0 +1,9 @@ +package objects + +import minio "github.com/minio/minio-go" + +// WithIcon is a minio object with an added icon +type WithIcon struct { + minio.ObjectInfo + Icon string +} diff --git a/pages.go b/pages.go deleted file mode 100644 index be40566..0000000 --- a/pages.go +++ /dev/null @@ -1,116 +0,0 @@ -package main - -import ( - "html/template" - "net/http" - "path" - - "github.com/gorilla/mux" - "github.com/minio/minio-go" -) - -// BucketPage defines the details page of a bucket -type BucketPage struct { - BucketName string - Objects []ObjectWithIcon -} - -// ObjectWithIcon is a minio object with an added icon -type ObjectWithIcon struct { - minio.ObjectInfo - Icon string -} - -// BucketPageHandler shows the details page of a bucket -func (s *Server) BucketPageHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - bucketName := mux.Vars(r)["bucketName"] - var objects []ObjectWithIcon - - lp := path.Join("templates", "layout.html") - bp := path.Join("templates", "bucket.html") - - t, err := template.ParseFiles(lp, bp) - if err != nil { - msg := "error parsing templates" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - - doneCh := make(chan struct{}) - objectCh := s.S3.ListObjectsV2(bucketName, "", false, doneCh) - for object := range objectCh { - if object.Err != nil { - msg := "error listing objects" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - objectWithIcon := ObjectWithIcon{object, icon(object.Key)} - objects = append(objects, objectWithIcon) - } - - bucketPage := BucketPage{ - BucketName: bucketName, - Objects: objects, - } - - err = t.ExecuteTemplate(w, "layout", bucketPage) - if err != nil { - msg := "error executing template" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - }) -} - -// BucketsPageHandler shows all buckets -func (s *Server) BucketsPageHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - lp := path.Join("templates", "layout.html") - ip := path.Join("templates", "index.html") - - t, err := template.ParseFiles(lp, ip) - if err != nil { - msg := "error parsing templates" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - - buckets, err := s.S3.ListBuckets() - if err != nil { - msg := "error listing buckets" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - - err = t.ExecuteTemplate(w, "layout", buckets) - if err != nil { - msg := "error executing template" - handleHTTPError(w, msg, err, http.StatusInternalServerError) - return - } - }) -} - -// IndexHandler forwards to "/buckets" -func IndexHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/buckets", http.StatusPermanentRedirect) - }) -} - -// icon returns an icon for a file type -func icon(fileName string) string { - e := path.Ext(fileName) - - switch e { - case ".tgz": - return "archive" - case ".png", ".jpg", ".gif", ".svg": - return "photo" - case ".mp3": - return "music_note" - } - - return "insert_drive_file" -} diff --git a/views/bucket-handler.go b/views/bucket-handler.go new file mode 100644 index 0000000..b46fc99 --- /dev/null +++ b/views/bucket-handler.go @@ -0,0 +1,60 @@ +package views + +import ( + "html/template" + "net/http" + "path" + + "github.com/gorilla/mux" + "github.com/mastertinner/s3-manager/objects" + "github.com/mastertinner/s3-manager/web" + minio "github.com/minio/minio-go" +) + +// BucketPage defines the details page of a bucket +type BucketPage struct { + BucketName string + Objects []objects.WithIcon +} + +// BucketHandler shows the details page of a bucket +func BucketHandler(s3 *minio.Client) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + bucketName := mux.Vars(r)["bucketName"] + var objs []objects.WithIcon + + lp := path.Join("views", "layout.html") + p := path.Join("views", "bucket.html") + + t, err := template.ParseFiles(lp, p) + if err != nil { + msg := "error parsing templates" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + + doneCh := make(chan struct{}) + objectCh := s3.ListObjectsV2(bucketName, "", false, doneCh) + for object := range objectCh { + if object.Err != nil { + msg := "error listing objects" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + objectWithIcon := objects.WithIcon{object, icon(object.Key)} + objs = append(objs, objectWithIcon) + } + + bucketPage := BucketPage{ + BucketName: bucketName, + Objects: objs, + } + + err = t.ExecuteTemplate(w, "layout", bucketPage) + if err != nil { + msg := "error executing template" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + }) +} diff --git a/templates/bucket.html b/views/bucket.html similarity index 100% rename from templates/bucket.html rename to views/bucket.html diff --git a/views/buckets-handler.go b/views/buckets-handler.go new file mode 100644 index 0000000..00aa1ba --- /dev/null +++ b/views/buckets-handler.go @@ -0,0 +1,39 @@ +package views + +import ( + "html/template" + "net/http" + "path" + + "github.com/mastertinner/s3-manager/web" + minio "github.com/minio/minio-go" +) + +// BucketsHandler shows all buckets +func BucketsHandler(s3 *minio.Client) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + lp := path.Join("views", "layout.html") + p := path.Join("views", "buckets.html") + + t, err := template.ParseFiles(lp, p) + if err != nil { + msg := "error parsing templates" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + + buckets, err := s3.ListBuckets() + if err != nil { + msg := "error listing buckets" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + + err = t.ExecuteTemplate(w, "layout", buckets) + if err != nil { + msg := "error executing template" + web.HandleHTTPError(w, msg, err, http.StatusInternalServerError) + return + } + }) +} diff --git a/templates/index.html b/views/buckets.html similarity index 100% rename from templates/index.html rename to views/buckets.html diff --git a/views/icon.go b/views/icon.go new file mode 100644 index 0000000..9922c37 --- /dev/null +++ b/views/icon.go @@ -0,0 +1,19 @@ +package views + +import "path" + +// icon returns an icon for a file type +func icon(fileName string) string { + e := path.Ext(fileName) + + switch e { + case ".tgz": + return "archive" + case ".png", ".jpg", ".gif", ".svg": + return "photo" + case ".mp3": + return "music_note" + } + + return "insert_drive_file" +} diff --git a/views/index-handler.go b/views/index-handler.go new file mode 100644 index 0000000..c1313b8 --- /dev/null +++ b/views/index-handler.go @@ -0,0 +1,10 @@ +package views + +import "net/http" + +// IndexHandler forwards to "/buckets" +func IndexHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/buckets", http.StatusPermanentRedirect) + }) +} diff --git a/templates/layout.html b/views/layout.html similarity index 80% rename from templates/layout.html rename to views/layout.html index f72b496..0978519 100644 --- a/templates/layout.html +++ b/views/layout.html @@ -10,15 +10,15 @@