feat: complete API-driven projects with infrastructure and business acceptance
This commit is contained in:
161
internal/storagefixture/multipart.go
Normal file
161
internal/storagefixture/multipart.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package storagefixture
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MultipartHandler checks protocol sequencing, not provider authentication.
|
||||
// The fixture is memory bounded and should only be mounted on httptest servers.
|
||||
func MultipartHandler() http.Handler {
|
||||
type part struct {
|
||||
Number int `xml:"PartNumber"`
|
||||
ETag string `xml:"ETag"`
|
||||
Size int64 `xml:"Size"`
|
||||
data []byte
|
||||
}
|
||||
type upload struct {
|
||||
key, contentType string
|
||||
parts map[int]part
|
||||
}
|
||||
var mu sync.Mutex
|
||||
uploads := map[string]*upload{}
|
||||
objects := map[string]value{}
|
||||
sequence := 0
|
||||
simple := Handler()
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
id := q.Get("uploadId")
|
||||
if _, start := q["uploads"]; !start && id == "" {
|
||||
mu.Lock()
|
||||
v, ok := objects[r.URL.Path]
|
||||
if ok && r.Method == "DELETE" {
|
||||
delete(objects, r.URL.Path)
|
||||
mu.Unlock()
|
||||
w.WriteHeader(204)
|
||||
return
|
||||
}
|
||||
mu.Unlock()
|
||||
if ok && (r.Method == "GET" || r.Method == "HEAD") {
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(v.data)))
|
||||
w.Header().Set("Content-Type", v.contentType)
|
||||
w.Header().Set("ETag", v.etag)
|
||||
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
|
||||
if r.Method == "GET" {
|
||||
w.Write(v.data)
|
||||
}
|
||||
return
|
||||
}
|
||||
simple.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
fail := func(code string, status int) {
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.WriteHeader(status)
|
||||
fmt.Fprintf(w, "<Error><Code>%s</Code><Message>fixture rejection</Message></Error>", code)
|
||||
}
|
||||
if r.Header.Get("Authorization") == "" && q.Get("X-Amz-Signature") == "" && q.Get("x-oss-signature") == "" {
|
||||
fail("AccessDenied", 403)
|
||||
return
|
||||
}
|
||||
if _, ok := q["uploads"]; ok && r.Method == "POST" {
|
||||
if len(uploads) >= 8 {
|
||||
fail("TooManyUploads", 503)
|
||||
return
|
||||
}
|
||||
sequence++
|
||||
id = strconv.Itoa(sequence)
|
||||
uploads[id] = &upload{key: r.URL.Path, contentType: r.Header.Get("Content-Type"), parts: map[int]part{}}
|
||||
fmt.Fprintf(w, "<InitiateMultipartUploadResult><Bucket>testbucket</Bucket><Key>file</Key><UploadId>%s</UploadId></InitiateMultipartUploadResult>", id)
|
||||
return
|
||||
}
|
||||
u, ok := uploads[id]
|
||||
if !ok || u.key != r.URL.Path {
|
||||
fail("NoSuchUpload", 404)
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case "PUT":
|
||||
number, _ := strconv.Atoi(q.Get("partNumber"))
|
||||
if number < 1 || number > 3 {
|
||||
fail("InvalidPart", 400)
|
||||
return
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(r.Body, (6<<20)+1))
|
||||
if err != nil || len(data) > 6<<20 {
|
||||
fail("EntityTooLarge", 413)
|
||||
return
|
||||
}
|
||||
p := part{Number: number, ETag: fmt.Sprintf("\"%x\"", md5.Sum(data)), Size: int64(len(data)), data: data}
|
||||
u.parts[number] = p
|
||||
w.Header().Set("ETag", p.ETag)
|
||||
case "GET":
|
||||
marker, _ := strconv.Atoi(q.Get("part-number-marker"))
|
||||
limit, _ := strconv.Atoi(q.Get("max-parts"))
|
||||
if limit < 1 {
|
||||
limit = 1000
|
||||
}
|
||||
numbers := []int{}
|
||||
for n := range u.parts {
|
||||
if n > marker {
|
||||
numbers = append(numbers, n)
|
||||
}
|
||||
}
|
||||
sort.Ints(numbers)
|
||||
var result struct {
|
||||
XMLName xml.Name `xml:"ListPartsResult"`
|
||||
Truncated bool `xml:"IsTruncated"`
|
||||
Next int `xml:"NextPartNumberMarker"`
|
||||
Parts []part `xml:"Part"`
|
||||
}
|
||||
if len(numbers) > limit {
|
||||
result.Truncated = true
|
||||
numbers = numbers[:limit]
|
||||
result.Next = numbers[len(numbers)-1]
|
||||
}
|
||||
for _, n := range numbers {
|
||||
result.Parts = append(result.Parts, u.parts[n])
|
||||
}
|
||||
xml.NewEncoder(w).Encode(result)
|
||||
case "POST":
|
||||
var request struct {
|
||||
Parts []part `xml:"Part"`
|
||||
}
|
||||
if xml.NewDecoder(io.LimitReader(r.Body, 65537)).Decode(&request) != nil || len(request.Parts) == 0 {
|
||||
fail("InvalidPart", 400)
|
||||
return
|
||||
}
|
||||
var data []byte
|
||||
for _, p := range request.Parts {
|
||||
actual, ok := u.parts[p.Number]
|
||||
if !ok || strings.Trim(p.ETag, "\"") != strings.Trim(actual.ETag, "\"") {
|
||||
fail("InvalidPart", 400)
|
||||
return
|
||||
}
|
||||
if len(data)+len(actual.data) > 7<<20 {
|
||||
fail("EntityTooLarge", 413)
|
||||
return
|
||||
}
|
||||
data = append(data, actual.data...)
|
||||
}
|
||||
objects[u.key] = value{data: data, contentType: u.contentType, etag: fmt.Sprintf("\"%x\"", md5.Sum(data))}
|
||||
delete(uploads, id)
|
||||
fmt.Fprint(w, "<CompleteMultipartUploadResult><Bucket>testbucket</Bucket><Key>file</Key><ETag>\"completed\"</ETag></CompleteMultipartUploadResult>")
|
||||
case "DELETE":
|
||||
delete(uploads, id)
|
||||
w.WriteHeader(204)
|
||||
default:
|
||||
fail("InvalidRequest", 400)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user