diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go
index 398510190..74dd17b9f 100644
--- a/cmd/bucket-handlers.go
+++ b/cmd/bucket-handlers.go
@@ -44,10 +44,8 @@ import (
)
const (
- getBucketVersioningResponse = ``
- objectLockConfig = "object-lock.xml"
- bucketObjectLockEnabledConfigFile = "object-lock-enabled.json"
- bucketObjectLockEnabledConfig = `{"x-amz-bucket-object-lock-enabled":true}`
+ getBucketVersioningResponse = ``
+ objectLockConfig = "object-lock.xml"
)
// Check if there are buckets on server without corresponding entry in etcd backend and
@@ -292,6 +290,14 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
+
+ for i := range bucketsInfo {
+ meta, err := loadBucketMetadata(ctx, objectAPI, bucketsInfo[i].Name)
+ logger.LogIf(ctx, err)
+ if err == nil {
+ bucketsInfo[i].Created = meta.Created
+ }
+ }
}
if s3Error == ErrAccessDenied {
@@ -532,12 +538,11 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
return
}
- if objectLockEnabled {
- configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
- if err = saveConfig(ctx, objectAPI, configFile, []byte(bucketObjectLockEnabledConfig)); err != nil {
- writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
- return
- }
+ meta := newBucketMetadata(bucket)
+ meta.LockEnabled = objectLockEnabled
+ if err := meta.save(ctx, objectAPI); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
+ return
}
if err = globalDNSConfig.Put(bucket); err != nil {
@@ -576,14 +581,17 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
return
}
- if objectLockEnabled && !globalIsGateway {
- configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
- if err = saveConfig(ctx, objectAPI, configFile, []byte(bucketObjectLockEnabledConfig)); err != nil {
+ if !globalIsGateway {
+ meta := newBucketMetadata(bucket)
+ meta.LockEnabled = objectLockEnabled
+ if err := meta.save(ctx, objectAPI); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
- globalBucketObjectLockConfig.Set(bucket, objectlock.Retention{})
- globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, objectlock.Retention{})
+ if objectLockEnabled {
+ globalBucketObjectLockConfig.Set(bucket, objectlock.Retention{})
+ globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, objectlock.Retention{})
+ }
}
// Make sure to add Location information here only for bucket
@@ -942,6 +950,9 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
}
}
+ // Delete metadata, only log errors.
+ logger.LogIf(ctx, newBucketMetadata(bucket).delete(ctx, objectAPI))
+
globalNotificationSys.DeleteBucket(ctx, bucket)
// Write success response.
@@ -1032,27 +1043,23 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r))
return
}
- configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
- configData, err := readConfig(ctx, objectAPI, configFile)
+
+ meta, err := loadBucketMetadata(ctx, objectAPI, bucket)
if err != nil {
- aerr := toAPIError(ctx, err)
- if err == errConfigNotFound {
- aerr = errorCodes.ToAPIErr(ErrObjectLockConfigurationNotAllowed)
- }
- writeErrorResponse(ctx, w, aerr, r.URL, guessIsBrowserReq(r))
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
-
- if string(configData) != bucketObjectLockEnabledConfig {
- writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL, guessIsBrowserReq(r))
+ if !meta.LockEnabled {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrObjectLockConfigurationNotAllowed), r.URL, guessIsBrowserReq(r))
return
}
+
data, err := xml.Marshal(config)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
- configFile = path.Join(bucketConfigPrefix, bucket, objectLockConfig)
+ configFile := path.Join(bucketConfigPrefix, bucket, objectLockConfig)
if err = saveConfig(ctx, objectAPI, configFile, data); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
@@ -1093,26 +1100,19 @@ func (api objectAPIHandlers) GetBucketObjectLockConfigHandler(w http.ResponseWri
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
- configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
- configData, err := readConfig(ctx, objectAPI, configFile)
+
+ meta, err := loadBucketMetadata(ctx, objectAPI, bucket)
if err != nil {
- var aerr APIError
- if err == errConfigNotFound {
- aerr = errorCodes.ToAPIErr(ErrObjectLockConfigurationNotFound)
- } else {
- aerr = toAPIError(ctx, err)
- }
- writeErrorResponse(ctx, w, aerr, r.URL, guessIsBrowserReq(r))
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
-
- if string(configData) != bucketObjectLockEnabledConfig {
- writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL, guessIsBrowserReq(r))
+ if !meta.LockEnabled {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrObjectLockConfigurationNotAllowed), r.URL, guessIsBrowserReq(r))
return
}
- configFile = path.Join(bucketConfigPrefix, bucket, objectLockConfig)
- configData, err = readConfig(ctx, objectAPI, configFile)
+ configFile := path.Join(bucketConfigPrefix, bucket, objectLockConfig)
+ configData, err := readConfig(ctx, objectAPI, configFile)
if err != nil {
if err != errConfigNotFound {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
diff --git a/cmd/bucket-meta.go b/cmd/bucket-meta.go
new file mode 100644
index 000000000..a8aff7ccc
--- /dev/null
+++ b/cmd/bucket-meta.go
@@ -0,0 +1,165 @@
+/*
+ * MinIO Cloud Storage, (C) 2020 MinIO, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+ "context"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "path"
+ "time"
+
+ "github.com/minio/minio/cmd/logger"
+)
+
+const (
+ legacyBucketObjectLockEnabledConfigFile = "object-lock-enabled.json"
+ legacyBucketObjectLockEnabledConfig = `{"x-amz-bucket-object-lock-enabled":true}`
+
+ bucketMetadataFile = ".metadata.bin"
+ bucketMetadataFormat = 1
+ bucketMetadataVersion = 1
+)
+
+//go:generate msgp -file $GOFILE -unexported
+
+// bucketMetadata contains bucket metadata.
+// When adding/removing fields, regenerate the marshal code using the go generate above.
+// Only changing meaning of fields requires a version bump.
+// bucketMetadataFormat refers to the format.
+// bucketMetadataVersion can be used to track a rolling upgrade of a field.
+type bucketMetadata struct {
+ Name string
+ Created time.Time
+ LockEnabled bool
+}
+
+// newBucketMetadata creates bucketMetadata with the supplied name and Created to Now.
+func newBucketMetadata(name string) bucketMetadata {
+ return bucketMetadata{
+ Name: name,
+ Created: UTCNow(),
+ }
+}
+
+// loadBucketMeta loads the metadata of bucket by name from ObjectLayer o.
+// If an error is returned the returned metadata will be default initialized.
+func loadBucketMeta(ctx context.Context, o ObjectLayer, name string) (bucketMetadata, error) {
+ b := newBucketMetadata(name)
+ configFile := path.Join(bucketConfigPrefix, name, bucketMetadataFile)
+ data, err := readConfig(ctx, o, configFile)
+ if err != nil {
+ return b, err
+ }
+ if len(data) <= 4 {
+ return b, fmt.Errorf("loadBucketMetadata: no data")
+ }
+ // Read header
+ switch binary.LittleEndian.Uint16(data[0:2]) {
+ case bucketMetadataFormat:
+ default:
+ return b, fmt.Errorf("loadBucketMetadata: unknown format: %d", binary.LittleEndian.Uint16(data[0:2]))
+ }
+ switch binary.LittleEndian.Uint16(data[2:4]) {
+ case bucketMetadataVersion:
+ default:
+ return b, fmt.Errorf("loadBucketMetadata: unknown version: %d", binary.LittleEndian.Uint16(data[2:4]))
+ }
+
+ // OK, parse data.
+ _, err = b.UnmarshalMsg(data[4:])
+ return b, err
+}
+
+var errMetaDataConverted = errors.New("metadata converted")
+
+// loadBucketMetadata loads and migrates to bucket metadata.
+func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket string) (bucketMetadata, error) {
+ meta, err := loadBucketMeta(ctx, objectAPI, bucket)
+ if err == nil {
+ return meta, nil
+ }
+
+ if err != errConfigNotFound {
+ return meta, err
+ }
+
+ // Control here means old bucket without bucket metadata. Hence we migrate existing settings.
+ if err = meta.convertLegacyLockconfig(ctx, objectAPI); err != nil {
+ return meta, err
+ }
+
+ return meta, errMetaDataConverted
+}
+
+func (b *bucketMetadata) convertLegacyLockconfig(ctx context.Context, objectAPI ObjectLayer) error {
+ configFile := path.Join(bucketConfigPrefix, b.Name, legacyBucketObjectLockEnabledConfigFile)
+
+ save := func() error {
+ if err := b.save(ctx, objectAPI); err != nil {
+ return err
+ }
+
+ logger.LogIf(ctx, deleteConfig(ctx, objectAPI, configFile))
+ return nil
+ }
+
+ configData, err := readConfig(ctx, objectAPI, configFile)
+ if err != nil {
+ if err != errConfigNotFound {
+ return err
+ }
+
+ return save()
+ }
+
+ if string(configData) != legacyBucketObjectLockEnabledConfig {
+ return fmt.Errorf("content mismatch in config file %v", configFile)
+ }
+
+ b.LockEnabled = true
+ return save()
+}
+
+// save config to supplied ObjectLayer o.
+func (b *bucketMetadata) save(ctx context.Context, o ObjectLayer) error {
+ data := make([]byte, 4, b.Msgsize()+4)
+ // Put header
+ binary.LittleEndian.PutUint16(data[0:2], bucketMetadataFormat)
+ binary.LittleEndian.PutUint16(data[2:4], bucketMetadataVersion)
+
+ // Add data
+ data, err := b.MarshalMsg(data)
+ if err != nil {
+ return err
+ }
+ configFile := path.Join(bucketConfigPrefix, b.Name, bucketMetadataFile)
+ return saveConfig(ctx, o, configFile, data)
+}
+
+// delete the config metadata.
+// If config does not exist no error is returned.
+func (b bucketMetadata) delete(ctx context.Context, o ObjectLayer) error {
+ configFile := path.Join(bucketConfigPrefix, b.Name, bucketMetadataFile)
+ err := deleteConfig(ctx, o, configFile)
+ if err == errConfigNotFound {
+ // We don't care
+ err = nil
+ }
+ return err
+}
diff --git a/cmd/bucket-meta_gen.go b/cmd/bucket-meta_gen.go
new file mode 100644
index 000000000..ae19e4aff
--- /dev/null
+++ b/cmd/bucket-meta_gen.go
@@ -0,0 +1,160 @@
+package cmd
+
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *bucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Name":
+ z.Name, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "Created":
+ z.Created, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "Created")
+ return
+ }
+ case "LockEnabled":
+ z.LockEnabled, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "LockEnabled")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z bucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 3
+ // write "Name"
+ err = en.Append(0x83, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Name)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ // write "Created"
+ err = en.Append(0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.Created)
+ if err != nil {
+ err = msgp.WrapError(err, "Created")
+ return
+ }
+ // write "LockEnabled"
+ err = en.Append(0xab, 0x4c, 0x6f, 0x63, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.LockEnabled)
+ if err != nil {
+ err = msgp.WrapError(err, "LockEnabled")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z bucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "Name"
+ o = append(o, 0x83, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ o = msgp.AppendString(o, z.Name)
+ // string "Created"
+ o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
+ o = msgp.AppendTime(o, z.Created)
+ // string "LockEnabled"
+ o = append(o, 0xab, 0x4c, 0x6f, 0x63, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.LockEnabled)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *bucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Name":
+ z.Name, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "Created":
+ z.Created, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Created")
+ return
+ }
+ case "LockEnabled":
+ z.LockEnabled, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LockEnabled")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z bucketMetadata) Msgsize() (s int) {
+ s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize
+ return
+}
diff --git a/cmd/bucket-meta_gen_test.go b/cmd/bucket-meta_gen_test.go
new file mode 100644
index 000000000..cd2243eca
--- /dev/null
+++ b/cmd/bucket-meta_gen_test.go
@@ -0,0 +1,123 @@
+package cmd
+
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalbucketMetadata(t *testing.T) {
+ v := bucketMetadata{}
+ bts, err := v.MarshalMsg(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ left, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
+ }
+
+ left, err = msgp.Skip(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
+ }
+}
+
+func BenchmarkMarshalMsgbucketMetadata(b *testing.B) {
+ v := bucketMetadata{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgbucketMetadata(b *testing.B) {
+ v := bucketMetadata{}
+ bts := make([]byte, 0, v.Msgsize())
+ bts, _ = v.MarshalMsg(bts[0:0])
+ b.SetBytes(int64(len(bts)))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ bts, _ = v.MarshalMsg(bts[0:0])
+ }
+}
+
+func BenchmarkUnmarshalbucketMetadata(b *testing.B) {
+ v := bucketMetadata{}
+ bts, _ := v.MarshalMsg(nil)
+ b.ReportAllocs()
+ b.SetBytes(int64(len(bts)))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestEncodeDecodebucketMetadata(t *testing.T) {
+ v := bucketMetadata{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodebucketMetadata Msgsize() is inaccurate")
+ }
+
+ vn := bucketMetadata{}
+ err := msgp.Decode(&buf, &vn)
+ if err != nil {
+ t.Error(err)
+ }
+
+ buf.Reset()
+ msgp.Encode(&buf, &v)
+ err = msgp.NewReader(&buf).Skip()
+ if err != nil {
+ t.Error(err)
+ }
+}
+
+func BenchmarkEncodebucketMetadata(b *testing.B) {
+ v := bucketMetadata{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+ b.SetBytes(int64(buf.Len()))
+ en := msgp.NewWriter(msgp.Nowhere)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.EncodeMsg(en)
+ }
+ en.Flush()
+}
+
+func BenchmarkDecodebucketMetadata(b *testing.B) {
+ v := bucketMetadata{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+ b.SetBytes(int64(buf.Len()))
+ rd := msgp.NewEndlessReader(buf.Bytes(), b)
+ dc := msgp.NewReader(rd)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ err := v.DecodeMsg(dc)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
diff --git a/cmd/object-lock.go b/cmd/object-lock.go
index aa1166871..2c446e306 100644
--- a/cmd/object-lock.go
+++ b/cmd/object-lock.go
@@ -376,22 +376,20 @@ func checkPutObjectLockAllowed(ctx context.Context, r *http.Request, bucket, obj
func initBucketObjectLockConfig(buckets []BucketInfo, objAPI ObjectLayer) error {
for _, bucket := range buckets {
ctx := logger.SetReqInfo(GlobalContext, &logger.ReqInfo{BucketName: bucket.Name})
- configFile := path.Join(bucketConfigPrefix, bucket.Name, bucketObjectLockEnabledConfigFile)
- bucketObjLockData, err := readConfig(ctx, objAPI, configFile)
+ meta, err := loadBucketMetadata(ctx, objAPI, bucket.Name)
if err != nil {
- if errors.Is(err, errConfigNotFound) {
- continue
+ if err != errMetaDataConverted {
+ return err
}
- return err
- }
- if string(bucketObjLockData) != bucketObjectLockEnabledConfig {
- // this should never happen
- logger.LogIf(ctx, objectlock.ErrMalformedBucketObjectConfig)
+ meta.Created = bucket.Created
+ logger.LogIf(ctx, meta.save(ctx, objAPI))
+ }
+ if !meta.LockEnabled {
continue
}
- configFile = path.Join(bucketConfigPrefix, bucket.Name, objectLockConfig)
+ configFile := path.Join(bucketConfigPrefix, bucket.Name, objectLockConfig)
configData, err := readConfig(ctx, objAPI, configFile)
if err != nil {
if errors.Is(err, errConfigNotFound) {