mirror of
https://github.com/ferdzo/fs.git
synced 2026-04-05 08:26:28 +00:00
Added ListObjectsV1
This commit is contained in:
222
api/api.go
222
api/api.go
@@ -298,6 +298,19 @@ func (h *Handler) handlePutObject(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ifNoneMatch := strings.TrimSpace(r.Header.Get("If-None-Match")); ifNoneMatch != "" {
|
||||||
|
manifest, err := h.svc.HeadObject(bucket, key)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, metadata.ErrObjectNotFound) {
|
||||||
|
writeMappedS3Error(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if ifNoneMatchPreconditionFailed(ifNoneMatch, manifest.ETag) {
|
||||||
|
writeS3Error(w, r, s3ErrPreconditionFailed, r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contentType := r.Header.Get("Content-Type")
|
contentType := r.Header.Get("Content-Type")
|
||||||
if contentType == "" {
|
if contentType == "" {
|
||||||
contentType = "application/octet-stream"
|
contentType = "application/octet-stream"
|
||||||
@@ -428,6 +441,25 @@ func decodeAWSChunkedPayload(src io.Reader, dst io.Writer) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ifNoneMatchPreconditionFailed(headerValue, etag string) bool {
|
||||||
|
for _, rawToken := range strings.Split(headerValue, ",") {
|
||||||
|
token := strings.TrimSpace(rawToken)
|
||||||
|
if token == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if token == "*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
token = strings.TrimPrefix(token, "W/")
|
||||||
|
token = strings.Trim(token, `"`)
|
||||||
|
if strings.EqualFold(token, etag) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) handlePutBucket(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handlePutBucket(w http.ResponseWriter, r *http.Request) {
|
||||||
bucket := chi.URLParam(r, "bucket")
|
bucket := chi.URLParam(r, "bucket")
|
||||||
if err := h.svc.CreateBucket(bucket); err != nil {
|
if err := h.svc.CreateBucket(bucket); err != nil {
|
||||||
@@ -670,26 +702,196 @@ func (h *Handler) handleGetBuckets(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (h *Handler) handleGetBucket(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleGetBucket(w http.ResponseWriter, r *http.Request) {
|
||||||
bucket := chi.URLParam(r, "bucket")
|
bucket := chi.URLParam(r, "bucket")
|
||||||
|
query := r.URL.Query()
|
||||||
|
|
||||||
if r.URL.Query().Get("list-type") == "2" {
|
if query.Has("location") {
|
||||||
h.handleListObjectsV2(w, r, bucket)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.URL.Query().Has("location") {
|
|
||||||
xmlResponse := `<?xml version="1.0" encoding="UTF-8"?>
|
xmlResponse := `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">us-east-1</LocationConstraint>`
|
<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">us-east-1</LocationConstraint>`
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(xmlResponse)))
|
w.Header().Set("Content-Length", strconv.Itoa(len(xmlResponse)))
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, err := w.Write([]byte(xmlResponse))
|
_, _ = w.Write([]byte(xmlResponse))
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeS3Error(w, r, s3ErrNotImplemented, r.URL.Path)
|
|
||||||
|
|
||||||
|
listType := strings.TrimSpace(query.Get("list-type"))
|
||||||
|
if listType == "2" {
|
||||||
|
h.handleListObjectsV2(w, r, bucket)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if listType != "" {
|
||||||
|
writeS3Error(w, r, s3ErrInvalidArgument, r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldUseListObjectsV1(query) {
|
||||||
|
h.handleListObjectsV1(w, r, bucket)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeS3Error(w, r, s3ErrNotImplemented, r.URL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldUseListObjectsV1(query url.Values) bool {
|
||||||
|
if len(query) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
listingParams := map[string]struct{}{
|
||||||
|
"delimiter": {},
|
||||||
|
"encoding-type": {},
|
||||||
|
"marker": {},
|
||||||
|
"max-keys": {},
|
||||||
|
"prefix": {},
|
||||||
|
}
|
||||||
|
for key := range query {
|
||||||
|
if _, ok := listingParams[key]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleListObjectsV1(w http.ResponseWriter, r *http.Request, bucket string) {
|
||||||
|
prefix := r.URL.Query().Get("prefix")
|
||||||
|
delimiter := r.URL.Query().Get("delimiter")
|
||||||
|
marker := r.URL.Query().Get("marker")
|
||||||
|
encodingType := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("encoding-type")))
|
||||||
|
if encodingType != "" && encodingType != "url" {
|
||||||
|
writeS3Error(w, r, s3ErrInvalidArgument, r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
maxKeys := 1000
|
||||||
|
if rawMaxKeys := strings.TrimSpace(r.URL.Query().Get("max-keys")); rawMaxKeys != "" {
|
||||||
|
parsed, err := strconv.Atoi(rawMaxKeys)
|
||||||
|
if err != nil || parsed < 0 {
|
||||||
|
writeS3Error(w, r, s3ErrInvalidArgument, r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if parsed > 1000 {
|
||||||
|
parsed = 1000
|
||||||
|
}
|
||||||
|
maxKeys = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
result := models.ListBucketResultV1{
|
||||||
|
Xmlns: "http://s3.amazonaws.com/doc/2006-03-01/",
|
||||||
|
Name: bucket,
|
||||||
|
Prefix: s3EncodeIfNeeded(prefix, encodingType),
|
||||||
|
Marker: s3EncodeIfNeeded(marker, encodingType),
|
||||||
|
Delimiter: s3EncodeIfNeeded(delimiter, encodingType),
|
||||||
|
MaxKeys: maxKeys,
|
||||||
|
EncodingType: encodingType,
|
||||||
|
}
|
||||||
|
|
||||||
|
type pageEntry struct {
|
||||||
|
Object *models.ObjectManifest
|
||||||
|
CommonPrefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := make([]pageEntry, 0, maxKeys)
|
||||||
|
seenCommonPrefixes := make(map[string]struct{})
|
||||||
|
truncated := false
|
||||||
|
stopErr := errors.New("list_v1_page_complete")
|
||||||
|
|
||||||
|
startKey := prefix
|
||||||
|
if marker != "" && marker > startKey {
|
||||||
|
startKey = marker
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxKeys > 0 {
|
||||||
|
err := h.svc.ForEachObjectFrom(bucket, startKey, func(object *models.ObjectManifest) error {
|
||||||
|
if object == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
key := object.Key
|
||||||
|
|
||||||
|
if prefix != "" {
|
||||||
|
if key < prefix {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(key, prefix) {
|
||||||
|
return stopErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if marker != "" && key <= marker {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if delimiter != "" {
|
||||||
|
relative := strings.TrimPrefix(key, prefix)
|
||||||
|
if idx := strings.Index(relative, delimiter); idx >= 0 {
|
||||||
|
commonPrefix := prefix + relative[:idx+len(delimiter)]
|
||||||
|
if marker != "" && commonPrefix <= marker {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, exists := seenCommonPrefixes[commonPrefix]; exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seenCommonPrefixes[commonPrefix] = struct{}{}
|
||||||
|
if len(entries) >= maxKeys {
|
||||||
|
truncated = true
|
||||||
|
return stopErr
|
||||||
|
}
|
||||||
|
entries = append(entries, pageEntry{
|
||||||
|
CommonPrefix: commonPrefix,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entries) >= maxKeys {
|
||||||
|
truncated = true
|
||||||
|
return stopErr
|
||||||
|
}
|
||||||
|
entries = append(entries, pageEntry{Object: object})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil && !errors.Is(err, stopErr) {
|
||||||
|
writeMappedS3Error(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Object != nil {
|
||||||
|
result.Contents = append(result.Contents, models.Contents{
|
||||||
|
Key: s3EncodeIfNeeded(entry.Object.Key, encodingType),
|
||||||
|
LastModified: time.Unix(entry.Object.CreatedAt, 0).UTC().Format("2006-01-02T15:04:05.000Z"),
|
||||||
|
ETag: `"` + entry.Object.ETag + `"`,
|
||||||
|
Size: entry.Object.Size,
|
||||||
|
StorageClass: "STANDARD",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
result.CommonPrefixes = append(result.CommonPrefixes, models.CommonPrefixes{
|
||||||
|
Prefix: s3EncodeIfNeeded(entry.CommonPrefix, encodingType),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.IsTruncated = truncated
|
||||||
|
if result.IsTruncated && result.NextMarker == "" && len(entries) > 0 {
|
||||||
|
last := entries[len(entries)-1]
|
||||||
|
if last.Object != nil {
|
||||||
|
result.NextMarker = s3EncodeIfNeeded(last.Object.Key, encodingType)
|
||||||
|
} else {
|
||||||
|
result.NextMarker = s3EncodeIfNeeded(last.CommonPrefix, encodingType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlResponse, err := xml.MarshalIndent(result, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
writeMappedS3Error(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(len(xml.Header)+len(xmlResponse)))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte(xml.Header))
|
||||||
|
_, _ = w.Write(xmlResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleListObjectsV2(w http.ResponseWriter, r *http.Request, bucket string) {
|
func (h *Handler) handleListObjectsV2(w http.ResponseWriter, r *http.Request, bucket string) {
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ var (
|
|||||||
Code: "InvalidRange",
|
Code: "InvalidRange",
|
||||||
Message: "The requested range is not satisfiable.",
|
Message: "The requested range is not satisfiable.",
|
||||||
}
|
}
|
||||||
|
s3ErrPreconditionFailed = s3APIError{
|
||||||
|
Status: http.StatusPreconditionFailed,
|
||||||
|
Code: "PreconditionFailed",
|
||||||
|
Message: "At least one of the pre-conditions you specified did not hold.",
|
||||||
|
}
|
||||||
s3ErrEntityTooSmall = s3APIError{
|
s3ErrEntityTooSmall = s3APIError{
|
||||||
Status: http.StatusBadRequest,
|
Status: http.StatusBadRequest,
|
||||||
Code: "EntityTooSmall",
|
Code: "EntityTooSmall",
|
||||||
|
|||||||
@@ -68,6 +68,23 @@ type ListBucketResult struct {
|
|||||||
CommonPrefixes []CommonPrefixes `xml:"CommonPrefixes,omitempty"`
|
CommonPrefixes []CommonPrefixes `xml:"CommonPrefixes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ListBucketResultV1 struct {
|
||||||
|
XMLName xml.Name `xml:"ListBucketResult"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
|
||||||
|
Name string `xml:"Name"`
|
||||||
|
Prefix string `xml:"Prefix"`
|
||||||
|
Marker string `xml:"Marker,omitempty"`
|
||||||
|
NextMarker string `xml:"NextMarker,omitempty"`
|
||||||
|
Delimiter string `xml:"Delimiter,omitempty"`
|
||||||
|
MaxKeys int `xml:"MaxKeys"`
|
||||||
|
IsTruncated bool `xml:"IsTruncated"`
|
||||||
|
EncodingType string `xml:"EncodingType,omitempty"`
|
||||||
|
|
||||||
|
Contents []Contents `xml:"Contents,omitempty"`
|
||||||
|
CommonPrefixes []CommonPrefixes `xml:"CommonPrefixes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type ListBucketResultV2 struct {
|
type ListBucketResultV2 struct {
|
||||||
XMLName xml.Name `xml:"ListBucketResult"`
|
XMLName xml.Name `xml:"ListBucketResult"`
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
|||||||
Reference in New Issue
Block a user