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  }