github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/docs/debugging/s3-check-md5/main.go (about) 1 // Copyright (c) 2015-2021 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package main 19 20 import ( 21 "context" 22 "crypto/md5" 23 "flag" 24 "fmt" 25 "io" 26 "log" 27 "net/url" 28 "os" 29 "path" 30 "strconv" 31 "strings" 32 "time" 33 34 "github.com/minio/minio-go/v7" 35 "github.com/minio/minio-go/v7/pkg/credentials" 36 ) 37 38 var ( 39 endpoint, accessKey, secretKey string 40 minModTimeStr string 41 bucket, prefix string 42 debug bool 43 versions bool 44 insecure bool 45 ) 46 47 // getMD5Sum returns MD5 sum of given data. 48 func getMD5Sum(data []byte) []byte { 49 hash := md5.New() 50 hash.Write(data) 51 return hash.Sum(nil) 52 } 53 54 func main() { 55 flag.StringVar(&endpoint, "endpoint", "https://play.min.io", "S3 endpoint URL") 56 flag.StringVar(&accessKey, "access-key", "Q3AM3UQ867SPQQA43P2F", "S3 Access Key") 57 flag.StringVar(&secretKey, "secret-key", "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG", "S3 Secret Key") 58 flag.StringVar(&bucket, "bucket", "", "Select a specific bucket") 59 flag.StringVar(&prefix, "prefix", "", "Select a prefix") 60 flag.BoolVar(&debug, "debug", false, "Prints HTTP network calls to S3 endpoint") 61 flag.BoolVar(&versions, "versions", false, "Verify all versions") 62 flag.BoolVar(&insecure, "insecure", false, "Disable TLS verification") 63 flag.StringVar(&minModTimeStr, "modified-since", "", "Specify a minimum object last modified time, e.g.: 2023-01-02T15:04:05Z") 64 flag.Parse() 65 66 if endpoint == "" { 67 log.Fatalln("Endpoint is not provided") 68 } 69 70 if accessKey == "" { 71 log.Fatalln("Access key is not provided") 72 } 73 74 if secretKey == "" { 75 log.Fatalln("Secret key is not provided") 76 } 77 78 if bucket == "" && prefix != "" { 79 log.Fatalln("--prefix is specified without --bucket.") 80 } 81 82 var minModTime time.Time 83 if minModTimeStr != "" { 84 var e error 85 minModTime, e = time.Parse(time.RFC3339, minModTimeStr) 86 if e != nil { 87 log.Fatalln("Unable to parse --modified-since:", e) 88 } 89 } 90 91 u, err := url.Parse(endpoint) 92 if err != nil { 93 log.Fatalln(err) 94 } 95 96 secure := strings.EqualFold(u.Scheme, "https") 97 transport, err := minio.DefaultTransport(secure) 98 if err != nil { 99 log.Fatalln(err) 100 } 101 if insecure { 102 // skip TLS verification 103 transport.TLSClientConfig.InsecureSkipVerify = true 104 } 105 106 s3Client, err := minio.New(u.Host, &minio.Options{ 107 Creds: credentials.NewStaticV4(accessKey, secretKey, ""), 108 Secure: secure, 109 Transport: transport, 110 }) 111 if err != nil { 112 log.Fatalln(err) 113 } 114 115 if debug { 116 s3Client.TraceOn(os.Stderr) 117 } 118 119 var buckets []string 120 if bucket != "" { 121 buckets = append(buckets, bucket) 122 } else { 123 bucketsInfo, err := s3Client.ListBuckets(context.Background()) 124 if err != nil { 125 log.Fatalln(err) 126 } 127 for _, b := range bucketsInfo { 128 buckets = append(buckets, b.Name) 129 } 130 } 131 132 for _, bucket := range buckets { 133 opts := minio.ListObjectsOptions{ 134 Recursive: true, 135 Prefix: prefix, 136 WithVersions: versions, 137 WithMetadata: true, 138 } 139 140 objFullPath := func(obj minio.ObjectInfo) (fpath string) { 141 fpath = path.Join(bucket, obj.Key) 142 if versions { 143 fpath += ":" + obj.VersionID 144 } 145 return 146 } 147 148 // List all objects from a bucket-name with a matching prefix. 149 for object := range s3Client.ListObjects(context.Background(), bucket, opts) { 150 if object.Err != nil { 151 log.Println("FAILED: LIST with error:", object.Err) 152 continue 153 } 154 if !minModTime.IsZero() && object.LastModified.Before(minModTime) { 155 continue 156 } 157 if object.IsDeleteMarker { 158 log.Println("SKIPPED: DELETE marker object:", objFullPath(object)) 159 continue 160 } 161 if _, ok := object.UserMetadata["X-Amz-Server-Side-Encryption-Customer-Algorithm"]; ok { 162 log.Println("SKIPPED: Objects encrypted with SSE-C do not have md5sum as ETag:", objFullPath(object)) 163 continue 164 } 165 if v, ok := object.UserMetadata["X-Amz-Server-Side-Encryption"]; ok && v == "aws:kms" { 166 log.Println("SKIPPED: encrypted with SSE-KMS do not have md5sum as ETag:", objFullPath(object)) 167 continue 168 } 169 parts := 1 170 multipart := false 171 s := strings.Split(object.ETag, "-") 172 switch len(s) { 173 case 1: 174 // nothing to do 175 case 2: 176 if p, err := strconv.Atoi(s[1]); err == nil { 177 parts = p 178 } else { 179 log.Println("FAILED: ETAG of", objFullPath(object), "has a wrong format:", err) 180 continue 181 } 182 multipart = true 183 default: 184 log.Println("FAILED: Unexpected ETAG", object.ETag, "for object:", objFullPath(object)) 185 continue 186 } 187 188 var partsMD5Sum [][]byte 189 var failedMD5 bool 190 for p := 1; p <= parts; p++ { 191 opts := minio.GetObjectOptions{ 192 VersionID: object.VersionID, 193 PartNumber: p, 194 } 195 obj, err := s3Client.GetObject(context.Background(), bucket, object.Key, opts) 196 if err != nil { 197 log.Println("FAILED: GET", objFullPath(object), "=>", err) 198 failedMD5 = true 199 break 200 } 201 h := md5.New() 202 if _, err := io.Copy(h, obj); err != nil { 203 log.Println("FAILED: MD5 calculation error:", objFullPath(object), "=>", err) 204 failedMD5 = true 205 break 206 } 207 partsMD5Sum = append(partsMD5Sum, h.Sum(nil)) 208 } 209 210 if failedMD5 { 211 log.Println("CORRUPTED object:", objFullPath(object)) 212 continue 213 } 214 215 corrupted := false 216 if !multipart { 217 md5sum := fmt.Sprintf("%x", partsMD5Sum[0]) 218 if md5sum != object.ETag { 219 corrupted = true 220 } 221 } else { 222 var totalMD5SumBytes []byte 223 for _, sum := range partsMD5Sum { 224 totalMD5SumBytes = append(totalMD5SumBytes, sum...) 225 } 226 s3MD5 := fmt.Sprintf("%x-%d", getMD5Sum(totalMD5SumBytes), parts) 227 if s3MD5 != object.ETag { 228 corrupted = true 229 } 230 } 231 232 if corrupted { 233 log.Println("CORRUPTED object:", objFullPath(object)) 234 } else { 235 log.Println("INTACT object:", objFullPath(object)) 236 } 237 } 238 } 239 }