func (g *GoFakeS3) deleteMulti(bucket string, w http.ResponseWriter, r *http.Request) error {
	g.log.Print(LogInfo, "delete multi", bucket)
	var in DeleteRequest
	defer r.Body.Close()
	dc := xml.NewDecoder(r.Body)
	if err := dc.Decode(&in); err != nil {
		return ErrorMessage(ErrMalformedXML, err.Error())
	}
	keys := make([]string, len(in.Objects))
	for i, o := range in.Objects {
		keys[i] = o.Key
	}
	out, err := g.storage.DeleteMulti(bucket, keys...)
	if err != nil {
		return err
	}
	if in.Quiet {
		out.Deleted = nil
	}
	return g.xmlEncoder(w).Encode(out)
}
deleteMulti deletes multiple S3 objects from the bucket.
https://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
package gofakes3
import (
	"bytes"
	"encoding/base64"
	"encoding/hex"
	"encoding/xml"
	"fmt"
	"io"
	"io/ioutil"
	"math"
	"net/http"
	"net/textproto"
	"net/url"
	"strconv"
	"strings"
	"sync/atomic"
	"time"
)
type GoFakeS3 struct {
	storage   Backend
	versioned VersionedBackend
	timeSource              TimeSource
	timeSkew                time.Duration
	metadataSizeLimit       int
	integrityCheck          bool
	failOnUnimplementedPage bool
	hostBucket              bool
	uploader                *uploader
	requestID               uint64
	log                     Logger
}
func New(backend Backend, options ...Option) *GoFakeS3 {
	s3 := &GoFakeS3{
		storage:           backend,
		timeSkew:          DefaultSkewLimit,
		metadataSizeLimit: DefaultMetadataSizeLimit,
		integrityCheck:    true,
		uploader:          newUploader(),
		requestID:         0,
	}
	s3.versioned, _ = backend.(VersionedBackend)
	for _, opt := range options {
		opt(s3)
	}
	if s3.log == nil {
		s3.log = DiscardLog()
	}
	if s3.timeSource == nil {
		s3.timeSource = DefaultTimeSource()
	}
	return s3
}
func (g *GoFakeS3) nextRequestID() uint64 {
	return atomic.AddUint64(&g.requestID, 1)
}
func (g *GoFakeS3) Server() http.Handler {
	var handler http.Handler = &withCORS{r: http.HandlerFunc(g.routeBase), log: g.log}
	if g.timeSkew != 0 {
		handler = g.timeSkewMiddleware(handler)
	}
	if g.hostBucket {
		handler = g.hostBucketMiddleware(handler)
	}
	return handler
}
func (g *GoFakeS3) timeSkewMiddleware(handler http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, rq *http.Request) {
		timeHdr := rq.Header.Get("x-amz-date")
		if timeHdr != "" {
			rqTime, _ := time.Parse("20060102T150405Z", timeHdr)
			at := g.timeSource.Now()
			skew := at.Sub(rqTime)
			if skew < -g.timeSkew || skew > g.timeSkew {
				g.httpError(w, rq, requestTimeTooSkewed(at, g.timeSkew))
				return
			}
		}
		handler.ServeHTTP(w, rq)
	})
}
func (g *GoFakeS3) hostBucketMiddleware(handler http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, rq *http.Request) {
		parts := strings.SplitN(rq.Host, ".", 2)
		bucket := parts[0]
		p := rq.URL.Path
		rq.URL.Path = "/" + bucket
		if p != "/" {
			rq.URL.Path += p
		}
		g.log.Print(LogInfo, p, "=>", rq.URL)
		handler.ServeHTTP(w, rq)
	})
}
func (g *GoFakeS3) httpError(w http.ResponseWriter, r *http.Request, err error) {
	resp := ensureErrorResponse(err, "")  
	if resp.ErrorCode() == ErrInternal {
		g.log.Print(LogErr, err)
	}
	w.WriteHeader(resp.ErrorCode().Status())
	if r.Method != http.MethodHead {
		if err := g.xmlEncoder(w).Encode(resp); err != nil {
			g.log.Print(LogErr, err)
			return
		}
	}
}
func (g *GoFakeS3) listBuckets(w http.ResponseWriter, r *http.Request) error {
	buckets, err := g.storage.ListBuckets()
	if err != nil {
		return err
	}
	s := &Storage{
		Xmlns:   "http://s3.amazonaws.com/doc/2006-03-01/",
		Buckets: buckets,
		Owner: &UserInfo{
			ID:          "fe7272ea58be830e56fe1663b10fafef",
			DisplayName: "GoFakeS3",
		},
	}
	return g.xmlEncoder(w).Encode(s)
}
func (g *GoFakeS3) listBucket(bucketName string, w http.ResponseWriter, r *http.Request) error {
	g.log.Print(LogInfo, "LIST BUCKET")
	q := r.URL.Query()
	prefix := prefixFromQuery(q)
	page, err := listBucketPageFromQuery(q)
	if err != nil {
		return err
	}
	isVersion2 := q.Get("list-type") == "2"
	g.log.Print(LogInfo, "bucketname:", bucketName)
	g.log.Print(LogInfo, "prefix    :", prefix)
	g.log.Print(LogInfo, "page      :", fmt.Sprintf("%+v", page))
	objects, err := g.storage.ListBucket(bucketName, &prefix, page)
	if err != nil {
		if err == ErrInternalPageNotImplemented && !g.failOnUnimplementedPage {
			objects, err = g.storage.ListBucket(bucketName, &prefix, ListBucketPage{})
			if err != nil {
				return err
			}
		} else if err == ErrInternalPageNotImplemented && g.failOnUnimplementedPage {
			return ErrNotImplemented
		} else {
			return err
		}
	}
	base := ListBucketResultBase{
		Xmlns:          "http://s3.amazonaws.com/doc/2006-03-01/",
		Name:           bucketName,
		CommonPrefixes: objects.CommonPrefixes,
		Contents:       objects.Contents,
		IsTruncated:    objects.IsTruncated,
		Delimiter:      prefix.Delimiter,
		Prefix:         prefix.Prefix,
		MaxKeys:        page.MaxKeys,
	}
	if !isVersion2 {
		var result = &ListBucketResult{
			ListBucketResultBase: base,
			Marker:               page.Marker,
		}
		if base.Delimiter != "" {
			result.NextMarker = objects.NextMarker
		}
		return g.xmlEncoder(w).Encode(result)
	} else {
		var result = &ListBucketResultV2{
			ListBucketResultBase: base,
			KeyCount:             int64(len(objects.CommonPrefixes) + len(objects.Contents)),
			StartAfter:           q.Get("start-after"),
			ContinuationToken:    q.Get("continuation-token"),
		}
		if objects.NextMarker != "" {
			result.NextContinuationToken = base64.URLEncoding.EncodeToString([]byte(objects.NextMarker))
		}
		if _, ok := q["fetch-owner"]; !ok {
			for _, v := range result.Contents {
				v.Owner = nil
			}
		}
		return g.xmlEncoder(w).Encode(result)
	}
}
func (g *GoFakeS3) getBucketLocation(bucketName string, w http.ResponseWriter, r *http.Request) error {
	g.log.Print(LogInfo, "GET BUCKET LOCATION")
	result := GetBucketLocation{
		Xmlns:              "http://s3.amazonaws.com/doc/2006-03-01/",
		LocationConstraint: "",
	}
	return g.xmlEncoder(w).Encode(result)
}
func (g *GoFakeS3) listBucketVersions(bucketName string, w http.ResponseWriter, r *http.Request) error {
	if g.versioned == nil {
		return ErrNotImplemented
	}
	q := r.URL.Query()
	prefix := prefixFromQuery(q)
	page, err := listBucketVersionsPageFromQuery(q)
	if err != nil {
		return err
	}
	if page.HasVersionIDMarker {
		if page.VersionIDMarker == "" {
			return ErrorInvalidArgument("version-id-marker", "", "A version-id marker cannot be empty.")
		} else if !page.HasKeyMarker {
			return ErrorInvalidArgument("version-id-marker", "", "A version-id marker cannot be specified without a key marker.")
		}
	} else if page.HasKeyMarker && page.KeyMarker == "" {
		page = ListBucketVersionsPage{}
	}
	bucket, err := g.versioned.ListBucketVersions(bucketName, &prefix, &page)
	if err != nil {
		return err
	}
	for _, ver := range bucket.Versions {
		if ver.GetVersionID() == "" {
			ver.setVersionID("null")
		}
	}
	return g.xmlEncoder(w).Encode(bucket)
}
func (g *GoFakeS3) createBucket(bucket string, w http.ResponseWriter, r *http.Request) error {
	g.log.Print(LogInfo, "CREATE BUCKET:", bucket)
	if err := ValidateBucketName(bucket); err != nil {
		return err
	}
	if err := g.storage.CreateBucket(bucket); err != nil {
		return err
	}
	w.Header().Set("Location", "/"+bucket)
	w.Write([]byte{})
	return nil
}
func (g *GoFakeS3) deleteBucket(bucket string, w http.ResponseWriter, r *http.Request) error {
	g.log.Print(LogInfo, "DELETE BUCKET:", bucket)
	if err := g.storage.DeleteBucket(bucket); err != nil {
		return err
	}
	w.WriteHeader(http.StatusNoContent)
	return nil
}
func (g *GoFakeS3) headBucket(bucket string, w http.ResponseWriter, r *http.Request) error {
	g.log.Print(LogInfo, "HEAD BUCKET", bucket)
	g.log.Print(LogInfo, "bucketname:", bucket)
	if err := g.ensureBucketExists(bucket); err != nil {
		return err
	}
	w.Write([]byte{})
	return nil
}
func (g *GoFakeS3) getObject(
	bucket, object string,
	versionID VersionID,
	w http.ResponseWriter,
	r *http.Request,
) error {
	g.log.Print(LogInfo, "GET OBJECT")
	g.log.Print(LogInfo, "Bucket:", bucket)
	g.log.Print(LogInfo, "└── Object:", object)
	rnge, err := parseRangeHeader(r.Header.Get("Range"))
	if err != nil {
		return err
	}
	var obj *Object
	{  
		if versionID == "" {
			obj, err = g.storage.GetObject(bucket, object, rnge)
			if err != nil {
				return err
			}
		} else {
			if g.versioned == nil {
				return ErrNotImplemented
			}
			obj, err = g.versioned.GetObjectVersion(bucket, object, versionID, rnge)
			if err != nil {
				return err
			}
		}
	}
	if obj == nil {
		g.log.Print(LogErr, "unexpected nil object for key", bucket, object)
		return ErrInternal
	}
	defer obj.Contents.Close()
	if err := g.writeGetOrHeadObjectResponse(obj, w, r); err != nil {
		return err
	}
	obj.Range.writeHeader(obj.Size, w)
	if _, err := io.Copy(w, obj.Contents); err != nil {
		return err
	}
	return nil
}
func (g *GoFakeS3) writeGetOrHeadObjectResponse(obj *Object, w http.ResponseWriter, r *http.Request) error {
	if obj.IsDeleteMarker {
		w.Header().Set("x-amz-version-id", string(obj.VersionID))
		w.Header().Set("x-amz-delete-marker", "true")
		return KeyNotFound(obj.Name)
	}
	for mk, mv := range obj.Metadata {
		w.Header().Set(mk, mv)
	}
	w.Header().Set("Accept-Ranges", "bytes")
	w.Header().Set("ETag", `"`+hex.EncodeToString(obj.Hash)+`"`)
	if obj.VersionID != "" {
		w.Header().Set("x-amz-version-id", string(obj.VersionID))
	}
	return nil
}
func (g *GoFakeS3) headObject(
	bucket, object string,
	versionID VersionID,
	w http.ResponseWriter,
	r *http.Request,
) error {
	g.log.Print(LogInfo, "HEAD OBJECT")
	g.log.Print(LogInfo, "Bucket:", bucket)
	g.log.Print(LogInfo, "└── Object:", object)
	obj, err := g.storage.HeadObject(bucket, object)
	if err != nil {
		return err
	}
	if obj == nil {
		g.log.Print(LogErr, "unexpected nil object for key", bucket, object)
		return ErrInternal
	}
	defer obj.Contents.Close()
	if err := g.writeGetOrHeadObjectResponse(obj, w, r); err != nil {
		return err
	}
	w.Header().Set("Content-Length", fmt.Sprintf("%d", obj.Size))
	return nil
}
func (g *GoFakeS3) createObjectBrowserUpload(bucket string, w http.ResponseWriter, r *http.Request) error {
	g.log.Print(LogInfo, "CREATE OBJECT THROUGH BROWSER UPLOAD")
	const _24MB = (1 << 20) * 24  
	if err := r.ParseMultipartForm(_24MB); nil != err {
		return ErrMalformedPOSTRequest
	}
	keyValues := r.MultipartForm.Value["key"]
	if len(keyValues) != 1 {
		return ErrIncorrectNumberOfFilesInPostRequest
	}
	key := keyValues[0]
	g.log.Print(LogInfo, "(BUC)", bucket)
	g.log.Print(LogInfo, "(KEY)", key)
	fileValues := r.MultipartForm.File["file"]
	if len(fileValues) != 1 {
		return ErrIncorrectNumberOfFilesInPostRequest
	}
	fileHeader := fileValues[0]
	infile, err := fileHeader.Open()
	if err != nil {
		return err
	}
	defer infile.Close()
	meta, err := metadataHeaders(r.MultipartForm.Value, g.timeSource.Now(), g.metadataSizeLimit)
	if err != nil {
		return err
	}
	if len(key) > KeySizeLimit {
		return ResourceError(ErrKeyTooLong, key)
	}
	rdr, err := newHashingReader(infile, "")
	if err != nil {
		return err
	}
	result, err := g.storage.PutObject(bucket, key, meta, rdr, fileHeader.Size)
	if err != nil {
		return err
	}
	if result.VersionID != "" {
		w.Header().Set("x-amz-version-id", string(result.VersionID))
	}
	w.Header().Set("ETag", `"`+hex.EncodeToString(rdr.Sum(nil))+`"`)
	return nil
}
func (g *GoFakeS3) createObject(bucket, object string, w http.ResponseWriter, r *http.Request) (err error) {
	g.log.Print(LogInfo, "CREATE OBJECT:", bucket, object)
	meta, err := metadataHeaders(r.Header, g.timeSource.Now(), g.metadataSizeLimit)
	if err != nil {
		return err
	}
	var (
		size int64
		body = r.Body
	)
	copySource := r.Header.Get(copySourceHeader)
	if copySource != "" {
		parts := strings.SplitN(copySource, "/", 2)
		srcBucket := parts[0]
		srcKey := parts[1]
		srcKey, err = url.QueryUnescape(srcKey)
		if err != nil {
			return err
		}
		src, err := g.storage.GetObject(srcBucket, srcKey, nil)
		if err != nil {
			return err
		}
		size = src.Size
		body = src.Contents
	} else {
		var err error
		size, err = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64)
		if err != nil || size <= 0 {
			return ErrMissingContentLength
		}
	}
	if len(object) > KeySizeLimit {
		return ResourceError(ErrKeyTooLong, object)
	}
	var md5Base64 string
	if g.integrityCheck {
		md5Base64 = r.Header.Get("Content-MD5")
		if _, ok := r.Header[textproto.CanonicalMIMEHeaderKey("Content-MD5")]; ok && md5Base64 == "" {
			return ErrInvalidDigest  
		}
	}
	rdr, err := newHashingReader(body, md5Base64)
	defer body.Close()
	if err != nil {
		return err
	}
	result, err := g.storage.PutObject(bucket, object, meta, rdr, size)
	if err != nil {
		return err
	}
	if copySource != "" {
		return g.xmlEncoder(w).Encode(CopyObjectResult{
			ETag:         `"` + hex.EncodeToString(rdr.Sum(nil)) + `"`,
			LastModified: NewContentTime(g.timeSource.Now()),
		})
	}
	if result.VersionID != "" {
		g.log.Print(LogInfo, "CREATED VERSION:", bucket, object, result.VersionID)
		w.Header().Set("x-amz-version-id", string(result.VersionID))
	}
	w.Header().Set("ETag", `"`+hex.EncodeToString(rdr.Sum(nil))+`"`)
	return nil
}
func (g *GoFakeS3) copyObject(bucket, object string, meta map[string]string, w http.ResponseWriter, r *http.Request) (err error) {
	source := meta["X-Amz-Copy-Source"]
	g.log.Print(LogInfo, "└── COPY:", source)
	if len(object) > KeySizeLimit {
		return ResourceError(ErrKeyTooLong, object)
	}
	parts := strings.SplitN(strings.TrimPrefix(source, "/"), "/", 2)
	srcBucket := parts[0]
	srcKey := strings.SplitN(parts[1], "?", 2)[0]
	srcKey, err = url.QueryUnescape(srcKey)
	if err != nil {
		return err
	}
	srcObj, err := g.storage.GetObject(srcBucket, srcKey, nil)
	if err != nil {
		return err
	}
	if srcObj == nil {
		g.log.Print(LogErr, "unexpected nil object for key", bucket, object)
		return ErrInternal
	}
	defer srcObj.Contents.Close()
	for k, v := range srcObj.Metadata {
		if _, found := meta[k]; !found && k != "X-Amz-Acl" {
			meta[k] = v
		}
	}
	result, err := g.storage.PutObject(bucket, object, meta, srcObj.Contents, srcObj.Size)
	if err != nil {
		return err
	}
	if srcObj.VersionID != "" {
		w.Header().Set("x-amz-copy-source-version-id", string(srcObj.VersionID))
	}
	if result.VersionID != "" {
		g.log.Print(LogInfo, "CREATED VERSION:", bucket, object, result.VersionID)
		w.Header().Set("x-amz-version-id", string(result.VersionID))
	}
	return g.xmlEncoder(w).Encode(CopyObjectResult{
		ETag:         `"` + hex.EncodeToString(srcObj.Hash) + `"`,
		LastModified: NewContentTime(g.timeSource.Now()),
	})
}
func (g *GoFakeS3) deleteObject(bucket, object string, w http.ResponseWriter, r *http.Request) error {
	g.log.Print(LogInfo, "DELETE:", bucket, object)
	result, err := g.storage.DeleteObject(bucket, object)
	if err != nil {
		return err
	}
	if result.IsDeleteMarker {
		w.Header().Set("x-amz-delete-marker", "true")
	} else {
		w.Header().Set("x-amz-delete-marker", "false")
	}
	if result.VersionID != "" {
		w.Header().Set("x-amz-version-id", string(result.VersionID))
	}
	w.WriteHeader(http.StatusNoContent)
	return nil
}
func (g *GoFakeS3) deleteObjectVersion(bucket, object string, version VersionID, w http.ResponseWriter, r *http.Request) error {
	if g.versioned == nil {
		return ErrNotImplemented
	}
	g.log.Print(LogInfo, "DELETE VERSION:", bucket, object, version)
	result, err := g.versioned.DeleteObjectVersion(bucket, object, version)
	if err != nil {
		return err
	}
	g.log.Print(LogInfo, "DELETED VERSION:", bucket, object, version)
	if result.IsDeleteMarker {
		w.Header().Set("x-amz-delete-marker", "true")
	} else {
		w.Header().Set("x-amz-delete-marker", "false")
	}
	if result.VersionID != "" {
		w.Header().Set("x-amz-version-id", string(result.VersionID))
	}
	w.WriteHeader(http.StatusNoContent)
	return nil
}