mirror of
https://github.com/pgsty/minio.git
synced 2026-11-12 10:35:33 +03:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9acdeab73d | |||
| d19b434ffc | |||
| 17a1eda702 | |||
| 7d50a0cfea | |||
| ceff7bcca5 | |||
| 7d1734d033 | |||
| 03ec6adfd0 | |||
| 309b10f201 | |||
| 2a8e40f19f | |||
| 5f7bd2b1da | |||
| c097ce9c32 | |||
| caad314faa | |||
| bc2ebe0021 | |||
| 21e8440423 | |||
| 11aa393ba7 | |||
| d0c910a6f3 | |||
| 81c90ae430 | |||
| d15a5ad4cc | |||
| 0ff246653b | |||
| 113bcbdb78 | |||
| 95411228db | |||
| 23774353b7 | |||
| 052b5262ff | |||
| a2a5ec93d3 | |||
| 331c517a5b | |||
| 27a774cbe9 | |||
| 8e6787a302 | |||
| 59352d0ac2 | |||
| 75d44b3bae | |||
| 95ae6c4b49 | |||
| 98ca770f81 | |||
| ccd967e3be | |||
| 0ebb73ee2e | |||
| c8b84a0e9e | |||
| 3acb5cff45 | |||
| ab801ad3d4 | |||
| 74116204ce | |||
| 2eb5f934d8 | |||
| b43d376a87 | |||
| e4a44f6224 | |||
| 0272973175 | |||
| adca28801d | |||
| d2a3f92452 | |||
| ede86845e5 | |||
| e57c742674 | |||
| bb5976d727 | |||
| 670724184c | |||
| f7c1a59de1 | |||
| 01a2ccc52f | |||
| 51ba1dac49 | |||
| a4463dd40f | |||
| 83a82d818e | |||
| 1d1c4430b2 | |||
| 4e00b47b52 | |||
| 43e6d1ce2d | |||
| 30da442a85 | |||
| 038d91feaa | |||
| e7ba78beee | |||
| ab43804efd | |||
| 1c865dd119 |
@@ -4,7 +4,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- release
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -12,7 +11,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.14.x]
|
go-version: [1.14.x, 1.15.x]
|
||||||
os: [ubuntu-latest, windows-latest]
|
os: [ubuntu-latest, windows-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -39,6 +38,9 @@ jobs:
|
|||||||
MINIO_CI_CD: 1
|
MINIO_CI_CD: 1
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install devscripts shellcheck
|
sudo apt-get install devscripts shellcheck
|
||||||
|
nancy_version=$(curl --retry 10 -Ls -o /dev/null -w "%{url_effective}" https://github.com/sonatype-nexus-community/nancy/releases/latest | sed "s/https:\/\/github.com\/sonatype-nexus-community\/nancy\/releases\/tag\///")
|
||||||
|
curl -L -o nancy https://github.com/sonatype-nexus-community/nancy/releases/download/${nancy_version}/nancy-linux.amd64-${nancy_version} && chmod +x nancy
|
||||||
|
go list -m all | ./nancy
|
||||||
make
|
make
|
||||||
diff -au <(gofmt -s -d cmd) <(printf "")
|
diff -au <(gofmt -s -d cmd) <(printf "")
|
||||||
diff -au <(gofmt -s -d pkg) <(printf "")
|
diff -au <(gofmt -s -d pkg) <(printf "")
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
CVE-2020-13223
|
||||||
|
CVE-2020-7220
|
||||||
|
CVE-2020-10661
|
||||||
|
CVE-2020-10660
|
||||||
|
CWE-190
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.13-alpine as builder
|
FROM golang:1.14-alpine as builder
|
||||||
|
|
||||||
LABEL maintainer="MinIO Inc <dev@min.io>"
|
LABEL maintainer="MinIO Inc <dev@min.io>"
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ checks:
|
|||||||
getdeps:
|
getdeps:
|
||||||
@mkdir -p ${GOPATH}/bin
|
@mkdir -p ${GOPATH}/bin
|
||||||
@which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.27.0)
|
@which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.27.0)
|
||||||
|
@which ruleguard 1>/dev/null || (echo "Installing ruleguard" && GO111MODULE=off go get github.com/quasilyte/go-ruleguard/...)
|
||||||
|
|
||||||
crosscompile:
|
crosscompile:
|
||||||
@(env bash $(PWD)/buildscripts/cross-compile.sh)
|
@(env bash $(PWD)/buildscripts/cross-compile.sh)
|
||||||
|
|
||||||
verifiers: getdeps fmt lint
|
verifiers: getdeps fmt lint ruleguard
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
@echo "Running $@ check"
|
@echo "Running $@ check"
|
||||||
@@ -33,6 +34,10 @@ lint:
|
|||||||
@GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean
|
@GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean
|
||||||
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
|
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
|
||||||
|
|
||||||
|
ruleguard:
|
||||||
|
@echo "Running $@ check"
|
||||||
|
@${GOPATH}/bin/ruleguard -rules ruleguard.rules.go github.com/minio/minio/...
|
||||||
|
|
||||||
# Builds minio, runs the verifiers then runs the tests.
|
# Builds minio, runs the verifiers then runs the tests.
|
||||||
check: test
|
check: test
|
||||||
test: verifiers build
|
test: verifiers build
|
||||||
|
|||||||
@@ -1270,10 +1270,9 @@ func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
deadlinedCtx, cancel := context.WithTimeout(ctx, deadline)
|
deadlinedCtx, cancel := context.WithTimeout(ctx, deadline)
|
||||||
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
nsLock := objectAPI.NewNSLock(deadlinedCtx, minioMetaBucket, "obd-in-progress")
|
nsLock := objectAPI.NewNSLock(ctx, minioMetaBucket, "obd-in-progress")
|
||||||
if err := nsLock.GetLock(newDynamicTimeout(deadline, deadline)); err != nil { // returns a locked lock
|
if err := nsLock.GetLock(newDynamicTimeout(deadline, deadline)); err != nil { // returns a locked lock
|
||||||
errResp(err)
|
errResp(err)
|
||||||
return
|
return
|
||||||
|
|||||||
+20
-11
@@ -502,13 +502,12 @@ func (h *healSequence) isQuitting() bool {
|
|||||||
// check if the heal sequence has ended
|
// check if the heal sequence has ended
|
||||||
func (h *healSequence) hasEnded() bool {
|
func (h *healSequence) hasEnded() bool {
|
||||||
h.mutex.RLock()
|
h.mutex.RLock()
|
||||||
|
defer h.mutex.RUnlock()
|
||||||
// background heal never ends
|
// background heal never ends
|
||||||
if h.clientToken == bgHealingUUID {
|
if h.clientToken == bgHealingUUID {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
ended := len(h.currentStatus.Items) == 0 || h.currentStatus.Summary == healStoppedStatus || h.currentStatus.Summary == healFinishedStatus
|
return len(h.currentStatus.Items) == 0 || h.currentStatus.Summary == healStoppedStatus || h.currentStatus.Summary == healFinishedStatus
|
||||||
h.mutex.RUnlock()
|
|
||||||
return ended
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// stops the heal sequence - safe to call multiple times.
|
// stops the heal sequence - safe to call multiple times.
|
||||||
@@ -655,9 +654,11 @@ func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItem
|
|||||||
if !h.reportProgress {
|
if !h.reportProgress {
|
||||||
// Object might have been deleted, by the time heal
|
// Object might have been deleted, by the time heal
|
||||||
// was attempted, we should ignore this object and
|
// was attempted, we should ignore this object and
|
||||||
// return success.
|
// return the error and not calculate this object
|
||||||
|
// as part of the metrics.
|
||||||
if isErrObjectNotFound(res.err) || isErrVersionNotFound(res.err) {
|
if isErrObjectNotFound(res.err) || isErrVersionNotFound(res.err) {
|
||||||
return nil
|
// Return the error so that caller can handle it.
|
||||||
|
return res.err
|
||||||
}
|
}
|
||||||
|
|
||||||
h.mutex.Lock()
|
h.mutex.Lock()
|
||||||
@@ -721,6 +722,8 @@ func (h *healSequence) healItemsFromSourceCh() error {
|
|||||||
if err := h.queueHealTask(source, itemType); err != nil {
|
if err := h.queueHealTask(source, itemType); err != nil {
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case ObjectExistsAsDirectory:
|
case ObjectExistsAsDirectory:
|
||||||
|
case ObjectNotFound:
|
||||||
|
case VersionNotFound:
|
||||||
default:
|
default:
|
||||||
logger.LogIf(h.ctx, fmt.Errorf("Heal attempt failed for %s: %w",
|
logger.LogIf(h.ctx, fmt.Errorf("Heal attempt failed for %s: %w",
|
||||||
pathJoin(source.bucket, source.object), err))
|
pathJoin(source.bucket, source.object), err))
|
||||||
@@ -794,17 +797,17 @@ func (h *healSequence) healMinioSysMeta(metaPrefix string) func() error {
|
|||||||
return errHealStopSignalled
|
return errHealStopSignalled
|
||||||
}
|
}
|
||||||
|
|
||||||
herr := h.queueHealTask(healSource{
|
err := h.queueHealTask(healSource{
|
||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
object: object,
|
object: object,
|
||||||
versionID: versionID,
|
versionID: versionID,
|
||||||
}, madmin.HealItemBucketMetadata)
|
}, madmin.HealItemBucketMetadata)
|
||||||
// Object might have been deleted, by the time heal
|
// Object might have been deleted, by the time heal
|
||||||
// was attempted we ignore this object an move on.
|
// was attempted we ignore this object an move on.
|
||||||
if isErrObjectNotFound(herr) || isErrVersionNotFound(herr) {
|
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return herr
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -876,9 +879,9 @@ func (h *healSequence) healBucket(bucket string, bucketsOnly bool) error {
|
|||||||
if h.object != "" {
|
if h.object != "" {
|
||||||
// Check if an object named as the objPrefix exists,
|
// Check if an object named as the objPrefix exists,
|
||||||
// and if so heal it.
|
// and if so heal it.
|
||||||
_, err := objectAPI.GetObjectInfo(h.ctx, bucket, h.object, ObjectOptions{})
|
oi, err := objectAPI.GetObjectInfo(h.ctx, bucket, h.object, ObjectOptions{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if err = h.healObject(bucket, h.object, ""); err != nil {
|
if err = h.healObject(bucket, h.object, oi.VersionID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -905,9 +908,15 @@ func (h *healSequence) healObject(bucket, object, versionID string) error {
|
|||||||
return errHealStopSignalled
|
return errHealStopSignalled
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.queueHealTask(healSource{
|
err := h.queueHealTask(healSource{
|
||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
object: object,
|
object: object,
|
||||||
versionID: versionID,
|
versionID: versionID,
|
||||||
}, madmin.HealItemObject)
|
}, madmin.HealItemObject)
|
||||||
|
// Object might have been deleted, by the time heal
|
||||||
|
// was attempted we ignore this object an move on.
|
||||||
|
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-20
@@ -663,20 +663,9 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "X-Amz-Date must be in the ISO8601 Long Format \"yyyyMMdd'T'HHmmss'Z'\"",
|
Description: "X-Amz-Date must be in the ISO8601 Long Format \"yyyyMMdd'T'HHmmss'Z'\"",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
|
|
||||||
// right Description: "Error parsing the X-Amz-Credential parameter; incorrect date format \"%s\". This date in the credential must be in the format \"yyyyMMdd\".",
|
|
||||||
// Need changes to make sure variable messages can be constructed.
|
|
||||||
ErrMalformedCredentialDate: {
|
ErrMalformedCredentialDate: {
|
||||||
Code: "AuthorizationQueryParametersError",
|
Code: "AuthorizationQueryParametersError",
|
||||||
Description: "Error parsing the X-Amz-Credential parameter; incorrect date format \"%s\". This date in the credential must be in the format \"yyyyMMdd\".",
|
Description: "Error parsing the X-Amz-Credential parameter; incorrect date format. This date in the credential must be in the format \"yyyyMMdd\".",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
|
|
||||||
// right Description: "Error parsing the X-Amz-Credential parameter; the region 'us-east-' is wrong; expecting 'us-east-1'".
|
|
||||||
// Need changes to make sure variable messages can be constructed.
|
|
||||||
ErrMalformedCredentialRegion: {
|
|
||||||
Code: "AuthorizationQueryParametersError",
|
|
||||||
Description: "Error parsing the X-Amz-Credential parameter; the region is wrong;",
|
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrInvalidRegion: {
|
ErrInvalidRegion: {
|
||||||
@@ -684,9 +673,6 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Region does not match.",
|
Description: "Region does not match.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
|
|
||||||
// right Description: "Error parsing the X-Amz-Credential parameter; incorrect service \"s4\". This endpoint belongs to \"s3\".".
|
|
||||||
// Need changes to make sure variable messages can be constructed.
|
|
||||||
ErrInvalidServiceS3: {
|
ErrInvalidServiceS3: {
|
||||||
Code: "AuthorizationParametersError",
|
Code: "AuthorizationParametersError",
|
||||||
Description: "Error parsing the Credential/X-Amz-Credential parameter; incorrect service. This endpoint belongs to \"s3\".",
|
Description: "Error parsing the Credential/X-Amz-Credential parameter; incorrect service. This endpoint belongs to \"s3\".",
|
||||||
@@ -697,9 +683,6 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Error parsing the Credential parameter; incorrect service. This endpoint belongs to \"sts\".",
|
Description: "Error parsing the Credential parameter; incorrect service. This endpoint belongs to \"sts\".",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
|
|
||||||
// Description: "Error parsing the X-Amz-Credential parameter; incorrect terminal "aws4_reque". This endpoint uses "aws4_request".
|
|
||||||
// Need changes to make sure variable messages can be constructed.
|
|
||||||
ErrInvalidRequestVersion: {
|
ErrInvalidRequestVersion: {
|
||||||
Code: "AuthorizationQueryParametersError",
|
Code: "AuthorizationQueryParametersError",
|
||||||
Description: "Error parsing the X-Amz-Credential parameter; incorrect terminal. This endpoint uses \"aws4_request\".",
|
Description: "Error parsing the X-Amz-Credential parameter; incorrect terminal. This endpoint uses \"aws4_request\".",
|
||||||
@@ -770,8 +753,6 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Your key is too long",
|
Description: "Your key is too long",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
|
||||||
// FIXME: Actual XML error response also contains the header which missed in list of signed header parameters.
|
|
||||||
ErrUnsignedHeaders: {
|
ErrUnsignedHeaders: {
|
||||||
Code: "AccessDenied",
|
Code: "AccessDenied",
|
||||||
Description: "There were headers present in the request which were not signed",
|
Description: "There were headers present in the request which were not signed",
|
||||||
|
|||||||
+6
-36
@@ -20,6 +20,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
@@ -35,7 +36,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
// RFC3339 a subset of the ISO8601 timestamp format. e.g 2014-04-29T18:30:38Z
|
// RFC3339 a subset of the ISO8601 timestamp format. e.g 2014-04-29T18:30:38Z
|
||||||
iso8601TimeFormat = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision.
|
iso8601TimeFormat = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision.
|
||||||
maxObjectList = 10000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
|
maxObjectList = 1000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
|
||||||
maxDeleteList = 10000 // Limit number of objects deleted in a delete call.
|
maxDeleteList = 10000 // Limit number of objects deleted in a delete call.
|
||||||
maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse.
|
maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse.
|
||||||
maxPartsList = 10000 // Limit number of parts in a listPartsResponse.
|
maxPartsList = 10000 // Limit number of parts in a listPartsResponse.
|
||||||
@@ -763,6 +764,10 @@ func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError
|
|||||||
// Set retry-after header to indicate user-agents to retry request after 120secs.
|
// Set retry-after header to indicate user-agents to retry request after 120secs.
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
||||||
w.Header().Set(xhttp.RetryAfter, "120")
|
w.Header().Set(xhttp.RetryAfter, "120")
|
||||||
|
case "InvalidRegion":
|
||||||
|
err.Description = fmt.Sprintf("Region does not match; expecting '%s'.", globalServerRegion)
|
||||||
|
case "AuthorizationHeaderMalformed":
|
||||||
|
err.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalServerRegion)
|
||||||
case "AccessDenied":
|
case "AccessDenied":
|
||||||
// The request is from browser and also if browser
|
// The request is from browser and also if browser
|
||||||
// is enabled we need to redirect.
|
// is enabled we need to redirect.
|
||||||
@@ -821,38 +826,3 @@ func writeCustomErrorResponseJSON(ctx context.Context, w http.ResponseWriter, er
|
|||||||
encodedErrorResponse := encodeResponseJSON(errorResponse)
|
encodedErrorResponse := encodeResponseJSON(errorResponse)
|
||||||
writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeJSON)
|
writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeCustomErrorResponseXML - similar to writeErrorResponse,
|
|
||||||
// but accepts the error message directly (this allows messages to be
|
|
||||||
// dynamically generated.)
|
|
||||||
func writeCustomErrorResponseXML(ctx context.Context, w http.ResponseWriter, err APIError, errBody string, reqURL *url.URL, browser bool) {
|
|
||||||
|
|
||||||
switch err.Code {
|
|
||||||
case "SlowDown", "XMinioServerNotInitialized", "XMinioReadQuorum", "XMinioWriteQuorum":
|
|
||||||
// Set retry-after header to indicate user-agents to retry request after 120secs.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
|
||||||
w.Header().Set(xhttp.RetryAfter, "120")
|
|
||||||
case "AccessDenied":
|
|
||||||
// The request is from browser and also if browser
|
|
||||||
// is enabled we need to redirect.
|
|
||||||
if browser && globalBrowserEnabled {
|
|
||||||
w.Header().Set(xhttp.Location, minioReservedBucketPath+reqURL.Path)
|
|
||||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reqInfo := logger.GetReqInfo(ctx)
|
|
||||||
errorResponse := APIErrorResponse{
|
|
||||||
Code: err.Code,
|
|
||||||
Message: errBody,
|
|
||||||
Resource: reqURL.Path,
|
|
||||||
BucketName: reqInfo.BucketName,
|
|
||||||
Key: reqInfo.ObjectName,
|
|
||||||
RequestID: w.Header().Get(xhttp.AmzRequestID),
|
|
||||||
HostID: globalDeploymentID,
|
|
||||||
}
|
|
||||||
|
|
||||||
encodedErrorResponse := encodeResponse(errorResponse)
|
|
||||||
writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeXML)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -55,19 +55,20 @@ func (h *healRoutine) queueHealTask(task healTask) {
|
|||||||
h.tasks <- task
|
h.tasks <- task
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForLowHTTPReq(tolerance int32) {
|
func waitForLowHTTPReq(tolerance int32, maxWait time.Duration) {
|
||||||
|
const wait = 10 * time.Millisecond
|
||||||
|
waitCount := maxWait / wait
|
||||||
|
|
||||||
// Bucket notification and http trace are not costly, it is okay to ignore them
|
// Bucket notification and http trace are not costly, it is okay to ignore them
|
||||||
// while counting the number of concurrent connections
|
// while counting the number of concurrent connections
|
||||||
tolerance += int32(globalHTTPListen.NumSubscribers() + globalHTTPTrace.NumSubscribers())
|
tolerance += int32(globalHTTPListen.NumSubscribers() + globalHTTPTrace.NumSubscribers())
|
||||||
|
|
||||||
if httpServer := newHTTPServerFn(); httpServer != nil {
|
if httpServer := newHTTPServerFn(); httpServer != nil {
|
||||||
// Wait at max 10 minute for an inprogress request before proceeding to heal
|
|
||||||
waitCount := 600
|
|
||||||
// Any requests in progress, delay the heal.
|
// Any requests in progress, delay the heal.
|
||||||
for (httpServer.GetRequestCount() >= tolerance) &&
|
for (httpServer.GetRequestCount() >= tolerance) &&
|
||||||
waitCount > 0 {
|
waitCount > 0 {
|
||||||
waitCount--
|
waitCount--
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(wait)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +83,7 @@ func (h *healRoutine) run(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait and proceed if there are active requests
|
// Wait and proceed if there are active requests
|
||||||
waitForLowHTTPReq(int32(globalEndpoints.NEndpoints()))
|
waitForLowHTTPReq(int32(globalEndpoints.NEndpoints()), time.Second)
|
||||||
|
|
||||||
var res madmin.HealResultItem
|
var res madmin.HealResultItem
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ func getLocalDisksToHeal(objAPI ObjectLayer) []Endpoints {
|
|||||||
// Try to connect to the current endpoint
|
// Try to connect to the current endpoint
|
||||||
// and reformat if the current disk is not formatted
|
// and reformat if the current disk is not formatted
|
||||||
_, _, err := connectEndpoint(endpoint)
|
_, _, err := connectEndpoint(endpoint)
|
||||||
if err == errUnformattedDisk {
|
if errors.Is(err, errUnformattedDisk) {
|
||||||
localDisksToHeal = append(localDisksToHeal, endpoint)
|
localDisksToHeal = append(localDisksToHeal, endpoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ func (b *streamingBitrotReader) ReadAt(buf []byte, offset int64) (int, error) {
|
|||||||
b.h.Write(buf)
|
b.h.Write(buf)
|
||||||
|
|
||||||
if !bytes.Equal(b.h.Sum(nil), b.hashBytes) {
|
if !bytes.Equal(b.h.Sum(nil), b.hashBytes) {
|
||||||
err := &errHashMismatch{fmt.Sprintf("hashes do not match expected %s, got %s",
|
err := &errHashMismatch{fmt.Sprintf("Disk: %s - content hash does not match - expected %s, got %s",
|
||||||
hex.EncodeToString(b.hashBytes), hex.EncodeToString(b.h.Sum(nil)))}
|
b.disk, hex.EncodeToString(b.hashBytes), hex.EncodeToString(b.h.Sum(nil)))}
|
||||||
logger.LogIf(GlobalContext, err)
|
logger.LogIf(GlobalContext, err)
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-5
@@ -17,6 +17,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
@@ -35,12 +36,12 @@ type wholeBitrotWriter struct {
|
|||||||
func (b *wholeBitrotWriter) Write(p []byte) (int, error) {
|
func (b *wholeBitrotWriter) Write(p []byte) (int, error) {
|
||||||
err := b.disk.AppendFile(b.volume, b.filePath, p)
|
err := b.disk.AppendFile(b.volume, b.filePath, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(GlobalContext, err)
|
logger.LogIf(GlobalContext, fmt.Errorf("Disk: %s returned %w", b.disk, err))
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
_, err = b.Hash.Write(p)
|
_, err = b.Hash.Write(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(GlobalContext, err)
|
logger.LogIf(GlobalContext, fmt.Errorf("Disk: %s returned %w", b.disk, err))
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return len(p), nil
|
return len(p), nil
|
||||||
@@ -69,9 +70,7 @@ func (b *wholeBitrotReader) ReadAt(buf []byte, offset int64) (n int, err error)
|
|||||||
if b.buf == nil {
|
if b.buf == nil {
|
||||||
b.buf = make([]byte, b.tillOffset-offset)
|
b.buf = make([]byte, b.tillOffset-offset)
|
||||||
if _, err := b.disk.ReadFile(b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
|
if _, err := b.disk.ReadFile(b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
|
||||||
ctx := GlobalContext
|
logger.LogIf(GlobalContext, fmt.Errorf("Disk: %s returned %w", b.disk, err))
|
||||||
logger.GetReqInfo(ctx).AppendTags("disk", b.disk.String())
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ func testBitrotReaderWriterAlgo(t *testing.T, bitrotAlgo BitrotAlgorithm) {
|
|||||||
volume := "testvol"
|
volume := "testvol"
|
||||||
filePath := "testfile"
|
filePath := "testfile"
|
||||||
|
|
||||||
disk, err := newXLStorage(tmpDir, "")
|
disk, err := newLocalXLStorage(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -762,7 +762,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
|
|
||||||
// Make sure formValues adhere to policy restrictions.
|
// Make sure formValues adhere to policy restrictions.
|
||||||
if err = checkPostPolicy(formValues, postPolicyForm); err != nil {
|
if err = checkPostPolicy(formValues, postPolicyForm); err != nil {
|
||||||
writeCustomErrorResponseXML(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), err.Error(), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrAccessDenied, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,12 +24,36 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/minio/minio/cmd/crypto"
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/bucket/policy"
|
"github.com/minio/minio/pkg/bucket/policy"
|
||||||
|
"github.com/minio/minio/pkg/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func concurrentDecryptETag(ctx context.Context, objects []ObjectInfo) {
|
||||||
|
inParallel := func(objects []ObjectInfo) {
|
||||||
|
g := errgroup.WithNErrs(len(objects))
|
||||||
|
for index := range objects {
|
||||||
|
index := index
|
||||||
|
g.Go(func() error {
|
||||||
|
objects[index].ETag = objects[index].GetActualETag(nil)
|
||||||
|
objects[index].Size, _ = objects[index].GetActualSize()
|
||||||
|
return nil
|
||||||
|
}, index)
|
||||||
|
}
|
||||||
|
g.Wait()
|
||||||
|
}
|
||||||
|
const maxConcurrent = 500
|
||||||
|
for {
|
||||||
|
if len(objects) < maxConcurrent {
|
||||||
|
inParallel(objects)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inParallel(objects[:maxConcurrent])
|
||||||
|
objects = objects[maxConcurrent:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate all the ListObjects query arguments, returns an APIErrorCode
|
// Validate all the ListObjects query arguments, returns an APIErrorCode
|
||||||
// if one of the args do not meet the required conditions.
|
// if one of the args do not meet the required conditions.
|
||||||
// Special conditions required by MinIO server are as below
|
// Special conditions required by MinIO server are as below
|
||||||
@@ -89,6 +113,10 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if proxyRequestByBucket(ctx, w, r, bucket) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
listObjectVersions := objectAPI.ListObjectVersions
|
listObjectVersions := objectAPI.ListObjectVersions
|
||||||
|
|
||||||
// Inititate a list object versions operation based on the input params.
|
// Inititate a list object versions operation based on the input params.
|
||||||
@@ -100,16 +128,7 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range listObjectVersionsInfo.Objects {
|
concurrentDecryptETag(ctx, listObjectVersionsInfo.Objects)
|
||||||
if crypto.IsEncrypted(listObjectVersionsInfo.Objects[i].UserDefined) {
|
|
||||||
listObjectVersionsInfo.Objects[i].ETag = getDecryptedETag(r.Header, listObjectVersionsInfo.Objects[i], false)
|
|
||||||
}
|
|
||||||
listObjectVersionsInfo.Objects[i].Size, err = listObjectVersionsInfo.Objects[i].GetActualSize()
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response := generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType, maxkeys, listObjectVersionsInfo)
|
response := generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType, maxkeys, listObjectVersionsInfo)
|
||||||
|
|
||||||
@@ -178,16 +197,7 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range listObjectsV2Info.Objects {
|
concurrentDecryptETag(ctx, listObjectsV2Info.Objects)
|
||||||
if crypto.IsEncrypted(listObjectsV2Info.Objects[i].UserDefined) {
|
|
||||||
listObjectsV2Info.Objects[i].ETag = getDecryptedETag(r.Header, listObjectsV2Info.Objects[i], false)
|
|
||||||
}
|
|
||||||
listObjectsV2Info.Objects[i].Size, err = listObjectsV2Info.Objects[i].GetActualSize()
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The next continuation token has id@node_index format to optimize paginated listing
|
// The next continuation token has id@node_index format to optimize paginated listing
|
||||||
nextContinuationToken := listObjectsV2Info.NextContinuationToken
|
nextContinuationToken := listObjectsV2Info.NextContinuationToken
|
||||||
@@ -264,16 +274,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range listObjectsV2Info.Objects {
|
concurrentDecryptETag(ctx, listObjectsV2Info.Objects)
|
||||||
if crypto.IsEncrypted(listObjectsV2Info.Objects[i].UserDefined) {
|
|
||||||
listObjectsV2Info.Objects[i].ETag = getDecryptedETag(r.Header, listObjectsV2Info.Objects[i], false)
|
|
||||||
}
|
|
||||||
listObjectsV2Info.Objects[i].Size, err = listObjectsV2Info.Objects[i].GetActualSize()
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The next continuation token has id@node_index format to optimize paginated listing
|
// The next continuation token has id@node_index format to optimize paginated listing
|
||||||
nextContinuationToken := listObjectsV2Info.NextContinuationToken
|
nextContinuationToken := listObjectsV2Info.NextContinuationToken
|
||||||
@@ -396,16 +397,7 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range listObjectsInfo.Objects {
|
concurrentDecryptETag(ctx, listObjectsInfo.Objects)
|
||||||
if crypto.IsEncrypted(listObjectsInfo.Objects[i].UserDefined) {
|
|
||||||
listObjectsInfo.Objects[i].ETag = getDecryptedETag(r.Header, listObjectsInfo.Objects[i], false)
|
|
||||||
}
|
|
||||||
listObjectsInfo.Objects[i].Size, err = listObjectsInfo.Objects[i].GetActualSize()
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType, maxKeys, listObjectsInfo)
|
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType, maxKeys, listObjectsInfo)
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/minio/minio-go/v7/pkg/tags"
|
"github.com/minio/minio-go/v7/pkg/tags"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
bucketsse "github.com/minio/minio/pkg/bucket/encryption"
|
bucketsse "github.com/minio/minio/pkg/bucket/encryption"
|
||||||
"github.com/minio/minio/pkg/bucket/lifecycle"
|
"github.com/minio/minio/pkg/bucket/lifecycle"
|
||||||
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
||||||
@@ -148,15 +149,27 @@ func (sys *BucketMetadataSys) Update(bucket string, configFile string, configDat
|
|||||||
meta.EncryptionConfigXML = configData
|
meta.EncryptionConfigXML = configData
|
||||||
case bucketTaggingConfig:
|
case bucketTaggingConfig:
|
||||||
meta.TaggingConfigXML = configData
|
meta.TaggingConfigXML = configData
|
||||||
case objectLockConfig:
|
|
||||||
meta.ObjectLockConfigXML = configData
|
|
||||||
case bucketVersioningConfig:
|
|
||||||
meta.VersioningConfigXML = configData
|
|
||||||
case bucketQuotaConfigFile:
|
case bucketQuotaConfigFile:
|
||||||
meta.QuotaConfigJSON = configData
|
meta.QuotaConfigJSON = configData
|
||||||
|
case objectLockConfig:
|
||||||
|
if !globalIsErasure && !globalIsDistErasure {
|
||||||
|
return NotImplemented{}
|
||||||
|
}
|
||||||
|
meta.ObjectLockConfigXML = configData
|
||||||
|
case bucketVersioningConfig:
|
||||||
|
if !globalIsErasure && !globalIsDistErasure {
|
||||||
|
return NotImplemented{}
|
||||||
|
}
|
||||||
|
meta.VersioningConfigXML = configData
|
||||||
case bucketReplicationConfig:
|
case bucketReplicationConfig:
|
||||||
|
if !globalIsErasure && !globalIsDistErasure {
|
||||||
|
return NotImplemented{}
|
||||||
|
}
|
||||||
meta.ReplicationConfigXML = configData
|
meta.ReplicationConfigXML = configData
|
||||||
case bucketTargetsFile:
|
case bucketTargetsFile:
|
||||||
|
if !globalIsErasure && !globalIsDistErasure {
|
||||||
|
return NotImplemented{}
|
||||||
|
}
|
||||||
meta.BucketTargetsConfigJSON = configData
|
meta.BucketTargetsConfigJSON = configData
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("Unknown bucket %s metadata update requested %s", bucket, configFile)
|
return fmt.Errorf("Unknown bucket %s metadata update requested %s", bucket, configFile)
|
||||||
@@ -176,6 +189,13 @@ func (sys *BucketMetadataSys) Update(bucket string, configFile string, configDat
|
|||||||
// If no metadata exists errConfigNotFound is returned and a new metadata is returned.
|
// If no metadata exists errConfigNotFound is returned and a new metadata is returned.
|
||||||
// Only a shallow copy is returned, so referenced data should not be modified,
|
// Only a shallow copy is returned, so referenced data should not be modified,
|
||||||
// but can be replaced atomically.
|
// but can be replaced atomically.
|
||||||
|
//
|
||||||
|
// This function should only be used with
|
||||||
|
// - GetBucketInfo
|
||||||
|
// - ListBuckets
|
||||||
|
// - ListBucketsHeal (only in case of erasure coding mode)
|
||||||
|
// For all other bucket specific metadata, use the relevant
|
||||||
|
// calls implemented specifically for each of those features.
|
||||||
func (sys *BucketMetadataSys) Get(bucket string) (BucketMetadata, error) {
|
func (sys *BucketMetadataSys) Get(bucket string) (BucketMetadata, error) {
|
||||||
if globalIsGateway || bucket == minioMetaBucket {
|
if globalIsGateway || bucket == minioMetaBucket {
|
||||||
return newBucketMetadata(bucket), errConfigNotFound
|
return newBucketMetadata(bucket), errConfigNotFound
|
||||||
@@ -397,8 +417,9 @@ func (sys *BucketMetadataSys) Init(ctx context.Context, buckets []BucketInfo, ob
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load PolicySys once during boot.
|
// Load bucket metadata sys in background
|
||||||
return sys.load(ctx, buckets, objAPI)
|
go logger.LogIf(ctx, sys.load(ctx, buckets, objAPI))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// concurrently load bucket metadata to speed up loading bucket metadata.
|
// concurrently load bucket metadata to speed up loading bucket metadata.
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ func getConditionValues(r *http.Request, lc string, username string, claims map[
|
|||||||
principalType := "Anonymous"
|
principalType := "Anonymous"
|
||||||
if username != "" {
|
if username != "" {
|
||||||
principalType = "User"
|
principalType = "User"
|
||||||
|
if len(claims) > 0 {
|
||||||
|
principalType = "AssumedRole"
|
||||||
|
}
|
||||||
|
if username == globalActiveCred.AccessKey {
|
||||||
|
principalType = "Account"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vid := r.URL.Query().Get("versionId")
|
vid := r.URL.Query().Get("versionId")
|
||||||
@@ -143,6 +149,10 @@ func getConditionValues(r *http.Request, lc string, username string, claims map[
|
|||||||
for k, v := range claims {
|
for k, v := range claims {
|
||||||
vStr, ok := v.(string)
|
vStr, ok := v.(string)
|
||||||
if ok {
|
if ok {
|
||||||
|
// Special case for AD/LDAP STS users
|
||||||
|
if k == ldapUser {
|
||||||
|
args[ldapUserPolicyVariable] = []string{vStr}
|
||||||
|
}
|
||||||
args[k] = []string{vStr}
|
args[k] = []string{vStr}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,8 +199,8 @@ func (sys *BucketTargetSys) Init(ctx context.Context, buckets []BucketInfo, objA
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load bucket targets once during boot.
|
// Load bucket targets once during boot in background.
|
||||||
sys.load(ctx, buckets, objAPI)
|
go sys.load(ctx, buckets, objAPI)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -455,28 +455,32 @@ func lookupConfigs(s config.Config, setDriveCount int) {
|
|||||||
for _, l := range loggerCfg.HTTP {
|
for _, l := range loggerCfg.HTTP {
|
||||||
if l.Enabled {
|
if l.Enabled {
|
||||||
// Enable http logging
|
// Enable http logging
|
||||||
logger.AddTarget(
|
if err = logger.AddTarget(
|
||||||
http.New(http.WithEndpoint(l.Endpoint),
|
http.New(http.WithEndpoint(l.Endpoint),
|
||||||
http.WithAuthToken(l.AuthToken),
|
http.WithAuthToken(l.AuthToken),
|
||||||
http.WithUserAgent(loggerUserAgent),
|
http.WithUserAgent(loggerUserAgent),
|
||||||
http.WithLogKind(string(logger.All)),
|
http.WithLogKind(string(logger.All)),
|
||||||
http.WithTransport(NewGatewayHTTPTransport()),
|
http.WithTransport(NewGatewayHTTPTransport()),
|
||||||
),
|
),
|
||||||
)
|
); err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize console HTTP target: %w", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, l := range loggerCfg.Audit {
|
for _, l := range loggerCfg.Audit {
|
||||||
if l.Enabled {
|
if l.Enabled {
|
||||||
// Enable http audit logging
|
// Enable http audit logging
|
||||||
logger.AddAuditTarget(
|
if err = logger.AddAuditTarget(
|
||||||
http.New(http.WithEndpoint(l.Endpoint),
|
http.New(http.WithEndpoint(l.Endpoint),
|
||||||
http.WithAuthToken(l.AuthToken),
|
http.WithAuthToken(l.AuthToken),
|
||||||
http.WithUserAgent(loggerUserAgent),
|
http.WithUserAgent(loggerUserAgent),
|
||||||
http.WithLogKind(string(logger.All)),
|
http.WithLogKind(string(logger.All)),
|
||||||
http.WithTransport(NewGatewayHTTPTransport()),
|
http.WithTransport(NewGatewayHTTPTransport()),
|
||||||
),
|
),
|
||||||
)
|
); err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize audit HTTP target: %w", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -559,6 +559,18 @@ var (
|
|||||||
Optional: true,
|
Optional: true,
|
||||||
Type: "number",
|
Type: "number",
|
||||||
},
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: target.ElasticUsername,
|
||||||
|
Description: "username for Elasticsearch basic-auth",
|
||||||
|
Optional: true,
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: target.ElasticPassword,
|
||||||
|
Description: "password for Elasticsearch basic-auth",
|
||||||
|
Optional: true,
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: config.Comment,
|
Key: config.Comment,
|
||||||
Description: config.DefaultComment,
|
Description: config.DefaultComment,
|
||||||
|
|||||||
@@ -201,6 +201,14 @@ func SetNotifyES(s config.Config, esName string, cfg target.ElasticsearchArgs) e
|
|||||||
Key: target.ElasticQueueLimit,
|
Key: target.ElasticQueueLimit,
|
||||||
Value: strconv.Itoa(int(cfg.QueueLimit)),
|
Value: strconv.Itoa(int(cfg.QueueLimit)),
|
||||||
},
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: target.ElasticUsername,
|
||||||
|
Value: cfg.Username,
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: target.ElasticPassword,
|
||||||
|
Value: cfg.Password,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ func FetchRegisteredTargets(cfg config.Config, doneCh <-chan struct{}, transport
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
esTargets, err := GetNotifyES(cfg[config.NotifyESSubSys])
|
esTargets, err := GetNotifyES(cfg[config.NotifyESSubSys], transport)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1532,11 +1532,19 @@ var (
|
|||||||
Key: target.ElasticQueueLimit,
|
Key: target.ElasticQueueLimit,
|
||||||
Value: "0",
|
Value: "0",
|
||||||
},
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: target.ElasticUsername,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: target.ElasticPassword,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetNotifyES - returns a map of registered notification 'elasticsearch' targets
|
// GetNotifyES - returns a map of registered notification 'elasticsearch' targets
|
||||||
func GetNotifyES(esKVS map[string]config.KVS) (map[string]target.ElasticsearchArgs, error) {
|
func GetNotifyES(esKVS map[string]config.KVS, transport *http.Transport) (map[string]target.ElasticsearchArgs, error) {
|
||||||
esTargets := make(map[string]target.ElasticsearchArgs)
|
esTargets := make(map[string]target.ElasticsearchArgs)
|
||||||
for k, kv := range mergeTargets(esKVS, target.EnvElasticEnable, DefaultESKVS) {
|
for k, kv := range mergeTargets(esKVS, target.EnvElasticEnable, DefaultESKVS) {
|
||||||
enableEnv := target.EnvElasticEnable
|
enableEnv := target.EnvElasticEnable
|
||||||
@@ -1586,6 +1594,16 @@ func GetNotifyES(esKVS map[string]config.KVS) (map[string]target.ElasticsearchAr
|
|||||||
queueDirEnv = queueDirEnv + config.Default + k
|
queueDirEnv = queueDirEnv + config.Default + k
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usernameEnv := target.EnvElasticUsername
|
||||||
|
if k != config.Default {
|
||||||
|
usernameEnv = usernameEnv + config.Default + k
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordEnv := target.EnvElasticPassword
|
||||||
|
if k != config.Default {
|
||||||
|
passwordEnv = passwordEnv + config.Default + k
|
||||||
|
}
|
||||||
|
|
||||||
esArgs := target.ElasticsearchArgs{
|
esArgs := target.ElasticsearchArgs{
|
||||||
Enable: enabled,
|
Enable: enabled,
|
||||||
Format: env.Get(formatEnv, kv.Get(target.ElasticFormat)),
|
Format: env.Get(formatEnv, kv.Get(target.ElasticFormat)),
|
||||||
@@ -1593,6 +1611,9 @@ func GetNotifyES(esKVS map[string]config.KVS) (map[string]target.ElasticsearchAr
|
|||||||
Index: env.Get(indexEnv, kv.Get(target.ElasticIndex)),
|
Index: env.Get(indexEnv, kv.Get(target.ElasticIndex)),
|
||||||
QueueDir: env.Get(queueDirEnv, kv.Get(target.ElasticQueueDir)),
|
QueueDir: env.Get(queueDirEnv, kv.Get(target.ElasticQueueDir)),
|
||||||
QueueLimit: uint64(queueLimit),
|
QueueLimit: uint64(queueLimit),
|
||||||
|
Transport: transport,
|
||||||
|
Username: env.Get(usernameEnv, kv.Get(target.ElasticUsername)),
|
||||||
|
Password: env.Get(passwordEnv, kv.Get(target.ElasticPassword)),
|
||||||
}
|
}
|
||||||
if err = esArgs.Validate(); err != nil {
|
if err = esArgs.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ func LookupConfig(kvs config.KVS, drivesPerSet int) (cfg Config, err error) {
|
|||||||
cfg.RRS.Parity = defaultRRSParity
|
cfg.RRS.Parity = defaultRRSParity
|
||||||
|
|
||||||
if err = config.CheckValidKeys(config.StorageClassSubSys, kvs, DefaultKVS); err != nil {
|
if err = config.CheckValidKeys(config.StorageClassSubSys, kvs, DefaultKVS); err != nil {
|
||||||
return cfg, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ssc := env.Get(StandardEnv, kvs.Get(ClassStandard))
|
ssc := env.Get(StandardEnv, kvs.Get(ClassStandard))
|
||||||
@@ -235,7 +235,7 @@ func LookupConfig(kvs config.KVS, drivesPerSet int) (cfg Config, err error) {
|
|||||||
if ssc != "" {
|
if ssc != "" {
|
||||||
cfg.Standard, err = parseStorageClass(ssc)
|
cfg.Standard, err = parseStorageClass(ssc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cfg, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cfg.Standard.Parity == 0 {
|
if cfg.Standard.Parity == 0 {
|
||||||
@@ -245,7 +245,7 @@ func LookupConfig(kvs config.KVS, drivesPerSet int) (cfg Config, err error) {
|
|||||||
if rrsc != "" {
|
if rrsc != "" {
|
||||||
cfg.RRS, err = parseStorageClass(rrsc)
|
cfg.RRS, err = parseStorageClass(rrsc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cfg, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cfg.RRS.Parity == 0 {
|
if cfg.RRS.Parity == 0 {
|
||||||
@@ -255,7 +255,7 @@ func LookupConfig(kvs config.KVS, drivesPerSet int) (cfg Config, err error) {
|
|||||||
// Validation is done after parsing both the storage classes. This is needed because we need one
|
// Validation is done after parsing both the storage classes. This is needed because we need one
|
||||||
// storage class value to deduce the correct value of the other storage class.
|
// storage class value to deduce the correct value of the other storage class.
|
||||||
if err = validateParity(cfg.Standard.Parity, cfg.RRS.Parity, drivesPerSet); err != nil {
|
if err = validateParity(cfg.Standard.Parity, cfg.RRS.Parity, drivesPerSet); err != nil {
|
||||||
return cfg, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
|
|||||||
@@ -117,6 +117,11 @@ func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh <-chan
|
|||||||
sys.pubsub.Subscribe(subCh, doneCh, filter)
|
sys.pubsub.Subscribe(subCh, doneCh, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate if HTTPConsoleLoggerSys is valid, always returns nil right now
|
||||||
|
func (sys *HTTPConsoleLoggerSys) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Send log message 'e' to console and publish to console
|
// Send log message 'e' to console and publish to console
|
||||||
// log pubsub system
|
// log pubsub system
|
||||||
func (sys *HTTPConsoleLoggerSys) Send(e interface{}, logKind string) error {
|
func (sys *HTTPConsoleLoggerSys) Send(e interface{}, logKind string) error {
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -148,6 +148,7 @@ func (s3KMS) ParseHTTP(h http.Header) (string, interface{}, error) {
|
|||||||
contextStr, ok := h[SSEKmsContext]
|
contextStr, ok := h[SSEKmsContext]
|
||||||
if ok {
|
if ok {
|
||||||
var context map[string]interface{}
|
var context map[string]interface{}
|
||||||
|
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
if err := json.Unmarshal([]byte(contextStr[0]), &context); err != nil {
|
if err := json.Unmarshal([]byte(contextStr[0]), &context); err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-24
@@ -16,9 +16,9 @@ package crypto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -30,10 +30,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
xnet "github.com/minio/minio/pkg/net"
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
|
||||||
// ErrKESKeyExists is the error returned a KES server
|
// ErrKESKeyExists is the error returned a KES server
|
||||||
// when a master key does exist.
|
// when a master key does exist.
|
||||||
var ErrKESKeyExists = NewKESError(http.StatusBadRequest, "key does already exist")
|
var ErrKESKeyExists = NewKESError(http.StatusBadRequest, "key does already exist")
|
||||||
@@ -113,15 +116,29 @@ func NewKes(cfg KesConfig) (KMS, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
certPool, err := loadCACertificates(cfg.CAPath)
|
if cfg.Transport.TLSClientConfig != nil {
|
||||||
if err != nil {
|
if err = loadCACertificates(cfg.CAPath,
|
||||||
|
cfg.Transport.TLSClientConfig.RootCAs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rootCAs, _ := x509.SystemCertPool()
|
||||||
|
if rootCAs == nil {
|
||||||
|
// In some systems (like Windows) system cert pool is
|
||||||
|
// not supported or no certificates are present on the
|
||||||
|
// system - so we create a new cert pool.
|
||||||
|
rootCAs = x509.NewCertPool()
|
||||||
|
}
|
||||||
|
if err = loadCACertificates(cfg.CAPath, rootCAs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
cfg.Transport.TLSClientConfig = &tls.Config{
|
cfg.Transport.TLSClientConfig = &tls.Config{
|
||||||
Certificates: []tls.Certificate{cert},
|
RootCAs: rootCAs,
|
||||||
RootCAs: certPool,
|
|
||||||
}
|
}
|
||||||
cfg.Transport.ForceAttemptHTTP2 = true
|
}
|
||||||
|
cfg.Transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
|
||||||
|
cfg.Transport.TLSClientConfig.NextProtos = []string{"h2"}
|
||||||
|
|
||||||
return &kesService{
|
return &kesService{
|
||||||
client: &kesClient{
|
client: &kesClient{
|
||||||
addr: cfg.Endpoint,
|
addr: cfg.Endpoint,
|
||||||
@@ -357,7 +374,16 @@ func parseErrorResponse(resp *http.Response) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *kesClient) post(url string, body io.Reader, limit int64) (io.Reader, error) {
|
func (c *kesClient) post(url string, body io.Reader, limit int64) (io.Reader, error) {
|
||||||
resp, err := c.httpClient.Post(url, "application/json", body)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -386,7 +412,10 @@ func (c *kesClient) postRetry(url string, body io.ReadSeeker, limit int64) (io.R
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !xnet.IsNetworkOrHostDown(err) && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
|
if !xnet.IsNetworkOrHostDown(err) &&
|
||||||
|
!errors.Is(err, io.EOF) &&
|
||||||
|
!errors.Is(err, io.ErrUnexpectedEOF) &&
|
||||||
|
!errors.Is(err, context.DeadlineExceeded) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,24 +442,17 @@ func (c *kesClient) postRetry(url string, body io.ReadSeeker, limit int64) (io.R
|
|||||||
// file as PEM-encoded certificate and add it to
|
// file as PEM-encoded certificate and add it to
|
||||||
// the CertPool. If a file is not a PEM certificate
|
// the CertPool. If a file is not a PEM certificate
|
||||||
// it will be ignored.
|
// it will be ignored.
|
||||||
func loadCACertificates(path string) (*x509.CertPool, error) {
|
func loadCACertificates(path string, rootCAs *x509.CertPool) error {
|
||||||
rootCAs, _ := x509.SystemCertPool()
|
|
||||||
if rootCAs == nil {
|
|
||||||
// In some systems (like Windows) system cert pool is
|
|
||||||
// not supported or no certificates are present on the
|
|
||||||
// system - so we create a new cert pool.
|
|
||||||
rootCAs = x509.NewCertPool()
|
|
||||||
}
|
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return rootCAs, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
stat, err := os.Stat(path)
|
stat, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) || os.IsPermission(err) {
|
if os.IsNotExist(err) || os.IsPermission(err) {
|
||||||
return rootCAs, nil
|
return nil
|
||||||
}
|
}
|
||||||
return nil, Errorf("crypto: cannot open '%s': %v", path, err)
|
return Errorf("crypto: cannot open '%s': %v", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If path is a file, parse as PEM-encoded certifcate
|
// If path is a file, parse as PEM-encoded certifcate
|
||||||
@@ -439,12 +461,12 @@ func loadCACertificates(path string) (*x509.CertPool, error) {
|
|||||||
if !stat.IsDir() {
|
if !stat.IsDir() {
|
||||||
cert, err := ioutil.ReadFile(path)
|
cert, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if !rootCAs.AppendCertsFromPEM(cert) {
|
if !rootCAs.AppendCertsFromPEM(cert) {
|
||||||
return nil, Errorf("crypto: '%s' is not a valid PEM-encoded certificate", path)
|
return Errorf("crypto: '%s' is not a valid PEM-encoded certificate", path)
|
||||||
}
|
}
|
||||||
return rootCAs, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If path is a directory then try
|
// If path is a directory then try
|
||||||
@@ -454,7 +476,7 @@ func loadCACertificates(path string) (*x509.CertPool, error) {
|
|||||||
// we ignore it.
|
// we ignore it.
|
||||||
files, err := ioutil.ReadDir(path)
|
files, err := ioutil.ReadDir(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
cert, err := ioutil.ReadFile(filepath.Join(path, file.Name()))
|
cert, err := ioutil.ReadFile(filepath.Join(path, file.Name()))
|
||||||
@@ -463,6 +485,6 @@ func loadCACertificates(path string) (*x509.CertPool, error) {
|
|||||||
}
|
}
|
||||||
rootCAs.AppendCertsFromPEM(cert) // ignore files which are not PEM certtificates
|
rootCAs.AppendCertsFromPEM(cert) // ignore files which are not PEM certtificates
|
||||||
}
|
}
|
||||||
return rootCAs, nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,17 @@ func RemoveInternalEntries(metadata map[string]string) {
|
|||||||
delete(metadata, S3KMSSealedKey)
|
delete(metadata, S3KMSSealedKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSourceEncrypted returns true if the source is encrypted
|
||||||
|
func IsSourceEncrypted(metadata map[string]string) bool {
|
||||||
|
if _, ok := metadata[SSECAlgorithm]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := metadata[SSEHeader]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// IsEncrypted returns true if the object metadata indicates
|
// IsEncrypted returns true if the object metadata indicates
|
||||||
// that it was uploaded using some form of server-side-encryption.
|
// that it was uploaded using some form of server-side-encryption.
|
||||||
//
|
//
|
||||||
|
|||||||
+126
-19
@@ -27,6 +27,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/madmin"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/config"
|
"github.com/minio/minio/cmd/config"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/bucket/lifecycle"
|
"github.com/minio/minio/pkg/bucket/lifecycle"
|
||||||
@@ -44,6 +46,7 @@ const (
|
|||||||
dataCrawlStartDelay = 5 * time.Minute // Time to wait on startup and between cycles.
|
dataCrawlStartDelay = 5 * time.Minute // Time to wait on startup and between cycles.
|
||||||
dataUsageUpdateDirCycles = 16 // Visit all folders every n cycles.
|
dataUsageUpdateDirCycles = 16 // Visit all folders every n cycles.
|
||||||
|
|
||||||
|
healDeleteDangling = true
|
||||||
)
|
)
|
||||||
|
|
||||||
// initDataCrawler will start the crawler unless disabled.
|
// initDataCrawler will start the crawler unless disabled.
|
||||||
@@ -87,12 +90,6 @@ func runDataCrawler(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
// Store new cycle...
|
// Store new cycle...
|
||||||
nextBloomCycle++
|
nextBloomCycle++
|
||||||
if nextBloomCycle%dataUpdateTrackerResetEvery == 0 {
|
|
||||||
if intDataUpdateTracker.debug {
|
|
||||||
logger.Info(color.Green("runDataCrawler:") + " Resetting bloom filter for next runs.")
|
|
||||||
}
|
|
||||||
nextBloomCycle++
|
|
||||||
}
|
|
||||||
var tmp [8]byte
|
var tmp [8]byte
|
||||||
binary.LittleEndian.PutUint64(tmp[:], nextBloomCycle)
|
binary.LittleEndian.PutUint64(tmp[:], nextBloomCycle)
|
||||||
r, err := hash.NewReader(bytes.NewReader(tmp[:]), int64(len(tmp)), "", "", int64(len(tmp)), false)
|
r, err := hash.NewReader(bytes.NewReader(tmp[:]), int64(len(tmp)), "", "", int64(len(tmp)), false)
|
||||||
@@ -113,6 +110,7 @@ func runDataCrawler(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
type cachedFolder struct {
|
type cachedFolder struct {
|
||||||
name string
|
name string
|
||||||
parent *dataUsageHash
|
parent *dataUsageHash
|
||||||
|
objectHealProbDiv uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type folderScanner struct {
|
type folderScanner struct {
|
||||||
@@ -125,6 +123,8 @@ type folderScanner struct {
|
|||||||
|
|
||||||
dataUsageCrawlMult float64
|
dataUsageCrawlMult float64
|
||||||
dataUsageCrawlDebug bool
|
dataUsageCrawlDebug bool
|
||||||
|
healFolderInclude uint32 // Include a clean folder one in n cycles.
|
||||||
|
healObjectSelect uint32 // Do a heal check on an object once every n cycles. Must divide into healFolderInclude
|
||||||
|
|
||||||
newFolders []cachedFolder
|
newFolders []cachedFolder
|
||||||
existingFolders []cachedFolder
|
existingFolders []cachedFolder
|
||||||
@@ -167,8 +167,17 @@ func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache,
|
|||||||
existingFolders: nil,
|
existingFolders: nil,
|
||||||
dataUsageCrawlMult: delayMult,
|
dataUsageCrawlMult: delayMult,
|
||||||
dataUsageCrawlDebug: intDataUpdateTracker.debug,
|
dataUsageCrawlDebug: intDataUpdateTracker.debug,
|
||||||
|
healFolderInclude: 0,
|
||||||
|
healObjectSelect: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable healing in XL mode.
|
||||||
|
if globalIsErasure {
|
||||||
|
// Include a clean folder one in n cycles.
|
||||||
|
s.healFolderInclude = 32
|
||||||
|
// Do a heal check on an object once every n cycles. Must divide into healFolderInclude
|
||||||
|
s.healObjectSelect = 512
|
||||||
|
}
|
||||||
if len(cache.Info.BloomFilter) > 0 {
|
if len(cache.Info.BloomFilter) > 0 {
|
||||||
s.withFilter = &bloomFilter{BloomFilter: &bloom.BloomFilter{}}
|
s.withFilter = &bloomFilter{BloomFilter: &bloom.BloomFilter{}}
|
||||||
_, err := s.withFilter.ReadFrom(bytes.NewBuffer(cache.Info.BloomFilter))
|
_, err := s.withFilter.ReadFrom(bytes.NewBuffer(cache.Info.BloomFilter))
|
||||||
@@ -189,7 +198,7 @@ func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Always scan flattenLevels deep. Cache root is level 0.
|
// Always scan flattenLevels deep. Cache root is level 0.
|
||||||
todo := []cachedFolder{{name: cache.Info.Name}}
|
todo := []cachedFolder{{name: cache.Info.Name, objectHealProbDiv: 1}}
|
||||||
for i := 0; i < flattenLevels; i++ {
|
for i := 0; i < flattenLevels; i++ {
|
||||||
if s.dataUsageCrawlDebug {
|
if s.dataUsageCrawlDebug {
|
||||||
logger.Info(logPrefix+"Level %v, scanning %v directories."+logSuffix, i, len(todo))
|
logger.Info(logPrefix+"Level %v, scanning %v directories."+logSuffix, i, len(todo))
|
||||||
@@ -218,7 +227,7 @@ func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache,
|
|||||||
return s.newCache, ctx.Err()
|
return s.newCache, ctx.Err()
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
du, err := s.deepScanFolder(ctx, folder.name)
|
du, err := s.deepScanFolder(ctx, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
continue
|
continue
|
||||||
@@ -249,26 +258,38 @@ func crawlDataFolder(ctx context.Context, basePath string, cache dataUsageCache,
|
|||||||
}
|
}
|
||||||
h := hashPath(folder.name)
|
h := hashPath(folder.name)
|
||||||
if !h.mod(s.oldCache.Info.NextCycle, dataUsageUpdateDirCycles) {
|
if !h.mod(s.oldCache.Info.NextCycle, dataUsageUpdateDirCycles) {
|
||||||
|
if !h.mod(s.oldCache.Info.NextCycle, s.healFolderInclude/folder.objectHealProbDiv) {
|
||||||
s.newCache.replaceHashed(h, folder.parent, s.oldCache.Cache[h.Key()])
|
s.newCache.replaceHashed(h, folder.parent, s.oldCache.Cache[h.Key()])
|
||||||
continue
|
continue
|
||||||
|
} else {
|
||||||
|
folder.objectHealProbDiv = s.healFolderInclude
|
||||||
|
}
|
||||||
|
folder.objectHealProbDiv = dataUsageUpdateDirCycles
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.withFilter != nil {
|
if s.withFilter != nil {
|
||||||
_, prefix := path2BucketObjectWithBasePath(basePath, folder.name)
|
_, prefix := path2BucketObjectWithBasePath(basePath, folder.name)
|
||||||
if s.oldCache.Info.lifeCycle == nil || !s.oldCache.Info.lifeCycle.HasActiveRules(prefix, true) {
|
if s.oldCache.Info.lifeCycle == nil || !s.oldCache.Info.lifeCycle.HasActiveRules(prefix, true) {
|
||||||
// If folder isn't in filter, skip it completely.
|
// If folder isn't in filter, skip it completely.
|
||||||
if !s.withFilter.containsDir(folder.name) {
|
if !s.withFilter.containsDir(folder.name) {
|
||||||
|
if !h.mod(s.oldCache.Info.NextCycle, s.healFolderInclude/folder.objectHealProbDiv) {
|
||||||
if s.dataUsageCrawlDebug {
|
if s.dataUsageCrawlDebug {
|
||||||
logger.Info(logPrefix+"Skipping non-updated folder: %v"+logSuffix, folder)
|
logger.Info(logPrefix+"Skipping non-updated folder: %v"+logSuffix, folder)
|
||||||
}
|
}
|
||||||
s.newCache.replaceHashed(h, folder.parent, s.oldCache.Cache[h.Key()])
|
s.newCache.replaceHashed(h, folder.parent, s.oldCache.Cache[h.Key()])
|
||||||
continue
|
continue
|
||||||
|
} else {
|
||||||
|
if s.dataUsageCrawlDebug {
|
||||||
|
logger.Info(logPrefix+"Adding non-updated folder to heal check: %v"+logSuffix, folder.name)
|
||||||
|
}
|
||||||
|
// Update probability of including objects
|
||||||
|
folder.objectHealProbDiv = s.healFolderInclude
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update on this cycle...
|
// Update on this cycle...
|
||||||
du, err := s.deepScanFolder(ctx, folder.name)
|
du, err := s.deepScanFolder(ctx, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
continue
|
continue
|
||||||
@@ -301,6 +322,7 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
thisHash := hashPath(folder.name)
|
thisHash := hashPath(folder.name)
|
||||||
|
existing := f.oldCache.findChildrenCopy(thisHash)
|
||||||
|
|
||||||
// If there are lifecycle rules for the prefix, remove the filter.
|
// If there are lifecycle rules for the prefix, remove the filter.
|
||||||
filter := f.withFilter
|
filter := f.withFilter
|
||||||
@@ -309,7 +331,7 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
|
|||||||
_, prefix := path2BucketObjectWithBasePath(f.root, folder.name)
|
_, prefix := path2BucketObjectWithBasePath(f.root, folder.name)
|
||||||
if f.oldCache.Info.lifeCycle.HasActiveRules(prefix, true) {
|
if f.oldCache.Info.lifeCycle.HasActiveRules(prefix, true) {
|
||||||
if f.dataUsageCrawlDebug {
|
if f.dataUsageCrawlDebug {
|
||||||
logger.Info(color.Green("data-usage:")+" Prefix %q has active rules", prefix)
|
logger.Info(color.Green("folder-scanner:")+" Prefix %q has active rules", prefix)
|
||||||
}
|
}
|
||||||
activeLifeCycle = f.oldCache.Info.lifeCycle
|
activeLifeCycle = f.oldCache.Info.lifeCycle
|
||||||
filter = nil
|
filter = nil
|
||||||
@@ -318,11 +340,19 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
|
|||||||
if _, ok := f.oldCache.Cache[thisHash.Key()]; filter != nil && ok {
|
if _, ok := f.oldCache.Cache[thisHash.Key()]; filter != nil && ok {
|
||||||
// If folder isn't in filter and we have data, skip it completely.
|
// If folder isn't in filter and we have data, skip it completely.
|
||||||
if folder.name != dataUsageRoot && !filter.containsDir(folder.name) {
|
if folder.name != dataUsageRoot && !filter.containsDir(folder.name) {
|
||||||
|
if !thisHash.mod(f.oldCache.Info.NextCycle, f.healFolderInclude/folder.objectHealProbDiv) {
|
||||||
f.newCache.copyWithChildren(&f.oldCache, thisHash, folder.parent)
|
f.newCache.copyWithChildren(&f.oldCache, thisHash, folder.parent)
|
||||||
if f.dataUsageCrawlDebug {
|
if f.dataUsageCrawlDebug {
|
||||||
logger.Info(color.Green("data-usage:")+" Skipping non-updated folder: %v", folder.name)
|
logger.Info(color.Green("folder-scanner:")+" Skipping non-updated folder: %v", folder.name)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
} else {
|
||||||
|
if f.dataUsageCrawlDebug {
|
||||||
|
logger.Info(color.Green("folder-scanner:")+" Adding non-updated folder to heal check: %v", folder.name)
|
||||||
|
}
|
||||||
|
// If probability was already crawlerHealFolderInclude, keep it.
|
||||||
|
folder.objectHealProbDiv = f.healFolderInclude
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f.waitForLowActiveIO()
|
f.waitForLowActiveIO()
|
||||||
@@ -336,14 +366,14 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
|
|||||||
bucket, prefix := path2BucketObjectWithBasePath(f.root, entName)
|
bucket, prefix := path2BucketObjectWithBasePath(f.root, entName)
|
||||||
if bucket == "" {
|
if bucket == "" {
|
||||||
if f.dataUsageCrawlDebug {
|
if f.dataUsageCrawlDebug {
|
||||||
logger.Info(color.Green("data-usage:")+" no bucket (%s,%s)", f.root, entName)
|
logger.Info(color.Green("folder-scanner:")+" no bucket (%s,%s)", f.root, entName)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if isReservedOrInvalidBucket(bucket, false) {
|
if isReservedOrInvalidBucket(bucket, false) {
|
||||||
if f.dataUsageCrawlDebug {
|
if f.dataUsageCrawlDebug {
|
||||||
logger.Info(color.Green("data-usage:")+" invalid bucket: %v, entry: %v", bucket, entName)
|
logger.Info(color.Green("folder-scanner:")+" invalid bucket: %v, entry: %v", bucket, entName)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -359,7 +389,8 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
|
|||||||
_, exists := f.oldCache.Cache[h.Key()]
|
_, exists := f.oldCache.Cache[h.Key()]
|
||||||
cache.addChildString(entName)
|
cache.addChildString(entName)
|
||||||
|
|
||||||
this := cachedFolder{name: entName, parent: &thisHash}
|
this := cachedFolder{name: entName, parent: &thisHash, objectHealProbDiv: folder.objectHealProbDiv}
|
||||||
|
delete(existing, h.Key())
|
||||||
cache.addChild(h)
|
cache.addChild(h)
|
||||||
if final {
|
if final {
|
||||||
if exists {
|
if exists {
|
||||||
@@ -385,11 +416,12 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
|
|||||||
objectName: path.Base(entName),
|
objectName: path.Base(entName),
|
||||||
debug: f.dataUsageCrawlDebug,
|
debug: f.dataUsageCrawlDebug,
|
||||||
lifeCycle: activeLifeCycle,
|
lifeCycle: activeLifeCycle,
|
||||||
|
heal: thisHash.mod(f.oldCache.Info.NextCycle, f.healObjectSelect/folder.objectHealProbDiv),
|
||||||
}
|
}
|
||||||
size, err := f.getSize(item)
|
size, err := f.getSize(item)
|
||||||
|
|
||||||
sleepDuration(time.Since(t), f.dataUsageCrawlMult)
|
sleepDuration(time.Since(t), f.dataUsageCrawlMult)
|
||||||
if err == errSkipFile || err == errFileNotFound {
|
if err == errSkipFile {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
@@ -402,19 +434,78 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if f.healObjectSelect == 0 {
|
||||||
|
// If we are not scanning, return now.
|
||||||
|
f.newCache.replaceHashed(thisHash, folder.parent, cache)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
objAPI := newObjectLayerWithoutSafeModeFn()
|
||||||
|
if objAPI == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
bgSeq, found := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whatever remains in 'existing' are folders at this level
|
||||||
|
// that existed in the previous run but wasn't found now.
|
||||||
|
//
|
||||||
|
// This may be because of 2 reasons:
|
||||||
|
//
|
||||||
|
// 1) The folder/object was deleted.
|
||||||
|
// 2) We come from another disk and this disk missed the write.
|
||||||
|
//
|
||||||
|
// We therefore perform a heal check.
|
||||||
|
// If that doesn't bring it back we remove the folder and assume it was deleted.
|
||||||
|
// This means that the next run will not look for it.
|
||||||
|
for k := range existing {
|
||||||
|
f.waitForLowActiveIO()
|
||||||
|
bucket, prefix := path2BucketObject(k)
|
||||||
|
if f.dataUsageCrawlDebug {
|
||||||
|
logger.Info(color.Green("folder-scanner:")+" checking disappeared folder: %v/%v", bucket, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = objAPI.HealObjects(ctx, bucket, prefix, madmin.HealOpts{Recursive: true, Remove: healDeleteDangling},
|
||||||
|
func(bucket, object, versionID string) error {
|
||||||
|
return bgSeq.queueHealTask(healSource{
|
||||||
|
bucket: bucket,
|
||||||
|
object: object,
|
||||||
|
versionID: versionID,
|
||||||
|
}, madmin.HealItemObject)
|
||||||
|
})
|
||||||
|
|
||||||
|
if f.dataUsageCrawlDebug && err != nil {
|
||||||
|
logger.Info(color.Green("healObjects:")+" checking returned value %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add unless healing returned an error.
|
||||||
|
if err == nil {
|
||||||
|
this := cachedFolder{name: k, parent: &thisHash, objectHealProbDiv: folder.objectHealProbDiv}
|
||||||
|
cache.addChild(hashPath(k))
|
||||||
|
if final {
|
||||||
|
f.existingFolders = append(f.existingFolders, this)
|
||||||
|
} else {
|
||||||
|
nextFolders = append(nextFolders, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
f.newCache.replaceHashed(thisHash, folder.parent, cache)
|
f.newCache.replaceHashed(thisHash, folder.parent, cache)
|
||||||
}
|
}
|
||||||
return nextFolders, nil
|
return nextFolders, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deepScanFolder will deep scan a folder and return the size if no error occurs.
|
// deepScanFolder will deep scan a folder and return the size if no error occurs.
|
||||||
func (f *folderScanner) deepScanFolder(ctx context.Context, folder string) (*dataUsageEntry, error) {
|
func (f *folderScanner) deepScanFolder(ctx context.Context, folder cachedFolder) (*dataUsageEntry, error) {
|
||||||
var cache dataUsageEntry
|
var cache dataUsageEntry
|
||||||
|
|
||||||
done := ctx.Done()
|
done := ctx.Done()
|
||||||
|
|
||||||
var addDir func(entName string, typ os.FileMode) error
|
var addDir func(entName string, typ os.FileMode) error
|
||||||
var dirStack = []string{f.root, folder}
|
var dirStack = []string{f.root, folder.name}
|
||||||
|
|
||||||
addDir = func(entName string, typ os.FileMode) error {
|
addDir = func(entName string, typ os.FileMode) error {
|
||||||
select {
|
select {
|
||||||
@@ -445,7 +536,7 @@ func (f *folderScanner) deepScanFolder(ctx context.Context, folder string) (*dat
|
|||||||
if f.oldCache.Info.lifeCycle != nil {
|
if f.oldCache.Info.lifeCycle != nil {
|
||||||
if f.oldCache.Info.lifeCycle.HasActiveRules(prefix, false) {
|
if f.oldCache.Info.lifeCycle.HasActiveRules(prefix, false) {
|
||||||
if f.dataUsageCrawlDebug {
|
if f.dataUsageCrawlDebug {
|
||||||
logger.Info(color.Green("data-usage:")+" Prefix %q has active rules", prefix)
|
logger.Info(color.Green("folder-scanner:")+" Prefix %q has active rules", prefix)
|
||||||
}
|
}
|
||||||
activeLifeCycle = f.oldCache.Info.lifeCycle
|
activeLifeCycle = f.oldCache.Info.lifeCycle
|
||||||
}
|
}
|
||||||
@@ -460,6 +551,7 @@ func (f *folderScanner) deepScanFolder(ctx context.Context, folder string) (*dat
|
|||||||
objectName: path.Base(entName),
|
objectName: path.Base(entName),
|
||||||
debug: f.dataUsageCrawlDebug,
|
debug: f.dataUsageCrawlDebug,
|
||||||
lifeCycle: activeLifeCycle,
|
lifeCycle: activeLifeCycle,
|
||||||
|
heal: hashPath(path.Join(prefix, entName)).mod(f.oldCache.Info.NextCycle, f.healObjectSelect/folder.objectHealProbDiv),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Don't sleep for really small amount of time
|
// Don't sleep for really small amount of time
|
||||||
@@ -490,6 +582,7 @@ type crawlItem struct {
|
|||||||
prefix string // Only the prefix if any, does not have final object name.
|
prefix string // Only the prefix if any, does not have final object name.
|
||||||
objectName string // Only the object name without prefixes.
|
objectName string // Only the object name without prefixes.
|
||||||
lifeCycle *lifecycle.Lifecycle
|
lifeCycle *lifecycle.Lifecycle
|
||||||
|
heal bool // Has the object been selected for heal check?
|
||||||
debug bool
|
debug bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,6 +615,20 @@ func (i *crawlItem) applyActions(ctx context.Context, o ObjectLayer, meta action
|
|||||||
if i.debug {
|
if i.debug {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
}
|
}
|
||||||
|
if i.heal {
|
||||||
|
if i.debug {
|
||||||
|
logger.Info(color.Green("applyActions:")+" heal checking: %v/%v v%s", i.bucket, i.objectPath(), meta.oi.VersionID)
|
||||||
|
}
|
||||||
|
res, err := o.HealObject(ctx, i.bucket, i.objectPath(), meta.oi.VersionID, madmin.HealOpts{Remove: healDeleteDangling})
|
||||||
|
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if !errors.Is(err, NotImplemented{}) {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
size = res.ObjectSize
|
||||||
|
}
|
||||||
if i.lifeCycle == nil {
|
if i.lifeCycle == nil {
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,9 +48,6 @@ const (
|
|||||||
dataUpdateTrackerFilename = dataUsageBucket + SlashSeparator + ".tracker.bin"
|
dataUpdateTrackerFilename = dataUsageBucket + SlashSeparator + ".tracker.bin"
|
||||||
dataUpdateTrackerVersion = 2
|
dataUpdateTrackerVersion = 2
|
||||||
dataUpdateTrackerSaveInterval = 5 * time.Minute
|
dataUpdateTrackerSaveInterval = 5 * time.Minute
|
||||||
|
|
||||||
// Reset bloom filters every n cycle
|
|
||||||
dataUpdateTrackerResetEvery = 1000
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -181,6 +178,8 @@ func (d *dataUpdateTracker) start(ctx context.Context, drives ...string) {
|
|||||||
}
|
}
|
||||||
d.load(ctx, drives...)
|
d.load(ctx, drives...)
|
||||||
go d.startCollector(ctx)
|
go d.startCollector(ctx)
|
||||||
|
// startSaver will unlock.
|
||||||
|
d.mu.Lock()
|
||||||
go d.startSaver(ctx, dataUpdateTrackerSaveInterval, drives)
|
go d.startSaver(ctx, dataUpdateTrackerSaveInterval, drives)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,17 +213,17 @@ func (d *dataUpdateTracker) load(ctx context.Context, drives ...string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// startSaver will start a saver that will write d to all supplied drives at specific intervals.
|
// startSaver will start a saver that will write d to all supplied drives at specific intervals.
|
||||||
|
// 'd' must be write locked when started and will be unlocked.
|
||||||
// The saver will save and exit when supplied context is closed.
|
// The saver will save and exit when supplied context is closed.
|
||||||
func (d *dataUpdateTracker) startSaver(ctx context.Context, interval time.Duration, drives []string) {
|
func (d *dataUpdateTracker) startSaver(ctx context.Context, interval time.Duration, drives []string) {
|
||||||
t := time.NewTicker(interval)
|
|
||||||
defer t.Stop()
|
|
||||||
var buf bytes.Buffer
|
|
||||||
d.mu.Lock()
|
|
||||||
saveNow := d.save
|
saveNow := d.save
|
||||||
exited := make(chan struct{})
|
exited := make(chan struct{})
|
||||||
d.saveExited = exited
|
d.saveExited = exited
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
|
t := time.NewTicker(interval)
|
||||||
|
defer t.Stop()
|
||||||
defer close(exited)
|
defer close(exited)
|
||||||
|
var buf bytes.Buffer
|
||||||
for {
|
for {
|
||||||
var exit bool
|
var exit bool
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ type testingLogger struct {
|
|||||||
t testLoggerI
|
t testLoggerI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *testingLogger) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (t *testingLogger) Send(entry interface{}, errKind string) error {
|
func (t *testingLogger) Send(entry interface{}, errKind string) error {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
defer t.mu.Unlock()
|
defer t.mu.Unlock()
|
||||||
|
|||||||
+17
-2
@@ -85,7 +85,12 @@ func (e *dataUsageEntry) merge(other dataUsageEntry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mod returns true if the hash mod cycles == cycle.
|
// mod returns true if the hash mod cycles == cycle.
|
||||||
|
// If cycles is 0 false is always returned.
|
||||||
|
// If cycles is 1 true is always returned (as expected).
|
||||||
func (h dataUsageHash) mod(cycle uint32, cycles uint32) bool {
|
func (h dataUsageHash) mod(cycle uint32, cycles uint32) bool {
|
||||||
|
if cycles <= 1 {
|
||||||
|
return cycles == 1
|
||||||
|
}
|
||||||
return uint32(xxhash.Sum64String(string(h)))%cycles == cycle%cycles
|
return uint32(xxhash.Sum64String(string(h)))%cycles == cycle%cycles
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +122,16 @@ func (d *dataUsageCache) find(path string) *dataUsageEntry {
|
|||||||
return &due
|
return &due
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findChildrenCopy returns a copy of the children of the supplied hash.
|
||||||
|
func (d *dataUsageCache) findChildrenCopy(h dataUsageHash) dataUsageHashMap {
|
||||||
|
ch := d.Cache[h.String()].Children
|
||||||
|
res := make(dataUsageHashMap, len(ch))
|
||||||
|
for k := range ch {
|
||||||
|
res[k] = struct{}{}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
// Returns nil if not found.
|
// Returns nil if not found.
|
||||||
func (d *dataUsageCache) subCache(path string) dataUsageCache {
|
func (d *dataUsageCache) subCache(path string) dataUsageCache {
|
||||||
dst := dataUsageCache{Info: dataUsageCacheInfo{
|
dst := dataUsageCache{Info: dataUsageCacheInfo{
|
||||||
@@ -328,7 +343,7 @@ func (d *dataUsageCache) bucketsUsageInfo(buckets []BucketInfo) map[string]Bucke
|
|||||||
flat := d.flatten(*e)
|
flat := d.flatten(*e)
|
||||||
dst[bucket.Name] = BucketUsageInfo{
|
dst[bucket.Name] = BucketUsageInfo{
|
||||||
Size: uint64(flat.Size),
|
Size: uint64(flat.Size),
|
||||||
ObjectsCount: uint64(flat.Objects),
|
ObjectsCount: flat.Objects,
|
||||||
ObjectSizesHistogram: flat.ObjSizes.toMap(),
|
ObjectSizesHistogram: flat.ObjSizes.toMap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,7 +360,7 @@ func (d *dataUsageCache) bucketUsageInfo(bucket string) BucketUsageInfo {
|
|||||||
flat := d.flatten(*e)
|
flat := d.flatten(*e)
|
||||||
return BucketUsageInfo{
|
return BucketUsageInfo{
|
||||||
Size: uint64(flat.Size),
|
Size: uint64(flat.Size),
|
||||||
ObjectsCount: uint64(flat.Objects),
|
ObjectsCount: flat.Objects,
|
||||||
ObjectSizesHistogram: flat.ObjSizes.toMap(),
|
ObjectSizesHistogram: flat.ObjSizes.toMap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,6 +360,13 @@ func TestDataUsageUpdatePrefix(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if got.root() == nil {
|
||||||
|
t.Log("cached folders:")
|
||||||
|
for folder := range got.Cache {
|
||||||
|
t.Log("folder:", folder)
|
||||||
|
}
|
||||||
|
t.Fatal("got nil root.")
|
||||||
|
}
|
||||||
|
|
||||||
// Test dirs
|
// Test dirs
|
||||||
var want = []struct {
|
var want = []struct {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const (
|
|||||||
cacheMetaJSONFile = "cache.json"
|
cacheMetaJSONFile = "cache.json"
|
||||||
cacheDataFile = "part.1"
|
cacheDataFile = "part.1"
|
||||||
cacheMetaVersion = "1.0.0"
|
cacheMetaVersion = "1.0.0"
|
||||||
cacheExpiryDays = time.Duration(90 * time.Hour * 24) // defaults to 90 days
|
cacheExpiryDays = 90 * time.Hour * 24 // defaults to 90 days
|
||||||
// SSECacheEncrypted is the metadata key indicating that the object
|
// SSECacheEncrypted is the metadata key indicating that the object
|
||||||
// is a cache entry encrypted with cache KMS master key in globalCacheKMS.
|
// is a cache entry encrypted with cache KMS master key in globalCacheKMS.
|
||||||
SSECacheEncrypted = "X-Minio-Internal-Encrypted-Cache"
|
SSECacheEncrypted = "X-Minio-Internal-Encrypted-Cache"
|
||||||
@@ -417,7 +417,7 @@ func (c *diskCache) Stat(ctx context.Context, bucket, object string) (oi ObjectI
|
|||||||
func (c *diskCache) statCachedMeta(ctx context.Context, cacheObjPath string) (meta *cacheMeta, partial bool, numHits int, err error) {
|
func (c *diskCache) statCachedMeta(ctx context.Context, cacheObjPath string) (meta *cacheMeta, partial bool, numHits int, err error) {
|
||||||
|
|
||||||
cLock := c.NewNSLockFn(ctx, cacheObjPath)
|
cLock := c.NewNSLockFn(ctx, cacheObjPath)
|
||||||
if err = cLock.GetRLock(globalObjectTimeout); err != nil {
|
if err = cLock.GetRLock(globalOperationTimeout); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,7 +499,7 @@ func (c *diskCache) statCache(ctx context.Context, cacheObjPath string) (meta *c
|
|||||||
func (c *diskCache) SaveMetadata(ctx context.Context, bucket, object string, meta map[string]string, actualSize int64, rs *HTTPRangeSpec, rsFileName string, incHitsOnly bool) error {
|
func (c *diskCache) SaveMetadata(ctx context.Context, bucket, object string, meta map[string]string, actualSize int64, rs *HTTPRangeSpec, rsFileName string, incHitsOnly bool) error {
|
||||||
cachedPath := getCacheSHADir(c.dir, bucket, object)
|
cachedPath := getCacheSHADir(c.dir, bucket, object)
|
||||||
cLock := c.NewNSLockFn(ctx, cachedPath)
|
cLock := c.NewNSLockFn(ctx, cachedPath)
|
||||||
if err := cLock.GetLock(globalObjectTimeout); err != nil {
|
if err := cLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer cLock.Unlock()
|
defer cLock.Unlock()
|
||||||
@@ -665,7 +665,7 @@ func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Read
|
|||||||
}
|
}
|
||||||
cachePath := getCacheSHADir(c.dir, bucket, object)
|
cachePath := getCacheSHADir(c.dir, bucket, object)
|
||||||
cLock := c.NewNSLockFn(ctx, cachePath)
|
cLock := c.NewNSLockFn(ctx, cachePath)
|
||||||
if err := cLock.GetLock(globalObjectTimeout); err != nil {
|
if err := cLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer cLock.Unlock()
|
defer cLock.Unlock()
|
||||||
@@ -871,7 +871,7 @@ func (c *diskCache) bitrotReadFromCache(ctx context.Context, filePath string, of
|
|||||||
func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, numHits int, err error) {
|
func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, numHits int, err error) {
|
||||||
cacheObjPath := getCacheSHADir(c.dir, bucket, object)
|
cacheObjPath := getCacheSHADir(c.dir, bucket, object)
|
||||||
cLock := c.NewNSLockFn(ctx, cacheObjPath)
|
cLock := c.NewNSLockFn(ctx, cacheObjPath)
|
||||||
if err := cLock.GetRLock(globalObjectTimeout); err != nil {
|
if err := cLock.GetRLock(globalOperationTimeout); err != nil {
|
||||||
return nil, numHits, err
|
return nil, numHits, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,7 +935,7 @@ func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRang
|
|||||||
// Deletes the cached object
|
// Deletes the cached object
|
||||||
func (c *diskCache) delete(ctx context.Context, cacheObjPath string) (err error) {
|
func (c *diskCache) delete(ctx context.Context, cacheObjPath string) (err error) {
|
||||||
cLock := c.NewNSLockFn(ctx, cacheObjPath)
|
cLock := c.NewNSLockFn(ctx, cacheObjPath)
|
||||||
if err := cLock.GetLock(globalObjectTimeout); err != nil {
|
if err := cLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer cLock.Unlock()
|
defer cLock.Unlock()
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func (c *cacheControl) isStale(modTime time.Time) bool {
|
|||||||
func cacheControlOpts(o ObjectInfo) *cacheControl {
|
func cacheControlOpts(o ObjectInfo) *cacheControl {
|
||||||
c := cacheControl{}
|
c := cacheControl{}
|
||||||
m := o.UserDefined
|
m := o.UserDefined
|
||||||
if o.Expires != timeSentinel {
|
if !o.Expires.Equal(timeSentinel) {
|
||||||
c.expiry = o.Expires
|
c.expiry = o.Expires
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func TestGetCacheControlOpts(t *testing.T) {
|
|||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
m := make(map[string]string)
|
m := make(map[string]string)
|
||||||
m["cache-control"] = testCase.cacheControlHeaderVal
|
m["cache-control"] = testCase.cacheControlHeaderVal
|
||||||
if testCase.expiryHeaderVal != timeSentinel {
|
if !testCase.expiryHeaderVal.Equal(timeSentinel) {
|
||||||
m["expires"] = testCase.expiryHeaderVal.String()
|
m["expires"] = testCase.expiryHeaderVal.String()
|
||||||
}
|
}
|
||||||
c := cacheControlOpts(ObjectInfo{UserDefined: m, Expires: testCase.expiryHeaderVal})
|
c := cacheControlOpts(ObjectInfo{UserDefined: m, Expires: testCase.expiryHeaderVal})
|
||||||
|
|||||||
+14
-8
@@ -172,7 +172,7 @@ func getMetadata(objInfo ObjectInfo) map[string]string {
|
|||||||
if objInfo.ContentEncoding != "" {
|
if objInfo.ContentEncoding != "" {
|
||||||
metadata["content-encoding"] = objInfo.ContentEncoding
|
metadata["content-encoding"] = objInfo.ContentEncoding
|
||||||
}
|
}
|
||||||
if objInfo.Expires != timeSentinel {
|
if !objInfo.Expires.Equal(timeSentinel) {
|
||||||
metadata["expires"] = objInfo.Expires.Format(http.TimeFormat)
|
metadata["expires"] = objInfo.Expires.Format(http.TimeFormat)
|
||||||
}
|
}
|
||||||
for k, v := range objInfo.UserDefined {
|
for k, v := range objInfo.UserDefined {
|
||||||
@@ -317,16 +317,18 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
|||||||
rs = nil
|
rs = nil
|
||||||
}
|
}
|
||||||
// fill cache in the background for range GET requests
|
// fill cache in the background for range GET requests
|
||||||
bReader, bErr := c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
|
bReader, bErr := c.GetObjectNInfoFn(GlobalContext, bucket, object, rs, h, lockType, opts)
|
||||||
if bErr != nil {
|
if bErr != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer bReader.Close()
|
defer bReader.Close()
|
||||||
oi, _, _, err := dcache.statRange(ctx, bucket, object, rs)
|
oi, _, _, err := dcache.statRange(GlobalContext, bucket, object, rs)
|
||||||
// avoid cache overwrite if another background routine filled cache
|
// avoid cache overwrite if another background routine filled cache
|
||||||
if err != nil || oi.ETag != bReader.ObjInfo.ETag {
|
if err != nil || oi.ETag != bReader.ObjInfo.ETag {
|
||||||
// use a new context to avoid locker prematurely timing out operation when the GetObjectNInfo returns.
|
// use a new context to avoid locker prematurely timing out operation when the GetObjectNInfo returns.
|
||||||
dcache.Put(context.Background(), bucket, object, bReader, bReader.ObjInfo.Size, rs, ObjectOptions{UserDefined: getMetadata(bReader.ObjInfo)}, false)
|
dcache.Put(GlobalContext, bucket, object, bReader, bReader.ObjInfo.Size, rs, ObjectOptions{
|
||||||
|
UserDefined: getMetadata(bReader.ObjInfo),
|
||||||
|
}, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -337,7 +339,11 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
|||||||
pipeReader, pipeWriter := io.Pipe()
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
teeReader := io.TeeReader(bkReader, pipeWriter)
|
teeReader := io.TeeReader(bkReader, pipeWriter)
|
||||||
go func() {
|
go func() {
|
||||||
putErr := dcache.Put(ctx, bucket, object, io.LimitReader(pipeReader, bkReader.ObjInfo.Size), bkReader.ObjInfo.Size, nil, ObjectOptions{UserDefined: getMetadata(bkReader.ObjInfo)}, false)
|
putErr := dcache.Put(ctx, bucket, object,
|
||||||
|
io.LimitReader(pipeReader, bkReader.ObjInfo.Size),
|
||||||
|
bkReader.ObjInfo.Size, nil, ObjectOptions{
|
||||||
|
UserDefined: getMetadata(bkReader.ObjInfo),
|
||||||
|
}, false)
|
||||||
// close the write end of the pipe, so the error gets
|
// close the write end of the pipe, so the error gets
|
||||||
// propagated to getObjReader
|
// propagated to getObjReader
|
||||||
pipeWriter.CloseWithError(putErr)
|
pipeWriter.CloseWithError(putErr)
|
||||||
@@ -634,15 +640,15 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
go func() {
|
go func() {
|
||||||
// fill cache in the background
|
// fill cache in the background
|
||||||
bReader, bErr := c.GetObjectNInfoFn(ctx, bucket, object, nil, http.Header{}, readLock, ObjectOptions{})
|
bReader, bErr := c.GetObjectNInfoFn(GlobalContext, bucket, object, nil, http.Header{}, readLock, ObjectOptions{})
|
||||||
if bErr != nil {
|
if bErr != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer bReader.Close()
|
defer bReader.Close()
|
||||||
oi, _, err := dcache.Stat(ctx, bucket, object)
|
oi, _, err := dcache.Stat(GlobalContext, bucket, object)
|
||||||
// avoid cache overwrite if another background routine filled cache
|
// avoid cache overwrite if another background routine filled cache
|
||||||
if err != nil || oi.ETag != bReader.ObjInfo.ETag {
|
if err != nil || oi.ETag != bReader.ObjInfo.ETag {
|
||||||
dcache.Put(ctx, bucket, object, bReader, bReader.ObjInfo.Size, nil, ObjectOptions{UserDefined: getMetadata(bReader.ObjInfo)}, false)
|
dcache.Put(GlobalContext, bucket, object, bReader, bReader.ObjInfo.Size, nil, ObjectOptions{UserDefined: getMetadata(bReader.ObjInfo)}, false)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func TestCacheMetadataObjInfo(t *testing.T) {
|
|||||||
if objInfo.Size != 0 {
|
if objInfo.Size != 0 {
|
||||||
t.Fatal("Unexpected object info value for Size", objInfo.Size)
|
t.Fatal("Unexpected object info value for Size", objInfo.Size)
|
||||||
}
|
}
|
||||||
if objInfo.ModTime != timeSentinel {
|
if !objInfo.ModTime.Equal(timeSentinel) {
|
||||||
t.Fatal("Unexpected object info value for ModTime ", objInfo.ModTime)
|
t.Fatal("Unexpected object info value for ModTime ", objInfo.ModTime)
|
||||||
}
|
}
|
||||||
if objInfo.IsDir {
|
if objInfo.IsDir {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const (
|
|||||||
dynamicTimeoutIncreaseThresholdPct = 0.33 // Upper threshold for failures in order to increase timeout
|
dynamicTimeoutIncreaseThresholdPct = 0.33 // Upper threshold for failures in order to increase timeout
|
||||||
dynamicTimeoutDecreaseThresholdPct = 0.10 // Lower threshold for failures in order to decrease timeout
|
dynamicTimeoutDecreaseThresholdPct = 0.10 // Lower threshold for failures in order to decrease timeout
|
||||||
dynamicTimeoutLogSize = 16
|
dynamicTimeoutLogSize = 16
|
||||||
maxDuration = time.Duration(1<<63 - 1)
|
maxDuration = 1<<63 - 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// timeouts that are dynamically adapted based on actual usage results
|
// timeouts that are dynamically adapted based on actual usage results
|
||||||
@@ -110,7 +110,7 @@ func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) {
|
|||||||
// We are hitting the timeout relatively few times, so decrease the timeout
|
// We are hitting the timeout relatively few times, so decrease the timeout
|
||||||
average = average * 125 / 100 // Add buffer of 25% on top of average
|
average = average * 125 / 100 // Add buffer of 25% on top of average
|
||||||
|
|
||||||
timeout := (atomic.LoadInt64(&dt.timeout) + int64(average)) / 2 // Middle between current timeout and average success
|
timeout := (atomic.LoadInt64(&dt.timeout) + average) / 2 // Middle between current timeout and average success
|
||||||
if timeout < dt.minimum {
|
if timeout < dt.minimum {
|
||||||
timeout = dt.minimum
|
timeout = dt.minimum
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -605,12 +605,14 @@ func getDecryptedETag(headers http.Header, objInfo ObjectInfo, copySource bool)
|
|||||||
if crypto.IsMultiPart(objInfo.UserDefined) {
|
if crypto.IsMultiPart(objInfo.UserDefined) {
|
||||||
return objInfo.ETag
|
return objInfo.ETag
|
||||||
}
|
}
|
||||||
|
|
||||||
if crypto.SSECopy.IsRequested(headers) {
|
if crypto.SSECopy.IsRequested(headers) {
|
||||||
key, err = crypto.SSECopy.ParseHTTP(headers)
|
key, err = crypto.SSECopy.ParseHTTP(headers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return objInfo.ETag
|
return objInfo.ETag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// As per AWS S3 Spec, ETag for SSE-C encrypted objects need not be MD5Sum of the data.
|
// As per AWS S3 Spec, ETag for SSE-C encrypted objects need not be MD5Sum of the data.
|
||||||
// Since server side copy with same source and dest just replaces the ETag, we save
|
// Since server side copy with same source and dest just replaces the ETag, we save
|
||||||
// encrypted content MD5Sum as ETag for both SSE-C and SSE-S3, we standardize the ETag
|
// encrypted content MD5Sum as ETag for both SSE-C and SSE-S3, we standardize the ETag
|
||||||
@@ -664,7 +666,7 @@ func (o *ObjectInfo) GetDecryptedRange(rs *HTTPRangeSpec) (encOff, encLength, sk
|
|||||||
|
|
||||||
if rs == nil {
|
if rs == nil {
|
||||||
// No range, so offsets refer to the whole object.
|
// No range, so offsets refer to the whole object.
|
||||||
return 0, int64(o.Size), 0, 0, 0, nil
|
return 0, o.Size, 0, 0, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assemble slice of (decrypted) part sizes in `sizes`
|
// Assemble slice of (decrypted) part sizes in `sizes`
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ func TestGetDecryptedRange(t *testing.T) {
|
|||||||
sum := int64(0)
|
sum := int64(0)
|
||||||
for i, s := range sizes {
|
for i, s := range sizes {
|
||||||
r[i].Number = i
|
r[i].Number = i
|
||||||
r[i].Size = int64(getEncSize(s))
|
r[i].Size = getEncSize(s)
|
||||||
sum += r[i].Size
|
sum += r[i].Size
|
||||||
}
|
}
|
||||||
return ObjectInfo{
|
return ObjectInfo{
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ func (e *Erasure) ShardFileSize(totalLength int64) int64 {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
numShards := totalLength / e.blockSize
|
numShards := totalLength / e.blockSize
|
||||||
lastBlockSize := totalLength % int64(e.blockSize)
|
lastBlockSize := totalLength % e.blockSize
|
||||||
lastShardSize := ceilFrac(lastBlockSize, int64(e.dataBlocks))
|
lastShardSize := ceilFrac(lastBlockSize, int64(e.dataBlocks))
|
||||||
return numShards*e.ShardSize() + lastShardSize
|
return numShards*e.ShardSize() + lastShardSize
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ func (e *Erasure) ShardFileSize(totalLength int64) int64 {
|
|||||||
func (e *Erasure) ShardFileOffset(startOffset, length, totalLength int64) int64 {
|
func (e *Erasure) ShardFileOffset(startOffset, length, totalLength int64) int64 {
|
||||||
shardSize := e.ShardSize()
|
shardSize := e.ShardSize()
|
||||||
shardFileSize := e.ShardFileSize(totalLength)
|
shardFileSize := e.ShardFileSize(totalLength)
|
||||||
endShard := (startOffset + int64(length)) / e.blockSize
|
endShard := (startOffset + length) / e.blockSize
|
||||||
tillOffset := endShard*shardSize + shardSize
|
tillOffset := endShard*shardSize + shardSize
|
||||||
if tillOffset > shardFileSize {
|
if tillOffset > shardFileSize {
|
||||||
tillOffset = shardFileSize
|
tillOffset = shardFileSize
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func TestCommonTime(t *testing.T) {
|
|||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
// Obtain a common mod time from modTimes slice.
|
// Obtain a common mod time from modTimes slice.
|
||||||
ctime, _ := commonTime(testCase.times)
|
ctime, _ := commonTime(testCase.times)
|
||||||
if testCase.time != ctime {
|
if !testCase.time.Equal(ctime) {
|
||||||
t.Fatalf("Test case %d, expect to pass but failed. Wanted modTime: %s, got modTime: %s\n", i+1, testCase.time, ctime)
|
t.Fatalf("Test case %d, expect to pass but failed. Wanted modTime: %s, got modTime: %s\n", i+1, testCase.time, ctime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio-go/v7/pkg/set"
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
@@ -69,6 +70,56 @@ func (er erasureObjects) removeObjectPart(bucket, object, uploadID, dataDir stri
|
|||||||
g.Wait()
|
g.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean-up the old multipart uploads. Should be run in a Go routine.
|
||||||
|
func (er erasureObjects) cleanupStaleMultipartUploads(ctx context.Context, cleanupInterval, expiry time.Duration) {
|
||||||
|
ticker := time.NewTicker(cleanupInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
var disk StorageAPI
|
||||||
|
for _, d := range er.getLoadBalancedDisks() {
|
||||||
|
if d != nil {
|
||||||
|
disk = d
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if disk == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
er.cleanupStaleMultipartUploadsOnDisk(ctx, disk, expiry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the old multipart uploads on the given disk.
|
||||||
|
func (er erasureObjects) cleanupStaleMultipartUploadsOnDisk(ctx context.Context, disk StorageAPI, expiry time.Duration) {
|
||||||
|
now := time.Now()
|
||||||
|
shaDirs, err := disk.ListDir(minioMetaMultipartBucket, "", -1)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, shaDir := range shaDirs {
|
||||||
|
uploadIDDirs, err := disk.ListDir(minioMetaMultipartBucket, shaDir, -1)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, uploadIDDir := range uploadIDDirs {
|
||||||
|
uploadIDPath := pathJoin(shaDir, uploadIDDir)
|
||||||
|
fi, err := disk.ReadVersion(minioMetaMultipartBucket, uploadIDPath, "")
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if now.Sub(fi.ModTime) > expiry {
|
||||||
|
er.deleteObject(ctx, minioMetaMultipartBucket, uploadIDPath, fi.Erasure.DataBlocks+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ListMultipartUploads - lists all the pending multipart
|
// ListMultipartUploads - lists all the pending multipart
|
||||||
// uploads for a particular object in a bucket.
|
// uploads for a particular object in a bucket.
|
||||||
//
|
//
|
||||||
|
|||||||
+52
-24
@@ -18,6 +18,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/crc32"
|
"hash/crc32"
|
||||||
"io"
|
"io"
|
||||||
@@ -137,7 +138,14 @@ func connectEndpoint(endpoint Endpoint) (StorageAPI, *formatErasureV3, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// Close the internal connection to avoid connection leaks.
|
// Close the internal connection to avoid connection leaks.
|
||||||
disk.Close()
|
disk.Close()
|
||||||
return nil, nil, err
|
if errors.Is(err, errUnformattedDisk) {
|
||||||
|
info, derr := disk.DiskInfo()
|
||||||
|
if derr != nil && info.RootDisk {
|
||||||
|
return nil, nil, fmt.Errorf("Disk: %s returned %w but its a root disk refusing to use it",
|
||||||
|
disk, derr) // make sure to '%w' to wrap the error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil, fmt.Errorf("Disk: %s returned %w", disk, err) // make sure to '%w' to wrap the error
|
||||||
}
|
}
|
||||||
|
|
||||||
return disk, format, nil
|
return disk, format, nil
|
||||||
@@ -353,6 +361,9 @@ func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []Sto
|
|||||||
bp: bp,
|
bp: bp,
|
||||||
mrfOpCh: make(chan partialOperation, 10000),
|
mrfOpCh: make(chan partialOperation, 10000),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go s.sets[i].cleanupStaleMultipartUploads(ctx,
|
||||||
|
GlobalMultipartCleanupInterval, GlobalMultipartExpiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the disk monitoring and connect routine.
|
// Start the disk monitoring and connect routine.
|
||||||
@@ -827,9 +838,17 @@ func (f *FileInfoCh) Push(fi FileInfo) {
|
|||||||
// if the caller wishes to list N entries to call lexicallySortedEntry
|
// if the caller wishes to list N entries to call lexicallySortedEntry
|
||||||
// N times until this boolean is 'false'.
|
// N times until this boolean is 'false'.
|
||||||
func lexicallySortedEntry(entryChs []FileInfoCh, entries []FileInfo, entriesValid []bool) (FileInfo, int, bool) {
|
func lexicallySortedEntry(entryChs []FileInfoCh, entries []FileInfo, entriesValid []bool) (FileInfo, int, bool) {
|
||||||
for i := range entryChs {
|
var wg sync.WaitGroup
|
||||||
entries[i], entriesValid[i] = entryChs[i].Pop()
|
for j := range entryChs {
|
||||||
|
j := j
|
||||||
|
wg.Add(1)
|
||||||
|
// Pop() entries in parallel for large drive setups.
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
entries[j], entriesValid[j] = entryChs[j].Pop()
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
var isTruncated = false
|
var isTruncated = false
|
||||||
for _, valid := range entriesValid {
|
for _, valid := range entriesValid {
|
||||||
@@ -891,9 +910,17 @@ func lexicallySortedEntry(entryChs []FileInfoCh, entries []FileInfo, entriesVali
|
|||||||
// if the caller wishes to list N entries to call lexicallySortedEntry
|
// if the caller wishes to list N entries to call lexicallySortedEntry
|
||||||
// N times until this boolean is 'false'.
|
// N times until this boolean is 'false'.
|
||||||
func lexicallySortedEntryVersions(entryChs []FileInfoVersionsCh, entries []FileInfoVersions, entriesValid []bool) (FileInfoVersions, int, bool) {
|
func lexicallySortedEntryVersions(entryChs []FileInfoVersionsCh, entries []FileInfoVersions, entriesValid []bool) (FileInfoVersions, int, bool) {
|
||||||
for i := range entryChs {
|
var wg sync.WaitGroup
|
||||||
entries[i], entriesValid[i] = entryChs[i].Pop()
|
for j := range entryChs {
|
||||||
|
j := j
|
||||||
|
wg.Add(1)
|
||||||
|
// Pop() entries in parallel for large drive setups.
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
entries[j], entriesValid[j] = entryChs[j].Pop()
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
var isTruncated = false
|
var isTruncated = false
|
||||||
for _, valid := range entriesValid {
|
for _, valid := range entriesValid {
|
||||||
@@ -1274,19 +1301,20 @@ func isTestSetup(infos []DiskInfo, errs []error) bool {
|
|||||||
return rootDiskCount == len(infos)
|
return rootDiskCount == len(infos)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHealDiskInfos(storageDisks []StorageAPI) ([]DiskInfo, []error) {
|
func getHealDiskInfos(storageDisks []StorageAPI, errs []error) ([]DiskInfo, []error) {
|
||||||
infos := make([]DiskInfo, len(storageDisks))
|
infos := make([]DiskInfo, len(storageDisks))
|
||||||
g := errgroup.WithNErrs(len(storageDisks))
|
g := errgroup.WithNErrs(len(storageDisks))
|
||||||
for index := range storageDisks {
|
for index := range storageDisks {
|
||||||
index := index
|
index := index
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
var err error
|
if errs[index] != nil && errs[index] != errUnformattedDisk {
|
||||||
if storageDisks[index] != nil {
|
return errs[index]
|
||||||
infos[index], err = storageDisks[index].DiskInfo()
|
|
||||||
} else {
|
|
||||||
// Disk not found.
|
|
||||||
err = errDiskNotFound
|
|
||||||
}
|
}
|
||||||
|
if storageDisks[index] == nil {
|
||||||
|
return errDiskNotFound
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
infos[index], err = storageDisks[index].DiskInfo()
|
||||||
return err
|
return err
|
||||||
}, index)
|
}, index)
|
||||||
}
|
}
|
||||||
@@ -1294,14 +1322,12 @@ func getHealDiskInfos(storageDisks []StorageAPI) ([]DiskInfo, []error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mark root disks as down so as not to heal them.
|
// Mark root disks as down so as not to heal them.
|
||||||
func markRootDisksAsDown(storageDisks []StorageAPI) {
|
func markRootDisksAsDown(storageDisks []StorageAPI, errs []error) {
|
||||||
infos, errs := getHealDiskInfos(storageDisks)
|
var infos []DiskInfo
|
||||||
if isTestSetup(infos, errs) {
|
infos, errs = getHealDiskInfos(storageDisks, errs)
|
||||||
// Allow healing of disks for test setups to help with testing.
|
if !isTestSetup(infos, errs) {
|
||||||
return
|
|
||||||
}
|
|
||||||
for i := range storageDisks {
|
for i := range storageDisks {
|
||||||
if infos[i].RootDisk {
|
if storageDisks[i] != nil && infos[i].RootDisk {
|
||||||
// We should not heal on root disk. i.e in a situation where the minio-administrator has unmounted a
|
// We should not heal on root disk. i.e in a situation where the minio-administrator has unmounted a
|
||||||
// defective drive we should not heal a path on the root disk.
|
// defective drive we should not heal a path on the root disk.
|
||||||
logger.Info("Disk `%s` is a root disk. Please ensure the disk is mounted properly, refusing to use root disk.",
|
logger.Info("Disk `%s` is a root disk. Please ensure the disk is mounted properly, refusing to use root disk.",
|
||||||
@@ -1309,6 +1335,7 @@ func markRootDisksAsDown(storageDisks []StorageAPI) {
|
|||||||
storageDisks[i] = nil
|
storageDisks[i] = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealFormat - heals missing `format.json` on fresh unformatted disks.
|
// HealFormat - heals missing `format.json` on fresh unformatted disks.
|
||||||
@@ -1326,13 +1353,14 @@ func (s *erasureSets) HealFormat(ctx context.Context, dryRun bool) (res madmin.H
|
|||||||
}
|
}
|
||||||
}(storageDisks)
|
}(storageDisks)
|
||||||
|
|
||||||
markRootDisksAsDown(storageDisks)
|
|
||||||
|
|
||||||
formats, sErrs := loadFormatErasureAll(storageDisks, true)
|
formats, sErrs := loadFormatErasureAll(storageDisks, true)
|
||||||
if err = checkFormatErasureValues(formats, s.drivesPerSet); err != nil {
|
if err = checkFormatErasureValues(formats, s.drivesPerSet); err != nil {
|
||||||
return madmin.HealResultItem{}, err
|
return madmin.HealResultItem{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark all root disks down
|
||||||
|
markRootDisksAsDown(storageDisks, sErrs)
|
||||||
|
|
||||||
// Prepare heal-result
|
// Prepare heal-result
|
||||||
res = madmin.HealResultItem{
|
res = madmin.HealResultItem{
|
||||||
Type: madmin.HealItemMetadata,
|
Type: madmin.HealItemMetadata,
|
||||||
@@ -1599,10 +1627,10 @@ func (s *erasureSets) HealObjects(ctx context.Context, bucket, prefix string, op
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait and proceed if there are active requests
|
|
||||||
waitForLowHTTPReq(int32(s.drivesPerSet))
|
|
||||||
|
|
||||||
for _, version := range entry.Versions {
|
for _, version := range entry.Versions {
|
||||||
|
// Wait and proceed if there are active requests
|
||||||
|
waitForLowHTTPReq(int32(s.drivesPerSet), time.Second)
|
||||||
|
|
||||||
if err := healObject(bucket, version.Name, version.VersionID); err != nil {
|
if err := healObject(bucket, version.Name, version.VersionID); err != nil {
|
||||||
return toObjectErr(err, bucket, version.Name)
|
return toObjectErr(err, bucket, version.Name)
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-19
@@ -22,6 +22,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -79,7 +80,7 @@ func newErasureZones(ctx context.Context, endpointZones EndpointZones) (ObjectLa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go intDataUpdateTracker.start(GlobalContext, localDrives...)
|
go intDataUpdateTracker.start(ctx, localDrives...)
|
||||||
return z, nil
|
return z, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,9 +371,11 @@ func (z *erasureZones) CrawlAndGetDataUsage(ctx context.Context, bf *bloomFilter
|
|||||||
case v := <-updateCloser:
|
case v := <-updateCloser:
|
||||||
update()
|
update()
|
||||||
// Enforce quotas when all is done.
|
// Enforce quotas when all is done.
|
||||||
|
if firstErr == nil {
|
||||||
for _, b := range allBuckets {
|
for _, b := range allBuckets {
|
||||||
enforceFIFOQuotaBucket(ctx, z, b.Name, allMerged.bucketUsageInfo(b.Name))
|
enforceFIFOQuotaBucket(ctx, z, b.Name, allMerged.bucketUsageInfo(b.Name))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
close(v)
|
close(v)
|
||||||
return
|
return
|
||||||
case <-updateTicker.C:
|
case <-updateTicker.C:
|
||||||
@@ -387,6 +390,9 @@ func (z *erasureZones) CrawlAndGetDataUsage(ctx context.Context, bf *bloomFilter
|
|||||||
case updateCloser <- ch:
|
case updateCloser <- ch:
|
||||||
<-ch
|
<-ch
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = ctx.Err()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return firstErr
|
return firstErr
|
||||||
}
|
}
|
||||||
@@ -457,12 +463,12 @@ func (z *erasureZones) GetObjectNInfo(ctx context.Context, bucket, object string
|
|||||||
lock := z.NewNSLock(ctx, bucket, object)
|
lock := z.NewNSLock(ctx, bucket, object)
|
||||||
switch lockType {
|
switch lockType {
|
||||||
case writeLock:
|
case writeLock:
|
||||||
if err = lock.GetLock(globalObjectTimeout); err != nil {
|
if err = lock.GetLock(globalOperationTimeout); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
nsUnlocker = lock.Unlock
|
nsUnlocker = lock.Unlock
|
||||||
case readLock:
|
case readLock:
|
||||||
if err = lock.GetRLock(globalObjectTimeout); err != nil {
|
if err = lock.GetRLock(globalOperationTimeout); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
nsUnlocker = lock.RUnlock
|
nsUnlocker = lock.RUnlock
|
||||||
@@ -491,7 +497,7 @@ func (z *erasureZones) GetObjectNInfo(ctx context.Context, bucket, object string
|
|||||||
func (z *erasureZones) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) error {
|
func (z *erasureZones) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) error {
|
||||||
// Lock the object before reading.
|
// Lock the object before reading.
|
||||||
lk := z.NewNSLock(ctx, bucket, object)
|
lk := z.NewNSLock(ctx, bucket, object)
|
||||||
if err := lk.GetRLock(globalObjectTimeout); err != nil {
|
if err := lk.GetRLock(globalOperationTimeout); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer lk.RUnlock()
|
defer lk.RUnlock()
|
||||||
@@ -515,7 +521,7 @@ func (z *erasureZones) GetObject(ctx context.Context, bucket, object string, sta
|
|||||||
func (z *erasureZones) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
func (z *erasureZones) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
// Lock the object before reading.
|
// Lock the object before reading.
|
||||||
lk := z.NewNSLock(ctx, bucket, object)
|
lk := z.NewNSLock(ctx, bucket, object)
|
||||||
if err := lk.GetRLock(globalObjectTimeout); err != nil {
|
if err := lk.GetRLock(globalOperationTimeout); err != nil {
|
||||||
return ObjectInfo{}, err
|
return ObjectInfo{}, err
|
||||||
}
|
}
|
||||||
defer lk.RUnlock()
|
defer lk.RUnlock()
|
||||||
@@ -543,7 +549,7 @@ func (z *erasureZones) GetObjectInfo(ctx context.Context, bucket, object string,
|
|||||||
func (z *erasureZones) PutObject(ctx context.Context, bucket string, object string, data *PutObjReader, opts ObjectOptions) (ObjectInfo, error) {
|
func (z *erasureZones) PutObject(ctx context.Context, bucket string, object string, data *PutObjReader, opts ObjectOptions) (ObjectInfo, error) {
|
||||||
// Lock the object.
|
// Lock the object.
|
||||||
lk := z.NewNSLock(ctx, bucket, object)
|
lk := z.NewNSLock(ctx, bucket, object)
|
||||||
if err := lk.GetLock(globalObjectTimeout); err != nil {
|
if err := lk.GetLock(globalOperationTimeout); err != nil {
|
||||||
return ObjectInfo{}, err
|
return ObjectInfo{}, err
|
||||||
}
|
}
|
||||||
defer lk.Unlock()
|
defer lk.Unlock()
|
||||||
@@ -624,7 +630,7 @@ func (z *erasureZones) CopyObject(ctx context.Context, srcBucket, srcObject, dst
|
|||||||
cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject))
|
cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject))
|
||||||
if !cpSrcDstSame {
|
if !cpSrcDstSame {
|
||||||
lk := z.NewNSLock(ctx, dstBucket, dstObject)
|
lk := z.NewNSLock(ctx, dstBucket, dstObject)
|
||||||
if err := lk.GetLock(globalObjectTimeout); err != nil {
|
if err := lk.GetLock(globalOperationTimeout); err != nil {
|
||||||
return objInfo, err
|
return objInfo, err
|
||||||
}
|
}
|
||||||
defer lk.Unlock()
|
defer lk.Unlock()
|
||||||
@@ -1731,7 +1737,7 @@ func (z *erasureZones) ListBuckets(ctx context.Context) (buckets []BucketInfo, e
|
|||||||
func (z *erasureZones) ReloadFormat(ctx context.Context, dryRun bool) error {
|
func (z *erasureZones) ReloadFormat(ctx context.Context, dryRun bool) error {
|
||||||
// Acquire lock on format.json
|
// Acquire lock on format.json
|
||||||
formatLock := z.NewNSLock(ctx, minioMetaBucket, formatConfigFile)
|
formatLock := z.NewNSLock(ctx, minioMetaBucket, formatConfigFile)
|
||||||
if err := formatLock.GetRLock(globalHealingTimeout); err != nil {
|
if err := formatLock.GetRLock(globalOperationTimeout); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer formatLock.RUnlock()
|
defer formatLock.RUnlock()
|
||||||
@@ -1747,7 +1753,7 @@ func (z *erasureZones) ReloadFormat(ctx context.Context, dryRun bool) error {
|
|||||||
func (z *erasureZones) HealFormat(ctx context.Context, dryRun bool) (madmin.HealResultItem, error) {
|
func (z *erasureZones) HealFormat(ctx context.Context, dryRun bool) (madmin.HealResultItem, error) {
|
||||||
// Acquire lock on format.json
|
// Acquire lock on format.json
|
||||||
formatLock := z.NewNSLock(ctx, minioMetaBucket, formatConfigFile)
|
formatLock := z.NewNSLock(ctx, minioMetaBucket, formatConfigFile)
|
||||||
if err := formatLock.GetLock(globalHealingTimeout); err != nil {
|
if err := formatLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
return madmin.HealResultItem{}, err
|
return madmin.HealResultItem{}, err
|
||||||
}
|
}
|
||||||
defer formatLock.Unlock()
|
defer formatLock.Unlock()
|
||||||
@@ -1897,7 +1903,7 @@ func (z *erasureZones) Walk(ctx context.Context, bucket, prefix string, results
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HealObjectFn closure function heals the object.
|
// HealObjectFn closure function heals the object.
|
||||||
type HealObjectFn func(string, string, string) error
|
type HealObjectFn func(bucket, object, versionID string) error
|
||||||
|
|
||||||
func (z *erasureZones) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, healObject HealObjectFn) error {
|
func (z *erasureZones) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, healObject HealObjectFn) error {
|
||||||
var zonesEntryChs [][]FileInfoVersionsCh
|
var zonesEntryChs [][]FileInfoVersionsCh
|
||||||
@@ -1922,42 +1928,51 @@ func (z *erasureZones) HealObjects(ctx context.Context, bucket, prefix string, o
|
|||||||
zonesEntriesValid = append(zonesEntriesValid, make([]bool, len(entryChs)))
|
zonesEntriesValid = append(zonesEntriesValid, make([]bool, len(entryChs)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If listing did not return any entries upon first attempt, we
|
||||||
|
// return `ObjectNotFound`, to indicate the caller for any
|
||||||
|
// actions they may want to take as if `prefix` is missing.
|
||||||
|
err := toObjectErr(errFileNotFound, bucket, prefix)
|
||||||
for {
|
for {
|
||||||
entry, quorumCount, zoneIndex, ok := lexicallySortedEntryZoneVersions(zonesEntryChs, zonesEntriesInfos, zonesEntriesValid)
|
entry, quorumCount, zoneIndex, ok := lexicallySortedEntryZoneVersions(zonesEntryChs, zonesEntriesInfos, zonesEntriesValid)
|
||||||
if !ok {
|
if !ok {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
// Indicate that first attempt was a success and subsequent loop
|
||||||
|
// knows that its not our first attempt at 'prefix'
|
||||||
|
err = nil
|
||||||
|
|
||||||
|
if zoneIndex >= len(zoneDrivesPerSet) || zoneIndex < 0 {
|
||||||
|
return fmt.Errorf("invalid zone index returned: %d", zoneIndex)
|
||||||
|
}
|
||||||
if quorumCount == zoneDrivesPerSet[zoneIndex] && opts.ScanMode == madmin.HealNormalScan {
|
if quorumCount == zoneDrivesPerSet[zoneIndex] && opts.ScanMode == madmin.HealNormalScan {
|
||||||
// Skip good entries.
|
// Skip good entries.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait and proceed if there are active requests
|
|
||||||
waitForLowHTTPReq(int32(zoneDrivesPerSet[zoneIndex]))
|
|
||||||
|
|
||||||
for _, version := range entry.Versions {
|
for _, version := range entry.Versions {
|
||||||
|
// Wait and proceed if there are active requests
|
||||||
|
waitForLowHTTPReq(int32(zoneDrivesPerSet[zoneIndex]), time.Second)
|
||||||
if err := healObject(bucket, version.Name, version.VersionID); err != nil {
|
if err := healObject(bucket, version.Name, version.VersionID); err != nil {
|
||||||
return toObjectErr(err, bucket, version.Name)
|
return toObjectErr(err, bucket, version.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z *erasureZones) HealObject(ctx context.Context, bucket, object, versionID string, opts madmin.HealOpts) (madmin.HealResultItem, error) {
|
func (z *erasureZones) HealObject(ctx context.Context, bucket, object, versionID string, opts madmin.HealOpts) (madmin.HealResultItem, error) {
|
||||||
lk := z.NewNSLock(ctx, bucket, object)
|
lk := z.NewNSLock(ctx, bucket, object)
|
||||||
if bucket == minioMetaBucket {
|
if bucket == minioMetaBucket {
|
||||||
// For .minio.sys bucket heals we should hold write locks.
|
// For .minio.sys bucket heals we should hold write locks.
|
||||||
if err := lk.GetLock(globalHealingTimeout); err != nil {
|
if err := lk.GetLock(globalOperationTimeout); err != nil {
|
||||||
return madmin.HealResultItem{}, err
|
return madmin.HealResultItem{}, err
|
||||||
}
|
}
|
||||||
defer lk.Unlock()
|
defer lk.Unlock()
|
||||||
} else {
|
} else {
|
||||||
// Lock the object before healing. Use read lock since healing
|
// Lock the object before healing. Use read lock since healing
|
||||||
// will only regenerate parts & xl.meta of outdated disks.
|
// will only regenerate parts & xl.meta of outdated disks.
|
||||||
if err := lk.GetRLock(globalHealingTimeout); err != nil {
|
if err := lk.GetRLock(globalOperationTimeout); err != nil {
|
||||||
return madmin.HealResultItem{}, err
|
return madmin.HealResultItem{}, err
|
||||||
}
|
}
|
||||||
defer lk.RUnlock()
|
defer lk.RUnlock()
|
||||||
@@ -2063,6 +2078,8 @@ func (z *erasureZones) Health(ctx context.Context, opts HealthOptions) HealthRes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reqInfo := (&logger.ReqInfo{}).AppendTags("maintenance", strconv.FormatBool(opts.Maintenance))
|
||||||
|
|
||||||
for zoneIdx := range erasureSetUpCount {
|
for zoneIdx := range erasureSetUpCount {
|
||||||
parityDrives := globalStorageClass.GetParityForSC(storageclass.STANDARD)
|
parityDrives := globalStorageClass.GetParityForSC(storageclass.STANDARD)
|
||||||
diskCount := z.zones[zoneIdx].drivesPerSet
|
diskCount := z.zones[zoneIdx].drivesPerSet
|
||||||
@@ -2076,7 +2093,8 @@ func (z *erasureZones) Health(ctx context.Context, opts HealthOptions) HealthRes
|
|||||||
}
|
}
|
||||||
for setIdx := range erasureSetUpCount[zoneIdx] {
|
for setIdx := range erasureSetUpCount[zoneIdx] {
|
||||||
if erasureSetUpCount[zoneIdx][setIdx] < writeQuorum {
|
if erasureSetUpCount[zoneIdx][setIdx] < writeQuorum {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Write quorum lost on zone: %d, set: %d, expected write quorum: %d",
|
logger.LogIf(logger.SetReqInfo(ctx, reqInfo),
|
||||||
|
fmt.Errorf("Write quorum may be lost on zone: %d, set: %d, expected write quorum: %d",
|
||||||
zoneIdx, setIdx, writeQuorum))
|
zoneIdx, setIdx, writeQuorum))
|
||||||
return HealthResult{
|
return HealthResult{
|
||||||
Healthy: false,
|
Healthy: false,
|
||||||
@@ -2101,14 +2119,14 @@ func (z *erasureZones) Health(ctx context.Context, opts HealthOptions) HealthRes
|
|||||||
// is not taken down for maintenance
|
// is not taken down for maintenance
|
||||||
aggHealStateResult, err := getAggregatedBackgroundHealState(ctx, true)
|
aggHealStateResult, err := getAggregatedBackgroundHealState(ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to verify global heal status: %w", err))
|
logger.LogIf(logger.SetReqInfo(ctx, reqInfo), fmt.Errorf("Unable to verify global heal status: %w", err))
|
||||||
return HealthResult{
|
return HealthResult{
|
||||||
Healthy: false,
|
Healthy: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(aggHealStateResult.HealDisks) > 0 {
|
if len(aggHealStateResult.HealDisks) > 0 {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Total drives to be healed %d", len(aggHealStateResult.HealDisks)))
|
logger.LogIf(logger.SetReqInfo(ctx, reqInfo), fmt.Errorf("Total drives to be healed %d", len(aggHealStateResult.HealDisks)))
|
||||||
}
|
}
|
||||||
|
|
||||||
healthy := len(aggHealStateResult.HealDisks) == 0
|
healthy := len(aggHealStateResult.HealDisks) == 0
|
||||||
|
|||||||
+49
-12
@@ -154,6 +154,7 @@ func getDisksInfo(disks []StorageAPI, endpoints []string) (disksInfo []madmin.Di
|
|||||||
UsedSpace: info.Used,
|
UsedSpace: info.Used,
|
||||||
AvailableSpace: info.Free,
|
AvailableSpace: info.Free,
|
||||||
UUID: info.ID,
|
UUID: info.ID,
|
||||||
|
RootDisk: info.RootDisk,
|
||||||
State: diskErrToDriveState(err),
|
State: diskErrToDriveState(err),
|
||||||
}
|
}
|
||||||
if info.Total > 0 {
|
if info.Total > 0 {
|
||||||
@@ -175,7 +176,27 @@ func getDisksInfo(disks []StorageAPI, endpoints []string) (disksInfo []madmin.Di
|
|||||||
onlineDisks[ep]++
|
onlineDisks[ep]++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rootDiskCount := 0
|
||||||
|
for _, di := range disksInfo {
|
||||||
|
if di.RootDisk {
|
||||||
|
rootDiskCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(disksInfo) == rootDiskCount {
|
||||||
// Success.
|
// Success.
|
||||||
|
return disksInfo, errs, onlineDisks, offlineDisks
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root disk should be considered offline
|
||||||
|
for i := range disksInfo {
|
||||||
|
ep := disksInfo[i].Endpoint
|
||||||
|
if disksInfo[i].RootDisk {
|
||||||
|
offlineDisks[ep]++
|
||||||
|
onlineDisks[ep]--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return disksInfo, errs, onlineDisks, offlineDisks
|
return disksInfo, errs, onlineDisks, offlineDisks
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,21 +256,45 @@ func (er erasureObjects) CrawlAndGetDataUsage(ctx context.Context, bf *bloomFilt
|
|||||||
// CrawlAndGetDataUsage will start crawling buckets and send updated totals as they are traversed.
|
// CrawlAndGetDataUsage will start crawling buckets and send updated totals as they are traversed.
|
||||||
// Updates are sent on a regular basis and the caller *must* consume them.
|
// Updates are sent on a regular basis and the caller *must* consume them.
|
||||||
func (er erasureObjects) crawlAndGetDataUsage(ctx context.Context, buckets []BucketInfo, bf *bloomFilter, updates chan<- dataUsageCache) error {
|
func (er erasureObjects) crawlAndGetDataUsage(ctx context.Context, buckets []BucketInfo, bf *bloomFilter, updates chan<- dataUsageCache) error {
|
||||||
var disks []StorageAPI
|
if len(buckets) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect any disk healing.
|
||||||
|
healing, err := getAggregatedBackgroundHealState(ctx, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
healDisks := make(map[string]struct{}, len(healing.HealDisks))
|
||||||
|
for _, disk := range healing.HealDisks {
|
||||||
|
healDisks[disk] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect disks we can use.
|
||||||
|
var disks []StorageAPI
|
||||||
for _, d := range er.getLoadBalancedDisks() {
|
for _, d := range er.getLoadBalancedDisks() {
|
||||||
if d == nil || !d.IsOnline() {
|
if d == nil || !d.IsOnline() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
di, err := d.DiskInfo()
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := healDisks[di.Endpoint]; ok {
|
||||||
|
logger.Info(color.Green("data-crawl:")+" Disk %q is Healing, skipping disk.", di.Endpoint)
|
||||||
|
continue
|
||||||
|
}
|
||||||
disks = append(disks, d)
|
disks = append(disks, d)
|
||||||
}
|
}
|
||||||
if len(disks) == 0 || len(buckets) == 0 {
|
if len(disks) == 0 {
|
||||||
|
logger.Info(color.Green("data-crawl:") + " No disks found, skipping crawl")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load bucket totals
|
// Load bucket totals
|
||||||
oldCache := dataUsageCache{}
|
oldCache := dataUsageCache{}
|
||||||
err := oldCache.load(ctx, er, dataUsageCacheName)
|
err = oldCache.load(ctx, er, dataUsageCacheName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -273,20 +318,12 @@ func (er erasureObjects) crawlAndGetDataUsage(ctx context.Context, buckets []Buc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add existing buckets if changes or lifecycles.
|
// Add existing buckets.
|
||||||
for _, b := range buckets {
|
for _, b := range buckets {
|
||||||
e := oldCache.find(b.Name)
|
e := oldCache.find(b.Name)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
cache.replace(b.Name, dataUsageRoot, *e)
|
cache.replace(b.Name, dataUsageRoot, *e)
|
||||||
lc, err := globalLifecycleSys.Get(b.Name)
|
|
||||||
activeLC := err == nil && lc.HasActiveRules("", true)
|
|
||||||
if activeLC || bf == nil || bf.containsDir(b.Name) {
|
|
||||||
bucketCh <- b
|
bucketCh <- b
|
||||||
} else {
|
|
||||||
if intDataUpdateTracker.debug {
|
|
||||||
logger.Info(color.Green("crawlAndGetDataUsage:")+" Skipping bucket %v, not updated", b.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ func migrateCacheData(ctx context.Context, c *diskCache, bucket, object, oldfile
|
|||||||
}
|
}
|
||||||
actualSize, _ = sio.EncryptedSize(uint64(st.Size()))
|
actualSize, _ = sio.EncryptedSize(uint64(st.Size()))
|
||||||
}
|
}
|
||||||
_, err = c.bitrotWriteToCache(destDir, cacheDataFile, reader, uint64(actualSize))
|
_, err = c.bitrotWriteToCache(destDir, cacheDataFile, reader, actualSize)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-12
@@ -335,11 +335,13 @@ func loadFormatErasureAll(storageDisks []StorageAPI, heal bool) ([]*formatErasur
|
|||||||
return formats, g.Wait()
|
return formats, g.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveFormatErasure(disk StorageAPI, format interface{}, diskID string) error {
|
func saveFormatErasure(disk StorageAPI, format *formatErasureV3) error {
|
||||||
if format == nil || disk == nil {
|
if disk == nil || format == nil {
|
||||||
return errDiskNotFound
|
return errDiskNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diskID := format.Erasure.This
|
||||||
|
|
||||||
if err := makeFormatErasureMetaVolumes(disk); err != nil {
|
if err := makeFormatErasureMetaVolumes(disk); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -549,7 +551,7 @@ func formatErasureFixLocalDeploymentID(endpoints Endpoints, storageDisks []Stora
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
format.ID = refFormat.ID
|
format.ID = refFormat.ID
|
||||||
if err := saveFormatErasure(storageDisks[index], format, format.Erasure.This); err != nil {
|
if err := saveFormatErasure(storageDisks[index], format); err != nil {
|
||||||
logger.LogIf(GlobalContext, err)
|
logger.LogIf(GlobalContext, err)
|
||||||
return fmt.Errorf("Unable to save format.json, %w", err)
|
return fmt.Errorf("Unable to save format.json, %w", err)
|
||||||
}
|
}
|
||||||
@@ -695,7 +697,7 @@ func saveFormatErasureAll(ctx context.Context, storageDisks []StorageAPI, format
|
|||||||
if formats[index] == nil {
|
if formats[index] == nil {
|
||||||
return errDiskNotFound
|
return errDiskNotFound
|
||||||
}
|
}
|
||||||
return saveFormatErasure(storageDisks[index], formats[index], formats[index].Erasure.This)
|
return saveFormatErasure(storageDisks[index], formats[index])
|
||||||
}, index)
|
}, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,13 +724,9 @@ func initStorageDisksWithErrors(endpoints Endpoints) ([]StorageAPI, []error) {
|
|||||||
g := errgroup.WithNErrs(len(endpoints))
|
g := errgroup.WithNErrs(len(endpoints))
|
||||||
for index := range endpoints {
|
for index := range endpoints {
|
||||||
index := index
|
index := index
|
||||||
g.Go(func() error {
|
g.Go(func() (err error) {
|
||||||
storageDisk, err := newStorageAPI(endpoints[index])
|
storageDisks[index], err = newStorageAPI(endpoints[index])
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
storageDisks[index] = storageDisk
|
|
||||||
return nil
|
|
||||||
}, index)
|
}, index)
|
||||||
}
|
}
|
||||||
return storageDisks, g.Wait()
|
return storageDisks, g.Wait()
|
||||||
@@ -773,7 +771,7 @@ func fixFormatErasureV3(storageDisks []StorageAPI, endpoints Endpoints, formats
|
|||||||
}
|
}
|
||||||
if formats[i].Erasure.This == "" {
|
if formats[i].Erasure.This == "" {
|
||||||
formats[i].Erasure.This = formats[i].Erasure.Sets[0][i]
|
formats[i].Erasure.This = formats[i].Erasure.Sets[0][i]
|
||||||
if err := saveFormatErasure(storageDisks[i], formats[i], formats[i].Erasure.This); err != nil {
|
if err := saveFormatErasure(storageDisks[i], formats[i]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -790,7 +788,7 @@ func fixFormatErasureV3(storageDisks []StorageAPI, endpoints Endpoints, formats
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initFormatErasure - save Erasure format configuration on all disks.
|
// initFormatErasure - save Erasure format configuration on all disks.
|
||||||
func initFormatErasure(ctx context.Context, storageDisks []StorageAPI, setCount, drivesPerSet int, deploymentID string) (*formatErasureV3, error) {
|
func initFormatErasure(ctx context.Context, storageDisks []StorageAPI, setCount, drivesPerSet int, deploymentID string, sErrs []error) (*formatErasureV3, error) {
|
||||||
format := newFormatErasureV3(setCount, drivesPerSet)
|
format := newFormatErasureV3(setCount, drivesPerSet)
|
||||||
formats := make([]*formatErasureV3, len(storageDisks))
|
formats := make([]*formatErasureV3, len(storageDisks))
|
||||||
wantAtMost := ecDrivesNoConfig(drivesPerSet)
|
wantAtMost := ecDrivesNoConfig(drivesPerSet)
|
||||||
@@ -831,6 +829,9 @@ func initFormatErasure(ctx context.Context, storageDisks []StorageAPI, setCount,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark all root disks down
|
||||||
|
markRootDisksAsDown(storageDisks, sErrs)
|
||||||
|
|
||||||
// Save formats `format.json` across all disks.
|
// Save formats `format.json` across all disks.
|
||||||
if err := saveFormatErasureAll(ctx, storageDisks, formats); err != nil {
|
if err := saveFormatErasureAll(ctx, storageDisks, formats); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -707,7 +707,7 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
|||||||
|
|
||||||
// Hold write lock on the object.
|
// Hold write lock on the object.
|
||||||
destLock := fs.NewNSLock(ctx, bucket, object)
|
destLock := fs.NewNSLock(ctx, bucket, object)
|
||||||
if err = destLock.GetLock(globalObjectTimeout); err != nil {
|
if err = destLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
return oi, err
|
return oi, err
|
||||||
}
|
}
|
||||||
defer destLock.Unlock()
|
defer destLock.Unlock()
|
||||||
|
|||||||
+30
-16
@@ -179,7 +179,7 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
|||||||
fs.fsFormatRlk = rlk
|
fs.fsFormatRlk = rlk
|
||||||
|
|
||||||
go fs.cleanupStaleMultipartUploads(ctx, GlobalMultipartCleanupInterval, GlobalMultipartExpiry)
|
go fs.cleanupStaleMultipartUploads(ctx, GlobalMultipartCleanupInterval, GlobalMultipartExpiry)
|
||||||
go intDataUpdateTracker.start(GlobalContext, fsPath)
|
go intDataUpdateTracker.start(ctx, fsPath)
|
||||||
|
|
||||||
// Return successfully initialized object layer.
|
// Return successfully initialized object layer.
|
||||||
return fs, nil
|
return fs, nil
|
||||||
@@ -352,6 +352,8 @@ func (fs *FSObjects) crawlBucket(ctx context.Context, bucket string, cache dataU
|
|||||||
if fiErr != nil {
|
if fiErr != nil {
|
||||||
return 0, errSkipFile
|
return 0, errSkipFile
|
||||||
}
|
}
|
||||||
|
// We cannot heal in FS mode.
|
||||||
|
item.heal = false
|
||||||
|
|
||||||
oi := fsMeta.ToObjectInfo(bucket, object, fi)
|
oi := fsMeta.ToObjectInfo(bucket, object, fi)
|
||||||
sz := item.applyActions(ctx, fs, actionMeta{oi: oi})
|
sz := item.applyActions(ctx, fs, actionMeta{oi: oi})
|
||||||
@@ -601,7 +603,7 @@ func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
|
|||||||
|
|
||||||
if !cpSrcDstSame {
|
if !cpSrcDstSame {
|
||||||
objectDWLock := fs.NewNSLock(ctx, dstBucket, dstObject)
|
objectDWLock := fs.NewNSLock(ctx, dstBucket, dstObject)
|
||||||
if err := objectDWLock.GetLock(globalObjectTimeout); err != nil {
|
if err := objectDWLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
return oi, err
|
return oi, err
|
||||||
}
|
}
|
||||||
defer objectDWLock.Unlock()
|
defer objectDWLock.Unlock()
|
||||||
@@ -691,12 +693,12 @@ func (fs *FSObjects) GetObjectNInfo(ctx context.Context, bucket, object string,
|
|||||||
lock := fs.NewNSLock(ctx, bucket, object)
|
lock := fs.NewNSLock(ctx, bucket, object)
|
||||||
switch lockType {
|
switch lockType {
|
||||||
case writeLock:
|
case writeLock:
|
||||||
if err = lock.GetLock(globalObjectTimeout); err != nil {
|
if err = lock.GetLock(globalOperationTimeout); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
nsUnlocker = lock.Unlock
|
nsUnlocker = lock.Unlock
|
||||||
case readLock:
|
case readLock:
|
||||||
if err = lock.GetRLock(globalObjectTimeout); err != nil {
|
if err = lock.GetRLock(globalOperationTimeout); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
nsUnlocker = lock.RUnlock
|
nsUnlocker = lock.RUnlock
|
||||||
@@ -709,7 +711,7 @@ func (fs *FSObjects) GetObjectNInfo(ctx context.Context, bucket, object string,
|
|||||||
nsUnlocker()
|
nsUnlocker()
|
||||||
return nil, toObjectErr(err, bucket, object)
|
return nil, toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
// For a directory, we need to send an reader that returns no bytes.
|
// For a directory, we need to return a reader that returns no bytes.
|
||||||
if HasSuffix(object, SlashSeparator) {
|
if HasSuffix(object, SlashSeparator) {
|
||||||
// The lock taken above is released when
|
// The lock taken above is released when
|
||||||
// objReader.Close() is called by the caller.
|
// objReader.Close() is called by the caller.
|
||||||
@@ -782,7 +784,7 @@ func (fs *FSObjects) GetObject(ctx context.Context, bucket, object string, offse
|
|||||||
|
|
||||||
// Lock the object before reading.
|
// Lock the object before reading.
|
||||||
lk := fs.NewNSLock(ctx, bucket, object)
|
lk := fs.NewNSLock(ctx, bucket, object)
|
||||||
if err := lk.GetRLock(globalObjectTimeout); err != nil {
|
if err := lk.GetRLock(globalOperationTimeout); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -958,6 +960,10 @@ func (fs *FSObjects) getObjectInfoNoFSLock(ctx context.Context, bucket, object s
|
|||||||
|
|
||||||
// getObjectInfo - wrapper for reading object metadata and constructs ObjectInfo.
|
// getObjectInfo - wrapper for reading object metadata and constructs ObjectInfo.
|
||||||
func (fs *FSObjects) getObjectInfo(ctx context.Context, bucket, object string) (oi ObjectInfo, e error) {
|
func (fs *FSObjects) getObjectInfo(ctx context.Context, bucket, object string) (oi ObjectInfo, e error) {
|
||||||
|
if strings.HasSuffix(object, SlashSeparator) && !fs.isObjectDir(bucket, object) {
|
||||||
|
return oi, errFileNotFound
|
||||||
|
}
|
||||||
|
|
||||||
fsMeta := fsMetaV1{}
|
fsMeta := fsMetaV1{}
|
||||||
if HasSuffix(object, SlashSeparator) {
|
if HasSuffix(object, SlashSeparator) {
|
||||||
fi, err := fsStatDir(ctx, pathJoin(fs.fsPath, bucket, object))
|
fi, err := fsStatDir(ctx, pathJoin(fs.fsPath, bucket, object))
|
||||||
@@ -1006,7 +1012,7 @@ func (fs *FSObjects) getObjectInfo(ctx context.Context, bucket, object string) (
|
|||||||
func (fs *FSObjects) getObjectInfoWithLock(ctx context.Context, bucket, object string) (oi ObjectInfo, e error) {
|
func (fs *FSObjects) getObjectInfoWithLock(ctx context.Context, bucket, object string) (oi ObjectInfo, e error) {
|
||||||
// Lock the object before reading.
|
// Lock the object before reading.
|
||||||
lk := fs.NewNSLock(ctx, bucket, object)
|
lk := fs.NewNSLock(ctx, bucket, object)
|
||||||
if err := lk.GetRLock(globalObjectTimeout); err != nil {
|
if err := lk.GetRLock(globalOperationTimeout); err != nil {
|
||||||
return oi, err
|
return oi, err
|
||||||
}
|
}
|
||||||
defer lk.RUnlock()
|
defer lk.RUnlock()
|
||||||
@@ -1044,7 +1050,7 @@ func (fs *FSObjects) GetObjectInfo(ctx context.Context, bucket, object string, o
|
|||||||
oi, err := fs.getObjectInfoWithLock(ctx, bucket, object)
|
oi, err := fs.getObjectInfoWithLock(ctx, bucket, object)
|
||||||
if err == errCorruptedFormat || err == io.EOF {
|
if err == errCorruptedFormat || err == io.EOF {
|
||||||
lk := fs.NewNSLock(ctx, bucket, object)
|
lk := fs.NewNSLock(ctx, bucket, object)
|
||||||
if err = lk.GetLock(globalObjectTimeout); err != nil {
|
if err = lk.GetLock(globalOperationTimeout); err != nil {
|
||||||
return oi, toObjectErr(err, bucket, object)
|
return oi, toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1095,7 +1101,7 @@ func (fs *FSObjects) PutObject(ctx context.Context, bucket string, object string
|
|||||||
|
|
||||||
// Lock the object.
|
// Lock the object.
|
||||||
lk := fs.NewNSLock(ctx, bucket, object)
|
lk := fs.NewNSLock(ctx, bucket, object)
|
||||||
if err := lk.GetLock(globalObjectTimeout); err != nil {
|
if err := lk.GetLock(globalOperationTimeout); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return objInfo, err
|
return objInfo, err
|
||||||
}
|
}
|
||||||
@@ -1328,23 +1334,31 @@ func (fs *FSObjects) DeleteObject(ctx context.Context, bucket, object string, op
|
|||||||
return ObjectInfo{Bucket: bucket, Name: object}, nil
|
return ObjectInfo{Bucket: bucket, Name: object}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fs *FSObjects) isLeafDir(bucket string, leafPath string) bool {
|
||||||
|
return fs.isObjectDir(bucket, leafPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FSObjects) isLeaf(bucket string, leafPath string) bool {
|
||||||
|
return !strings.HasSuffix(leafPath, slashSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
// Returns function "listDir" of the type listDirFunc.
|
// Returns function "listDir" of the type listDirFunc.
|
||||||
// isLeaf - is used by listDir function to check if an entry
|
// isLeaf - is used by listDir function to check if an entry
|
||||||
// is a leaf or non-leaf entry.
|
// is a leaf or non-leaf entry.
|
||||||
func (fs *FSObjects) listDirFactory() ListDirFunc {
|
func (fs *FSObjects) listDirFactory() ListDirFunc {
|
||||||
// listDir - lists all the entries at a given prefix and given entry in the prefix.
|
// listDir - lists all the entries at a given prefix and given entry in the prefix.
|
||||||
listDir := func(bucket, prefixDir, prefixEntry string) (emptyDir bool, entries []string) {
|
listDir := func(bucket, prefixDir, prefixEntry string) (emptyDir bool, entries []string, delayIsLeaf bool) {
|
||||||
var err error
|
var err error
|
||||||
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
|
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
|
||||||
if err != nil && err != errFileNotFound {
|
if err != nil && err != errFileNotFound {
|
||||||
logger.LogIf(GlobalContext, err)
|
logger.LogIf(GlobalContext, err)
|
||||||
return false, nil
|
return false, nil, false
|
||||||
}
|
}
|
||||||
if len(entries) == 0 {
|
if len(entries) == 0 {
|
||||||
return true, nil
|
return true, nil, false
|
||||||
}
|
}
|
||||||
sort.Strings(entries)
|
entries, delayIsLeaf = filterListEntries(bucket, prefixDir, entries, prefixEntry, fs.isLeaf)
|
||||||
return false, filterMatchingPrefix(entries, prefixEntry)
|
return false, entries, delayIsLeaf
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return list factory instance.
|
// Return list factory instance.
|
||||||
@@ -1447,7 +1461,7 @@ func (fs *FSObjects) ListObjects(ctx context.Context, bucket, prefix, marker, de
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
return listObjects(ctx, fs, bucket, prefix, marker, delimiter, maxKeys, fs.listPool,
|
return listObjects(ctx, fs, bucket, prefix, marker, delimiter, maxKeys, fs.listPool,
|
||||||
fs.listDirFactory(), fs.getObjectInfoNoFSLock, fs.getObjectInfoNoFSLock)
|
fs.listDirFactory(), fs.isLeaf, fs.isLeafDir, fs.getObjectInfoNoFSLock, fs.getObjectInfoNoFSLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetObjectTags - get object tags from an existing object
|
// GetObjectTags - get object tags from an existing object
|
||||||
@@ -1544,7 +1558,7 @@ func (fs *FSObjects) HealBucket(ctx context.Context, bucket string, dryRun, remo
|
|||||||
// error walker returns error. Optionally if context.Done() is received
|
// error walker returns error. Optionally if context.Done() is received
|
||||||
// then Walk() stops the walker.
|
// then Walk() stops the walker.
|
||||||
func (fs *FSObjects) Walk(ctx context.Context, bucket, prefix string, results chan<- ObjectInfo, opts ObjectOptions) error {
|
func (fs *FSObjects) Walk(ctx context.Context, bucket, prefix string, results chan<- ObjectInfo, opts ObjectOptions) error {
|
||||||
return fsWalk(ctx, fs, bucket, prefix, fs.listDirFactory(), results, fs.getObjectInfoNoFSLock, fs.getObjectInfoNoFSLock)
|
return fsWalk(ctx, fs, bucket, prefix, fs.listDirFactory(), fs.isLeaf, fs.isLeafDir, results, fs.getObjectInfoNoFSLock, fs.getObjectInfoNoFSLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealObjects - no-op for fs. Valid only for Erasure.
|
// HealObjects - no-op for fs. Valid only for Erasure.
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ var (
|
|||||||
// ListObjects function alias.
|
// ListObjects function alias.
|
||||||
ListObjects = listObjects
|
ListObjects = listObjects
|
||||||
|
|
||||||
// FilterMatchingPrefix function alias.
|
// FilterListEntries function alias.
|
||||||
FilterMatchingPrefix = filterMatchingPrefix
|
FilterListEntries = filterListEntries
|
||||||
|
|
||||||
// IsStringEqual is string equal.
|
// IsStringEqual is string equal.
|
||||||
IsStringEqual = isStringEqual
|
IsStringEqual = isStringEqual
|
||||||
|
|||||||
+6
-1
@@ -180,6 +180,11 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
|||||||
globalRootCAs, err = certs.GetRootCAs(globalCertsCADir.Get())
|
globalRootCAs, err = certs.GetRootCAs(globalCertsCADir.Get())
|
||||||
logger.FatalIf(err, "Failed to read root CAs (%v)", err)
|
logger.FatalIf(err, "Failed to read root CAs (%v)", err)
|
||||||
|
|
||||||
|
// Add the global public crts as part of global root CAs
|
||||||
|
for _, publicCrt := range globalPublicCerts {
|
||||||
|
globalRootCAs.AddCert(publicCrt)
|
||||||
|
}
|
||||||
|
|
||||||
// Register root CAs for remote ENVs
|
// Register root CAs for remote ENVs
|
||||||
env.RegisterGlobalCAs(globalRootCAs)
|
env.RegisterGlobalCAs(globalRootCAs)
|
||||||
|
|
||||||
@@ -310,7 +315,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(err, "Unable to list buckets")
|
logger.Fatal(err, "Unable to list buckets")
|
||||||
}
|
}
|
||||||
logger.FatalIf(globalNotificationSys.Init(buckets, newObject), "Unable to initialize notification system")
|
logger.FatalIf(globalNotificationSys.Init(GlobalContext, buckets, newObject), "Unable to initialize notification system")
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalEtcdClient != nil {
|
if globalEtcdClient != nil {
|
||||||
|
|||||||
@@ -530,7 +530,7 @@ func checkAzureUploadID(ctx context.Context, uploadID string) (err error) {
|
|||||||
func parseAzurePart(metaPartFileName, prefix string) (partID int, err error) {
|
func parseAzurePart(metaPartFileName, prefix string) (partID int, err error) {
|
||||||
partStr := strings.TrimPrefix(metaPartFileName, prefix+minio.SlashSeparator)
|
partStr := strings.TrimPrefix(metaPartFileName, prefix+minio.SlashSeparator)
|
||||||
if partID, err = strconv.Atoi(partStr); err != nil || partID <= 0 {
|
if partID, err = strconv.Atoi(partStr); err != nil || partID <= 0 {
|
||||||
err = fmt.Errorf("invalid part number in block id '%s'", string(partID))
|
err = fmt.Errorf("invalid part number in block id '%d'", partID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -478,7 +478,7 @@ func TestGCSAttrsToObjectInfo(t *testing.T) {
|
|||||||
if objInfo.Bucket != attrs.Bucket {
|
if objInfo.Bucket != attrs.Bucket {
|
||||||
t.Fatalf("Test failed with Bucket mistmatch, expected %s, got %s", attrs.Bucket, objInfo.Bucket)
|
t.Fatalf("Test failed with Bucket mistmatch, expected %s, got %s", attrs.Bucket, objInfo.Bucket)
|
||||||
}
|
}
|
||||||
if objInfo.ModTime != attrs.Updated {
|
if !objInfo.ModTime.Equal(attrs.Updated) {
|
||||||
t.Fatalf("Test failed with ModTime mistmatch, expected %s, got %s", attrs.Updated, objInfo.ModTime)
|
t.Fatalf("Test failed with ModTime mistmatch, expected %s, got %s", attrs.Updated, objInfo.ModTime)
|
||||||
}
|
}
|
||||||
if objInfo.Size != attrs.Size {
|
if objInfo.Size != attrs.Size {
|
||||||
|
|||||||
@@ -348,9 +348,17 @@ func (n *hdfsObjects) ListBuckets(ctx context.Context) (buckets []minio.BucketIn
|
|||||||
return buckets, nil
|
return buckets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *hdfsObjects) isLeafDir(bucket, leafPath string) bool {
|
||||||
|
return n.isObjectDir(context.Background(), bucket, leafPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *hdfsObjects) isLeaf(bucket, leafPath string) bool {
|
||||||
|
return !strings.HasSuffix(leafPath, hdfsSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
func (n *hdfsObjects) listDirFactory() minio.ListDirFunc {
|
func (n *hdfsObjects) listDirFactory() minio.ListDirFunc {
|
||||||
// listDir - lists all the entries at a given prefix and given entry in the prefix.
|
// listDir - lists all the entries at a given prefix and given entry in the prefix.
|
||||||
listDir := func(bucket, prefixDir, prefixEntry string) (emptyDir bool, entries []string) {
|
listDir := func(bucket, prefixDir, prefixEntry string) (emptyDir bool, entries []string, delayIsLeaf bool) {
|
||||||
f, err := n.clnt.Open(n.hdfsPathJoin(bucket, prefixDir))
|
f, err := n.clnt.Open(n.hdfsPathJoin(bucket, prefixDir))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -366,7 +374,7 @@ func (n *hdfsObjects) listDirFactory() minio.ListDirFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(fis) == 0 {
|
if len(fis) == 0 {
|
||||||
return true, nil
|
return true, nil, false
|
||||||
}
|
}
|
||||||
for _, fi := range fis {
|
for _, fi := range fis {
|
||||||
if fi.IsDir() {
|
if fi.IsDir() {
|
||||||
@@ -375,7 +383,8 @@ func (n *hdfsObjects) listDirFactory() minio.ListDirFunc {
|
|||||||
entries = append(entries, fi.Name())
|
entries = append(entries, fi.Name())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, minio.FilterMatchingPrefix(entries, prefixEntry)
|
entries, delayIsLeaf = minio.FilterListEntries(bucket, prefixDir, entries, prefixEntry, n.isLeaf)
|
||||||
|
return false, entries, delayIsLeaf
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return list factory instance.
|
// Return list factory instance.
|
||||||
@@ -426,7 +435,7 @@ func (n *hdfsObjects) ListObjects(ctx context.Context, bucket, prefix, marker, d
|
|||||||
return objectInfo, nil
|
return objectInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return minio.ListObjects(ctx, n, bucket, prefix, marker, delimiter, maxKeys, n.listPool, n.listDirFactory(), getObjectInfo, getObjectInfo)
|
return minio.ListObjects(ctx, n, bucket, prefix, marker, delimiter, maxKeys, n.listPool, n.listDirFactory(), n.isLeaf, n.isLeafDir, getObjectInfo, getObjectInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lists a path's direct, first-level entries and populates them in the `fileInfos` cache which maps
|
// Lists a path's direct, first-level entries and populates them in the `fileInfos` cache which maps
|
||||||
|
|||||||
+1
-68
@@ -29,8 +29,6 @@ const (
|
|||||||
// sleep for an hour after a lock timeout
|
// sleep for an hour after a lock timeout
|
||||||
// before retrying to acquire lock again.
|
// before retrying to acquire lock again.
|
||||||
leaderLockTimeoutSleepInterval = time.Hour
|
leaderLockTimeoutSleepInterval = time.Hour
|
||||||
// heal entire namespace once in 30 days
|
|
||||||
healInterval = 30 * 24 * time.Hour
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var leaderLockTimeout = newDynamicTimeout(30*time.Second, time.Minute)
|
var leaderLockTimeout = newDynamicTimeout(30*time.Second, time.Minute)
|
||||||
@@ -86,7 +84,7 @@ func getLocalBackgroundHealStatus() (madmin.BgHealState, bool) {
|
|||||||
ScannedItemsCount: bgSeq.getScannedItemsCount(),
|
ScannedItemsCount: bgSeq.getScannedItemsCount(),
|
||||||
LastHealActivity: bgSeq.lastHealActivity,
|
LastHealActivity: bgSeq.lastHealActivity,
|
||||||
HealDisks: healDisks,
|
HealDisks: healDisks,
|
||||||
NextHealRound: UTCNow().Add(durationToNextHealRound(bgSeq.lastHealActivity)),
|
NextHealRound: UTCNow().Add(dataCrawlStartDelay),
|
||||||
}, true
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,68 +182,3 @@ func deepHealObject(bucket, object, versionID string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the duration to the next background healing round
|
|
||||||
func durationToNextHealRound(lastHeal time.Time) time.Duration {
|
|
||||||
if lastHeal.IsZero() {
|
|
||||||
lastHeal = globalBootTime
|
|
||||||
}
|
|
||||||
|
|
||||||
d := lastHeal.Add(healInterval).Sub(UTCNow())
|
|
||||||
if d < 0 {
|
|
||||||
return time.Second
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// Healing leader will take the charge of healing all erasure sets
|
|
||||||
func execLeaderTasks(ctx context.Context, z *erasureZones) {
|
|
||||||
// So that we don't heal immediately, but after one month.
|
|
||||||
lastScanTime := UTCNow()
|
|
||||||
// Get background heal sequence to send elements to heal
|
|
||||||
var bgSeq *healSequence
|
|
||||||
var ok bool
|
|
||||||
for {
|
|
||||||
bgSeq, ok = globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
|
|
||||||
if ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-time.NewTimer(durationToNextHealRound(lastScanTime)).C:
|
|
||||||
bgSeq.resetHealStatusCounters()
|
|
||||||
for _, zone := range z.zones {
|
|
||||||
// Heal set by set
|
|
||||||
for i, set := range zone.sets {
|
|
||||||
if err := healErasureSet(ctx, i, set, zone.drivesPerSet); err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastScanTime = UTCNow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startGlobalHeal(ctx context.Context, objAPI ObjectLayer) {
|
|
||||||
zones, ok := objAPI.(*erasureZones)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
execLeaderTasks(ctx, zones)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initGlobalHeal(ctx context.Context, objAPI ObjectLayer) {
|
|
||||||
go startGlobalHeal(ctx, objAPI)
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-3
@@ -208,9 +208,7 @@ var (
|
|||||||
globalDomainNames []string // Root domains for virtual host style requests
|
globalDomainNames []string // Root domains for virtual host style requests
|
||||||
globalDomainIPs set.StringSet // Root domain IP address(s) for a distributed MinIO deployment
|
globalDomainIPs set.StringSet // Root domain IP address(s) for a distributed MinIO deployment
|
||||||
|
|
||||||
globalObjectTimeout = newDynamicTimeout( /*1*/ 10*time.Minute /*10*/, 600*time.Second) // timeout for Object API related ops
|
globalOperationTimeout = newDynamicTimeout(10*time.Minute, 5*time.Minute) // default timeout for general ops
|
||||||
globalOperationTimeout = newDynamicTimeout(10*time.Minute /*30*/, 600*time.Second) // default timeout for general ops
|
|
||||||
globalHealingTimeout = newDynamicTimeout(30*time.Minute /*1*/, 30*time.Minute) // timeout for healing related ops
|
|
||||||
|
|
||||||
globalBucketObjectLockSys *BucketObjectLockSys
|
globalBucketObjectLockSys *BucketObjectLockSys
|
||||||
globalBucketQuotaSys *BucketQuotaSys
|
globalBucketQuotaSys *BucketQuotaSys
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClusterCheckHandler returns if the server is ready for requests.
|
// ClusterCheckHandler returns if the server is ready for requests.
|
||||||
@@ -38,6 +39,8 @@ func ClusterCheckHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
opts := HealthOptions{Maintenance: r.URL.Query().Get("maintenance") == "true"}
|
opts := HealthOptions{Maintenance: r.URL.Query().Get("maintenance") == "true"}
|
||||||
result := objLayer.Health(ctx, opts)
|
result := objLayer.Health(ctx, opts)
|
||||||
if !result.Healthy {
|
if !result.Healthy {
|
||||||
|
// return how many drives are being healed if any
|
||||||
|
w.Header().Set("X-Minio-Healing-Drives", strconv.Itoa(result.HealingDrives))
|
||||||
// As a maintenance call we are purposefully asked to be taken
|
// As a maintenance call we are purposefully asked to be taken
|
||||||
// down, this is for orchestrators to know if we can safely
|
// down, this is for orchestrators to know if we can safely
|
||||||
// take this server down, return appropriate error.
|
// take this server down, return appropriate error.
|
||||||
|
|||||||
@@ -36,22 +36,22 @@ func setInternalTCPParameters(c syscall.RawConn) error {
|
|||||||
// TCPFastOpenConnect sets the underlying socket to use
|
// TCPFastOpenConnect sets the underlying socket to use
|
||||||
// the TCP fast open connect. This feature is supported
|
// the TCP fast open connect. This feature is supported
|
||||||
// since Linux 4.11.
|
// since Linux 4.11.
|
||||||
_ = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_FASTOPEN_CONNECT, 1)
|
_ = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, unix.TCP_FASTOPEN_CONNECT, 1)
|
||||||
|
|
||||||
// The time (in seconds) the connection needs to remain idle before
|
// The time (in seconds) the connection needs to remain idle before
|
||||||
// TCP starts sending keepalive probes, set this to 5 secs
|
// TCP starts sending keepalive probes, set this to 5 secs
|
||||||
// system defaults to 7200 secs!!!
|
// system defaults to 7200 secs!!!
|
||||||
_ = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPIDLE, 5)
|
_ = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPIDLE, 5)
|
||||||
|
|
||||||
// Number of probes.
|
// Number of probes.
|
||||||
// ~ cat /proc/sys/net/ipv4/tcp_keepalive_probes (defaults to 9, we reduce it to 5)
|
// ~ cat /proc/sys/net/ipv4/tcp_keepalive_probes (defaults to 9, we reduce it to 5)
|
||||||
// 9
|
// 9
|
||||||
_ = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPCNT, 5)
|
_ = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPCNT, 5)
|
||||||
|
|
||||||
// Wait time after successful probe in seconds.
|
// Wait time after successful probe in seconds.
|
||||||
// ~ cat /proc/sys/net/ipv4/tcp_keepalive_intvl (defaults to 75 secs, we reduce it to 3 secs)
|
// ~ cat /proc/sys/net/ipv4/tcp_keepalive_intvl (defaults to 75 secs, we reduce it to 3 secs)
|
||||||
// 75
|
// 75
|
||||||
_ = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPINTVL, 3)
|
_ = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPINTVL, 3)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ func NewCustomDialContext(dialTimeout time.Duration) DialContext {
|
|||||||
// TCPFastOpenConnect sets the underlying socket to use
|
// TCPFastOpenConnect sets the underlying socket to use
|
||||||
// the TCP fast open connect. This feature is supported
|
// the TCP fast open connect. This feature is supported
|
||||||
// since Linux 4.11.
|
// since Linux 4.11.
|
||||||
_ = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_FASTOPEN_CONNECT, 1)
|
_ = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, unix.TCP_FASTOPEN_CONNECT, 1)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ func (iamOS *IAMObjectStore) migrateUsersConfigToV1(ctx context.Context, isSTS b
|
|||||||
cred.AccessKey = user
|
cred.AccessKey = user
|
||||||
u := newUserIdentity(cred)
|
u := newUserIdentity(cred)
|
||||||
if err := iamOS.saveIAMConfig(u, identityPath); err != nil {
|
if err := iamOS.saveIAMConfig(u, identityPath); err != nil {
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(ctx, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ func (iamOS *IAMObjectStore) saveIAMConfig(item interface{}, path string) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return saveConfig(context.Background(), iamOS.objAPI, path, data)
|
return saveConfig(GlobalContext, iamOS.objAPI, path, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (iamOS *IAMObjectStore) loadIAMConfig(item interface{}, path string) error {
|
func (iamOS *IAMObjectStore) loadIAMConfig(item interface{}, path string) error {
|
||||||
@@ -586,7 +586,7 @@ func listIAMConfigItems(ctx context.Context, objAPI ObjectLayer, pathPrefix stri
|
|||||||
|
|
||||||
marker := ""
|
marker := ""
|
||||||
for {
|
for {
|
||||||
lo, err := objAPI.ListObjects(context.Background(),
|
lo, err := objAPI.ListObjects(ctx,
|
||||||
minioMetaBucket, pathPrefix, marker, SlashSeparator, maxObjectList)
|
minioMetaBucket, pathPrefix, marker, SlashSeparator, maxObjectList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
select {
|
select {
|
||||||
|
|||||||
+7
-4
@@ -449,10 +449,13 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
rquorum := InsufficientReadQuorum{}
|
rquorum := InsufficientReadQuorum{}
|
||||||
wquorum := InsufficientWriteQuorum{}
|
wquorum := InsufficientWriteQuorum{}
|
||||||
|
|
||||||
|
// allocate dynamic timeout once before the loop
|
||||||
|
iamLockTimeout := newDynamicTimeout(3*time.Second, 5*time.Second)
|
||||||
|
|
||||||
for range retry.NewTimerWithJitter(retryCtx, time.Second, 5*time.Second, retry.MaxJitter) {
|
for range retry.NewTimerWithJitter(retryCtx, time.Second, 5*time.Second, retry.MaxJitter) {
|
||||||
// let one of the server acquire the lock, if not let them timeout.
|
// let one of the server acquire the lock, if not let them timeout.
|
||||||
// which shall be retried again by this loop.
|
// which shall be retried again by this loop.
|
||||||
if err := txnLk.GetLock(newDynamicTimeout(3*time.Second, 5*time.Second)); err != nil {
|
if err := txnLk.GetLock(iamLockTimeout); err != nil {
|
||||||
logger.Info("Waiting for all MinIO IAM sub-system to be initialized.. trying to acquire lock")
|
logger.Info("Waiting for all MinIO IAM sub-system to be initialized.. trying to acquire lock")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1640,7 +1643,7 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parent string) b
|
|||||||
subPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(spolicyStr)))
|
subPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(spolicyStr)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log any error in input session policy config.
|
// Log any error in input session policy config.
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(GlobalContext, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1783,7 +1786,7 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args) bool {
|
|||||||
subPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(spolicyStr)))
|
subPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(spolicyStr)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log any error in input session policy config.
|
// Log any error in input session policy config.
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(GlobalContext, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1807,7 +1810,7 @@ func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool {
|
|||||||
if globalPolicyOPA != nil {
|
if globalPolicyOPA != nil {
|
||||||
ok, err := globalPolicyOPA.IsAllowed(args)
|
ok, err := globalPolicyOPA.IsAllowed(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(GlobalContext, err)
|
||||||
}
|
}
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ func (api objectAPIHandlers) ListenNotificationHandler(w http.ResponseWriter, r
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case evI := <-listenCh:
|
case evI := <-listenCh:
|
||||||
ev := evI.(event.Event)
|
ev, ok := evI.(event.Event)
|
||||||
if len(string(ev.EventName)) > 0 {
|
if ok {
|
||||||
if err := enc.Encode(struct{ Records []event.Event }{[]event.Event{ev}}); err != nil {
|
if err := enc.Encode(struct{ Records []event.Event }{[]event.Event{ev}}); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ func registerLockRESTHandlers(router *mux.Router, endpointZones EndpointZones) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subrouter := router.PathPrefix(path.Join(lockRESTPrefix, endpoint.Path)).Subrouter()
|
subrouter := router.PathPrefix(path.Join(lockRESTPrefix, endpoint.Path)).Subrouter()
|
||||||
subrouter.Methods(http.MethodPost).Path(lockRESTVersionPrefix + lockRESTMethodHealth).HandlerFunc(httpTraceHdrs(lockServer.HealthHandler)).Queries(queries...)
|
subrouter.Methods(http.MethodPost).Path(lockRESTVersionPrefix + lockRESTMethodHealth).HandlerFunc(httpTraceHdrs(lockServer.HealthHandler))
|
||||||
subrouter.Methods(http.MethodPost).Path(lockRESTVersionPrefix + lockRESTMethodLock).HandlerFunc(httpTraceHdrs(lockServer.LockHandler)).Queries(queries...)
|
subrouter.Methods(http.MethodPost).Path(lockRESTVersionPrefix + lockRESTMethodLock).HandlerFunc(httpTraceHdrs(lockServer.LockHandler)).Queries(queries...)
|
||||||
subrouter.Methods(http.MethodPost).Path(lockRESTVersionPrefix + lockRESTMethodRLock).HandlerFunc(httpTraceHdrs(lockServer.RLockHandler)).Queries(queries...)
|
subrouter.Methods(http.MethodPost).Path(lockRESTVersionPrefix + lockRESTMethodRLock).HandlerFunc(httpTraceHdrs(lockServer.RLockHandler)).Queries(queries...)
|
||||||
subrouter.Methods(http.MethodPost).Path(lockRESTVersionPrefix + lockRESTMethodUnlock).HandlerFunc(httpTraceHdrs(lockServer.UnlockHandler)).Queries(queries...)
|
subrouter.Methods(http.MethodPost).Path(lockRESTVersionPrefix + lockRESTMethodUnlock).HandlerFunc(httpTraceHdrs(lockServer.UnlockHandler)).Queries(queries...)
|
||||||
|
|||||||
@@ -125,15 +125,6 @@ func (lrw *ResponseWriter) Size() int {
|
|||||||
return lrw.bytesWritten
|
return lrw.bytesWritten
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuditTargets is the list of enabled audit loggers
|
|
||||||
var AuditTargets = []Target{}
|
|
||||||
|
|
||||||
// AddAuditTarget adds a new audit logger target to the
|
|
||||||
// list of enabled loggers
|
|
||||||
func AddAuditTarget(t Target) {
|
|
||||||
AuditTargets = append(AuditTargets, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuditLog - logs audit logs to all audit targets.
|
// AuditLog - logs audit logs to all audit targets.
|
||||||
func AuditLog(w http.ResponseWriter, r *http.Request, api string, reqClaims map[string]interface{}, filterKeys ...string) {
|
func AuditLog(w http.ResponseWriter, r *http.Request, api string, reqClaims map[string]interface{}, filterKeys ...string) {
|
||||||
// Fast exit if there is not audit target configured
|
// Fast exit if there is not audit target configured
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ import (
|
|||||||
// in plain or json format to the standard output.
|
// in plain or json format to the standard output.
|
||||||
type Target struct{}
|
type Target struct{}
|
||||||
|
|
||||||
|
// Validate - validate if the tty can be written to
|
||||||
|
func (c *Target) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Send log message 'e' to console
|
// Send log message 'e' to console
|
||||||
func (c *Target) Send(e interface{}, logKind string) error {
|
func (c *Target) Send(e interface{}, logKind string) error {
|
||||||
entry, ok := e.(log.Entry)
|
entry, ok := e.(log.Entry)
|
||||||
|
|||||||
@@ -18,12 +18,16 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Target implements logger.Target and sends the json
|
// Target implements logger.Target and sends the json
|
||||||
@@ -45,6 +49,47 @@ type Target struct {
|
|||||||
client http.Client
|
client http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate validate the http target
|
||||||
|
func (h *Target) Validate() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.endpoint, strings.NewReader(`{}`))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set(xhttp.ContentType, "application/json")
|
||||||
|
|
||||||
|
// Set user-agent to indicate MinIO release
|
||||||
|
// version to the configured log endpoint
|
||||||
|
req.Header.Set("User-Agent", h.userAgent)
|
||||||
|
|
||||||
|
if h.authToken != "" {
|
||||||
|
req.Header.Set("Authorization", h.authToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain any response.
|
||||||
|
xhttp.DrainBody(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusForbidden:
|
||||||
|
return fmt.Errorf("%s returned '%s', please check if your auth token is correctly set",
|
||||||
|
h.endpoint, resp.Status)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s returned '%s', please check your endpoint configuration",
|
||||||
|
h.endpoint, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Target) startHTTPLogger() {
|
func (h *Target) startHTTPLogger() {
|
||||||
// Create a routine which sends json logs received
|
// Create a routine which sends json logs received
|
||||||
// from an internal channel.
|
// from an internal channel.
|
||||||
@@ -55,8 +100,11 @@ func (h *Target) startHTTPLogger() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, h.endpoint, bytes.NewReader(logJSON))
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||||
|
h.endpoint, bytes.NewReader(logJSON))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
cancel()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
req.Header.Set(xhttp.ContentType, "application/json")
|
req.Header.Set(xhttp.ContentType, "application/json")
|
||||||
@@ -70,13 +118,26 @@ func (h *Target) startHTTPLogger() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.client.Do(req)
|
resp, err := h.client.Do(req)
|
||||||
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.client.CloseIdleConnections()
|
logger.LogIf(ctx, fmt.Errorf("%s returned '%w', please check your endpoint configuration\n",
|
||||||
|
h.endpoint, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain any response.
|
// Drain any response.
|
||||||
xhttp.DrainBody(resp.Body)
|
xhttp.DrainBody(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusForbidden:
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("%s returned '%s', please check if your auth token is correctly set",
|
||||||
|
h.endpoint, resp.Status))
|
||||||
|
default:
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("%s returned '%s', please check your endpoint configuration",
|
||||||
|
h.endpoint, resp.Status))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-1
@@ -20,14 +20,33 @@ package logger
|
|||||||
// a single log entry and Send it to the log target
|
// a single log entry and Send it to the log target
|
||||||
// e.g. Send the log to a http server
|
// e.g. Send the log to a http server
|
||||||
type Target interface {
|
type Target interface {
|
||||||
|
Validate() error
|
||||||
Send(entry interface{}, errKind string) error
|
Send(entry interface{}, errKind string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Targets is the set of enabled loggers
|
// Targets is the set of enabled loggers
|
||||||
var Targets = []Target{}
|
var Targets = []Target{}
|
||||||
|
|
||||||
|
// AuditTargets is the list of enabled audit loggers
|
||||||
|
var AuditTargets = []Target{}
|
||||||
|
|
||||||
|
// AddAuditTarget adds a new audit logger target to the
|
||||||
|
// list of enabled loggers
|
||||||
|
func AddAuditTarget(t Target) error {
|
||||||
|
if err := t.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
AuditTargets = append(AuditTargets, t)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// AddTarget adds a new logger target to the
|
// AddTarget adds a new logger target to the
|
||||||
// list of enabled loggers
|
// list of enabled loggers
|
||||||
func AddTarget(t Target) {
|
func AddTarget(t Target) error {
|
||||||
|
if err := t.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
Targets = append(Targets, t)
|
Targets = append(Targets, t)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -106,7 +106,7 @@ func healingMetricsPrometheus(ch chan<- prometheus.Metric) {
|
|||||||
}
|
}
|
||||||
healMetricsNamespace := "self_heal"
|
healMetricsNamespace := "self_heal"
|
||||||
|
|
||||||
dur := time.Duration(-1)
|
var dur time.Duration
|
||||||
if !bgSeq.lastHealActivity.IsZero() {
|
if !bgSeq.lastHealActivity.IsZero() {
|
||||||
dur = time.Since(bgSeq.lastHealActivity)
|
dur = time.Since(bgSeq.lastHealActivity)
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-2
@@ -28,6 +28,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/config/storageclass"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/dsync"
|
"github.com/minio/minio/pkg/dsync"
|
||||||
"github.com/minio/minio/pkg/lsync"
|
"github.com/minio/minio/pkg/lsync"
|
||||||
@@ -147,7 +148,18 @@ func (di *distLockInstance) GetLock(timeout *dynamicTimeout) (timedOutErr error)
|
|||||||
lockSource := getSource(2)
|
lockSource := getSource(2)
|
||||||
start := UTCNow()
|
start := UTCNow()
|
||||||
|
|
||||||
if !di.rwMutex.GetLock(di.ctx, di.opsID, lockSource, timeout.Timeout()) {
|
// Lockers default to standard storage class always, why because
|
||||||
|
// we always dictate storage tolerance in terms of standard
|
||||||
|
// storage class be it number of drives or a multiplicative
|
||||||
|
// of number of nodes, defaulting lockers to this value
|
||||||
|
// simply means that locking is always similar in behavior
|
||||||
|
// and effect with erasure coded drive tolerance.
|
||||||
|
tolerance := globalStorageClass.GetParityForSC(storageclass.STANDARD)
|
||||||
|
|
||||||
|
if !di.rwMutex.GetLock(di.ctx, di.opsID, lockSource, dsync.Options{
|
||||||
|
Timeout: timeout.Timeout(),
|
||||||
|
Tolerance: tolerance,
|
||||||
|
}) {
|
||||||
timeout.LogFailure()
|
timeout.LogFailure()
|
||||||
return OperationTimedOut{}
|
return OperationTimedOut{}
|
||||||
}
|
}
|
||||||
@@ -164,7 +176,14 @@ func (di *distLockInstance) Unlock() {
|
|||||||
func (di *distLockInstance) GetRLock(timeout *dynamicTimeout) (timedOutErr error) {
|
func (di *distLockInstance) GetRLock(timeout *dynamicTimeout) (timedOutErr error) {
|
||||||
lockSource := getSource(2)
|
lockSource := getSource(2)
|
||||||
start := UTCNow()
|
start := UTCNow()
|
||||||
if !di.rwMutex.GetRLock(di.ctx, di.opsID, lockSource, timeout.Timeout()) {
|
|
||||||
|
// Lockers default to standard storage class always.
|
||||||
|
tolerance := globalStorageClass.GetParityForSC(storageclass.STANDARD)
|
||||||
|
|
||||||
|
if !di.rwMutex.GetRLock(di.ctx, di.opsID, lockSource, dsync.Options{
|
||||||
|
Timeout: timeout.Timeout(),
|
||||||
|
Tolerance: tolerance,
|
||||||
|
}) {
|
||||||
timeout.LogFailure()
|
timeout.LogFailure()
|
||||||
return OperationTimedOut{}
|
return OperationTimedOut{}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-7
@@ -280,22 +280,21 @@ func TestExtractHostPort(t *testing.T) {
|
|||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
host, port, err := extractHostPort(testCase.addr)
|
host, port, err := extractHostPort(testCase.addr)
|
||||||
if testCase.expectedErr == nil {
|
if testCase.expectedErr == nil && err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d: should succeed but failed with err: %v", i+1, err)
|
t.Fatalf("Test %d: should succeed but failed with err: %v", i+1, err)
|
||||||
}
|
}
|
||||||
|
if testCase.expectedErr != nil && err == nil {
|
||||||
|
t.Fatalf("Test %d:, should fail but succeeded.", i+1)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
if host != testCase.host {
|
if host != testCase.host {
|
||||||
t.Fatalf("Test %d: expected: %v, found: %v", i+1, testCase.host, host)
|
t.Fatalf("Test %d: expected: %v, found: %v", i+1, testCase.host, host)
|
||||||
}
|
}
|
||||||
if port != testCase.port {
|
if port != testCase.port {
|
||||||
t.Fatalf("Test %d: expected: %v, found: %v", i+1, testCase.port, port)
|
t.Fatalf("Test %d: expected: %v, found: %v", i+1, testCase.port, port)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
if testCase.expectedErr != nil {
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Test %d:, should fail but succeeded.", i+1)
|
|
||||||
}
|
}
|
||||||
|
if testCase.expectedErr != nil && err != nil {
|
||||||
if testCase.expectedErr.Error() != err.Error() {
|
if testCase.expectedErr.Error() != err.Error() {
|
||||||
t.Fatalf("Test %d: failed with different error, expected: '%v', found:'%v'.", i+1, testCase.expectedErr, err)
|
t.Fatalf("Test %d: failed with different error, expected: '%v', found:'%v'.", i+1, testCase.expectedErr, err)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-30
@@ -588,31 +588,6 @@ func (sys *NotificationSys) DeleteBucketMetadata(ctx context.Context, bucketName
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddRemoteTarget - adds event rules map, HTTP/PeerRPC client target to bucket name.
|
|
||||||
func (sys *NotificationSys) AddRemoteTarget(bucketName string, target event.Target, rulesMap event.RulesMap) error {
|
|
||||||
if err := sys.targetList.Add(target); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sys.Lock()
|
|
||||||
targetMap := sys.bucketRemoteTargetRulesMap[bucketName]
|
|
||||||
if targetMap == nil {
|
|
||||||
targetMap = make(map[event.TargetID]event.RulesMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
rulesMap = rulesMap.Clone()
|
|
||||||
targetMap[target.ID()] = rulesMap
|
|
||||||
sys.bucketRemoteTargetRulesMap[bucketName] = targetMap
|
|
||||||
|
|
||||||
rulesMap = rulesMap.Clone()
|
|
||||||
rulesMap.Add(sys.bucketRulesMap[bucketName])
|
|
||||||
sys.bucketRulesMap[bucketName] = rulesMap
|
|
||||||
|
|
||||||
sys.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads notification policies for all buckets into NotificationSys.
|
// Loads notification policies for all buckets into NotificationSys.
|
||||||
func (sys *NotificationSys) load(buckets []BucketInfo, objAPI ObjectLayer) error {
|
func (sys *NotificationSys) load(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||||
for _, bucket := range buckets {
|
for _, bucket := range buckets {
|
||||||
@@ -634,7 +609,7 @@ func (sys *NotificationSys) load(buckets []BucketInfo, objAPI ObjectLayer) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init - initializes notification system from notification.xml and listenxl.meta of all buckets.
|
// Init - initializes notification system from notification.xml and listenxl.meta of all buckets.
|
||||||
func (sys *NotificationSys) Init(buckets []BucketInfo, objAPI ObjectLayer) error {
|
func (sys *NotificationSys) Init(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
return errServerNotInitialized
|
return errServerNotInitialized
|
||||||
}
|
}
|
||||||
@@ -657,13 +632,13 @@ func (sys *NotificationSys) Init(buckets []BucketInfo, objAPI ObjectLayer) error
|
|||||||
if res.Err != nil {
|
if res.Err != nil {
|
||||||
reqInfo := &logger.ReqInfo{}
|
reqInfo := &logger.ReqInfo{}
|
||||||
reqInfo.AppendTags("targetID", res.ID.Name)
|
reqInfo.AppendTags("targetID", res.ID.Name)
|
||||||
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
logger.LogOnceIf(logger.SetReqInfo(GlobalContext, reqInfo), res.Err, res.ID)
|
||||||
logger.LogOnceIf(ctx, res.Err, res.ID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return sys.load(buckets, objAPI)
|
go logger.LogIf(ctx, sys.load(buckets, objAPI))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddRulesMap - adds rules map for bucket name.
|
// AddRulesMap - adds rules map for bucket name.
|
||||||
@@ -1188,7 +1163,7 @@ func (sys *NotificationSys) GetLocalDiskIDs(ctx context.Context) (localDiskIDs [
|
|||||||
|
|
||||||
// NewNotificationSys - creates new notification system object.
|
// NewNotificationSys - creates new notification system object.
|
||||||
func NewNotificationSys(endpoints EndpointZones) *NotificationSys {
|
func NewNotificationSys(endpoints EndpointZones) *NotificationSys {
|
||||||
// bucketRulesMap/bucketRemoteTargetRulesMap are initialized by NotificationSys.Init()
|
// targetList/bucketRulesMap/bucketRemoteTargetRulesMap are populated by NotificationSys.Init()
|
||||||
return &NotificationSys{
|
return &NotificationSys{
|
||||||
targetList: event.NewTargetList(),
|
targetList: event.NewTargetList(),
|
||||||
targetResCh: make(chan event.TargetIDResult),
|
targetResCh: make(chan event.TargetIDResult),
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func dirObjectInfo(bucket, object string, size int64, metadata map[string]string
|
|||||||
// Depending on the disk type network or local, initialize storage API.
|
// Depending on the disk type network or local, initialize storage API.
|
||||||
func newStorageAPI(endpoint Endpoint) (storage StorageAPI, err error) {
|
func newStorageAPI(endpoint Endpoint) (storage StorageAPI, err error) {
|
||||||
if endpoint.IsLocal {
|
if endpoint.IsLocal {
|
||||||
storage, err := newXLStorage(endpoint.Path, endpoint.Host)
|
storage, err := newXLStorage(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -139,11 +139,11 @@ func cleanupDir(ctx context.Context, storage StorageAPI, volume, dirPath string)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func listObjectsNonSlash(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int, tpool *TreeWalkPool, listDir ListDirFunc, getObjInfo func(context.Context, string, string) (ObjectInfo, error), getObjectInfoDirs ...func(context.Context, string, string) (ObjectInfo, error)) (loi ListObjectsInfo, err error) {
|
func listObjectsNonSlash(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int, tpool *TreeWalkPool, listDir ListDirFunc, isLeaf IsLeafFunc, isLeafDir IsLeafDirFunc, getObjInfo func(context.Context, string, string) (ObjectInfo, error), getObjectInfoDirs ...func(context.Context, string, string) (ObjectInfo, error)) (loi ListObjectsInfo, err error) {
|
||||||
endWalkCh := make(chan struct{})
|
endWalkCh := make(chan struct{})
|
||||||
defer close(endWalkCh)
|
defer close(endWalkCh)
|
||||||
recursive := true
|
recursive := true
|
||||||
walkResultCh := startTreeWalk(ctx, bucket, prefix, "", recursive, listDir, endWalkCh)
|
walkResultCh := startTreeWalk(ctx, bucket, prefix, "", recursive, listDir, isLeaf, isLeafDir, endWalkCh)
|
||||||
|
|
||||||
var objInfos []ObjectInfo
|
var objInfos []ObjectInfo
|
||||||
var eof bool
|
var eof bool
|
||||||
@@ -227,14 +227,14 @@ func listObjectsNonSlash(ctx context.Context, bucket, prefix, marker, delimiter
|
|||||||
// to allocate a receive channel for ObjectInfo, upon any unhandled
|
// to allocate a receive channel for ObjectInfo, upon any unhandled
|
||||||
// error walker returns error. Optionally if context.Done() is received
|
// error walker returns error. Optionally if context.Done() is received
|
||||||
// then Walk() stops the walker.
|
// then Walk() stops the walker.
|
||||||
func fsWalk(ctx context.Context, obj ObjectLayer, bucket, prefix string, listDir ListDirFunc, results chan<- ObjectInfo, getObjInfo func(context.Context, string, string) (ObjectInfo, error), getObjectInfoDirs ...func(context.Context, string, string) (ObjectInfo, error)) error {
|
func fsWalk(ctx context.Context, obj ObjectLayer, bucket, prefix string, listDir ListDirFunc, isLeaf IsLeafFunc, isLeafDir IsLeafDirFunc, results chan<- ObjectInfo, getObjInfo func(context.Context, string, string) (ObjectInfo, error), getObjectInfoDirs ...func(context.Context, string, string) (ObjectInfo, error)) error {
|
||||||
if err := checkListObjsArgs(ctx, bucket, prefix, "", obj); err != nil {
|
if err := checkListObjsArgs(ctx, bucket, prefix, "", obj); err != nil {
|
||||||
// Upon error close the channel.
|
// Upon error close the channel.
|
||||||
close(results)
|
close(results)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
walkResultCh := startTreeWalk(ctx, bucket, prefix, "", true, listDir, ctx.Done())
|
walkResultCh := startTreeWalk(ctx, bucket, prefix, "", true, listDir, isLeaf, isLeafDir, ctx.Done())
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(results)
|
defer close(results)
|
||||||
@@ -277,9 +277,9 @@ func fsWalk(ctx context.Context, obj ObjectLayer, bucket, prefix string, listDir
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func listObjects(ctx context.Context, obj ObjectLayer, bucket, prefix, marker, delimiter string, maxKeys int, tpool *TreeWalkPool, listDir ListDirFunc, getObjInfo func(context.Context, string, string) (ObjectInfo, error), getObjectInfoDirs ...func(context.Context, string, string) (ObjectInfo, error)) (loi ListObjectsInfo, err error) {
|
func listObjects(ctx context.Context, obj ObjectLayer, bucket, prefix, marker, delimiter string, maxKeys int, tpool *TreeWalkPool, listDir ListDirFunc, isLeaf IsLeafFunc, isLeafDir IsLeafDirFunc, getObjInfo func(context.Context, string, string) (ObjectInfo, error), getObjectInfoDirs ...func(context.Context, string, string) (ObjectInfo, error)) (loi ListObjectsInfo, err error) {
|
||||||
if delimiter != SlashSeparator && delimiter != "" {
|
if delimiter != SlashSeparator && delimiter != "" {
|
||||||
return listObjectsNonSlash(ctx, bucket, prefix, marker, delimiter, maxKeys, tpool, listDir, getObjInfo, getObjectInfoDirs...)
|
return listObjectsNonSlash(ctx, bucket, prefix, marker, delimiter, maxKeys, tpool, listDir, isLeaf, isLeafDir, getObjInfo, getObjectInfoDirs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := checkListObjsArgs(ctx, bucket, prefix, marker, obj); err != nil {
|
if err := checkListObjsArgs(ctx, bucket, prefix, marker, obj); err != nil {
|
||||||
@@ -322,7 +322,7 @@ func listObjects(ctx context.Context, obj ObjectLayer, bucket, prefix, marker, d
|
|||||||
walkResultCh, endWalkCh := tpool.Release(listParams{bucket, recursive, marker, prefix})
|
walkResultCh, endWalkCh := tpool.Release(listParams{bucket, recursive, marker, prefix})
|
||||||
if walkResultCh == nil {
|
if walkResultCh == nil {
|
||||||
endWalkCh = make(chan struct{})
|
endWalkCh = make(chan struct{})
|
||||||
walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, endWalkCh)
|
walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, isLeaf, isLeafDir, endWalkCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
var objInfos []ObjectInfo
|
var objInfos []ObjectInfo
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* MinIO Cloud Storage, (C) 2015-2016, 2017 MinIO, Inc.
|
* MinIO Cloud Storage, (C) 2015-2020 MinIO, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -405,7 +405,7 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
|||||||
Prefixes: []string{"newzen/"},
|
Prefixes: []string{"newzen/"},
|
||||||
},
|
},
|
||||||
// ListObjectsResult-29.
|
// ListObjectsResult-29.
|
||||||
// Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase and delimeter set, (testCase 61).
|
// Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase and delimiter set, (testCase 61).
|
||||||
{
|
{
|
||||||
IsTruncated: false,
|
IsTruncated: false,
|
||||||
Objects: []ObjectInfo{
|
Objects: []ObjectInfo{
|
||||||
@@ -448,7 +448,9 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
|||||||
{Name: "temporary/0/"},
|
{Name: "temporary/0/"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// ListObjectsResult-34 Listing with marker > last object should return empty
|
// ListObjectsResult-34:
|
||||||
|
// * Listing with marker > last object should return empty
|
||||||
|
// * Listing an object with a trailing slash and '/' delimiter
|
||||||
{
|
{
|
||||||
IsTruncated: false,
|
IsTruncated: false,
|
||||||
Objects: []ObjectInfo{},
|
Objects: []ObjectInfo{},
|
||||||
@@ -460,7 +462,7 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
|||||||
bucketName string
|
bucketName string
|
||||||
prefix string
|
prefix string
|
||||||
marker string
|
marker string
|
||||||
delimeter string
|
delimiter string
|
||||||
maxKeys int32
|
maxKeys int32
|
||||||
// Expected output of ListObjects.
|
// Expected output of ListObjects.
|
||||||
result ListObjectsInfo
|
result ListObjectsInfo
|
||||||
@@ -550,7 +552,7 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
|||||||
{"test-bucket-list-object", "Asia/India/", "", "", 10, resultCases[23], nil, true},
|
{"test-bucket-list-object", "Asia/India/", "", "", 10, resultCases[23], nil, true},
|
||||||
{"test-bucket-list-object", "Asia", "", "", 10, resultCases[24], nil, true},
|
{"test-bucket-list-object", "Asia", "", "", 10, resultCases[24], nil, true},
|
||||||
// Tests with prefix and delimiter (55-57).
|
// Tests with prefix and delimiter (55-57).
|
||||||
// With delimeter the code should not recurse into the sub-directories of prefix Dir.
|
// With delimiter the code should not recurse into the sub-directories of prefix Dir.
|
||||||
{"test-bucket-list-object", "Asia", "", SlashSeparator, 10, resultCases[25], nil, true},
|
{"test-bucket-list-object", "Asia", "", SlashSeparator, 10, resultCases[25], nil, true},
|
||||||
{"test-bucket-list-object", "new", "", SlashSeparator, 10, resultCases[26], nil, true},
|
{"test-bucket-list-object", "new", "", SlashSeparator, 10, resultCases[26], nil, true},
|
||||||
{"test-bucket-list-object", "Asia/India/", "", SlashSeparator, 10, resultCases[27], nil, true},
|
{"test-bucket-list-object", "Asia/India/", "", SlashSeparator, 10, resultCases[27], nil, true},
|
||||||
@@ -569,13 +571,15 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
|||||||
{"test-bucket-empty-dir", "", "temporary/", "", 10, resultCases[33], nil, true},
|
{"test-bucket-empty-dir", "", "temporary/", "", 10, resultCases[33], nil, true},
|
||||||
// Test listing with marker > last object such that response should be empty (65)
|
// Test listing with marker > last object such that response should be empty (65)
|
||||||
{"test-bucket-single-object", "", "A/C", "", 1000, resultCases[34], nil, true},
|
{"test-bucket-single-object", "", "A/C", "", 1000, resultCases[34], nil, true},
|
||||||
|
// Test listing an object with a trailing slash and a slash delimiter (66)
|
||||||
|
{"test-bucket-list-object", "Asia-maps.png/", "", "/", 1000, resultCases[34], nil, true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
testCase := testCase
|
testCase := testCase
|
||||||
t.Run(fmt.Sprintf("%s-Test%d", instanceType, i+1), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%s-Test%d", instanceType, i+1), func(t *testing.T) {
|
||||||
result, err := obj.ListObjects(context.Background(), testCase.bucketName,
|
result, err := obj.ListObjects(context.Background(), testCase.bucketName,
|
||||||
testCase.prefix, testCase.marker, testCase.delimeter, int(testCase.maxKeys))
|
testCase.prefix, testCase.marker, testCase.delimiter, int(testCase.maxKeys))
|
||||||
if err != nil && testCase.shouldPass {
|
if err != nil && testCase.shouldPass {
|
||||||
t.Errorf("Test %d: %s: Expected to pass, but failed with: <ERROR> %s", i+1, instanceType, err.Error())
|
t.Errorf("Test %d: %s: Expected to pass, but failed with: <ERROR> %s", i+1, instanceType, err.Error())
|
||||||
}
|
}
|
||||||
@@ -636,7 +640,632 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
|||||||
// Take ListObject treeWalk go-routine to completion, if available in the treewalk pool.
|
// Take ListObject treeWalk go-routine to completion, if available in the treewalk pool.
|
||||||
if result.IsTruncated {
|
if result.IsTruncated {
|
||||||
_, err = obj.ListObjects(context.Background(), testCase.bucketName,
|
_, err = obj.ListObjects(context.Background(), testCase.bucketName,
|
||||||
testCase.prefix, result.NextMarker, testCase.delimeter, 1000)
|
testCase.prefix, result.NextMarker, testCase.delimiter, 1000)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper for calling ListObjectVersions tests for both Erasure multiple disks and single node setup.
|
||||||
|
func TestListObjectVersions(t *testing.T) {
|
||||||
|
ExecObjectLayerTest(t, testListObjectVersions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unit test for ListObjectVersions
|
||||||
|
func testListObjectVersions(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
||||||
|
t, _ := t1.(*testing.T)
|
||||||
|
testBuckets := []string{
|
||||||
|
// This bucket is used for testing ListObject operations.
|
||||||
|
"test-bucket-list-object",
|
||||||
|
// This bucket will be tested with empty directories
|
||||||
|
"test-bucket-empty-dir",
|
||||||
|
// Will not store any objects in this bucket,
|
||||||
|
// Its to test ListObjects on an empty bucket.
|
||||||
|
"empty-bucket",
|
||||||
|
// Listing the case where the marker > last object.
|
||||||
|
"test-bucket-single-object",
|
||||||
|
}
|
||||||
|
for _, bucket := range testBuckets {
|
||||||
|
err := obj.MakeBucketWithLocation(context.Background(), bucket, BucketOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
testObjects := []struct {
|
||||||
|
parentBucket string
|
||||||
|
name string
|
||||||
|
content string
|
||||||
|
meta map[string]string
|
||||||
|
}{
|
||||||
|
{testBuckets[0], "Asia-maps.png", "asis-maps", map[string]string{"content-type": "image/png"}},
|
||||||
|
{testBuckets[0], "Asia/India/India-summer-photos-1", "contentstring", nil},
|
||||||
|
{testBuckets[0], "Asia/India/Karnataka/Bangalore/Koramangala/pics", "contentstring", nil},
|
||||||
|
{testBuckets[0], "newPrefix0", "newPrefix0", nil},
|
||||||
|
{testBuckets[0], "newPrefix1", "newPrefix1", nil},
|
||||||
|
{testBuckets[0], "newzen/zen/recurse/again/again/again/pics", "recurse", nil},
|
||||||
|
{testBuckets[0], "obj0", "obj0", nil},
|
||||||
|
{testBuckets[0], "obj1", "obj1", nil},
|
||||||
|
{testBuckets[0], "obj2", "obj2", nil},
|
||||||
|
{testBuckets[1], "obj1", "obj1", nil},
|
||||||
|
{testBuckets[1], "obj2", "obj2", nil},
|
||||||
|
{testBuckets[1], "temporary/0/", "", nil},
|
||||||
|
{testBuckets[3], "A/B", "contentstring", nil},
|
||||||
|
}
|
||||||
|
for _, object := range testObjects {
|
||||||
|
md5Bytes := md5.Sum([]byte(object.content))
|
||||||
|
_, err = obj.PutObject(context.Background(), object.parentBucket, object.name, mustGetPutObjReader(t, bytes.NewBufferString(object.content),
|
||||||
|
int64(len(object.content)), hex.EncodeToString(md5Bytes[:]), ""), ObjectOptions{UserDefined: object.meta})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formualting the result data set to be expected from ListObjects call inside the tests,
|
||||||
|
// This will be used in testCases and used for asserting the correctness of ListObjects output in the tests.
|
||||||
|
|
||||||
|
resultCases := []ListObjectsInfo{
|
||||||
|
// ListObjectsResult-0.
|
||||||
|
// Testing for listing all objects in the bucket, (testCase 20,21,22).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "Asia-maps.png"},
|
||||||
|
{Name: "Asia/India/India-summer-photos-1"},
|
||||||
|
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||||
|
{Name: "newPrefix0"},
|
||||||
|
{Name: "newPrefix1"},
|
||||||
|
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||||
|
{Name: "obj0"},
|
||||||
|
{Name: "obj1"},
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-1.
|
||||||
|
// Used for asserting the truncated case, (testCase 23).
|
||||||
|
{
|
||||||
|
IsTruncated: true,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "Asia-maps.png"},
|
||||||
|
{Name: "Asia/India/India-summer-photos-1"},
|
||||||
|
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||||
|
{Name: "newPrefix0"},
|
||||||
|
{Name: "newPrefix1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-2.
|
||||||
|
// (TestCase 24).
|
||||||
|
{
|
||||||
|
IsTruncated: true,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "Asia-maps.png"},
|
||||||
|
{Name: "Asia/India/India-summer-photos-1"},
|
||||||
|
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||||
|
{Name: "newPrefix0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-3.
|
||||||
|
// (TestCase 25).
|
||||||
|
{
|
||||||
|
IsTruncated: true,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "Asia-maps.png"},
|
||||||
|
{Name: "Asia/India/India-summer-photos-1"},
|
||||||
|
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-4.
|
||||||
|
// Again used for truncated case.
|
||||||
|
// (TestCase 26).
|
||||||
|
{
|
||||||
|
IsTruncated: true,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "Asia-maps.png"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-5.
|
||||||
|
// Used for Asserting prefixes.
|
||||||
|
// Used for test case with prefix "new", (testCase 27-29).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "newPrefix0"},
|
||||||
|
{Name: "newPrefix1"},
|
||||||
|
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-6.
|
||||||
|
// Used for Asserting prefixes.
|
||||||
|
// Used for test case with prefix = "obj", (testCase 30).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "obj0"},
|
||||||
|
{Name: "obj1"},
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-7.
|
||||||
|
// Used for Asserting prefixes and truncation.
|
||||||
|
// Used for test case with prefix = "new" and maxKeys = 1, (testCase 31).
|
||||||
|
{
|
||||||
|
IsTruncated: true,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "newPrefix0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-8.
|
||||||
|
// Used for Asserting prefixes.
|
||||||
|
// Used for test case with prefix = "obj" and maxKeys = 2, (testCase 32).
|
||||||
|
{
|
||||||
|
IsTruncated: true,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "obj0"},
|
||||||
|
{Name: "obj1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-9.
|
||||||
|
// Used for asserting the case with marker, but without prefix.
|
||||||
|
//marker is set to "newPrefix0" in the testCase, (testCase 33).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "newPrefix1"},
|
||||||
|
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||||
|
{Name: "obj0"},
|
||||||
|
{Name: "obj1"},
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-10.
|
||||||
|
//marker is set to "newPrefix1" in the testCase, (testCase 34).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||||
|
{Name: "obj0"},
|
||||||
|
{Name: "obj1"},
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-11.
|
||||||
|
//marker is set to "obj0" in the testCase, (testCase 35).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "obj1"},
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-12.
|
||||||
|
// Marker is set to "obj1" in the testCase, (testCase 36).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-13.
|
||||||
|
// Marker is set to "man" in the testCase, (testCase37).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "newPrefix0"},
|
||||||
|
{Name: "newPrefix1"},
|
||||||
|
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||||
|
{Name: "obj0"},
|
||||||
|
{Name: "obj1"},
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-14.
|
||||||
|
// Marker is set to "Abc" in the testCase, (testCase 39).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "Asia-maps.png"},
|
||||||
|
{Name: "Asia/India/India-summer-photos-1"},
|
||||||
|
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||||
|
{Name: "newPrefix0"},
|
||||||
|
{Name: "newPrefix1"},
|
||||||
|
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||||
|
{Name: "obj0"},
|
||||||
|
{Name: "obj1"},
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-15.
|
||||||
|
// Marker is set to "Asia/India/India-summer-photos-1" in the testCase, (testCase 40).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||||
|
{Name: "newPrefix0"},
|
||||||
|
{Name: "newPrefix1"},
|
||||||
|
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||||
|
{Name: "obj0"},
|
||||||
|
{Name: "obj1"},
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-16.
|
||||||
|
// Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase, (testCase 41).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "newPrefix0"},
|
||||||
|
{Name: "newPrefix1"},
|
||||||
|
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||||
|
{Name: "obj0"},
|
||||||
|
{Name: "obj1"},
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-17.
|
||||||
|
// Used for asserting the case with marker, without prefix but with truncation.
|
||||||
|
// Marker = "newPrefix0" & maxKeys = 3 in the testCase, (testCase42).
|
||||||
|
// Output truncated to 3 values.
|
||||||
|
{
|
||||||
|
IsTruncated: true,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "newPrefix1"},
|
||||||
|
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||||
|
{Name: "obj0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-18.
|
||||||
|
// Marker = "newPrefix1" & maxkeys = 1 in the testCase, (testCase43).
|
||||||
|
// Output truncated to 1 value.
|
||||||
|
{
|
||||||
|
IsTruncated: true,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-19.
|
||||||
|
// Marker = "obj0" & maxKeys = 1 in the testCase, (testCase44).
|
||||||
|
// Output truncated to 1 value.
|
||||||
|
{
|
||||||
|
IsTruncated: true,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "obj1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-20.
|
||||||
|
// Marker = "obj0" & prefix = "obj" in the testCase, (testCase 45).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "obj1"},
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-21.
|
||||||
|
// Marker = "obj1" & prefix = "obj" in the testCase, (testCase 46).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-22.
|
||||||
|
// Marker = "newPrefix0" & prefix = "new" in the testCase,, (testCase 47).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "newPrefix1"},
|
||||||
|
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-23.
|
||||||
|
// Prefix is set to "Asia/India/" in the testCase, and delimiter is not set (testCase 55).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "Asia/India/India-summer-photos-1"},
|
||||||
|
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ListObjectsResult-24.
|
||||||
|
// Prefix is set to "Asia" in the testCase, and delimiter is not set (testCase 56).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "Asia-maps.png"},
|
||||||
|
{Name: "Asia/India/India-summer-photos-1"},
|
||||||
|
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ListObjectsResult-25.
|
||||||
|
// Prefix is set to "Asia" in the testCase, and delimiter is set (testCase 57).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "Asia-maps.png"},
|
||||||
|
},
|
||||||
|
Prefixes: []string{"Asia/"},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-26.
|
||||||
|
// prefix = "new" and delimiter is set in the testCase.(testCase 58).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "newPrefix0"},
|
||||||
|
{Name: "newPrefix1"},
|
||||||
|
},
|
||||||
|
Prefixes: []string{"newzen/"},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-27.
|
||||||
|
// Prefix is set to "Asia/India/" in the testCase, and delimiter is set to forward slash '/' (testCase 59).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "Asia/India/India-summer-photos-1"},
|
||||||
|
},
|
||||||
|
Prefixes: []string{"Asia/India/Karnataka/"},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-28.
|
||||||
|
// Marker is set to "Asia/India/India-summer-photos-1" and delimiter set in the testCase, (testCase 60).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "newPrefix0"},
|
||||||
|
{Name: "newPrefix1"},
|
||||||
|
{Name: "obj0"},
|
||||||
|
{Name: "obj1"},
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
Prefixes: []string{"newzen/"},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-29.
|
||||||
|
// Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase and delimiter set, (testCase 61).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "newPrefix0"},
|
||||||
|
{Name: "newPrefix1"},
|
||||||
|
{Name: "obj0"},
|
||||||
|
{Name: "obj1"},
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
Prefixes: []string{"newzen/"},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-30.
|
||||||
|
// Prefix and Delimiter is set to '/', (testCase 62).
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-31 Empty directory, recursive listing
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "obj1"},
|
||||||
|
{Name: "obj2"},
|
||||||
|
{Name: "temporary/0/"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-32 Empty directory, non recursive listing
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "obj1"},
|
||||||
|
{Name: "obj2"},
|
||||||
|
},
|
||||||
|
Prefixes: []string{"temporary/"},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-33 Listing empty directory only
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{
|
||||||
|
{Name: "temporary/0/"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ListObjectsResult-34:
|
||||||
|
// * Listing with marker > last object should return empty
|
||||||
|
// * Listing an object with a trailing slash and '/' delimiter
|
||||||
|
{
|
||||||
|
IsTruncated: false,
|
||||||
|
Objects: []ObjectInfo{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
// Inputs to ListObjects.
|
||||||
|
bucketName string
|
||||||
|
prefix string
|
||||||
|
marker string
|
||||||
|
delimiter string
|
||||||
|
maxKeys int32
|
||||||
|
// Expected output of ListObjects.
|
||||||
|
result ListObjectsInfo
|
||||||
|
err error
|
||||||
|
// Flag indicating whether the test is expected to pass or not.
|
||||||
|
shouldPass bool
|
||||||
|
}{
|
||||||
|
// Test cases with invalid bucket names ( Test number 1-4).
|
||||||
|
{".test", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: ".test"}, false},
|
||||||
|
{"Test", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "Test"}, false},
|
||||||
|
{"---", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "---"}, false},
|
||||||
|
{"ad", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "ad"}, false},
|
||||||
|
// Using an existing file for bucket name, but its not a directory (5).
|
||||||
|
{"simple-file.txt", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "simple-file.txt"}, false},
|
||||||
|
// Valid bucket names, but they donot exist (6-8).
|
||||||
|
{"volatile-bucket-1", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false},
|
||||||
|
{"volatile-bucket-2", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false},
|
||||||
|
{"volatile-bucket-3", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-3"}, false},
|
||||||
|
// Testing for failure cases with both perfix and marker (9).
|
||||||
|
// The prefix and marker combination to be valid it should satisfy strings.HasPrefix(marker, prefix).
|
||||||
|
{"test-bucket-list-object", "asia", "europe-object", "", 0, ListObjectsInfo{}, fmt.Errorf("Invalid combination of marker '%s' and prefix '%s'", "europe-object", "asia"), false},
|
||||||
|
// Setting a non-existing directory to be prefix (10-11).
|
||||||
|
{"empty-bucket", "europe/france/", "", "", 1, ListObjectsInfo{}, nil, true},
|
||||||
|
{"empty-bucket", "africa/tunisia/", "", "", 1, ListObjectsInfo{}, nil, true},
|
||||||
|
// Testing on empty bucket, that is, bucket without any objects in it (12).
|
||||||
|
{"empty-bucket", "", "", "", 0, ListObjectsInfo{}, nil, true},
|
||||||
|
// Setting maxKeys to negative value (13-14).
|
||||||
|
{"empty-bucket", "", "", "", -1, ListObjectsInfo{}, nil, true},
|
||||||
|
{"empty-bucket", "", "", "", 1, ListObjectsInfo{}, nil, true},
|
||||||
|
// Setting maxKeys to a very large value (15).
|
||||||
|
{"empty-bucket", "", "", "", 111100000, ListObjectsInfo{}, nil, true},
|
||||||
|
// Testing for all 10 objects in the bucket (16).
|
||||||
|
{"test-bucket-list-object", "", "", "", 10, resultCases[0], nil, true},
|
||||||
|
//Testing for negative value of maxKey, this should set maxKeys to listObjectsLimit (17).
|
||||||
|
{"test-bucket-list-object", "", "", "", -1, resultCases[0], nil, true},
|
||||||
|
// Testing for very large value of maxKey, this should set maxKeys to listObjectsLimit (18).
|
||||||
|
{"test-bucket-list-object", "", "", "", 1234567890, resultCases[0], nil, true},
|
||||||
|
// Testing for trancated value (19-22).
|
||||||
|
{"test-bucket-list-object", "", "", "", 5, resultCases[1], nil, true},
|
||||||
|
{"test-bucket-list-object", "", "", "", 4, resultCases[2], nil, true},
|
||||||
|
{"test-bucket-list-object", "", "", "", 3, resultCases[3], nil, true},
|
||||||
|
{"test-bucket-list-object", "", "", "", 1, resultCases[4], nil, true},
|
||||||
|
// Testing with prefix (23-26).
|
||||||
|
{"test-bucket-list-object", "new", "", "", 3, resultCases[5], nil, true},
|
||||||
|
{"test-bucket-list-object", "new", "", "", 4, resultCases[5], nil, true},
|
||||||
|
{"test-bucket-list-object", "new", "", "", 5, resultCases[5], nil, true},
|
||||||
|
{"test-bucket-list-object", "obj", "", "", 3, resultCases[6], nil, true},
|
||||||
|
// Testing with prefix and truncation (27-28).
|
||||||
|
{"test-bucket-list-object", "new", "", "", 1, resultCases[7], nil, true},
|
||||||
|
{"test-bucket-list-object", "obj", "", "", 2, resultCases[8], nil, true},
|
||||||
|
// Testing with marker, but without prefix and truncation (29-33).
|
||||||
|
{"test-bucket-list-object", "", "newPrefix0", "", 6, resultCases[9], nil, true},
|
||||||
|
{"test-bucket-list-object", "", "newPrefix1", "", 5, resultCases[10], nil, true},
|
||||||
|
{"test-bucket-list-object", "", "obj0", "", 4, resultCases[11], nil, true},
|
||||||
|
{"test-bucket-list-object", "", "obj1", "", 2, resultCases[12], nil, true},
|
||||||
|
{"test-bucket-list-object", "", "man", "", 11, resultCases[13], nil, true},
|
||||||
|
// Marker being set to a value which is greater than and all object names when sorted (34).
|
||||||
|
// Expected to send an empty response in this case.
|
||||||
|
{"test-bucket-list-object", "", "zen", "", 10, ListObjectsInfo{}, nil, true},
|
||||||
|
// Marker being set to a value which is lesser than and all object names when sorted (35).
|
||||||
|
// Expected to send all the objects in the bucket in this case.
|
||||||
|
{"test-bucket-list-object", "", "Abc", "", 10, resultCases[14], nil, true},
|
||||||
|
// Marker is to a hierarhical value (36-37).
|
||||||
|
{"test-bucket-list-object", "", "Asia/India/India-summer-photos-1", "", 10, resultCases[15], nil, true},
|
||||||
|
{"test-bucket-list-object", "", "Asia/India/Karnataka/Bangalore/Koramangala/pics", "", 10, resultCases[16], nil, true},
|
||||||
|
// Testing with marker and truncation, but no prefix (38-40).
|
||||||
|
{"test-bucket-list-object", "", "newPrefix0", "", 3, resultCases[17], nil, true},
|
||||||
|
{"test-bucket-list-object", "", "newPrefix1", "", 1, resultCases[18], nil, true},
|
||||||
|
{"test-bucket-list-object", "", "obj0", "", 1, resultCases[19], nil, true},
|
||||||
|
// Testing with both marker and prefix, but without truncation (41-43).
|
||||||
|
// The valid combination of marker and prefix should satisfy strings.HasPrefix(marker, prefix).
|
||||||
|
{"test-bucket-list-object", "obj", "obj0", "", 2, resultCases[20], nil, true},
|
||||||
|
{"test-bucket-list-object", "obj", "obj1", "", 1, resultCases[21], nil, true},
|
||||||
|
{"test-bucket-list-object", "new", "newPrefix0", "", 2, resultCases[22], nil, true},
|
||||||
|
// Testing with maxKeys set to 0 (44-50).
|
||||||
|
// The parameters have to valid.
|
||||||
|
{"test-bucket-list-object", "", "obj1", "", 0, ListObjectsInfo{}, nil, true},
|
||||||
|
{"test-bucket-list-object", "", "obj0", "", 0, ListObjectsInfo{}, nil, true},
|
||||||
|
{"test-bucket-list-object", "new", "", "", 0, ListObjectsInfo{}, nil, true},
|
||||||
|
{"test-bucket-list-object", "obj", "", "", 0, ListObjectsInfo{}, nil, true},
|
||||||
|
{"test-bucket-list-object", "obj", "obj0", "", 0, ListObjectsInfo{}, nil, true},
|
||||||
|
{"test-bucket-list-object", "obj", "obj1", "", 0, ListObjectsInfo{}, nil, true},
|
||||||
|
{"test-bucket-list-object", "new", "newPrefix0", "", 0, ListObjectsInfo{}, nil, true},
|
||||||
|
// Tests on hierarchical key names as prefix.
|
||||||
|
// Without delimteter the code should recurse into the prefix Dir.
|
||||||
|
// Tests with prefix, but without delimiter (51-52).
|
||||||
|
{"test-bucket-list-object", "Asia/India/", "", "", 10, resultCases[23], nil, true},
|
||||||
|
{"test-bucket-list-object", "Asia", "", "", 10, resultCases[24], nil, true},
|
||||||
|
// Tests with prefix and delimiter (53-55).
|
||||||
|
// With delimiter the code should not recurse into the sub-directories of prefix Dir.
|
||||||
|
{"test-bucket-list-object", "Asia", "", SlashSeparator, 10, resultCases[25], nil, true},
|
||||||
|
{"test-bucket-list-object", "new", "", SlashSeparator, 10, resultCases[26], nil, true},
|
||||||
|
{"test-bucket-list-object", "Asia/India/", "", SlashSeparator, 10, resultCases[27], nil, true},
|
||||||
|
// Test with marker set as hierarhical value and with delimiter. (56-57)
|
||||||
|
{"test-bucket-list-object", "", "Asia/India/India-summer-photos-1", SlashSeparator, 10, resultCases[28], nil, true},
|
||||||
|
{"test-bucket-list-object", "", "Asia/India/Karnataka/Bangalore/Koramangala/pics", SlashSeparator, 10, resultCases[29], nil, true},
|
||||||
|
// Test with prefix and delimiter set to '/'. (58)
|
||||||
|
{"test-bucket-list-object", SlashSeparator, "", SlashSeparator, 10, resultCases[30], nil, true},
|
||||||
|
// Test with invalid prefix (59)
|
||||||
|
{"test-bucket-list-object", "\\", "", SlashSeparator, 10, ListObjectsInfo{}, nil, true},
|
||||||
|
// Test listing an empty directory in recursive mode (60)
|
||||||
|
{"test-bucket-empty-dir", "", "", "", 10, resultCases[31], nil, true},
|
||||||
|
// Test listing an empty directory in a non recursive mode (61)
|
||||||
|
{"test-bucket-empty-dir", "", "", SlashSeparator, 10, resultCases[32], nil, true},
|
||||||
|
// Test listing a directory which contains an empty directory (62)
|
||||||
|
{"test-bucket-empty-dir", "", "temporary/", "", 10, resultCases[33], nil, true},
|
||||||
|
// Test listing with marker > last object such that response should be empty (63)
|
||||||
|
{"test-bucket-single-object", "", "A/C", "", 1000, resultCases[34], nil, true},
|
||||||
|
// Test listing an object with a trailing slash and a slash delimiter (64)
|
||||||
|
{"test-bucket-list-object", "Asia-maps.png/", "", "/", 1000, resultCases[34], nil, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(fmt.Sprintf("%s-Test%d", instanceType, i+1), func(t *testing.T) {
|
||||||
|
result, err := obj.ListObjectVersions(context.Background(), testCase.bucketName,
|
||||||
|
testCase.prefix, testCase.marker, "", testCase.delimiter, int(testCase.maxKeys))
|
||||||
|
if _, ok := err.(NotImplemented); ok {
|
||||||
|
// Not implemented should be skipped
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
if err != nil && testCase.shouldPass {
|
||||||
|
t.Errorf("%s: Expected to pass, but failed with: <ERROR> %s", instanceType, err.Error())
|
||||||
|
}
|
||||||
|
if err == nil && !testCase.shouldPass {
|
||||||
|
t.Errorf("%s: Expected to fail with <ERROR> \"%s\", but passed instead", instanceType, testCase.err.Error())
|
||||||
|
}
|
||||||
|
// Failed as expected, but does it fail for the expected reason.
|
||||||
|
if err != nil && !testCase.shouldPass {
|
||||||
|
if !strings.Contains(err.Error(), testCase.err.Error()) {
|
||||||
|
t.Errorf("%s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", instanceType, testCase.err.Error(), err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Since there are cases for which ListObjects fails, this is
|
||||||
|
// necessary. Test passes as expected, but the output values
|
||||||
|
// are verified for correctness here.
|
||||||
|
if err == nil && testCase.shouldPass {
|
||||||
|
// The length of the expected ListObjectsResult.Objects
|
||||||
|
// should match in both expected result from test cases
|
||||||
|
// and in the output. On failure calling t.Fatalf,
|
||||||
|
// otherwise it may lead to index out of range error in
|
||||||
|
// assertion following this.
|
||||||
|
if len(testCase.result.Objects) != len(result.Objects) {
|
||||||
|
t.Fatalf("%s: Expected number of object in the result to be '%d', but found '%d' objects instead", instanceType, len(testCase.result.Objects), len(result.Objects))
|
||||||
|
}
|
||||||
|
for j := 0; j < len(testCase.result.Objects); j++ {
|
||||||
|
if testCase.result.Objects[j].Name != result.Objects[j].Name {
|
||||||
|
t.Errorf("%s: Expected object name to be \"%s\", but found \"%s\" instead", instanceType, testCase.result.Objects[j].Name, result.Objects[j].Name)
|
||||||
|
}
|
||||||
|
// FIXME: we should always check for ETag
|
||||||
|
if result.Objects[j].ETag == "" && !strings.HasSuffix(result.Objects[j].Name, SlashSeparator) {
|
||||||
|
t.Errorf("%s: Expected ETag to be not empty, but found empty instead (%v)", instanceType, result.Objects[j].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(testCase.result.Prefixes) != len(result.Prefixes) {
|
||||||
|
fmt.Println(testCase, testCase.result.Prefixes, result.Prefixes)
|
||||||
|
t.Fatalf("%s: Expected number of prefixes in the result to be '%d', but found '%d' prefixes instead", instanceType, len(testCase.result.Prefixes), len(result.Prefixes))
|
||||||
|
}
|
||||||
|
for j := 0; j < len(testCase.result.Prefixes); j++ {
|
||||||
|
if testCase.result.Prefixes[j] != result.Prefixes[j] {
|
||||||
|
t.Errorf("%s: Expected prefix name to be \"%s\", but found \"%s\" instead", instanceType, testCase.result.Prefixes[j], result.Prefixes[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if testCase.result.IsTruncated != result.IsTruncated {
|
||||||
|
t.Errorf("%s: Expected IsTruncated flag to be %v, but instead found it to be %v", instanceType, testCase.result.IsTruncated, result.IsTruncated)
|
||||||
|
}
|
||||||
|
|
||||||
|
if testCase.result.IsTruncated && result.NextMarker == "" {
|
||||||
|
t.Errorf("%s: Expected NextContinuationToken to contain a string since listing is truncated, but instead found it to be empty", instanceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.result.IsTruncated && result.NextMarker != "" {
|
||||||
|
t.Errorf("%s: Expected NextContinuationToken to be empty since listing is not truncated, but instead found `%v`", instanceType, result.NextMarker)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// Take ListObject treeWalk go-routine to completion, if available in the treewalk pool.
|
||||||
|
if result.IsTruncated {
|
||||||
|
_, err = obj.ListObjectVersions(context.Background(), testCase.bucketName,
|
||||||
|
testCase.prefix, result.NextMarker, "", testCase.delimiter, 1000)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -400,6 +400,15 @@ func (o ObjectInfo) IsCompressedOK() (bool, error) {
|
|||||||
return true, fmt.Errorf("unknown compression scheme: %s", scheme)
|
return true, fmt.Errorf("unknown compression scheme: %s", scheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetActualETag - returns the actual etag of the stored object
|
||||||
|
// decrypts SSE objects.
|
||||||
|
func (o ObjectInfo) GetActualETag(h http.Header) string {
|
||||||
|
if !crypto.IsEncrypted(o.UserDefined) {
|
||||||
|
return o.ETag
|
||||||
|
}
|
||||||
|
return getDecryptedETag(h, o, false)
|
||||||
|
}
|
||||||
|
|
||||||
// GetActualSize - returns the actual size of the stored object
|
// GetActualSize - returns the actual size of the stored object
|
||||||
func (o ObjectInfo) GetActualSize() (int64, error) {
|
func (o ObjectInfo) GetActualSize() (int64, error) {
|
||||||
if crypto.IsEncrypted(o.UserDefined) {
|
if crypto.IsEncrypted(o.UserDefined) {
|
||||||
|
|||||||
@@ -1021,6 +1021,8 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
rawReader := srcInfo.Reader
|
rawReader := srcInfo.Reader
|
||||||
pReader := NewPutObjReader(srcInfo.Reader, nil, nil)
|
pReader := NewPutObjReader(srcInfo.Reader, nil, nil)
|
||||||
|
|
||||||
|
// Check if either the source is encrypted or the destination will be encrypted.
|
||||||
|
objectEncryption := crypto.IsSourceEncrypted(srcInfo.UserDefined) || crypto.IsRequested(r.Header)
|
||||||
var encMetadata = make(map[string]string)
|
var encMetadata = make(map[string]string)
|
||||||
if objectAPI.IsEncryptionSupported() && !isCompressed {
|
if objectAPI.IsEncryptionSupported() && !isCompressed {
|
||||||
// Encryption parameters not applicable for this object.
|
// Encryption parameters not applicable for this object.
|
||||||
@@ -1202,7 +1204,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
// if encryption is enabled we do not need explicit "REPLACE" metadata to
|
// if encryption is enabled we do not need explicit "REPLACE" metadata to
|
||||||
// be enabled as well - this is to allow for key-rotation.
|
// be enabled as well - this is to allow for key-rotation.
|
||||||
if !isDirectiveReplace(r.Header.Get(xhttp.AmzMetadataDirective)) && !isDirectiveReplace(r.Header.Get(xhttp.AmzTagDirective)) &&
|
if !isDirectiveReplace(r.Header.Get(xhttp.AmzMetadataDirective)) && !isDirectiveReplace(r.Header.Get(xhttp.AmzTagDirective)) &&
|
||||||
srcInfo.metadataOnly && !crypto.IsEncrypted(srcInfo.UserDefined) && srcOpts.VersionID == "" {
|
srcInfo.metadataOnly && !crypto.IsEncrypted(srcInfo.UserDefined) && srcOpts.VersionID == "" && !objectEncryption {
|
||||||
// If x-amz-metadata-directive is not set to REPLACE then we need
|
// If x-amz-metadata-directive is not set to REPLACE then we need
|
||||||
// to error out if source and destination are same.
|
// to error out if source and destination are same.
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidCopyDest), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidCopyDest), r.URL, guessIsBrowserReq(r))
|
||||||
@@ -1261,7 +1263,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
response := generateCopyObjectResponse(objInfo.ETag, objInfo.ModTime)
|
response := generateCopyObjectResponse(objInfo.ETag, objInfo.ModTime)
|
||||||
encodedSuccessResponse := encodeResponse(response)
|
encodedSuccessResponse := encodeResponse(response)
|
||||||
if mustReplicate(ctx, r, dstBucket, dstObject, objInfo.UserDefined, objInfo.ReplicationStatus.String()) {
|
if mustReplicate(ctx, r, dstBucket, dstObject, objInfo.UserDefined, objInfo.ReplicationStatus.String()) {
|
||||||
defer replicateObject(context.Background(), dstBucket, dstObject, objInfo.VersionID, objectAPI, &eventArgs{
|
defer replicateObject(GlobalContext, dstBucket, dstObject, objInfo.VersionID, objectAPI, &eventArgs{
|
||||||
EventName: event.ObjectCreatedCopy,
|
EventName: event.ObjectCreatedCopy,
|
||||||
BucketName: dstBucket,
|
BucketName: dstBucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
@@ -1577,7 +1579,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if mustReplicate(ctx, r, bucket, object, metadata, "") {
|
if mustReplicate(ctx, r, bucket, object, metadata, "") {
|
||||||
defer replicateObject(context.Background(), bucket, object, objInfo.VersionID, objectAPI, &eventArgs{
|
defer replicateObject(GlobalContext, bucket, object, objInfo.VersionID, objectAPI, &eventArgs{
|
||||||
EventName: event.ObjectCreatedPut,
|
EventName: event.ObjectCreatedPut,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
@@ -2652,7 +2654,7 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
|||||||
|
|
||||||
setPutObjHeaders(w, objInfo, false)
|
setPutObjHeaders(w, objInfo, false)
|
||||||
if mustReplicate(ctx, r, bucket, object, objInfo.UserDefined, objInfo.ReplicationStatus.String()) {
|
if mustReplicate(ctx, r, bucket, object, objInfo.UserDefined, objInfo.ReplicationStatus.String()) {
|
||||||
defer replicateObject(context.Background(), bucket, object, objInfo.VersionID, objectAPI, &eventArgs{
|
defer replicateObject(GlobalContext, bucket, object, objInfo.VersionID, objectAPI, &eventArgs{
|
||||||
EventName: event.ObjectCreatedCompleteMultipartUpload,
|
EventName: event.ObjectCreatedCompleteMultipartUpload,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
|
|||||||
@@ -22,5 +22,5 @@ package cmd
|
|||||||
import "syscall"
|
import "syscall"
|
||||||
|
|
||||||
func direntInode(dirent *syscall.Dirent) uint64 {
|
func direntInode(dirent *syscall.Dirent) uint64 {
|
||||||
return uint64(dirent.Ino)
|
return dirent.Ino
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ import (
|
|||||||
// refer https://github.com/golang/go/issues/24015
|
// refer https://github.com/golang/go/issues/24015
|
||||||
const blockSize = 8 << 10 // 8192
|
const blockSize = 8 << 10 // 8192
|
||||||
|
|
||||||
// By default atleast 1000 entries in single getdents call
|
// By default atleast 20 entries in single getdents call
|
||||||
var direntPool = sync.Pool{
|
var direntPool = sync.Pool{
|
||||||
New: func() interface{} {
|
New: func() interface{} {
|
||||||
buf := make([]byte, blockSize*1000)
|
buf := make([]byte, blockSize*20)
|
||||||
return &buf
|
return &buf
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,14 +217,14 @@ func (client *peerRESTClient) doNetOBDTest(ctx context.Context, dataSize int64,
|
|||||||
finish()
|
finish()
|
||||||
end := time.Now()
|
end := time.Now()
|
||||||
|
|
||||||
latency := float64(end.Sub(start).Seconds())
|
latency := end.Sub(start).Seconds()
|
||||||
|
|
||||||
if latency > maxLatencyForSizeThreads(dataSize, threadCount) {
|
if latency > maxLatencyForSizeThreads(dataSize, threadCount) {
|
||||||
slowSample()
|
slowSample()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Throughput = (total data transferred across all threads / time taken) */
|
/* Throughput = (total data transferred across all threads / time taken) */
|
||||||
throughput := float64(float64((after - before)) / latency)
|
throughput := float64((after - before)) / latency
|
||||||
|
|
||||||
latencies = append(latencies, latency)
|
latencies = append(latencies, latency)
|
||||||
throughputs = append(throughputs, throughput)
|
throughputs = append(throughputs, throughput)
|
||||||
@@ -272,7 +272,7 @@ func maxLatencyForSizeThreads(size int64, threadCount uint) float64 {
|
|||||||
// 10 Gbit | 2s
|
// 10 Gbit | 2s
|
||||||
// 1 Gbit | inf
|
// 1 Gbit | inf
|
||||||
|
|
||||||
throughput := float64(int64(size) * int64(threadCount))
|
throughput := float64(size * int64(threadCount))
|
||||||
if throughput >= Gbit100 {
|
if throughput >= Gbit100 {
|
||||||
return 2.0
|
return 2.0
|
||||||
} else if throughput >= Gbit40 {
|
} else if throughput >= Gbit40 {
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ func (s *peerRESTServer) DeleteServiceAccountHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := globalIAMSys.DeleteServiceAccount(context.Background(), accessKey); err != nil {
|
if err := globalIAMSys.DeleteServiceAccount(r.Context(), accessKey); err != nil {
|
||||||
s.writeErrorResponse(w, err)
|
s.writeErrorResponse(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErr
|
|||||||
{
|
{
|
||||||
objectName: "test",
|
objectName: "test",
|
||||||
data: []byte("Hello, World"),
|
data: []byte("Hello, World"),
|
||||||
expectedRespStatus: http.StatusBadRequest,
|
expectedRespStatus: http.StatusForbidden,
|
||||||
accessKey: "",
|
accessKey: "",
|
||||||
secretKey: "",
|
secretKey: "",
|
||||||
malformedBody: false,
|
malformedBody: false,
|
||||||
@@ -293,7 +293,7 @@ func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErr
|
|||||||
{
|
{
|
||||||
objectName: "test",
|
objectName: "test",
|
||||||
data: []byte("Hello, World"),
|
data: []byte("Hello, World"),
|
||||||
expectedRespStatus: http.StatusBadRequest,
|
expectedRespStatus: http.StatusForbidden,
|
||||||
accessKey: "",
|
accessKey: "",
|
||||||
secretKey: "",
|
secretKey: "",
|
||||||
dates: []interface{}{},
|
dates: []interface{}{},
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ func connectLoadInitFormats(retryCount int, firstDisk bool, endpoints Endpoints,
|
|||||||
humanize.Ordinal(zoneCount), setCount, drivesPerSet)
|
humanize.Ordinal(zoneCount), setCount, drivesPerSet)
|
||||||
|
|
||||||
// Initialize erasure code format on disks
|
// Initialize erasure code format on disks
|
||||||
format, err = initFormatErasure(GlobalContext, storageDisks, setCount, drivesPerSet, deploymentID)
|
format, err = initFormatErasure(GlobalContext, storageDisks, setCount, drivesPerSet, deploymentID, sErrs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -301,6 +301,9 @@ func connectLoadInitFormats(retryCount int, firstDisk bool, endpoints Endpoints,
|
|||||||
return nil, nil, errFirstDiskWait
|
return nil, nil, errFirstDiskWait
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark all root disks down
|
||||||
|
markRootDisksAsDown(storageDisks, sErrs)
|
||||||
|
|
||||||
// Following function is added to fix a regressions which was introduced
|
// Following function is added to fix a regressions which was introduced
|
||||||
// in release RELEASE.2018-03-16T22-52-12Z after migrating v1 to v2 to v3.
|
// in release RELEASE.2018-03-16T22-52-12Z after migrating v1 to v2 to v3.
|
||||||
// This migration failed to capture '.This' field properly which indicates
|
// This migration failed to capture '.This' field properly which indicates
|
||||||
|
|||||||
+11
-7
@@ -122,6 +122,11 @@ func serverHandleCmdArgs(ctx *cli.Context) {
|
|||||||
globalRootCAs, err = certs.GetRootCAs(globalCertsCADir.Get())
|
globalRootCAs, err = certs.GetRootCAs(globalCertsCADir.Get())
|
||||||
logger.FatalIf(err, "Failed to read root CAs (%v)", err)
|
logger.FatalIf(err, "Failed to read root CAs (%v)", err)
|
||||||
|
|
||||||
|
// Add the global public crts as part of global root CAs
|
||||||
|
for _, publicCrt := range globalPublicCerts {
|
||||||
|
globalRootCAs.AddCert(publicCrt)
|
||||||
|
}
|
||||||
|
|
||||||
// Register root CAs for remote ENVs
|
// Register root CAs for remote ENVs
|
||||||
env.RegisterGlobalCAs(globalRootCAs)
|
env.RegisterGlobalCAs(globalRootCAs)
|
||||||
|
|
||||||
@@ -224,6 +229,9 @@ func initSafeMode(ctx context.Context, newObject ObjectLayer) (err error) {
|
|||||||
initAutoHeal(ctx, newObject)
|
initAutoHeal(ctx, newObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allocate dynamic timeout once before the loop
|
||||||
|
configLockTimeout := newDynamicTimeout(3*time.Second, 5*time.Second)
|
||||||
|
|
||||||
// **** WARNING ****
|
// **** WARNING ****
|
||||||
// Migrating to encrypted backend should happen before initialization of any
|
// Migrating to encrypted backend should happen before initialization of any
|
||||||
// sub-systems, make sure that we do not move the above codeblock elsewhere.
|
// sub-systems, make sure that we do not move the above codeblock elsewhere.
|
||||||
@@ -239,7 +247,7 @@ func initSafeMode(ctx context.Context, newObject ObjectLayer) (err error) {
|
|||||||
for range retry.NewTimer(retryCtx) {
|
for range retry.NewTimer(retryCtx) {
|
||||||
// let one of the server acquire the lock, if not let them timeout.
|
// let one of the server acquire the lock, if not let them timeout.
|
||||||
// which shall be retried again by this loop.
|
// which shall be retried again by this loop.
|
||||||
if err = txnLk.GetLock(newDynamicTimeout(3*time.Second, 3*time.Second)); err != nil {
|
if err = txnLk.GetLock(configLockTimeout); err != nil {
|
||||||
logger.Info("Waiting for all MinIO sub-systems to be initialized.. trying to acquire lock")
|
logger.Info("Waiting for all MinIO sub-systems to be initialized.. trying to acquire lock")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -346,12 +354,12 @@ func initAllSubsystems(ctx context.Context, newObject ObjectLayer) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize notification system.
|
// Initialize notification system.
|
||||||
if err = globalNotificationSys.Init(buckets, newObject); err != nil {
|
if err = globalNotificationSys.Init(ctx, buckets, newObject); err != nil {
|
||||||
return fmt.Errorf("Unable to initialize notification system: %w", err)
|
return fmt.Errorf("Unable to initialize notification system: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize bucket targets sub-system.
|
// Initialize bucket targets sub-system.
|
||||||
if err = globalBucketTargetSys.Init(GlobalContext, buckets, newObject); err != nil {
|
if err = globalBucketTargetSys.Init(ctx, buckets, newObject); err != nil {
|
||||||
return fmt.Errorf("Unable to initialize bucket target sub-system: %w", err)
|
return fmt.Errorf("Unable to initialize bucket target sub-system: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,10 +379,6 @@ func startBackgroundOps(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
// No unlock for "leader" lock.
|
// No unlock for "leader" lock.
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalIsErasure {
|
|
||||||
initGlobalHeal(ctx, objAPI)
|
|
||||||
}
|
|
||||||
|
|
||||||
initDataCrawler(ctx, objAPI)
|
initDataCrawler(ctx, objAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,13 +94,13 @@ func printStartupSafeModeMessage(apiEndpoints []string, err error) {
|
|||||||
if color.IsTerminal() && !globalCLIContext.Anonymous {
|
if color.IsTerminal() && !globalCLIContext.Anonymous {
|
||||||
logStartupMessage(color.RedBold("\nCommand-line Access: ") + mcAdminQuickStartGuide)
|
logStartupMessage(color.RedBold("\nCommand-line Access: ") + mcAdminQuickStartGuide)
|
||||||
if runtime.GOOS == globalWindowsOSName {
|
if runtime.GOOS == globalWindowsOSName {
|
||||||
mcMessage := fmt.Sprintf("> mc.exe config host add %s %s %s %s --api s3v4", alias,
|
mcMessage := fmt.Sprintf("> mc.exe alias set %s %s %s %s --api s3v4", alias,
|
||||||
endPoint, cred.AccessKey, cred.SecretKey)
|
endPoint, cred.AccessKey, cred.SecretKey)
|
||||||
logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
||||||
mcMessage = "> mc.exe admin config --help"
|
mcMessage = "> mc.exe admin config --help"
|
||||||
logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
||||||
} else {
|
} else {
|
||||||
mcMessage := fmt.Sprintf("$ mc config host add %s %s %s %s --api s3v4", alias,
|
mcMessage := fmt.Sprintf("$ mc alias set %s %s %s %s --api s3v4", alias,
|
||||||
endPoint, cred.AccessKey, cred.SecretKey)
|
endPoint, cred.AccessKey, cred.SecretKey)
|
||||||
logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
||||||
mcMessage = "$ mc admin config --help"
|
mcMessage = "$ mc admin config --help"
|
||||||
@@ -233,11 +233,11 @@ func printCLIAccessMsg(endPoint string, alias string) {
|
|||||||
if color.IsTerminal() && !globalCLIContext.Anonymous {
|
if color.IsTerminal() && !globalCLIContext.Anonymous {
|
||||||
logStartupMessage(color.Blue("\nCommand-line Access: ") + mcQuickStartGuide)
|
logStartupMessage(color.Blue("\nCommand-line Access: ") + mcQuickStartGuide)
|
||||||
if runtime.GOOS == globalWindowsOSName {
|
if runtime.GOOS == globalWindowsOSName {
|
||||||
mcMessage := fmt.Sprintf("$ mc.exe config host add %s %s %s %s", alias,
|
mcMessage := fmt.Sprintf("$ mc.exe alias set %s %s %s %s", alias,
|
||||||
endPoint, cred.AccessKey, cred.SecretKey)
|
endPoint, cred.AccessKey, cred.SecretKey)
|
||||||
logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
||||||
} else {
|
} else {
|
||||||
mcMessage := fmt.Sprintf("$ mc config host add %s %s %s %s", alias,
|
mcMessage := fmt.Sprintf("$ mc alias set %s %s %s %s", alias,
|
||||||
endPoint, cred.AccessKey, cred.SecretKey)
|
endPoint, cred.AccessKey, cred.SecretKey)
|
||||||
logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
||||||
}
|
}
|
||||||
@@ -301,8 +301,8 @@ func printStorageInfo(storageInfo StorageInfo) {
|
|||||||
|
|
||||||
func printCacheStorageInfo(storageInfo CacheStorageInfo) {
|
func printCacheStorageInfo(storageInfo CacheStorageInfo) {
|
||||||
msg := fmt.Sprintf("%s %s Free, %s Total", color.Blue("Cache Capacity:"),
|
msg := fmt.Sprintf("%s %s Free, %s Total", color.Blue("Cache Capacity:"),
|
||||||
humanize.IBytes(uint64(storageInfo.Free)),
|
humanize.IBytes(storageInfo.Free),
|
||||||
humanize.IBytes(uint64(storageInfo.Total)))
|
humanize.IBytes(storageInfo.Total))
|
||||||
logStartupMessage(msg)
|
logStartupMessage(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-31
@@ -22,6 +22,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,17 +49,17 @@ func (c credentialHeader) getScope() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getReqAccessKeyV4(r *http.Request, region string, stype serviceType) (auth.Credentials, bool, APIErrorCode) {
|
func getReqAccessKeyV4(r *http.Request, region string, stype serviceType) (auth.Credentials, bool, APIErrorCode) {
|
||||||
ch, err := parseCredentialHeader("Credential="+r.URL.Query().Get("X-Amz-Credential"), region, stype)
|
ch, s3Err := parseCredentialHeader("Credential="+r.URL.Query().Get(xhttp.AmzCredential), region, stype)
|
||||||
if err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
// Strip off the Algorithm prefix.
|
// Strip off the Algorithm prefix.
|
||||||
v4Auth := strings.TrimPrefix(r.Header.Get("Authorization"), signV4Algorithm)
|
v4Auth := strings.TrimPrefix(r.Header.Get("Authorization"), signV4Algorithm)
|
||||||
authFields := strings.Split(strings.TrimSpace(v4Auth), ",")
|
authFields := strings.Split(strings.TrimSpace(v4Auth), ",")
|
||||||
if len(authFields) != 3 {
|
if len(authFields) != 3 {
|
||||||
return auth.Credentials{}, false, ErrMissingFields
|
return auth.Credentials{}, false, ErrMissingFields
|
||||||
}
|
}
|
||||||
ch, err = parseCredentialHeader(authFields[0], region, stype)
|
ch, s3Err = parseCredentialHeader(authFields[0], region, stype)
|
||||||
if err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
return auth.Credentials{}, false, err
|
return auth.Credentials{}, false, s3Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return checkKeyValid(ch.accessKey)
|
return checkKeyValid(ch.accessKey)
|
||||||
@@ -179,7 +180,7 @@ type preSignValues struct {
|
|||||||
//
|
//
|
||||||
// verifies if any of the necessary query params are missing in the presigned request.
|
// verifies if any of the necessary query params are missing in the presigned request.
|
||||||
func doesV4PresignParamsExist(query url.Values) APIErrorCode {
|
func doesV4PresignParamsExist(query url.Values) APIErrorCode {
|
||||||
v4PresignQueryParams := []string{"X-Amz-Algorithm", "X-Amz-Credential", "X-Amz-Signature", "X-Amz-Date", "X-Amz-SignedHeaders", "X-Amz-Expires"}
|
v4PresignQueryParams := []string{xhttp.AmzAlgorithm, xhttp.AmzCredential, xhttp.AmzSignature, xhttp.AmzDate, xhttp.AmzSignedHeaders, xhttp.AmzExpires}
|
||||||
for _, v4PresignQueryParam := range v4PresignQueryParams {
|
for _, v4PresignQueryParam := range v4PresignQueryParams {
|
||||||
if _, ok := query[v4PresignQueryParam]; !ok {
|
if _, ok := query[v4PresignQueryParam]; !ok {
|
||||||
return ErrInvalidQueryParams
|
return ErrInvalidQueryParams
|
||||||
@@ -191,13 +192,13 @@ func doesV4PresignParamsExist(query url.Values) APIErrorCode {
|
|||||||
// Parses all the presigned signature values into separate elements.
|
// Parses all the presigned signature values into separate elements.
|
||||||
func parsePreSignV4(query url.Values, region string, stype serviceType) (psv preSignValues, aec APIErrorCode) {
|
func parsePreSignV4(query url.Values, region string, stype serviceType) (psv preSignValues, aec APIErrorCode) {
|
||||||
// verify whether the required query params exist.
|
// verify whether the required query params exist.
|
||||||
err := doesV4PresignParamsExist(query)
|
aec = doesV4PresignParamsExist(query)
|
||||||
if err != ErrNone {
|
if aec != ErrNone {
|
||||||
return psv, err
|
return psv, aec
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify if the query algorithm is supported or not.
|
// Verify if the query algorithm is supported or not.
|
||||||
if query.Get("X-Amz-Algorithm") != signV4Algorithm {
|
if query.Get(xhttp.AmzAlgorithm) != signV4Algorithm {
|
||||||
return psv, ErrInvalidQuerySignatureAlgo
|
return psv, ErrInvalidQuerySignatureAlgo
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,20 +206,20 @@ func parsePreSignV4(query url.Values, region string, stype serviceType) (psv pre
|
|||||||
preSignV4Values := preSignValues{}
|
preSignV4Values := preSignValues{}
|
||||||
|
|
||||||
// Save credential.
|
// Save credential.
|
||||||
preSignV4Values.Credential, err = parseCredentialHeader("Credential="+query.Get("X-Amz-Credential"), region, stype)
|
preSignV4Values.Credential, aec = parseCredentialHeader("Credential="+query.Get(xhttp.AmzCredential), region, stype)
|
||||||
if err != ErrNone {
|
if aec != ErrNone {
|
||||||
return psv, err
|
return psv, aec
|
||||||
}
|
}
|
||||||
|
|
||||||
var e error
|
var e error
|
||||||
// Save date in native time.Time.
|
// Save date in native time.Time.
|
||||||
preSignV4Values.Date, e = time.Parse(iso8601Format, query.Get("X-Amz-Date"))
|
preSignV4Values.Date, e = time.Parse(iso8601Format, query.Get(xhttp.AmzDate))
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return psv, ErrMalformedPresignedDate
|
return psv, ErrMalformedPresignedDate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save expires in native time.Duration.
|
// Save expires in native time.Duration.
|
||||||
preSignV4Values.Expires, e = time.ParseDuration(query.Get("X-Amz-Expires") + "s")
|
preSignV4Values.Expires, e = time.ParseDuration(query.Get(xhttp.AmzExpires) + "s")
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return psv, ErrMalformedExpires
|
return psv, ErrMalformedExpires
|
||||||
}
|
}
|
||||||
@@ -233,15 +234,15 @@ func parsePreSignV4(query url.Values, region string, stype serviceType) (psv pre
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save signed headers.
|
// Save signed headers.
|
||||||
preSignV4Values.SignedHeaders, err = parseSignedHeader("SignedHeaders=" + query.Get("X-Amz-SignedHeaders"))
|
preSignV4Values.SignedHeaders, aec = parseSignedHeader("SignedHeaders=" + query.Get(xhttp.AmzSignedHeaders))
|
||||||
if err != ErrNone {
|
if aec != ErrNone {
|
||||||
return psv, err
|
return psv, aec
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save signature.
|
// Save signature.
|
||||||
preSignV4Values.Signature, err = parseSignature("Signature=" + query.Get("X-Amz-Signature"))
|
preSignV4Values.Signature, aec = parseSignature("Signature=" + query.Get(xhttp.AmzSignature))
|
||||||
if err != ErrNone {
|
if aec != ErrNone {
|
||||||
return psv, err
|
return psv, aec
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return structed form of signature query string.
|
// Return structed form of signature query string.
|
||||||
@@ -279,23 +280,23 @@ func parseSignV4(v4Auth string, region string, stype serviceType) (sv signValues
|
|||||||
// Initialize signature version '4' structured header.
|
// Initialize signature version '4' structured header.
|
||||||
signV4Values := signValues{}
|
signV4Values := signValues{}
|
||||||
|
|
||||||
var err APIErrorCode
|
var s3Err APIErrorCode
|
||||||
// Save credentail values.
|
// Save credentail values.
|
||||||
signV4Values.Credential, err = parseCredentialHeader(strings.TrimSpace(credElement), region, stype)
|
signV4Values.Credential, s3Err = parseCredentialHeader(strings.TrimSpace(credElement), region, stype)
|
||||||
if err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
return sv, err
|
return sv, s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save signed headers.
|
// Save signed headers.
|
||||||
signV4Values.SignedHeaders, err = parseSignedHeader(authFields[1])
|
signV4Values.SignedHeaders, s3Err = parseSignedHeader(authFields[1])
|
||||||
if err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
return sv, err
|
return sv, s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save signature.
|
// Save signature.
|
||||||
signV4Values.Signature, err = parseSignature(authFields[2])
|
signV4Values.Signature, s3Err = parseSignature(authFields[2])
|
||||||
if err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
return sv, err
|
return sv, s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the structure here.
|
// Return the structure here.
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func validateCredentialfields(t *testing.T, testNum int, expectedCredentials cre
|
|||||||
if expectedCredentials.accessKey != actualCredential.accessKey {
|
if expectedCredentials.accessKey != actualCredential.accessKey {
|
||||||
t.Errorf("Test %d: AccessKey mismatch: Expected \"%s\", got \"%s\"", testNum, expectedCredentials.accessKey, actualCredential.accessKey)
|
t.Errorf("Test %d: AccessKey mismatch: Expected \"%s\", got \"%s\"", testNum, expectedCredentials.accessKey, actualCredential.accessKey)
|
||||||
}
|
}
|
||||||
if expectedCredentials.scope.date != actualCredential.scope.date {
|
if !expectedCredentials.scope.date.Equal(actualCredential.scope.date) {
|
||||||
t.Errorf("Test %d: Date mismatch:Expected \"%s\", got \"%s\"", testNum, expectedCredentials.scope.date, actualCredential.scope.date)
|
t.Errorf("Test %d: Date mismatch:Expected \"%s\", got \"%s\"", testNum, expectedCredentials.scope.date, actualCredential.scope.date)
|
||||||
}
|
}
|
||||||
if expectedCredentials.scope.region != actualCredential.scope.region {
|
if expectedCredentials.scope.region != actualCredential.scope.region {
|
||||||
|
|||||||
+3
-3
@@ -173,9 +173,9 @@ func doesPolicySignatureV4Match(formValues http.Header) APIErrorCode {
|
|||||||
region := globalServerRegion
|
region := globalServerRegion
|
||||||
|
|
||||||
// Parse credential tag.
|
// Parse credential tag.
|
||||||
credHeader, err := parseCredentialHeader("Credential="+formValues.Get(xhttp.AmzCredential), region, serviceS3)
|
credHeader, s3Err := parseCredentialHeader("Credential="+formValues.Get(xhttp.AmzCredential), region, serviceS3)
|
||||||
if err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
return ErrMissingFields
|
return s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
cred, _, s3Err := checkKeyValid(credHeader.accessKey)
|
cred, _, s3Err := checkKeyValid(credHeader.accessKey)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func TestDoesPolicySignatureMatch(t *testing.T) {
|
|||||||
// (0) It should fail if 'X-Amz-Credential' is missing.
|
// (0) It should fail if 'X-Amz-Credential' is missing.
|
||||||
{
|
{
|
||||||
form: http.Header{},
|
form: http.Header{},
|
||||||
expected: ErrMissingFields,
|
expected: ErrCredMalformed,
|
||||||
},
|
},
|
||||||
// (1) It should fail if the access key is incorrect.
|
// (1) It should fail if the access key is incorrect.
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type StorageAPI interface {
|
|||||||
// Storage operations.
|
// Storage operations.
|
||||||
IsOnline() bool // Returns true if disk is online.
|
IsOnline() bool // Returns true if disk is online.
|
||||||
IsLocal() bool
|
IsLocal() bool
|
||||||
|
|
||||||
Hostname() string // Returns host name if remote host.
|
Hostname() string // Returns host name if remote host.
|
||||||
Close() error
|
Close() error
|
||||||
GetDiskID() (string, error)
|
GetDiskID() (string, error)
|
||||||
|
|||||||
@@ -334,6 +334,7 @@ func (client *storageRESTClient) CheckParts(volume, path string, fi FileInfo) er
|
|||||||
|
|
||||||
var reader bytes.Buffer
|
var reader bytes.Buffer
|
||||||
if err := gob.NewEncoder(&reader).Encode(fi); err != nil {
|
if err := gob.NewEncoder(&reader).Encode(fi); err != nil {
|
||||||
|
logger.LogIf(context.Background(), err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +479,7 @@ func (client *storageRESTClient) WalkVersions(volume, dirPath, marker string, re
|
|||||||
if gerr := decoder.Decode(&fi); gerr != nil {
|
if gerr := decoder.Decode(&fi); gerr != nil {
|
||||||
// Upon error return
|
// Upon error return
|
||||||
if gerr != io.EOF {
|
if gerr != io.EOF {
|
||||||
logger.LogIf(context.Background(), gerr)
|
logger.LogIf(GlobalContext, gerr)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -826,7 +826,7 @@ func registerStorageRESTHandlers(router *mux.Router, endpointZones EndpointZones
|
|||||||
if !endpoint.IsLocal {
|
if !endpoint.IsLocal {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
storage, err := newXLStorage(endpoint.Path, endpoint.Host)
|
storage, err := newXLStorage(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == errMinDiskSize {
|
if err == errMinDiskSize {
|
||||||
logger.Fatal(config.ErrUnableToWriteInBackend(err).Hint(err.Error()), "Unable to initialize backend")
|
logger.Fatal(config.ErrUnableToWriteInBackend(err).Hint(err.Error()), "Unable to initialize backend")
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ func TestParseHexUint(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
got, err := parseHexUint([]byte(tt.in))
|
got, err := parseHexUint([]byte(tt.in))
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
if !strings.Contains(fmt.Sprint(err), tt.wantErr) {
|
if err != nil && !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
t.Errorf("parseHexUint(%q) = %v, %v; want error %q", tt.in, got, err, tt.wantErr)
|
t.Errorf("parseHexUint(%q) = %v, %v; want error %q", tt.in, got, err, tt.wantErr)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+1
-2
@@ -19,7 +19,6 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
@@ -46,7 +45,7 @@ func writeSTSErrorResponse(ctx context.Context, w http.ResponseWriter, isErrCode
|
|||||||
stsErrorResponse.RequestID = w.Header().Get(xhttp.AmzRequestID)
|
stsErrorResponse.RequestID = w.Header().Get(xhttp.AmzRequestID)
|
||||||
stsErrorResponse.Error.Message = err.Description
|
stsErrorResponse.Error.Message = err.Description
|
||||||
if errCtxt != nil {
|
if errCtxt != nil {
|
||||||
stsErrorResponse.Error.Message = fmt.Sprintf("%v", errCtxt)
|
stsErrorResponse.Error.Message = errCtxt.Error()
|
||||||
}
|
}
|
||||||
var logKind logger.Kind
|
var logKind logger.Kind
|
||||||
switch errCode {
|
switch errCode {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ const (
|
|||||||
|
|
||||||
// LDAP claim keys
|
// LDAP claim keys
|
||||||
ldapUser = "ldapUser"
|
ldapUser = "ldapUser"
|
||||||
|
ldapUserPolicyVariable = "ldap:user"
|
||||||
)
|
)
|
||||||
|
|
||||||
// stsAPIHandlers implements and provides http handlers for AWS STS API.
|
// stsAPIHandlers implements and provides http handlers for AWS STS API.
|
||||||
|
|||||||
@@ -158,8 +158,8 @@ func calculateStreamContentLength(dataLen, chunkSize int64) int64 {
|
|||||||
if dataLen <= 0 {
|
if dataLen <= 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
chunksCount := int64(dataLen / chunkSize)
|
chunksCount := dataLen / chunkSize
|
||||||
remainingBytes := int64(dataLen % chunkSize)
|
remainingBytes := dataLen % chunkSize
|
||||||
var streamLen int64
|
var streamLen int64
|
||||||
streamLen += chunksCount * calculateSignedChunkLength(chunkSize)
|
streamLen += chunksCount * calculateSignedChunkLength(chunkSize)
|
||||||
if remainingBytes > 0 {
|
if remainingBytes > 0 {
|
||||||
|
|||||||
+100
-11
@@ -51,15 +51,84 @@ func filterMatchingPrefix(entries []string, prefixEntry string) []string {
|
|||||||
}
|
}
|
||||||
end--
|
end--
|
||||||
}
|
}
|
||||||
sort.Strings(entries[start:end])
|
|
||||||
return entries[start:end]
|
return entries[start:end]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// xl.ListDir returns entries with trailing "/" for directories. At the object layer
|
||||||
|
// we need to remove this trailing "/" for objects and retain "/" for prefixes before
|
||||||
|
// sorting because the trailing "/" can affect the sorting results for certain cases.
|
||||||
|
// Ex. lets say entries = ["a-b/", "a/"] and both are objects.
|
||||||
|
// sorting with out trailing "/" = ["a", "a-b"]
|
||||||
|
// sorting with trailing "/" = ["a-b/", "a/"]
|
||||||
|
// Hence if entries[] does not have a case like the above example then isLeaf() check
|
||||||
|
// can be delayed till the entry is pushed into the TreeWalkResult channel.
|
||||||
|
// delayIsLeafCheck() returns true if isLeaf can be delayed or false if
|
||||||
|
// isLeaf should be done in listDir()
|
||||||
|
func delayIsLeafCheck(entries []string) bool {
|
||||||
|
for i, entry := range entries {
|
||||||
|
if i == len(entries)-1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// If any byte in the "entry" string is less than '/' then the
|
||||||
|
// next "entry" should not contain '/' at the same same byte position.
|
||||||
|
for j := 0; j < len(entry); j++ {
|
||||||
|
if entry[j] < '/' {
|
||||||
|
if len(entries[i+1]) > j {
|
||||||
|
if entries[i+1][j] == '/' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// ListDirFunc - "listDir" function of type listDirFunc returned by listDirFactory() - explained below.
|
// ListDirFunc - "listDir" function of type listDirFunc returned by listDirFactory() - explained below.
|
||||||
type ListDirFunc func(bucket, prefixDir, prefixEntry string) (emptyDir bool, entries []string)
|
type ListDirFunc func(bucket, prefixDir, prefixEntry string) (emptyDir bool, entries []string, delayIsLeaf bool)
|
||||||
|
|
||||||
|
// IsLeafFunc - A function isLeaf of type isLeafFunc is used to detect if an
|
||||||
|
// entry is a leaf entry. There are 2 scenarios where isLeaf should behave
|
||||||
|
// differently depending on the backend:
|
||||||
|
// 1. FS backend object listing - isLeaf is true if the entry
|
||||||
|
// has no trailing "/"
|
||||||
|
// 2. Erasure backend object listing - isLeaf is true if the entry
|
||||||
|
// is a directory and contains xl.meta
|
||||||
|
type IsLeafFunc func(string, string) bool
|
||||||
|
|
||||||
|
// IsLeafDirFunc - A function isLeafDir of type isLeafDirFunc is used to detect
|
||||||
|
// if an entry is empty directory.
|
||||||
|
type IsLeafDirFunc func(string, string) bool
|
||||||
|
|
||||||
|
func filterListEntries(bucket, prefixDir string, entries []string, prefixEntry string, isLeaf IsLeafFunc) ([]string, bool) {
|
||||||
|
// Listing needs to be sorted.
|
||||||
|
sort.Strings(entries)
|
||||||
|
|
||||||
|
// Filter entries that have the prefix prefixEntry.
|
||||||
|
entries = filterMatchingPrefix(entries, prefixEntry)
|
||||||
|
|
||||||
|
// Can isLeaf() check be delayed till when it has to be sent down the
|
||||||
|
// TreeWalkResult channel?
|
||||||
|
delayIsLeaf := delayIsLeafCheck(entries)
|
||||||
|
if delayIsLeaf {
|
||||||
|
return entries, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLeaf() check has to happen here so that trailing "/" for objects can be removed.
|
||||||
|
for i, entry := range entries {
|
||||||
|
if isLeaf(bucket, pathJoin(prefixDir, entry)) {
|
||||||
|
entries[i] = strings.TrimSuffix(entry, slashSeparator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort again after removing trailing "/" for objects as the previous sort
|
||||||
|
// does not hold good anymore.
|
||||||
|
sort.Strings(entries)
|
||||||
|
return entries, false
|
||||||
|
}
|
||||||
|
|
||||||
// treeWalk walks directory tree recursively pushing TreeWalkResult into the channel as and when it encounters files.
|
// treeWalk walks directory tree recursively pushing TreeWalkResult into the channel as and when it encounters files.
|
||||||
func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker string, recursive bool, listDir ListDirFunc, resultCh chan TreeWalkResult, endWalkCh <-chan struct{}, isEnd bool) (emptyDir bool, treeErr error) {
|
func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker string, recursive bool, listDir ListDirFunc, isLeaf IsLeafFunc, isLeafDir IsLeafDirFunc, resultCh chan TreeWalkResult, endWalkCh <-chan struct{}, isEnd bool) (emptyDir bool, treeErr error) {
|
||||||
// Example:
|
// Example:
|
||||||
// if prefixDir="one/two/three/" and marker="four/five.txt" treeWalk is recursively
|
// if prefixDir="one/two/three/" and marker="four/five.txt" treeWalk is recursively
|
||||||
// called with prefixDir="one/two/three/four/" and marker="five.txt"
|
// called with prefixDir="one/two/three/four/" and marker="five.txt"
|
||||||
@@ -75,7 +144,12 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emptyDir, entries := listDir(bucket, prefixDir, entryPrefixMatch)
|
emptyDir, entries, delayIsLeaf := listDir(bucket, prefixDir, entryPrefixMatch)
|
||||||
|
// When isleaf check is delayed, make sure that it is set correctly here.
|
||||||
|
if delayIsLeaf && isLeaf == nil || isLeafDir == nil {
|
||||||
|
return false, errInvalidArgument
|
||||||
|
}
|
||||||
|
|
||||||
// For an empty list return right here.
|
// For an empty list return right here.
|
||||||
if emptyDir {
|
if emptyDir {
|
||||||
return true, nil
|
return true, nil
|
||||||
@@ -94,8 +168,23 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, entry := range entries {
|
for i, entry := range entries {
|
||||||
pentry := pathJoin(prefixDir, entry)
|
var leaf, leafDir bool
|
||||||
isDir := HasSuffix(pentry, SlashSeparator)
|
|
||||||
|
// Decision to do isLeaf check was pushed from listDir() to here.
|
||||||
|
if delayIsLeaf {
|
||||||
|
leaf = isLeaf(bucket, pathJoin(prefixDir, entry))
|
||||||
|
if leaf {
|
||||||
|
entry = strings.TrimSuffix(entry, slashSeparator)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
leaf = !strings.HasSuffix(entry, slashSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(entry, slashSeparator) {
|
||||||
|
leafDir = isLeafDir(bucket, pathJoin(prefixDir, entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
isDir := !leafDir && !leaf
|
||||||
|
|
||||||
if i == 0 && markerDir == entry {
|
if i == 0 && markerDir == entry {
|
||||||
if !recursive {
|
if !recursive {
|
||||||
@@ -123,8 +212,8 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
|
|||||||
// markIsEnd is passed to this entry's treeWalk() so that treeWalker.end can be marked
|
// markIsEnd is passed to this entry's treeWalk() so that treeWalker.end can be marked
|
||||||
// true at the end of the treeWalk stream.
|
// true at the end of the treeWalk stream.
|
||||||
markIsEnd := i == len(entries)-1 && isEnd
|
markIsEnd := i == len(entries)-1 && isEnd
|
||||||
emptyDir, err := doTreeWalk(ctx, bucket, pentry, prefixMatch, markerArg, recursive,
|
emptyDir, err := doTreeWalk(ctx, bucket, pathJoin(prefixDir, entry), prefixMatch, markerArg, recursive,
|
||||||
listDir, resultCh, endWalkCh, markIsEnd)
|
listDir, isLeaf, isLeafDir, resultCh, endWalkCh, markIsEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -142,7 +231,7 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
|
|||||||
select {
|
select {
|
||||||
case <-endWalkCh:
|
case <-endWalkCh:
|
||||||
return false, errWalkAbort
|
return false, errWalkAbort
|
||||||
case resultCh <- TreeWalkResult{entry: pentry, end: isEOF}:
|
case resultCh <- TreeWalkResult{entry: pathJoin(prefixDir, entry), end: isEOF}:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +240,7 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initiate a new treeWalk in a goroutine.
|
// Initiate a new treeWalk in a goroutine.
|
||||||
func startTreeWalk(ctx context.Context, bucket, prefix, marker string, recursive bool, listDir ListDirFunc, endWalkCh <-chan struct{}) chan TreeWalkResult {
|
func startTreeWalk(ctx context.Context, bucket, prefix, marker string, recursive bool, listDir ListDirFunc, isLeaf IsLeafFunc, isLeafDir IsLeafDirFunc, endWalkCh <-chan struct{}) chan TreeWalkResult {
|
||||||
// Example 1
|
// Example 1
|
||||||
// If prefix is "one/two/three/" and marker is "one/two/three/four/five.txt"
|
// If prefix is "one/two/three/" and marker is "one/two/three/four/five.txt"
|
||||||
// treeWalk is called with prefixDir="one/two/three/" and marker="four/five.txt"
|
// treeWalk is called with prefixDir="one/two/three/" and marker="four/five.txt"
|
||||||
@@ -173,7 +262,7 @@ func startTreeWalk(ctx context.Context, bucket, prefix, marker string, recursive
|
|||||||
marker = strings.TrimPrefix(marker, prefixDir)
|
marker = strings.TrimPrefix(marker, prefixDir)
|
||||||
go func() {
|
go func() {
|
||||||
isEnd := true // Indication to start walking the tree with end as true.
|
isEnd := true // Indication to start walking the tree with end as true.
|
||||||
doTreeWalk(ctx, bucket, prefixDir, entryPrefixMatch, marker, recursive, listDir, resultCh, endWalkCh, isEnd)
|
doTreeWalk(ctx, bucket, prefixDir, entryPrefixMatch, marker, recursive, listDir, isLeaf, isLeafDir, resultCh, endWalkCh, isEnd)
|
||||||
close(resultCh)
|
close(resultCh)
|
||||||
}()
|
}()
|
||||||
return resultCh
|
return resultCh
|
||||||
|
|||||||
+68
-21
@@ -23,6 +23,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -85,27 +86,27 @@ func createNamespace(disk StorageAPI, volume string, files []string) error {
|
|||||||
|
|
||||||
// Returns function "listDir" of the type listDirFunc.
|
// Returns function "listDir" of the type listDirFunc.
|
||||||
// disks - used for doing disk.ListDir()
|
// disks - used for doing disk.ListDir()
|
||||||
func listDirFactory(ctx context.Context, disk StorageAPI) ListDirFunc {
|
func listDirFactory(ctx context.Context, disk StorageAPI, isLeaf IsLeafFunc) ListDirFunc {
|
||||||
return func(volume, dirPath, dirEntry string) (emptyDir bool, entries []string) {
|
return func(volume, dirPath, dirEntry string) (emptyDir bool, entries []string, delayIsLeaf bool) {
|
||||||
entries, err := disk.ListDir(volume, dirPath, -1)
|
entries, err := disk.ListDir(volume, dirPath, -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil
|
return false, nil, false
|
||||||
}
|
}
|
||||||
if len(entries) == 0 {
|
if len(entries) == 0 {
|
||||||
return true, nil
|
return true, nil, false
|
||||||
}
|
}
|
||||||
sort.Strings(entries)
|
entries, delayIsLeaf = filterListEntries(volume, dirPath, entries, dirEntry, isLeaf)
|
||||||
return false, filterMatchingPrefix(entries, dirEntry)
|
return false, entries, delayIsLeaf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test if tree walker returns entries matching prefix alone are received
|
// Test if tree walker returns entries matching prefix alone are received
|
||||||
// when a non empty prefix is supplied.
|
// when a non empty prefix is supplied.
|
||||||
func testTreeWalkPrefix(t *testing.T, listDir ListDirFunc) {
|
func testTreeWalkPrefix(t *testing.T, listDir ListDirFunc, isLeaf IsLeafFunc, isLeafDir IsLeafDirFunc) {
|
||||||
// Start the tree walk go-routine.
|
// Start the tree walk go-routine.
|
||||||
prefix := "d/"
|
prefix := "d/"
|
||||||
endWalkCh := make(chan struct{})
|
endWalkCh := make(chan struct{})
|
||||||
twResultCh := startTreeWalk(context.Background(), volume, prefix, "", true, listDir, endWalkCh)
|
twResultCh := startTreeWalk(context.Background(), volume, prefix, "", true, listDir, isLeaf, isLeafDir, endWalkCh)
|
||||||
|
|
||||||
// Check if all entries received on the channel match the prefix.
|
// Check if all entries received on the channel match the prefix.
|
||||||
for res := range twResultCh {
|
for res := range twResultCh {
|
||||||
@@ -116,11 +117,11 @@ func testTreeWalkPrefix(t *testing.T, listDir ListDirFunc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test if entries received on tree walk's channel appear after the supplied marker.
|
// Test if entries received on tree walk's channel appear after the supplied marker.
|
||||||
func testTreeWalkMarker(t *testing.T, listDir ListDirFunc) {
|
func testTreeWalkMarker(t *testing.T, listDir ListDirFunc, isLeaf IsLeafFunc, isLeafDir IsLeafDirFunc) {
|
||||||
// Start the tree walk go-routine.
|
// Start the tree walk go-routine.
|
||||||
prefix := ""
|
prefix := ""
|
||||||
endWalkCh := make(chan struct{})
|
endWalkCh := make(chan struct{})
|
||||||
twResultCh := startTreeWalk(context.Background(), volume, prefix, "d/g", true, listDir, endWalkCh)
|
twResultCh := startTreeWalk(context.Background(), volume, prefix, "d/g", true, listDir, isLeaf, isLeafDir, endWalkCh)
|
||||||
|
|
||||||
// Check if only 3 entries, namely d/g/h, i/j/k, lmn are received on the channel.
|
// Check if only 3 entries, namely d/g/h, i/j/k, lmn are received on the channel.
|
||||||
expectedCount := 3
|
expectedCount := 3
|
||||||
@@ -157,12 +158,22 @@ func TestTreeWalk(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
listDir := listDirFactory(context.Background(), disk)
|
isLeaf := func(bucket, leafPath string) bool {
|
||||||
|
return !strings.HasSuffix(leafPath, slashSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
isLeafDir := func(bucket, leafPath string) bool {
|
||||||
|
entries, _ := disk.ListDir(bucket, leafPath, 1)
|
||||||
|
return len(entries) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
listDir := listDirFactory(context.Background(), disk, isLeaf)
|
||||||
|
|
||||||
// Simple test for prefix based walk.
|
// Simple test for prefix based walk.
|
||||||
testTreeWalkPrefix(t, listDir)
|
testTreeWalkPrefix(t, listDir, isLeaf, isLeafDir)
|
||||||
|
|
||||||
// Simple test when marker is set.
|
// Simple test when marker is set.
|
||||||
testTreeWalkMarker(t, listDir)
|
testTreeWalkMarker(t, listDir, isLeaf, isLeafDir)
|
||||||
|
|
||||||
err = os.RemoveAll(fsDir)
|
err = os.RemoveAll(fsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -191,7 +202,16 @@ func TestTreeWalkTimeout(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
listDir := listDirFactory(context.Background(), disk)
|
isLeaf := func(bucket, leafPath string) bool {
|
||||||
|
return !strings.HasSuffix(leafPath, slashSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
isLeafDir := func(bucket, leafPath string) bool {
|
||||||
|
entries, _ := disk.ListDir(bucket, leafPath, 1)
|
||||||
|
return len(entries) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
listDir := listDirFactory(context.Background(), disk, isLeaf)
|
||||||
|
|
||||||
// TreeWalk pool with 2 seconds timeout for tree-walk go routines.
|
// TreeWalk pool with 2 seconds timeout for tree-walk go routines.
|
||||||
pool := NewTreeWalkPool(2 * time.Second)
|
pool := NewTreeWalkPool(2 * time.Second)
|
||||||
@@ -200,7 +220,7 @@ func TestTreeWalkTimeout(t *testing.T) {
|
|||||||
prefix := ""
|
prefix := ""
|
||||||
marker := ""
|
marker := ""
|
||||||
recursive := true
|
recursive := true
|
||||||
resultCh := startTreeWalk(context.Background(), volume, prefix, marker, recursive, listDir, endWalkCh)
|
resultCh := startTreeWalk(context.Background(), volume, prefix, marker, recursive, listDir, isLeaf, isLeafDir, endWalkCh)
|
||||||
|
|
||||||
params := listParams{
|
params := listParams{
|
||||||
bucket: volume,
|
bucket: volume,
|
||||||
@@ -250,8 +270,17 @@ func TestRecursiveTreeWalk(t *testing.T) {
|
|||||||
t.Fatalf("Unable to create StorageAPI: %s", err)
|
t.Fatalf("Unable to create StorageAPI: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLeaf := func(bucket, leafPath string) bool {
|
||||||
|
return !strings.HasSuffix(leafPath, slashSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
isLeafDir := func(bucket, leafPath string) bool {
|
||||||
|
entries, _ := disk1.ListDir(bucket, leafPath, 1)
|
||||||
|
return len(entries) == 0
|
||||||
|
}
|
||||||
|
|
||||||
// Create listDir function.
|
// Create listDir function.
|
||||||
listDir := listDirFactory(context.Background(), disk1)
|
listDir := listDirFactory(context.Background(), disk1, isLeaf)
|
||||||
|
|
||||||
// Create the namespace.
|
// Create the namespace.
|
||||||
var files = []string{
|
var files = []string{
|
||||||
@@ -329,7 +358,7 @@ func TestRecursiveTreeWalk(t *testing.T) {
|
|||||||
t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
|
t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
|
||||||
for entry := range startTreeWalk(context.Background(), volume,
|
for entry := range startTreeWalk(context.Background(), volume,
|
||||||
testCase.prefix, testCase.marker, testCase.recursive,
|
testCase.prefix, testCase.marker, testCase.recursive,
|
||||||
listDir, endWalkCh) {
|
listDir, isLeaf, isLeafDir, endWalkCh) {
|
||||||
if _, found := testCase.expected[entry.entry]; !found {
|
if _, found := testCase.expected[entry.entry]; !found {
|
||||||
t.Errorf("Expected %s, but couldn't find", entry.entry)
|
t.Errorf("Expected %s, but couldn't find", entry.entry)
|
||||||
}
|
}
|
||||||
@@ -355,8 +384,17 @@ func TestSortedness(t *testing.T) {
|
|||||||
t.Fatalf("Unable to create StorageAPI: %s", err)
|
t.Fatalf("Unable to create StorageAPI: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLeaf := func(bucket, leafPath string) bool {
|
||||||
|
return !strings.HasSuffix(leafPath, slashSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
isLeafDir := func(bucket, leafPath string) bool {
|
||||||
|
entries, _ := disk1.ListDir(bucket, leafPath, 1)
|
||||||
|
return len(entries) == 0
|
||||||
|
}
|
||||||
|
|
||||||
// Create listDir function.
|
// Create listDir function.
|
||||||
listDir := listDirFactory(context.Background(), disk1)
|
listDir := listDirFactory(context.Background(), disk1, isLeaf)
|
||||||
|
|
||||||
// Create the namespace.
|
// Create the namespace.
|
||||||
var files = []string{
|
var files = []string{
|
||||||
@@ -398,7 +436,7 @@ func TestSortedness(t *testing.T) {
|
|||||||
var actualEntries []string
|
var actualEntries []string
|
||||||
for entry := range startTreeWalk(context.Background(), volume,
|
for entry := range startTreeWalk(context.Background(), volume,
|
||||||
test.prefix, test.marker, test.recursive,
|
test.prefix, test.marker, test.recursive,
|
||||||
listDir, endWalkCh) {
|
listDir, isLeaf, isLeafDir, endWalkCh) {
|
||||||
actualEntries = append(actualEntries, entry.entry)
|
actualEntries = append(actualEntries, entry.entry)
|
||||||
}
|
}
|
||||||
if !sort.IsSorted(sort.StringSlice(actualEntries)) {
|
if !sort.IsSorted(sort.StringSlice(actualEntries)) {
|
||||||
@@ -426,8 +464,17 @@ func TestTreeWalkIsEnd(t *testing.T) {
|
|||||||
t.Fatalf("Unable to create StorageAPI: %s", err)
|
t.Fatalf("Unable to create StorageAPI: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLeaf := func(bucket, leafPath string) bool {
|
||||||
|
return !strings.HasSuffix(leafPath, slashSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
isLeafDir := func(bucket, leafPath string) bool {
|
||||||
|
entries, _ := disk1.ListDir(bucket, leafPath, 1)
|
||||||
|
return len(entries) == 0
|
||||||
|
}
|
||||||
|
|
||||||
// Create listDir function.
|
// Create listDir function.
|
||||||
listDir := listDirFactory(context.Background(), disk1)
|
listDir := listDirFactory(context.Background(), disk1, isLeaf)
|
||||||
|
|
||||||
// Create the namespace.
|
// Create the namespace.
|
||||||
var files = []string{
|
var files = []string{
|
||||||
@@ -469,7 +516,7 @@ func TestTreeWalkIsEnd(t *testing.T) {
|
|||||||
for i, test := range testCases {
|
for i, test := range testCases {
|
||||||
var entry TreeWalkResult
|
var entry TreeWalkResult
|
||||||
for entry = range startTreeWalk(context.Background(), volume, test.prefix,
|
for entry = range startTreeWalk(context.Background(), volume, test.prefix,
|
||||||
test.marker, test.recursive, listDir, endWalkCh) {
|
test.marker, test.recursive, listDir, isLeaf, isLeafDir, endWalkCh) {
|
||||||
}
|
}
|
||||||
if entry.entry != test.expectedEntry {
|
if entry.entry != test.expectedEntry {
|
||||||
t.Errorf("Test %d: Expected entry %s, but received %s with the EOF marker", i, test.expectedEntry, entry.entry)
|
t.Errorf("Test %d: Expected entry %s, but received %s with the EOF marker", i, test.expectedEntry, entry.entry)
|
||||||
|
|||||||
+1
-1
@@ -75,7 +75,7 @@ func TestReleaseTagToNFromTimeConversion(t *testing.T) {
|
|||||||
if err != nil && err.Error() != testCase.errStr {
|
if err != nil && err.Error() != testCase.errStr {
|
||||||
t.Errorf("Test %d: Expected %v but got %v", i+1, testCase.errStr, err.Error())
|
t.Errorf("Test %d: Expected %v but got %v", i+1, testCase.errStr, err.Error())
|
||||||
}
|
}
|
||||||
if err == nil && tagTime != testCase.t {
|
if err == nil && !tagTime.Equal(testCase.t) {
|
||||||
t.Errorf("Test %d: Expected %v but got %v", i+1, testCase.t, tagTime)
|
t.Errorf("Test %d: Expected %v but got %v", i+1, testCase.t, tagTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-13
@@ -1130,9 +1130,6 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Ensure that metadata does not contain sensitive information
|
// Ensure that metadata does not contain sensitive information
|
||||||
crypto.RemoveSensitiveEntries(metadata)
|
crypto.RemoveSensitiveEntries(metadata)
|
||||||
|
|
||||||
retentionRequested := objectlock.IsObjectLockRetentionRequested(r.Header)
|
|
||||||
legalHoldRequested := objectlock.IsObjectLockLegalHoldRequested(r.Header)
|
|
||||||
|
|
||||||
putObject := objectAPI.PutObject
|
putObject := objectAPI.PutObject
|
||||||
getObjectInfo := objectAPI.GetObjectInfo
|
getObjectInfo := objectAPI.GetObjectInfo
|
||||||
if web.CacheAPI() != nil {
|
if web.CacheAPI() != nil {
|
||||||
@@ -1140,20 +1137,15 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
|||||||
getObjectInfo = web.CacheAPI().GetObjectInfo
|
getObjectInfo = web.CacheAPI().GetObjectInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
if retentionRequested || legalHoldRequested {
|
|
||||||
// enforce object retention rules
|
// enforce object retention rules
|
||||||
retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, getObjectInfo, retPerms, holdPerms)
|
retentionMode, retentionDate, _, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, getObjectInfo, retPerms, holdPerms)
|
||||||
if s3Err == ErrNone && retentionMode != "" {
|
|
||||||
opts.UserDefined[xhttp.AmzObjectLockMode] = string(retentionMode)
|
|
||||||
opts.UserDefined[xhttp.AmzObjectLockRetainUntilDate] = retentionDate.UTC().Format(iso8601TimeFormat)
|
|
||||||
}
|
|
||||||
if s3Err == ErrNone && legalHold.Status != "" {
|
|
||||||
opts.UserDefined[xhttp.AmzObjectLockLegalHold] = string(legalHold.Status)
|
|
||||||
}
|
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if retentionMode != "" {
|
||||||
|
opts.UserDefined[xhttp.AmzObjectLockMode] = string(retentionMode)
|
||||||
|
opts.UserDefined[xhttp.AmzObjectLockRetainUntilDate] = retentionDate.UTC().Format(iso8601TimeFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
objInfo, err := putObject(GlobalContext, bucket, object, pReader, opts)
|
objInfo, err := putObject(GlobalContext, bucket, object, pReader, opts)
|
||||||
@@ -1173,7 +1165,7 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if mustReplicate {
|
if mustReplicate {
|
||||||
defer replicateObject(context.Background(), bucket, object, objInfo.VersionID, objectAPI, &eventArgs{
|
defer replicateObject(GlobalContext, bucket, object, objInfo.VersionID, objectAPI, &eventArgs{
|
||||||
EventName: event.ObjectCreatedPut,
|
EventName: event.ObjectCreatedPut,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
|
|||||||
@@ -380,10 +380,10 @@ func (j xlMetaV2Object) ToFileInfo(volume, path string) (FileInfo, error) {
|
|||||||
}
|
}
|
||||||
fi.Parts = make([]ObjectPartInfo, len(j.PartNumbers))
|
fi.Parts = make([]ObjectPartInfo, len(j.PartNumbers))
|
||||||
for i := range fi.Parts {
|
for i := range fi.Parts {
|
||||||
fi.Parts[i].Number = int(j.PartNumbers[i])
|
fi.Parts[i].Number = j.PartNumbers[i]
|
||||||
fi.Parts[i].Size = int64(j.PartSizes[i])
|
fi.Parts[i].Size = j.PartSizes[i]
|
||||||
fi.Parts[i].ETag = j.PartETags[i]
|
fi.Parts[i].ETag = j.PartETags[i]
|
||||||
fi.Parts[i].ActualSize = int64(j.PartActualSizes[i])
|
fi.Parts[i].ActualSize = j.PartActualSizes[i]
|
||||||
}
|
}
|
||||||
fi.Erasure.Checksums = make([]ChecksumInfo, len(j.PartSizes))
|
fi.Erasure.Checksums = make([]ChecksumInfo, len(j.PartSizes))
|
||||||
for i := range fi.Parts {
|
for i := range fi.Parts {
|
||||||
|
|||||||
+111
-76
@@ -26,11 +26,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
slashpath "path"
|
slashpath "path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -45,7 +46,6 @@ import (
|
|||||||
"github.com/minio/minio/pkg/disk"
|
"github.com/minio/minio/pkg/disk"
|
||||||
"github.com/minio/minio/pkg/env"
|
"github.com/minio/minio/pkg/env"
|
||||||
xioutil "github.com/minio/minio/pkg/ioutil"
|
xioutil "github.com/minio/minio/pkg/ioutil"
|
||||||
"github.com/minio/minio/pkg/mountinfo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -92,12 +92,13 @@ type xlStorage struct {
|
|||||||
|
|
||||||
diskPath string
|
diskPath string
|
||||||
hostname string
|
hostname string
|
||||||
|
endpoint string
|
||||||
|
|
||||||
pool sync.Pool
|
pool sync.Pool
|
||||||
|
|
||||||
globalSync bool
|
globalSync bool
|
||||||
|
|
||||||
diskMount bool // indicates if the path is an actual mount.
|
rootDisk bool
|
||||||
|
|
||||||
diskID string
|
diskID string
|
||||||
|
|
||||||
@@ -234,15 +235,32 @@ func isDirEmpty(dirname string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a new storage disk.
|
// Initialize a new storage disk.
|
||||||
func newXLStorage(path string, hostname string) (*xlStorage, error) {
|
func newLocalXLStorage(path string) (*xlStorage, error) {
|
||||||
|
u := url.URL{Path: path}
|
||||||
|
return newXLStorage(Endpoint{
|
||||||
|
URL: &u,
|
||||||
|
IsLocal: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a new storage disk.
|
||||||
|
func newXLStorage(ep Endpoint) (*xlStorage, error) {
|
||||||
|
path := ep.Path
|
||||||
|
hostname := ep.Host
|
||||||
var err error
|
var err error
|
||||||
if path, err = getValidPath(path, true); err != nil {
|
if path, err = getValidPath(path, true); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rootDisk, err := disk.IsRootDisk(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
p := &xlStorage{
|
p := &xlStorage{
|
||||||
diskPath: path,
|
diskPath: path,
|
||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
|
endpoint: ep.String(),
|
||||||
pool: sync.Pool{
|
pool: sync.Pool{
|
||||||
New: func() interface{} {
|
New: func() interface{} {
|
||||||
b := disk.AlignedBlock(readBlockSize)
|
b := disk.AlignedBlock(readBlockSize)
|
||||||
@@ -250,13 +268,13 @@ func newXLStorage(path string, hostname string) (*xlStorage, error) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
globalSync: env.Get(config.EnvFSOSync, config.EnableOff) == config.EnableOn,
|
globalSync: env.Get(config.EnvFSOSync, config.EnableOff) == config.EnableOn,
|
||||||
diskMount: mountinfo.IsLikelyMountPoint(path),
|
|
||||||
// Allow disk usage crawler to run with up to 2 concurrent
|
// Allow disk usage crawler to run with up to 2 concurrent
|
||||||
// I/O ops, if and when activeIOCount reaches this
|
// I/O ops, if and when activeIOCount reaches this
|
||||||
// value disk usage routine suspends the crawler
|
// value disk usage routine suspends the crawler
|
||||||
// and waits until activeIOCount reaches below this threshold.
|
// and waits until activeIOCount reaches below this threshold.
|
||||||
maxActiveIOCount: 3,
|
maxActiveIOCount: 3,
|
||||||
ctx: GlobalContext,
|
ctx: GlobalContext,
|
||||||
|
rootDisk: rootDisk,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
@@ -412,17 +430,13 @@ func (s *xlStorage) DiskInfo() (info DiskInfo, err error) {
|
|||||||
return info, err
|
return info, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rootDisk, err := disk.IsRootDisk(s.diskPath)
|
|
||||||
if err != nil {
|
|
||||||
return info, err
|
|
||||||
}
|
|
||||||
|
|
||||||
info = DiskInfo{
|
info = DiskInfo{
|
||||||
Total: di.Total,
|
Total: di.Total,
|
||||||
Free: di.Free,
|
Free: di.Free,
|
||||||
Used: di.Total - di.Free,
|
Used: di.Total - di.Free,
|
||||||
RootDisk: rootDisk,
|
RootDisk: s.rootDisk,
|
||||||
MountPath: s.diskPath,
|
MountPath: s.diskPath,
|
||||||
|
Endpoint: s.endpoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
diskID, err := s.GetDiskID()
|
diskID, err := s.GetDiskID()
|
||||||
@@ -719,8 +733,6 @@ func (s *xlStorage) ListDirSplunk(volume, dirPath string, count int) (entries []
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const receiptJSON = "receipt.json"
|
|
||||||
|
|
||||||
atomic.AddInt32(&s.activeIOCount, 1)
|
atomic.AddInt32(&s.activeIOCount, 1)
|
||||||
defer func() {
|
defer func() {
|
||||||
atomic.AddInt32(&s.activeIOCount, -1)
|
atomic.AddInt32(&s.activeIOCount, -1)
|
||||||
@@ -751,24 +763,45 @@ func (s *xlStorage) ListDirSplunk(volume, dirPath string, count int) (entries []
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, entry := range entries {
|
return entries, nil
|
||||||
if entry != receiptJSON {
|
}
|
||||||
continue
|
|
||||||
}
|
func (s *xlStorage) isLeafSplunk(volume string, leafPath string) bool {
|
||||||
_, err = os.Stat(pathJoin(dirPathAbs, entry, xlStorageFormatFile))
|
const receiptJSON = "receipt.json"
|
||||||
if err == nil {
|
|
||||||
entries[i] = strings.TrimSuffix(entry, SlashSeparator)
|
if path.Base(leafPath) != receiptJSON {
|
||||||
continue
|
return false
|
||||||
}
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
if err = s.renameLegacyMetadata(volume, pathJoin(dirPath, entry)); err == nil {
|
|
||||||
// Rename was successful means we found old `xl.json`
|
|
||||||
entries[i] = strings.TrimSuffix(entry, SlashSeparator)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return s.isLeaf(volume, leafPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *xlStorage) isLeaf(volume string, leafPath string) bool {
|
||||||
|
volumeDir, err := s.getVolDir(volume)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries, nil
|
_, err = os.Stat(pathJoin(volumeDir, leafPath, xlStorageFormatFile))
|
||||||
|
if err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// We need a fallback code where directory might contain
|
||||||
|
// legacy `xl.json`, in such situation we just rename
|
||||||
|
// and proceed if rename is successful we know that it
|
||||||
|
// is the leaf since `xl.json` was present.
|
||||||
|
return s.renameLegacyMetadata(volume, leafPath) == nil
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *xlStorage) isLeafDir(volume, leafPath string) bool {
|
||||||
|
volumeDir, err := s.getVolDir(volume)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDirEmpty(pathJoin(volumeDir, leafPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
// WalkSplunk - is a sorted walker which returns file entries in lexically
|
// WalkSplunk - is a sorted walker which returns file entries in lexically
|
||||||
@@ -796,19 +829,19 @@ func (s *xlStorage) WalkSplunk(volume, dirPath, marker string, endWalkCh <-chan
|
|||||||
ch = make(chan FileInfo)
|
ch = make(chan FileInfo)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(ch)
|
defer close(ch)
|
||||||
listDir := func(volume, dirPath, dirEntry string) (bool, []string) {
|
listDir := func(volume, dirPath, dirEntry string) (emptyDir bool, entries []string, delayIsLeaf bool) {
|
||||||
entries, err := s.ListDirSplunk(volume, dirPath, -1)
|
entries, err := s.ListDirSplunk(volume, dirPath, -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil
|
return false, nil, false
|
||||||
}
|
}
|
||||||
if len(entries) == 0 {
|
if len(entries) == 0 {
|
||||||
return true, nil
|
return true, nil, false
|
||||||
}
|
}
|
||||||
sort.Strings(entries)
|
entries, delayIsLeaf = filterListEntries(volume, dirPath, entries, dirEntry, s.isLeafSplunk)
|
||||||
return false, filterMatchingPrefix(entries, dirEntry)
|
return false, entries, delayIsLeaf
|
||||||
}
|
}
|
||||||
|
|
||||||
walkResultCh := startTreeWalk(GlobalContext, volume, dirPath, marker, true, listDir, endWalkCh)
|
walkResultCh := startTreeWalk(GlobalContext, volume, dirPath, marker, true, listDir, s.isLeafSplunk, s.isLeafDir, endWalkCh)
|
||||||
for {
|
for {
|
||||||
walkResult, ok := <-walkResultCh
|
walkResult, ok := <-walkResultCh
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -873,27 +906,36 @@ func (s *xlStorage) WalkVersions(volume, dirPath, marker string, recursive bool,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// buffer channel matches the S3 ListObjects implementation
|
// Fast exit track to check if we are listing an object with
|
||||||
ch = make(chan FileInfoVersions, maxObjectList)
|
// a trailing slash, this will avoid to list the object content.
|
||||||
go func() {
|
if HasSuffix(dirPath, SlashSeparator) {
|
||||||
defer close(ch)
|
if st, err := os.Stat(pathJoin(volumeDir, dirPath, xlStorageFormatFile)); err == nil && st.Mode().IsRegular() {
|
||||||
listDir := func(volume, dirPath, dirEntry string) (emptyDir bool, entries []string) {
|
return nil, errFileNotFound
|
||||||
entries, err := s.ListDir(volume, dirPath, -1)
|
|
||||||
if err != nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
if len(entries) == 0 {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
sort.Strings(entries)
|
|
||||||
return false, filterMatchingPrefix(entries, dirEntry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
walkResultCh := startTreeWalk(GlobalContext, volume, dirPath, marker, recursive, listDir, endWalkCh)
|
ch = make(chan FileInfoVersions)
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
listDir := func(volume, dirPath, dirEntry string) (emptyDir bool, entries []string, delayIsLeaf bool) {
|
||||||
|
entries, err := s.ListDir(volume, dirPath, -1)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, false
|
||||||
|
}
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return true, nil, false
|
||||||
|
}
|
||||||
|
entries, delayIsLeaf = filterListEntries(volume, dirPath, entries, dirEntry, s.isLeaf)
|
||||||
|
return false, entries, delayIsLeaf
|
||||||
|
}
|
||||||
|
|
||||||
|
walkResultCh := startTreeWalk(GlobalContext, volume, dirPath, marker, recursive, listDir, s.isLeaf, s.isLeafDir, endWalkCh)
|
||||||
for walkResult := range walkResultCh {
|
for walkResult := range walkResultCh {
|
||||||
var fiv FileInfoVersions
|
var fiv FileInfoVersions
|
||||||
if HasSuffix(walkResult.entry, SlashSeparator) {
|
if HasSuffix(walkResult.entry, SlashSeparator) {
|
||||||
fiv = FileInfoVersions{
|
fiv = FileInfoVersions{
|
||||||
|
Volume: volume,
|
||||||
|
Name: walkResult.entry,
|
||||||
Versions: []FileInfo{
|
Versions: []FileInfo{
|
||||||
{
|
{
|
||||||
Volume: volume,
|
Volume: volume,
|
||||||
@@ -949,23 +991,30 @@ func (s *xlStorage) Walk(volume, dirPath, marker string, recursive bool, endWalk
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// buffer channel matches the S3 ListObjects implementation
|
// Fast exit track to check if we are listing an object with
|
||||||
ch = make(chan FileInfo, maxObjectList)
|
// a trailing slash, this will avoid to list the object content.
|
||||||
go func() {
|
if HasSuffix(dirPath, SlashSeparator) {
|
||||||
defer close(ch)
|
if st, err := os.Stat(pathJoin(volumeDir, dirPath, xlStorageFormatFile)); err == nil && st.Mode().IsRegular() {
|
||||||
listDir := func(volume, dirPath, dirEntry string) (emptyDir bool, entries []string) {
|
return nil, errFileNotFound
|
||||||
entries, err := s.ListDir(volume, dirPath, -1)
|
|
||||||
if err != nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
if len(entries) == 0 {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
sort.Strings(entries)
|
|
||||||
return false, filterMatchingPrefix(entries, dirEntry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
walkResultCh := startTreeWalk(GlobalContext, volume, dirPath, marker, recursive, listDir, endWalkCh)
|
ch = make(chan FileInfo)
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
listDir := func(volume, dirPath, dirEntry string) (emptyDir bool, entries []string, delayIsLeaf bool) {
|
||||||
|
entries, err := s.ListDir(volume, dirPath, -1)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, false
|
||||||
|
}
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return true, nil, false
|
||||||
|
}
|
||||||
|
entries, delayIsLeaf = filterListEntries(volume, dirPath, entries, dirEntry, s.isLeaf)
|
||||||
|
return false, entries, delayIsLeaf
|
||||||
|
}
|
||||||
|
|
||||||
|
walkResultCh := startTreeWalk(GlobalContext, volume, dirPath, marker, recursive, listDir, s.isLeaf, s.isLeafDir, endWalkCh)
|
||||||
for walkResult := range walkResultCh {
|
for walkResult := range walkResultCh {
|
||||||
var fi FileInfo
|
var fi FileInfo
|
||||||
if HasSuffix(walkResult.entry, SlashSeparator) {
|
if HasSuffix(walkResult.entry, SlashSeparator) {
|
||||||
@@ -1034,20 +1083,6 @@ func (s *xlStorage) ListDir(volume, dirPath string, count int) (entries []string
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, entry := range entries {
|
|
||||||
_, err = os.Stat(pathJoin(dirPathAbs, entry, xlStorageFormatFile))
|
|
||||||
if err == nil {
|
|
||||||
entries[i] = strings.TrimSuffix(entry, SlashSeparator)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
if err = s.renameLegacyMetadata(volume, pathJoin(dirPath, entry)); err == nil {
|
|
||||||
// if rename was successful, means we did find old `xl.json`
|
|
||||||
entries[i] = strings.TrimSuffix(entry, SlashSeparator)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user