mirror of
				https://github.com/minio/minio.git
				synced 2025-10-29 15:55:00 -04:00 
			
		
		
		
	make sure listParts returns parts that are valid (#20390)
This commit is contained in:
		
							parent
							
								
									241be9709c
								
							
						
					
					
						commit
						b6b7cddc9c
					
				| @ -216,6 +216,7 @@ func (fi FileInfo) ReplicationInfoEquals(ofi FileInfo) bool { | ||||
| } | ||||
| 
 | ||||
| // objectPartIndex - returns the index of matching object part number. | ||||
| // Returns -1 if the part cannot be found. | ||||
| func objectPartIndex(parts []ObjectPartInfo, partNumber int) int { | ||||
| 	for i, part := range parts { | ||||
| 		if partNumber == part.Number { | ||||
| @ -225,6 +226,17 @@ func objectPartIndex(parts []ObjectPartInfo, partNumber int) int { | ||||
| 	return -1 | ||||
| } | ||||
| 
 | ||||
| // objectPartIndexNums returns the index of the specified part number. | ||||
| // Returns -1 if the part cannot be found. | ||||
| func objectPartIndexNums(parts []int, partNumber int) int { | ||||
| 	for i, part := range parts { | ||||
| 		if part != 0 && partNumber == part { | ||||
| 			return i | ||||
| 		} | ||||
| 	} | ||||
| 	return -1 | ||||
| } | ||||
| 
 | ||||
| // AddObjectPart - add a new object part in order. | ||||
| func (fi *FileInfo) AddObjectPart(partNumber int, partETag string, partSize, actualSize int64, modTime time.Time, idx []byte, checksums map[string]string) { | ||||
| 	partInfo := ObjectPartInfo{ | ||||
|  | ||||
| @ -795,6 +795,61 @@ func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, u | ||||
| 	return result, nil | ||||
| } | ||||
| 
 | ||||
| func (er erasureObjects) listParts(ctx context.Context, onlineDisks []StorageAPI, partPath string, readQuorum int) ([]int, error) { | ||||
| 	g := errgroup.WithNErrs(len(onlineDisks)) | ||||
| 
 | ||||
| 	objectParts := make([][]string, len(onlineDisks)) | ||||
| 	// List uploaded parts from drives. | ||||
| 	for index := range onlineDisks { | ||||
| 		index := index | ||||
| 		g.Go(func() (err error) { | ||||
| 			if onlineDisks[index] == nil { | ||||
| 				return errDiskNotFound | ||||
| 			} | ||||
| 			objectParts[index], err = onlineDisks[index].ListDir(ctx, minioMetaMultipartBucket, minioMetaMultipartBucket, partPath, -1) | ||||
| 			return err | ||||
| 		}, index) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := reduceReadQuorumErrs(ctx, g.Wait(), objectOpIgnoredErrs, readQuorum); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	partQuorumMap := make(map[int]int) | ||||
| 	for _, driveParts := range objectParts { | ||||
| 		partsWithMetaCount := make(map[int]int, len(driveParts)) | ||||
| 		// part files can be either part.N or part.N.meta | ||||
| 		for _, partPath := range driveParts { | ||||
| 			var partNum int | ||||
| 			if _, err := fmt.Sscanf(partPath, "part.%d", &partNum); err == nil { | ||||
| 				partsWithMetaCount[partNum]++ | ||||
| 				continue | ||||
| 			} | ||||
| 			if _, err := fmt.Sscanf(partPath, "part.%d.meta", &partNum); err == nil { | ||||
| 				partsWithMetaCount[partNum]++ | ||||
| 			} | ||||
| 		} | ||||
| 		// Include only part.N.meta files with corresponding part.N | ||||
| 		for partNum, cnt := range partsWithMetaCount { | ||||
| 			if cnt < 2 { | ||||
| 				continue | ||||
| 			} | ||||
| 			partQuorumMap[partNum]++ | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var partNums []int | ||||
| 	for partNum, count := range partQuorumMap { | ||||
| 		if count < readQuorum { | ||||
| 			continue | ||||
| 		} | ||||
| 		partNums = append(partNums, partNum) | ||||
| 	} | ||||
| 
 | ||||
| 	sort.Ints(partNums) | ||||
| 	return partNums, nil | ||||
| } | ||||
| 
 | ||||
| // ListObjectParts - lists all previously uploaded parts for a given | ||||
| // object and uploadID.  Takes additional input of part-number-marker | ||||
| // to indicate where the listing should begin from. | ||||
| @ -821,6 +876,14 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up | ||||
| 	} | ||||
| 
 | ||||
| 	uploadIDPath := er.getUploadIDDir(bucket, object, uploadID) | ||||
| 	if partNumberMarker < 0 { | ||||
| 		partNumberMarker = 0 | ||||
| 	} | ||||
| 
 | ||||
| 	// Limit output to maxPartsList. | ||||
| 	if maxParts > maxPartsList { | ||||
| 		maxParts = maxPartsList | ||||
| 	} | ||||
| 
 | ||||
| 	// Populate the result stub. | ||||
| 	result.Bucket = bucket | ||||
| @ -831,126 +894,77 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up | ||||
| 	result.UserDefined = cloneMSS(fi.Metadata) | ||||
| 	result.ChecksumAlgorithm = fi.Metadata[hash.MinIOMultipartChecksum] | ||||
| 
 | ||||
| 	if partNumberMarker < 0 { | ||||
| 		partNumberMarker = 0 | ||||
| 	} | ||||
| 
 | ||||
| 	// Limit output to maxPartsList. | ||||
| 	if maxParts > maxPartsList-partNumberMarker { | ||||
| 		maxParts = maxPartsList - partNumberMarker | ||||
| 	} | ||||
| 
 | ||||
| 	if maxParts == 0 { | ||||
| 		return result, nil | ||||
| 	} | ||||
| 
 | ||||
| 	onlineDisks := er.getDisks() | ||||
| 	readQuorum := fi.ReadQuorum(er.defaultRQuorum()) | ||||
| 	// Read Part info for all parts | ||||
| 	partPath := pathJoin(uploadIDPath, fi.DataDir) + "/" | ||||
| 	req := ReadMultipleReq{ | ||||
| 		Bucket:       minioMetaMultipartBucket, | ||||
| 		Prefix:       partPath, | ||||
| 		MaxSize:      1 << 20, // Each part should realistically not be > 1MiB. | ||||
| 		MaxResults:   maxParts + 1, | ||||
| 		MetadataOnly: true, | ||||
| 	} | ||||
| 	partPath := pathJoin(uploadIDPath, fi.DataDir) + SlashSeparator | ||||
| 
 | ||||
| 	start := partNumberMarker + 1 | ||||
| 	end := start + maxParts | ||||
| 
 | ||||
| 	// Parts are 1 based, so index 0 is part one, etc. | ||||
| 	for i := start; i <= end; i++ { | ||||
| 		req.Files = append(req.Files, fmt.Sprintf("part.%d.meta", i)) | ||||
| 	} | ||||
| 
 | ||||
| 	var disk StorageAPI | ||||
| 	disks := er.getOnlineLocalDisks() | ||||
| 	if len(disks) == 0 { | ||||
| 		// using er.getOnlineLocalDisks() has one side-affect where | ||||
| 		// on a pooled setup all disks are remote, add a fallback | ||||
| 		disks = er.getOnlineDisks() | ||||
| 	} | ||||
| 
 | ||||
| 	for _, disk = range disks { | ||||
| 		if disk == nil { | ||||
| 			continue | ||||
| 	// List parts in quorum | ||||
| 	partNums, err := er.listParts(ctx, onlineDisks, partPath, readQuorum) | ||||
| 	if err != nil { | ||||
| 		// This means that fi.DataDir, is not yet populated so we | ||||
| 		// return an empty response. | ||||
| 		if errors.Is(err, errFileNotFound) { | ||||
| 			return result, nil | ||||
| 		} | ||||
| 
 | ||||
| 		if !disk.IsOnline() { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		break | ||||
| 		return result, toObjectErr(err, bucket, object, uploadID) | ||||
| 	} | ||||
| 
 | ||||
| 	g := errgroup.WithNErrs(len(req.Files)).WithConcurrency(32) | ||||
| 
 | ||||
| 	partsInfo := make([]*ObjectPartInfo, len(req.Files)) | ||||
| 	for i, file := range req.Files { | ||||
| 		file := file | ||||
| 		partN := i + start | ||||
| 		i := i | ||||
| 
 | ||||
| 		g.Go(func() error { | ||||
| 			buf, err := disk.ReadAll(ctx, minioMetaMultipartBucket, pathJoin(partPath, file)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			pinfo := &ObjectPartInfo{} | ||||
| 			_, err = pinfo.UnmarshalMsg(buf) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			if partN != pinfo.Number { | ||||
| 				return fmt.Errorf("part.%d.meta has incorrect corresponding part number: expected %d, got %d", partN, partN, pinfo.Number) | ||||
| 			} | ||||
| 
 | ||||
| 			partsInfo[i] = pinfo | ||||
| 			return nil | ||||
| 		}, i) | ||||
| 	if len(partNums) == 0 { | ||||
| 		return result, nil | ||||
| 	} | ||||
| 
 | ||||
| 	g.Wait() | ||||
| 
 | ||||
| 	for _, part := range partsInfo { | ||||
| 		if part != nil && part.Number != 0 && !part.ModTime.IsZero() { | ||||
| 			fi.AddObjectPart(part.Number, part.ETag, part.Size, part.ActualSize, part.ModTime, part.Index, part.Checksums) | ||||
| 		} | ||||
| 	start := objectPartIndexNums(partNums, partNumberMarker) | ||||
| 	if start != -1 { | ||||
| 		partNums = partNums[start+1:] | ||||
| 	} | ||||
| 
 | ||||
| 	// Only parts with higher part numbers will be listed. | ||||
| 	parts := fi.Parts | ||||
| 	result.Parts = make([]PartInfo, 0, len(parts)) | ||||
| 	for _, part := range parts { | ||||
| 	result.Parts = make([]PartInfo, 0, len(partNums)) | ||||
| 	partMetaPaths := make([]string, len(partNums)) | ||||
| 	for i, part := range partNums { | ||||
| 		partMetaPaths[i] = pathJoin(partPath, fmt.Sprintf("part.%d.meta", part)) | ||||
| 	} | ||||
| 
 | ||||
| 	// Read parts in quorum | ||||
| 	objParts, err := readParts(ctx, onlineDisks, minioMetaMultipartBucket, partMetaPaths, | ||||
| 		partNums, readQuorum) | ||||
| 	if err != nil { | ||||
| 		return result, toObjectErr(err, bucket, object, uploadID) | ||||
| 	} | ||||
| 
 | ||||
| 	count := maxParts | ||||
| 	for _, objPart := range objParts { | ||||
| 		result.Parts = append(result.Parts, PartInfo{ | ||||
| 			PartNumber:     part.Number, | ||||
| 			ETag:           part.ETag, | ||||
| 			LastModified:   part.ModTime, | ||||
| 			ActualSize:     part.ActualSize, | ||||
| 			Size:           part.Size, | ||||
| 			ChecksumCRC32:  part.Checksums["CRC32"], | ||||
| 			ChecksumCRC32C: part.Checksums["CRC32C"], | ||||
| 			ChecksumSHA1:   part.Checksums["SHA1"], | ||||
| 			ChecksumSHA256: part.Checksums["SHA256"], | ||||
| 			PartNumber:     objPart.Number, | ||||
| 			LastModified:   objPart.ModTime, | ||||
| 			ETag:           objPart.ETag, | ||||
| 			Size:           objPart.Size, | ||||
| 			ActualSize:     objPart.ActualSize, | ||||
| 			ChecksumCRC32:  objPart.Checksums["CRC32"], | ||||
| 			ChecksumCRC32C: objPart.Checksums["CRC32C"], | ||||
| 			ChecksumSHA1:   objPart.Checksums["SHA1"], | ||||
| 			ChecksumSHA256: objPart.Checksums["SHA256"], | ||||
| 		}) | ||||
| 		if len(result.Parts) >= maxParts { | ||||
| 		count-- | ||||
| 		if count == 0 { | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// If listed entries are more than maxParts, we set IsTruncated as true. | ||||
| 	if len(parts) > len(result.Parts) { | ||||
| 	if len(objParts) > len(result.Parts) { | ||||
| 		result.IsTruncated = true | ||||
| 		// Make sure to fill next part number marker if IsTruncated is | ||||
| 		// true for subsequent listing. | ||||
| 		nextPartNumberMarker := result.Parts[len(result.Parts)-1].PartNumber | ||||
| 		result.NextPartNumberMarker = nextPartNumberMarker | ||||
| 		// Make sure to fill next part number marker if IsTruncated is true for subsequent listing. | ||||
| 		result.NextPartNumberMarker = result.Parts[len(result.Parts)-1].PartNumber | ||||
| 	} | ||||
| 
 | ||||
| 	return result, nil | ||||
| } | ||||
| 
 | ||||
| func readParts(ctx context.Context, disks []StorageAPI, bucket string, partMetaPaths []string, partNumbers []int, readQuorum int) ([]*ObjectPartInfo, error) { | ||||
| func readParts(ctx context.Context, disks []StorageAPI, bucket string, partMetaPaths []string, partNumbers []int, readQuorum int) ([]ObjectPartInfo, error) { | ||||
| 	g := errgroup.WithNErrs(len(disks)) | ||||
| 
 | ||||
| 	objectPartInfos := make([][]*ObjectPartInfo, len(disks)) | ||||
| @ -970,7 +984,7 @@ func readParts(ctx context.Context, disks []StorageAPI, bucket string, partMetaP | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	partInfosInQuorum := make([]*ObjectPartInfo, len(partMetaPaths)) | ||||
| 	partInfosInQuorum := make([]ObjectPartInfo, len(partMetaPaths)) | ||||
| 	partMetaQuorumMap := make(map[string]int, len(partNumbers)) | ||||
| 	for pidx := range partMetaPaths { | ||||
| 		var pinfos []*ObjectPartInfo | ||||
| @ -1016,22 +1030,22 @@ func readParts(ctx context.Context, disks []StorageAPI, bucket string, partMetaP | ||||
| 		} | ||||
| 
 | ||||
| 		if pinfo != nil && pinfo.ETag != "" && partMetaQuorumMap[maxETag] >= readQuorum { | ||||
| 			partInfosInQuorum[pidx] = pinfo | ||||
| 			partInfosInQuorum[pidx] = *pinfo | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		if partMetaQuorumMap[maxPartMeta] == len(disks) { | ||||
| 			if pinfo != nil && pinfo.Error != "" { | ||||
| 				partInfosInQuorum[pidx] = &ObjectPartInfo{Error: pinfo.Error} | ||||
| 				partInfosInQuorum[pidx] = ObjectPartInfo{Error: pinfo.Error} | ||||
| 			} else { | ||||
| 				partInfosInQuorum[pidx] = &ObjectPartInfo{ | ||||
| 				partInfosInQuorum[pidx] = ObjectPartInfo{ | ||||
| 					Error: InvalidPart{ | ||||
| 						PartNumber: partNumbers[pidx], | ||||
| 					}.Error(), | ||||
| 				} | ||||
| 			} | ||||
| 		} else { | ||||
| 			partInfosInQuorum[pidx] = &ObjectPartInfo{Error: errErasureReadQuorum.Error()} | ||||
| 			partInfosInQuorum[pidx] = ObjectPartInfo{Error: errErasureReadQuorum.Error()} | ||||
| 		} | ||||
| 	} | ||||
| 	return partInfosInQuorum, nil | ||||
|  | ||||
| @ -1202,6 +1202,292 @@ func testListMultipartUploads(obj ObjectLayer, instanceType string, t TestErrHan | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Wrapper for calling TestListObjectPartsStale tests for both Erasure multiple disks and single node setup. | ||||
| func TestListObjectPartsStale(t *testing.T) { | ||||
| 	ExecObjectLayerDiskAlteredTest(t, testListObjectPartsStale) | ||||
| } | ||||
| 
 | ||||
| // testListObjectPartsStale - Tests validate listing of object parts when parts are stale | ||||
| func testListObjectPartsStale(obj ObjectLayer, instanceType string, disks []string, t *testing.T) { | ||||
| 	bucketNames := []string{"minio-bucket", "minio-2-bucket"} | ||||
| 	objectNames := []string{"minio-object-1.txt"} | ||||
| 	uploadIDs := []string{} | ||||
| 
 | ||||
| 	globalStorageClass.Update(storageclass.Config{ | ||||
| 		RRS: storageclass.StorageClass{ | ||||
| 			Parity: 2, | ||||
| 		}, | ||||
| 		Standard: storageclass.StorageClass{ | ||||
| 			Parity: 4, | ||||
| 		}, | ||||
| 	}) | ||||
| 
 | ||||
| 	// bucketnames[0]. | ||||
| 	// objectNames[0]. | ||||
| 	// uploadIds [0]. | ||||
| 	// Create bucket before initiating NewMultipartUpload. | ||||
| 	err := obj.MakeBucket(context.Background(), bucketNames[0], MakeBucketOptions{}) | ||||
| 	if err != nil { | ||||
| 		// Failed to create newbucket, abort. | ||||
| 		t.Fatalf("%s : %s", instanceType, err.Error()) | ||||
| 	} | ||||
| 	opts := ObjectOptions{} | ||||
| 	// Initiate Multipart Upload on the above created bucket. | ||||
| 	res, err := obj.NewMultipartUpload(context.Background(), bucketNames[0], objectNames[0], opts) | ||||
| 	if err != nil { | ||||
| 		// Failed to create NewMultipartUpload, abort. | ||||
| 		t.Fatalf("%s : %s", instanceType, err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	z := obj.(*erasureServerPools) | ||||
| 	er := z.serverPools[0].sets[0] | ||||
| 
 | ||||
| 	uploadIDs = append(uploadIDs, res.UploadID) | ||||
| 
 | ||||
| 	// Create multipart parts. | ||||
| 	// Need parts to be uploaded before MultipartLists can be called and tested. | ||||
| 	createPartCases := []struct { | ||||
| 		bucketName      string | ||||
| 		objName         string | ||||
| 		uploadID        string | ||||
| 		PartID          int | ||||
| 		inputReaderData string | ||||
| 		inputMd5        string | ||||
| 		inputDataSize   int64 | ||||
| 		expectedMd5     string | ||||
| 	}{ | ||||
| 		// Case 1-4. | ||||
| 		// Creating sequence of parts for same uploadID. | ||||
| 		// Used to ensure that the ListMultipartResult produces one output for the four parts uploaded below for the given upload ID. | ||||
| 		{bucketNames[0], objectNames[0], uploadIDs[0], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd")), "e2fc714c4727ee9395f324cd2e7f331f"}, | ||||
| 		{bucketNames[0], objectNames[0], uploadIDs[0], 2, "efgh", "1f7690ebdd9b4caf8fab49ca1757bf27", int64(len("efgh")), "1f7690ebdd9b4caf8fab49ca1757bf27"}, | ||||
| 		{bucketNames[0], objectNames[0], uploadIDs[0], 3, "ijkl", "09a0877d04abf8759f99adec02baf579", int64(len("abcd")), "09a0877d04abf8759f99adec02baf579"}, | ||||
| 		{bucketNames[0], objectNames[0], uploadIDs[0], 4, "mnop", "e132e96a5ddad6da8b07bba6f6131fef", int64(len("abcd")), "e132e96a5ddad6da8b07bba6f6131fef"}, | ||||
| 	} | ||||
| 	sha256sum := "" | ||||
| 	// Iterating over creatPartCases to generate multipart chunks. | ||||
| 	for _, testCase := range createPartCases { | ||||
| 		_, err := obj.PutObjectPart(context.Background(), testCase.bucketName, testCase.objName, testCase.uploadID, testCase.PartID, mustGetPutObjReader(t, bytes.NewBufferString(testCase.inputReaderData), testCase.inputDataSize, testCase.inputMd5, sha256sum), opts) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("%s : %s", instanceType, err.Error()) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	erasureDisks := er.getDisks() | ||||
| 	uploadIDPath := er.getUploadIDDir(bucketNames[0], objectNames[0], uploadIDs[0]) | ||||
| 	dataDirs, err := erasureDisks[0].ListDir(context.Background(), minioMetaMultipartBucket, minioMetaMultipartBucket, uploadIDPath, -1) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("%s : %s", instanceType, err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	var dataDir string | ||||
| 	for _, folder := range dataDirs { | ||||
| 		if strings.HasSuffix(folder, SlashSeparator) { | ||||
| 			dataDir = folder | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	toDel := (len(erasureDisks) / 2) + 1 | ||||
| 	for _, disk := range erasureDisks[:toDel] { | ||||
| 		disk.DeleteBulk(context.Background(), minioMetaMultipartBucket, []string{pathJoin(uploadIDPath, dataDir, "part.2")}...) | ||||
| 	} | ||||
| 
 | ||||
| 	partInfos := []ListPartsInfo{ | ||||
| 		// partinfos - 0. | ||||
| 		{ | ||||
| 			Bucket:   bucketNames[0], | ||||
| 			Object:   objectNames[0], | ||||
| 			MaxParts: 10, | ||||
| 			UploadID: uploadIDs[0], | ||||
| 			Parts: []PartInfo{ | ||||
| 				{ | ||||
| 					PartNumber: 1, | ||||
| 					Size:       4, | ||||
| 					ETag:       "e2fc714c4727ee9395f324cd2e7f331f", | ||||
| 				}, | ||||
| 				{ | ||||
| 					PartNumber: 3, | ||||
| 					Size:       4, | ||||
| 					ETag:       "09a0877d04abf8759f99adec02baf579", | ||||
| 				}, | ||||
| 				{ | ||||
| 					PartNumber: 4, | ||||
| 					Size:       4, | ||||
| 					ETag:       "e132e96a5ddad6da8b07bba6f6131fef", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		// partinfos - 1. | ||||
| 		{ | ||||
| 			Bucket:   bucketNames[0], | ||||
| 			Object:   objectNames[0], | ||||
| 			MaxParts: 3, | ||||
| 			UploadID: uploadIDs[0], | ||||
| 			Parts: []PartInfo{ | ||||
| 				{ | ||||
| 					PartNumber: 1, | ||||
| 					Size:       4, | ||||
| 					ETag:       "e2fc714c4727ee9395f324cd2e7f331f", | ||||
| 				}, | ||||
| 				{ | ||||
| 					PartNumber: 3, | ||||
| 					Size:       4, | ||||
| 					ETag:       "09a0877d04abf8759f99adec02baf579", | ||||
| 				}, | ||||
| 				{ | ||||
| 					PartNumber: 4, | ||||
| 					Size:       4, | ||||
| 					ETag:       "e132e96a5ddad6da8b07bba6f6131fef", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		// partinfos - 2. | ||||
| 		{ | ||||
| 			Bucket:               bucketNames[0], | ||||
| 			Object:               objectNames[0], | ||||
| 			MaxParts:             2, | ||||
| 			NextPartNumberMarker: 3, | ||||
| 			IsTruncated:          true, | ||||
| 			UploadID:             uploadIDs[0], | ||||
| 			Parts: []PartInfo{ | ||||
| 				{ | ||||
| 					PartNumber: 1, | ||||
| 					Size:       4, | ||||
| 					ETag:       "e2fc714c4727ee9395f324cd2e7f331f", | ||||
| 				}, | ||||
| 				{ | ||||
| 					PartNumber: 3, | ||||
| 					Size:       4, | ||||
| 					ETag:       "09a0877d04abf8759f99adec02baf579", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		// partinfos - 3. | ||||
| 		{ | ||||
| 			Bucket:           bucketNames[0], | ||||
| 			Object:           objectNames[0], | ||||
| 			MaxParts:         2, | ||||
| 			IsTruncated:      false, | ||||
| 			UploadID:         uploadIDs[0], | ||||
| 			PartNumberMarker: 3, | ||||
| 			Parts: []PartInfo{ | ||||
| 				{ | ||||
| 					PartNumber: 4, | ||||
| 					Size:       4, | ||||
| 					ETag:       "e132e96a5ddad6da8b07bba6f6131fef", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	// Collection of non-exhaustive ListObjectParts test cases, valid errors | ||||
| 	// and success responses. | ||||
| 	testCases := []struct { | ||||
| 		bucket           string | ||||
| 		object           string | ||||
| 		uploadID         string | ||||
| 		partNumberMarker int | ||||
| 		maxParts         int | ||||
| 		// Expected output of ListPartsInfo. | ||||
| 		expectedResult ListPartsInfo | ||||
| 		expectedErr    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, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: ".test"}, false}, | ||||
| 		{"Test", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: "Test"}, false}, | ||||
| 		{"---", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: "---"}, false}, | ||||
| 		{"ad", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: "ad"}, false}, | ||||
| 		// Test cases for listing uploadID with single part. | ||||
| 		// Valid bucket names, but they do not exist (Test number 5-7). | ||||
| 		{"volatile-bucket-1", "test1", "", 0, 0, ListPartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false}, | ||||
| 		{"volatile-bucket-2", "test1", "", 0, 0, ListPartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false}, | ||||
| 		{"volatile-bucket-3", "test1", "", 0, 0, ListPartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-3"}, false}, | ||||
| 		// Test case for Asserting for invalid objectName (Test number 8). | ||||
| 		{bucketNames[0], "", "", 0, 0, ListPartsInfo{}, ObjectNameInvalid{Bucket: bucketNames[0]}, false}, | ||||
| 		// Asserting for Invalid UploadID (Test number 9). | ||||
| 		{bucketNames[0], objectNames[0], "abc", 0, 0, ListPartsInfo{}, InvalidUploadID{UploadID: "abc"}, false}, | ||||
| 		// Test case for uploadID with multiple parts (Test number 12). | ||||
| 		{bucketNames[0], objectNames[0], uploadIDs[0], 0, 10, partInfos[0], nil, true}, | ||||
| 		// Test case with maxParts set to less than number of parts (Test number 13). | ||||
| 		{bucketNames[0], objectNames[0], uploadIDs[0], 0, 3, partInfos[1], nil, true}, | ||||
| 		// Test case with partNumberMarker set (Test number 14)-. | ||||
| 		{bucketNames[0], objectNames[0], uploadIDs[0], 0, 2, partInfos[2], nil, true}, | ||||
| 		// Test case with partNumberMarker set (Test number 15)-. | ||||
| 		{bucketNames[0], objectNames[0], uploadIDs[0], 3, 2, partInfos[3], nil, true}, | ||||
| 	} | ||||
| 
 | ||||
| 	for i, testCase := range testCases { | ||||
| 		actualResult, actualErr := obj.ListObjectParts(context.Background(), testCase.bucket, testCase.object, testCase.uploadID, testCase.partNumberMarker, testCase.maxParts, ObjectOptions{}) | ||||
| 		if actualErr != nil && testCase.shouldPass { | ||||
| 			t.Errorf("Test %d: %s: Expected to pass, but failed with: <ERROR> %s", i+1, instanceType, actualErr.Error()) | ||||
| 		} | ||||
| 		if actualErr == nil && !testCase.shouldPass { | ||||
| 			t.Errorf("Test %d: %s: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, instanceType, testCase.expectedErr.Error()) | ||||
| 		} | ||||
| 		// Failed as expected, but does it fail for the expected reason. | ||||
| 		if actualErr != nil && !testCase.shouldPass { | ||||
| 			if !strings.Contains(actualErr.Error(), testCase.expectedErr.Error()) { | ||||
| 				t.Errorf("Test %d: %s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", i+1, instanceType, testCase.expectedErr, actualErr) | ||||
| 			} | ||||
| 		} | ||||
| 		// Passes as expected, but asserting the results. | ||||
| 		if actualErr == nil && testCase.shouldPass { | ||||
| 			expectedResult := testCase.expectedResult | ||||
| 			// Asserting the MaxParts. | ||||
| 			if actualResult.MaxParts != expectedResult.MaxParts { | ||||
| 				t.Errorf("Test %d: %s: Expected the MaxParts to be %d, but instead found it to be %d", i+1, instanceType, expectedResult.MaxParts, actualResult.MaxParts) | ||||
| 			} | ||||
| 			// Asserting Object Name. | ||||
| 			if actualResult.Object != expectedResult.Object { | ||||
| 				t.Errorf("Test %d: %s: Expected Object name to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.Object, actualResult.Object) | ||||
| 			} | ||||
| 			// Asserting UploadID. | ||||
| 			if actualResult.UploadID != expectedResult.UploadID { | ||||
| 				t.Errorf("Test %d: %s: Expected UploadID to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.UploadID, actualResult.UploadID) | ||||
| 			} | ||||
| 			// Asserting NextPartNumberMarker. | ||||
| 			if actualResult.NextPartNumberMarker != expectedResult.NextPartNumberMarker { | ||||
| 				t.Errorf("Test %d: %s: Expected NextPartNumberMarker to be \"%d\", but instead found it to be \"%d\"", i+1, instanceType, expectedResult.NextPartNumberMarker, actualResult.NextPartNumberMarker) | ||||
| 			} | ||||
| 			// Asserting PartNumberMarker. | ||||
| 			if actualResult.PartNumberMarker != expectedResult.PartNumberMarker { | ||||
| 				t.Errorf("Test %d: %s: Expected PartNumberMarker to be \"%d\", but instead found it to be \"%d\"", i+1, instanceType, expectedResult.PartNumberMarker, actualResult.PartNumberMarker) | ||||
| 			} | ||||
| 			// Asserting the BucketName. | ||||
| 			if actualResult.Bucket != expectedResult.Bucket { | ||||
| 				t.Errorf("Test %d: %s: Expected Bucket to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.Bucket, actualResult.Bucket) | ||||
| 			} | ||||
| 			// Asserting IsTruncated. | ||||
| 			if actualResult.IsTruncated != testCase.expectedResult.IsTruncated { | ||||
| 				t.Errorf("Test %d: %s: Expected IsTruncated to be \"%v\", but found it to \"%v\"", i+1, instanceType, expectedResult.IsTruncated, actualResult.IsTruncated) | ||||
| 			} | ||||
| 			// Asserting the number of Parts. | ||||
| 			if len(expectedResult.Parts) != len(actualResult.Parts) { | ||||
| 				t.Errorf("Test %d: %s: Expected the result to contain info of %d Parts, but found %d instead", i+1, instanceType, len(expectedResult.Parts), len(actualResult.Parts)) | ||||
| 			} else { | ||||
| 				// Iterating over the partInfos and asserting the fields. | ||||
| 				for j, actualMetaData := range actualResult.Parts { | ||||
| 					//  Asserting the PartNumber in the PartInfo. | ||||
| 					if actualMetaData.PartNumber != expectedResult.Parts[j].PartNumber { | ||||
| 						t.Errorf("Test %d: %s: Part %d: Expected PartNumber to be \"%d\", but instead found \"%d\"", i+1, instanceType, j+1, expectedResult.Parts[j].PartNumber, actualMetaData.PartNumber) | ||||
| 					} | ||||
| 					//  Asserting the Size in the PartInfo. | ||||
| 					if actualMetaData.Size != expectedResult.Parts[j].Size { | ||||
| 						t.Errorf("Test %d: %s: Part %d: Expected Part Size to be \"%d\", but instead found \"%d\"", i+1, instanceType, j+1, expectedResult.Parts[j].Size, actualMetaData.Size) | ||||
| 					} | ||||
| 					//  Asserting the ETag in the PartInfo. | ||||
| 					if actualMetaData.ETag != expectedResult.Parts[j].ETag { | ||||
| 						t.Errorf("Test %d: %s: Part %d: Expected Etag to be \"%s\", but instead found \"%s\"", i+1, instanceType, j+1, expectedResult.Parts[j].ETag, actualMetaData.ETag) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Wrapper for calling TestListObjectPartsDiskNotFound tests for both Erasure multiple disks and single node setup. | ||||
| func TestListObjectPartsDiskNotFound(t *testing.T) { | ||||
| 	ExecObjectLayerDiskAlteredTest(t, testListObjectPartsDiskNotFound) | ||||
|  | ||||
| @ -23,6 +23,9 @@ catch() { | ||||
| 	pkill minio | ||||
| 	pkill mc | ||||
| 	rm -rf /tmp/xl/ | ||||
| 	if [ $# -ne 0 ]; then | ||||
| 		exit $# | ||||
| 	fi | ||||
| } | ||||
| 
 | ||||
| catch | ||||
| @ -86,11 +89,11 @@ echo "=== myminio2" | ||||
| 
 | ||||
| versionId="$(./mc ls --json --versions myminio1/testbucket/dir/ | tail -n1 | jq -r .versionId)" | ||||
| 
 | ||||
| aws configure set aws_access_key_id minioadmin --profile minioadmin | ||||
| aws configure set aws_secret_access_key minioadmin --profile minioadmin | ||||
| aws configure set default.region us-east-1 --profile minioadmin | ||||
| export AWS_ACCESS_KEY_ID=minioadmin | ||||
| export AWS_SECRET_ACCESS_KEY=minioadmin | ||||
| export AWS_REGION=us-east-1 | ||||
| 
 | ||||
| aws s3api --endpoint-url http://localhost:9001 --profile minioadmin delete-object --bucket testbucket --key dir/file --version-id "$versionId" | ||||
| aws s3api --endpoint-url http://localhost:9001 delete-object --bucket testbucket --key dir/file --version-id "$versionId" | ||||
| 
 | ||||
| ./mc ls -r --versions myminio1/testbucket >/tmp/myminio1.txt | ||||
| ./mc ls -r --versions myminio2/testbucket >/tmp/myminio2.txt | ||||
| @ -127,7 +130,7 @@ versionId="$(./mc ls --json --versions myminio1/foobucket/dir/ | jq -r .versionI | ||||
| 
 | ||||
| kill ${pid2} && wait ${pid2} || true | ||||
| 
 | ||||
| aws s3api --endpoint-url http://localhost:9001 --profile minioadmin delete-object --bucket foobucket --key dir/file --version-id "$versionId" | ||||
| aws s3api --endpoint-url http://localhost:9001 delete-object --bucket foobucket --key dir/file --version-id "$versionId" | ||||
| 
 | ||||
| out="$(./mc ls myminio1/foobucket/dir/)" | ||||
| if [ "$out" != "" ]; then | ||||
|  | ||||
| @ -24,6 +24,9 @@ catch() { | ||||
| 	rm -rf /tmp/multisitea | ||||
| 	rm -rf /tmp/multisiteb | ||||
| 	rm -rf /tmp/data | ||||
| 	if [ $# -ne 0 ]; then | ||||
| 		exit $# | ||||
| 	fi | ||||
| } | ||||
| 
 | ||||
| catch | ||||
|  | ||||
| @ -26,6 +26,9 @@ catch() { | ||||
| 	rm -rf /tmp/multisitea | ||||
| 	rm -rf /tmp/multisiteb | ||||
| 	rm -rf /tmp/multisitec | ||||
| 	if [ $# -ne 0 ]; then | ||||
| 		exit $# | ||||
| 	fi | ||||
| } | ||||
| 
 | ||||
| catch | ||||
|  | ||||
| @ -24,6 +24,9 @@ catch() { | ||||
| 	rm -rf /tmp/multisitec | ||||
| 	rm -rf /tmp/multisited | ||||
| 	rm -rf /tmp/data | ||||
| 	if [ $# -ne 0 ]; then | ||||
| 		exit $# | ||||
| 	fi | ||||
| } | ||||
| 
 | ||||
| catch | ||||
|  | ||||
| @ -20,6 +20,9 @@ catch() { | ||||
| 	pkill minio | ||||
| 	pkill -9 minio | ||||
| 	rm -rf /tmp/multisitea | ||||
| 	if [ $# -ne 0 ]; then | ||||
| 		exit $# | ||||
| 	fi | ||||
| } | ||||
| 
 | ||||
| catch | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user