|
|
|
@ -17,26 +17,12 @@ |
|
|
|
|
package minioapi |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
"bytes" |
|
|
|
|
"encoding/json" |
|
|
|
|
"encoding/xml" |
|
|
|
|
"log" |
|
|
|
|
"net/http" |
|
|
|
|
"strconv" |
|
|
|
|
"strings" |
|
|
|
|
"time" |
|
|
|
|
|
|
|
|
|
"github.com/gorilla/mux" |
|
|
|
|
mstorage "github.com/minio-io/minio/pkg/storage" |
|
|
|
|
"github.com/minio-io/minio/pkg/utils/config" |
|
|
|
|
"github.com/minio-io/minio/pkg/utils/crypto/signers" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
type contentType int |
|
|
|
|
|
|
|
|
|
const ( |
|
|
|
|
xmlType contentType = iota |
|
|
|
|
jsonType |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
const ( |
|
|
|
@ -47,11 +33,6 @@ type minioApi struct { |
|
|
|
|
storage mstorage.Storage |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type vHandler struct { |
|
|
|
|
conf config.Config |
|
|
|
|
handler http.Handler |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// No encoder interface exists, so we create one.
|
|
|
|
|
type encoder interface { |
|
|
|
|
Encode(v interface{}) error |
|
|
|
@ -77,59 +58,6 @@ func HttpHandler(storage mstorage.Storage) http.Handler { |
|
|
|
|
return validateHandler(conf, ignoreUnimplementedResources(mux)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// grab AccessKey from authorization header
|
|
|
|
|
func stripAccessKey(r *http.Request) string { |
|
|
|
|
fields := strings.Fields(r.Header.Get("Authorization")) |
|
|
|
|
if len(fields) < 2 { |
|
|
|
|
return "" |
|
|
|
|
} |
|
|
|
|
splits := strings.Split(fields[1], ":") |
|
|
|
|
if len(splits) < 2 { |
|
|
|
|
return "" |
|
|
|
|
} |
|
|
|
|
return splits[0] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func validateHandler(conf config.Config, h http.Handler) http.Handler { |
|
|
|
|
return vHandler{conf, h} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (h vHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
accessKey := stripAccessKey(r) |
|
|
|
|
if accessKey != "" { |
|
|
|
|
if err := h.conf.ReadConfig(); err != nil { |
|
|
|
|
w.WriteHeader(http.StatusInternalServerError) |
|
|
|
|
} else { |
|
|
|
|
user := h.conf.GetKey(accessKey) |
|
|
|
|
ok, err := signers.ValidateRequest(user, r) |
|
|
|
|
if ok { |
|
|
|
|
h.handler.ServeHTTP(w, r) |
|
|
|
|
} else { |
|
|
|
|
w.WriteHeader(http.StatusUnauthorized) |
|
|
|
|
w.Write([]byte(err.Error())) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
//No access key found, handle this more appropriately
|
|
|
|
|
//TODO: Remove this after adding tests to support signature
|
|
|
|
|
//request
|
|
|
|
|
h.handler.ServeHTTP(w, r) |
|
|
|
|
//Add this line, to reply back for invalid requests
|
|
|
|
|
//w.WriteHeader(http.StatusUnauthorized)
|
|
|
|
|
//w.Write([]byte("Authorization header malformed")
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func ignoreUnimplementedResources(h http.Handler) http.Handler { |
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
if ignoreUnImplementedObjectResources(r) || ignoreUnImplementedBucketResources(r) { |
|
|
|
|
w.WriteHeader(http.StatusNotImplemented) |
|
|
|
|
} else { |
|
|
|
|
h.ServeHTTP(w, r) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (server *minioApi) listBucketsHandler(w http.ResponseWriter, req *http.Request) { |
|
|
|
|
vars := mux.Vars(req) |
|
|
|
|
prefix, ok := vars["prefix"] |
|
|
|
@ -139,13 +67,36 @@ func (server *minioApi) listBucketsHandler(w http.ResponseWriter, req *http.Requ |
|
|
|
|
|
|
|
|
|
acceptsContentType := getContentType(req) |
|
|
|
|
buckets, err := server.storage.ListBuckets(prefix) |
|
|
|
|
if err != nil { |
|
|
|
|
log.Println(err) |
|
|
|
|
w.WriteHeader(http.StatusInternalServerError) |
|
|
|
|
return |
|
|
|
|
switch err := err.(type) { |
|
|
|
|
case nil: |
|
|
|
|
{ |
|
|
|
|
response := generateBucketsListResult(buckets) |
|
|
|
|
w.Write(writeObjectHeadersAndResponse(w, response, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.BucketNameInvalid: |
|
|
|
|
{ |
|
|
|
|
error := errorCodeError(InvalidBucketName) |
|
|
|
|
errorResponse := getErrorResponse(error, prefix) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.ImplementationError: |
|
|
|
|
{ |
|
|
|
|
log.Println(err) |
|
|
|
|
error := errorCodeError(InternalError) |
|
|
|
|
errorResponse := getErrorResponse(error, prefix) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.BackendCorrupted: |
|
|
|
|
{ |
|
|
|
|
log.Println(err) |
|
|
|
|
error := errorCodeError(InternalError) |
|
|
|
|
errorResponse := getErrorResponse(error, prefix) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
response := generateBucketsListResult(buckets) |
|
|
|
|
w.Write(writeObjectHeadersAndResponse(w, response, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (server *minioApi) listObjectsHandler(w http.ResponseWriter, req *http.Request) { |
|
|
|
@ -161,19 +112,40 @@ func (server *minioApi) listObjectsHandler(w http.ResponseWriter, req *http.Requ |
|
|
|
|
objects, isTruncated, err := server.storage.ListObjects(bucket, prefix, 1000) |
|
|
|
|
switch err := err.(type) { |
|
|
|
|
case nil: // success
|
|
|
|
|
response := generateObjectsListResult(bucket, objects, isTruncated) |
|
|
|
|
w.Write(writeObjectHeadersAndResponse(w, response, acceptsContentType)) |
|
|
|
|
{ |
|
|
|
|
response := generateObjectsListResult(bucket, objects, isTruncated) |
|
|
|
|
w.Write(writeObjectHeadersAndResponse(w, response, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.BucketNotFound: |
|
|
|
|
log.Println(err) |
|
|
|
|
w.WriteHeader(http.StatusNotFound) |
|
|
|
|
w.Write([]byte(err.Error())) |
|
|
|
|
{ |
|
|
|
|
error := errorCodeError(NoSuchBucket) |
|
|
|
|
errorResponse := getErrorResponse(error, bucket) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.ImplementationError: |
|
|
|
|
log.Println(err) |
|
|
|
|
w.WriteHeader(http.StatusInternalServerError) |
|
|
|
|
w.Write([]byte(err.Error())) |
|
|
|
|
default: |
|
|
|
|
w.WriteHeader(http.StatusBadRequest) |
|
|
|
|
w.Write([]byte(err.Error())) |
|
|
|
|
{ |
|
|
|
|
// Embed error log on server side
|
|
|
|
|
log.Println(err) |
|
|
|
|
error := errorCodeError(InternalError) |
|
|
|
|
errorResponse := getErrorResponse(error, bucket) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.BucketNameInvalid: |
|
|
|
|
{ |
|
|
|
|
error := errorCodeError(InvalidBucketName) |
|
|
|
|
errorResponse := getErrorResponse(error, bucket) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.ObjectNameInvalid: |
|
|
|
|
{ |
|
|
|
|
error := errorCodeError(NoSuchKey) |
|
|
|
|
errorResponse := getErrorResponse(error, prefix) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -181,16 +153,38 @@ func (server *minioApi) putBucketHandler(w http.ResponseWriter, req *http.Reques |
|
|
|
|
vars := mux.Vars(req) |
|
|
|
|
bucket := vars["bucket"] |
|
|
|
|
err := server.storage.StoreBucket(bucket) |
|
|
|
|
|
|
|
|
|
acceptsContentType := getContentType(req) |
|
|
|
|
|
|
|
|
|
switch err := err.(type) { |
|
|
|
|
case nil: |
|
|
|
|
w.Header().Set("Server", "Minio") |
|
|
|
|
w.Header().Set("Connection", "close") |
|
|
|
|
{ |
|
|
|
|
w.Header().Set("Server", "Minio") |
|
|
|
|
w.Header().Set("Connection", "close") |
|
|
|
|
} |
|
|
|
|
case mstorage.BucketNameInvalid: |
|
|
|
|
w.WriteHeader(http.StatusBadRequest) |
|
|
|
|
w.Write([]byte(err.Error())) |
|
|
|
|
{ |
|
|
|
|
error := errorCodeError(InvalidBucketName) |
|
|
|
|
errorResponse := getErrorResponse(error, bucket) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.BucketExists: |
|
|
|
|
w.WriteHeader(http.StatusConflict) |
|
|
|
|
w.Write([]byte(err.Error())) |
|
|
|
|
{ |
|
|
|
|
error := errorCodeError(BucketAlreadyExists) |
|
|
|
|
errorResponse := getErrorResponse(error, bucket) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.ImplementationError: |
|
|
|
|
{ |
|
|
|
|
// Embed errors log on serve side
|
|
|
|
|
log.Println(err) |
|
|
|
|
error := errorCodeError(InternalError) |
|
|
|
|
errorResponse := getErrorResponse(error, bucket) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -199,6 +193,8 @@ func (server *minioApi) getObjectHandler(w http.ResponseWriter, req *http.Reques |
|
|
|
|
bucket := vars["bucket"] |
|
|
|
|
object := vars["object"] |
|
|
|
|
|
|
|
|
|
acceptsContentType := getContentType(req) |
|
|
|
|
|
|
|
|
|
metadata, err := server.storage.GetObjectMetadata(bucket, object) |
|
|
|
|
switch err := err.(type) { |
|
|
|
|
case nil: // success
|
|
|
|
@ -211,15 +207,33 @@ func (server *minioApi) getObjectHandler(w http.ResponseWriter, req *http.Reques |
|
|
|
|
} |
|
|
|
|
case mstorage.ObjectNotFound: |
|
|
|
|
{ |
|
|
|
|
log.Println(err) |
|
|
|
|
w.WriteHeader(http.StatusNotFound) |
|
|
|
|
w.Write([]byte(err.Error())) |
|
|
|
|
error := errorCodeError(NoSuchKey) |
|
|
|
|
errorResponse := getErrorResponse(error, "/"+bucket+"/"+object) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
default: |
|
|
|
|
case mstorage.ObjectNameInvalid: |
|
|
|
|
{ |
|
|
|
|
error := errorCodeError(NoSuchKey) |
|
|
|
|
errorResponse := getErrorResponse(error, "/"+bucket+"/"+object) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.BucketNameInvalid: |
|
|
|
|
{ |
|
|
|
|
error := errorCodeError(InvalidBucketName) |
|
|
|
|
errorResponse := getErrorResponse(error, "/"+bucket+"/"+object) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.ImplementationError: |
|
|
|
|
{ |
|
|
|
|
// Embed errors log on serve side
|
|
|
|
|
log.Println(err) |
|
|
|
|
w.WriteHeader(http.StatusInternalServerError) |
|
|
|
|
w.Write([]byte(err.Error())) |
|
|
|
|
error := errorCodeError(InternalError) |
|
|
|
|
errorResponse := getErrorResponse(error, "/"+bucket+"/"+object) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
@ -229,16 +243,35 @@ func (server *minioApi) headObjectHandler(w http.ResponseWriter, req *http.Reque |
|
|
|
|
bucket := vars["bucket"] |
|
|
|
|
object := vars["object"] |
|
|
|
|
|
|
|
|
|
acceptsContentType := getContentType(req) |
|
|
|
|
|
|
|
|
|
metadata, err := server.storage.GetObjectMetadata(bucket, object) |
|
|
|
|
switch err := err.(type) { |
|
|
|
|
case nil: |
|
|
|
|
writeObjectHeaders(w, metadata) |
|
|
|
|
case mstorage.ObjectNotFound: |
|
|
|
|
log.Println(err) |
|
|
|
|
w.WriteHeader(http.StatusNotFound) |
|
|
|
|
default: |
|
|
|
|
log.Println(err) |
|
|
|
|
w.WriteHeader(http.StatusBadRequest) |
|
|
|
|
{ |
|
|
|
|
error := errorCodeError(NoSuchKey) |
|
|
|
|
errorResponse := getErrorResponse(error, "/"+bucket+"/"+object) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.ObjectNameInvalid: |
|
|
|
|
{ |
|
|
|
|
error := errorCodeError(NoSuchKey) |
|
|
|
|
errorResponse := getErrorResponse(error, "/"+bucket+"/"+object) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.ImplementationError: |
|
|
|
|
{ |
|
|
|
|
// Embed error log on server side
|
|
|
|
|
log.Println(err) |
|
|
|
|
error := errorCodeError(InternalError) |
|
|
|
|
errorResponse := getErrorResponse(error, "/"+bucket+"/"+object) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -246,41 +279,46 @@ func (server *minioApi) putObjectHandler(w http.ResponseWriter, req *http.Reques |
|
|
|
|
vars := mux.Vars(req) |
|
|
|
|
bucket := vars["bucket"] |
|
|
|
|
object := vars["object"] |
|
|
|
|
err := server.storage.StoreObject(bucket, object, "", req.Body) |
|
|
|
|
if err != nil { |
|
|
|
|
w.WriteHeader(http.StatusBadRequest) |
|
|
|
|
w.Write([]byte(err.Error())) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
w.Header().Set("Server", "Minio") |
|
|
|
|
w.Header().Set("Connection", "close") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func writeObjectHeadersAndResponse(w http.ResponseWriter, response interface{}, acceptsType contentType) []byte { |
|
|
|
|
var bytesBuffer bytes.Buffer |
|
|
|
|
var encoder encoder |
|
|
|
|
if acceptsType == xmlType { |
|
|
|
|
w.Header().Set("Content-Type", "application/xml") |
|
|
|
|
encoder = xml.NewEncoder(&bytesBuffer) |
|
|
|
|
} else if acceptsType == jsonType { |
|
|
|
|
w.Header().Set("Content-Type", "application/json") |
|
|
|
|
encoder = json.NewEncoder(&bytesBuffer) |
|
|
|
|
acceptsContentType := getContentType(req) |
|
|
|
|
|
|
|
|
|
err := server.storage.StoreObject(bucket, object, "", req.Body) |
|
|
|
|
switch err := err.(type) { |
|
|
|
|
case nil: |
|
|
|
|
w.Header().Set("Server", "Minio") |
|
|
|
|
w.Header().Set("Connection", "close") |
|
|
|
|
case mstorage.ImplementationError: |
|
|
|
|
{ |
|
|
|
|
// Embed error log on server side
|
|
|
|
|
log.Println(err) |
|
|
|
|
error := errorCodeError(InternalError) |
|
|
|
|
errorResponse := getErrorResponse(error, "/"+bucket+"/"+object) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.BucketNotFound: |
|
|
|
|
{ |
|
|
|
|
error := errorCodeError(NoSuchBucket) |
|
|
|
|
errorResponse := getErrorResponse(error, "/"+bucket+"/"+object) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.BucketNameInvalid: |
|
|
|
|
{ |
|
|
|
|
error := errorCodeError(InvalidBucketName) |
|
|
|
|
errorResponse := getErrorResponse(error, "/"+bucket+"/"+object) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
case mstorage.ObjectExists: |
|
|
|
|
{ |
|
|
|
|
error := errorCodeError(NotImplemented) |
|
|
|
|
errorResponse := getErrorResponse(error, "/"+bucket+"/"+object) |
|
|
|
|
w.WriteHeader(error.HttpStatusCode) |
|
|
|
|
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType)) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
w.Header().Set("Server", "Minio") |
|
|
|
|
w.Header().Set("Connection", "close") |
|
|
|
|
encoder.Encode(response) |
|
|
|
|
return bytesBuffer.Bytes() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Write Object Header helper
|
|
|
|
|
func writeObjectHeaders(w http.ResponseWriter, metadata mstorage.ObjectMetadata) { |
|
|
|
|
lastModified := metadata.Created.Format(time.RFC1123) |
|
|
|
|
w.Header().Set("ETag", metadata.ETag) |
|
|
|
|
w.Header().Set("Server", "Minio") |
|
|
|
|
w.Header().Set("Last-Modified", lastModified) |
|
|
|
|
w.Header().Set("Content-Length", strconv.FormatInt(metadata.Size, 10)) |
|
|
|
|
w.Header().Set("Content-Type", metadata.ContentType) |
|
|
|
|
w.Header().Set("Connection", "close") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func generateBucketsListResult(buckets []mstorage.BucketMetadata) BucketListResponse { |
|
|
|
@ -304,38 +342,6 @@ func generateBucketsListResult(buckets []mstorage.BucketMetadata) BucketListResp |
|
|
|
|
return data |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//// helpers
|
|
|
|
|
|
|
|
|
|
// Checks requests for unimplemented resources
|
|
|
|
|
func ignoreUnImplementedBucketResources(req *http.Request) bool { |
|
|
|
|
q := req.URL.Query() |
|
|
|
|
for name := range q { |
|
|
|
|
if unimplementedBucketResourceNames[name] { |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func ignoreUnImplementedObjectResources(req *http.Request) bool { |
|
|
|
|
q := req.URL.Query() |
|
|
|
|
for name := range q { |
|
|
|
|
if unimplementedObjectResourceNames[name] { |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func getContentType(req *http.Request) contentType { |
|
|
|
|
if _, ok := req.Header["Accept"]; ok { |
|
|
|
|
if req.Header["Accept"][0] == "application/json" { |
|
|
|
|
return jsonType |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return xmlType |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// takes a set of objects and prepares the objects for serialization
|
|
|
|
|
// input:
|
|
|
|
|
// bucket name
|
|
|
|
|