testSuite/cmd/testfile.go (241 lines of code) (raw):

package cmd import ( "context" "crypto/md5" "fmt" "io" "net/http" "net/url" "os" "strings" "github.com/Azure/azure-sdk-for-go/sdk/storage/azfile/directory" "github.com/Azure/azure-sdk-for-go/sdk/storage/azfile/file" "github.com/Azure/azure-storage-azcopy/v10/common" "github.com/spf13/cobra" ) // TestFileCommand represents the struct to get command // for validating azcopy operations. type TestFileCommand struct { // object is the resource which needs to be validated against a resource in bucket(share/container). Object string //Subject is the remote resource against which object needs to be validated. Subject string // IsObjectDirectory defines if the object is a directory or not. // If the object is directory, then validation goes through another path. IsObjectDirectory bool // IsRecursive defines if recursive switch is on during transfer. IsRecursive bool // Metadata of the file to be validated. MetaData string // NoGuessMimeType represent the azcopy NoGuessMimeType flag set while uploading the file. NoGuessMimeType bool // Content Type of the file to be validated. ContentType string // Content Encoding of the file to be validated. ContentEncoding string ContentDisposition string ContentLanguage string CacheControl string CheckContentMD5 bool // Represents the flag to determine whether number of blocks or pages needs // to be verified or not. // todo always set this to true VerifyBlockOrPageSize bool // FileType of the resource to be validated. FileType string // Number of Blocks or Pages Expected from the file. NumberOfBlocksOrPages uint64 // todo : numberofblockorpages can be an array with offset : end url. //todo consecutive page ranges get squashed. // PreserveLastModifiedTime represents the azcopy PreserveLastModifiedTime flag while downloading the file. PreserveLastModifiedTime bool } // initializes the testfile command, its aliases and description. // also adds the possible flags that can be supplied with testFile command. func init() { cmdInput := TestFileCommand{} testFileCmd := &cobra.Command{ Use: "testFile", Aliases: []string{"tFile"}, Short: "tests the file created using AZCopy v2", Args: func(cmd *cobra.Command, args []string) error { if len(args) != 2 { return fmt.Errorf("invalid arguments for test file command") } // first argument is the resource name. cmdInput.Object = args[0] // second argument is the test directory. cmdInput.Subject = args[1] return nil }, Run: func(cmd *cobra.Command, args []string) { verifyFile(cmdInput) }, } rootCmd.AddCommand(testFileCmd) // add flags. testFileCmd.PersistentFlags().StringVar(&cmdInput.MetaData, "metadata", "", "metadata expected from the file in the container") testFileCmd.PersistentFlags().StringVar(&cmdInput.ContentType, "content-type", "", "content type expected from the file in the container") testFileCmd.PersistentFlags().StringVar(&cmdInput.ContentEncoding, "content-encoding", "", "validate the given HTTP header.") testFileCmd.PersistentFlags().StringVar(&cmdInput.ContentDisposition, "content-disposition", "", "validate the given HTTP header.") testFileCmd.PersistentFlags().StringVar(&cmdInput.ContentLanguage, "content-language", "", "validate the given HTTP header.") testFileCmd.PersistentFlags().StringVar(&cmdInput.CacheControl, "cache-control", "", "validate the given HTTP header.") testFileCmd.PersistentFlags().BoolVar(&cmdInput.CheckContentMD5, "check-content-md5", false, "Validate content MD5 is not empty.") testFileCmd.PersistentFlags().BoolVar(&cmdInput.IsObjectDirectory, "is-object-dir", false, "set the type of object to verify against the subject") testFileCmd.PersistentFlags().BoolVar(&cmdInput.IsRecursive, "is-recursive", true, "Set whether to validate against subject recursively when object is directory.") // TODO: parameter name doesn't match file scenario, discuss and refactor. testFileCmd.PersistentFlags().Uint64Var(&cmdInput.NumberOfBlocksOrPages, "number-blocks-or-pages", 0, "Use this block size to verify the number of blocks uploaded") testFileCmd.PersistentFlags().BoolVar(&cmdInput.VerifyBlockOrPageSize, "verify-block-size", false, "this flag verify the block size by determining the number of blocks") testFileCmd.PersistentFlags().BoolVar(&cmdInput.NoGuessMimeType, "no-guess-mime-type", false, "This sets the content-type based on the extension of the file.") testFileCmd.PersistentFlags().BoolVar(&cmdInput.PreserveLastModifiedTime, "preserve-last-modified-time", false, "Only available when destination is file system.") } // verify the file downloaded or uploaded. func verifyFile(testFileCmd TestFileCommand) { if testFileCmd.IsObjectDirectory { verifyFileDirUpload(testFileCmd) } else { verifySingleFileUpload(testFileCmd) } } // verifyFileDirUpload verifies the directory recursively uploaded to the share or directory. func verifyFileDirUpload(testFileCmd TestFileCommand) { directoryClient, _ := directory.NewClientWithNoCredential(testFileCmd.Subject, nil) // get the original dir path, which can be used to get file relative path during enumerating and comparing fileURLParts, err := file.ParseURL(testFileCmd.Subject) if err != nil { os.Exit(1) } baseAzureDirPath := fileURLParts.DirectoryOrFilePath // validate azure directory validateAzureDirWithLocalFile(directoryClient, baseAzureDirPath, testFileCmd.Object, testFileCmd.IsRecursive) } // recursively validate files in azure directories and sub-directories func validateAzureDirWithLocalFile(curAzureDirURL *directory.Client, baseAzureDirPath string, localBaseDir string, isRecursive bool) { pager := curAzureDirURL.NewListFilesAndDirectoriesPager(nil) for pager.More() { // look for all files that in current directory listFile, err := pager.NextPage(context.Background()) if err != nil { // fmt.Printf("fail to list files and directories inside the directory. Please check the directory sas, %v\n", err) os.Exit(1) } if isRecursive { for _, dirInfo := range listFile.Segment.Directories { newDirURL := curAzureDirURL.NewSubdirectoryClient(*dirInfo.Name) validateAzureDirWithLocalFile(newDirURL, baseAzureDirPath, localBaseDir, isRecursive) } } // Process the files returned in this result segment (if the segment is empty, the loop body won't execute) for _, fileInfo := range listFile.Segment.Files { curFileURL := curAzureDirURL.NewFileClient(*fileInfo.Name) get, err := curFileURL.DownloadStream(context.Background(), nil) if err != nil { fmt.Printf("fail to download the file %s\n", *fileInfo.Name) os.Exit(1) } retryReader := get.NewRetryReader(context.Background(), &file.RetryReaderOptions{MaxRetries: 3}) // read all bytes. fileBytesDownloaded, err := io.ReadAll(retryReader) if err != nil { fmt.Printf("fail to read the body of file %s downloaded and failed with error %s\n", *fileInfo.Name, err.Error()) os.Exit(1) } retryReader.Close() url, err := url.Parse(curFileURL.URL()) if err != nil { fmt.Printf("fail to parse the file URL %s\n", curFileURL.URL()) os.Exit(1) } tokens := strings.SplitAfterN(url.Path, baseAzureDirPath, 2) if len(tokens) < 2 { fmt.Printf("fail to get sub directory and file name, file URL '%s', original dir path '%s'\n", curFileURL.URL(), baseAzureDirPath) os.Exit(1) } subDirAndFileName := tokens[1] var objectLocalPath string if subDirAndFileName != "" && subDirAndFileName[0] != '/' { objectLocalPath = localBaseDir + "/" + subDirAndFileName } else { objectLocalPath = localBaseDir + subDirAndFileName } // opening the file locally and memory mapping it. sFileInfo, err := os.Stat(objectLocalPath) if err != nil { fmt.Println("fail to get the subject file info on local disk ") os.Exit(1) } sFile, err := os.Open(objectLocalPath) if err != nil { fmt.Println("fail to open file ", sFile) os.Exit(1) } sMap, err := NewMMF(sFile, false, 0, sFileInfo.Size()) if err != nil { fmt.Println("fail to memory mapping the file ", sFileInfo.Name()) } // calculating the md5 of file on container. actualMd5 := md5.Sum(fileBytesDownloaded) // calculating md5 of resource locally. expectedMd5 := md5.Sum(sMap) if actualMd5 != expectedMd5 { fmt.Println("the upload file md5 is not equal to the md5 of actual file on disk for file ", fileInfo.Name) os.Exit(1) } } } } // verifySingleFileUpload verifies the pagefile uploaded or downloaded // against the file locally. func verifySingleFileUpload(testFileCmd TestFileCommand) { fileInfo, err := os.Stat(testFileCmd.Object) if err != nil { fmt.Println("error opening the destination localFile on local disk ") os.Exit(1) } localFile, err := os.Open(testFileCmd.Object) if err != nil { fmt.Println("error opening the localFile ", testFileCmd.Object) } fileClient, _ := file.NewClientWithNoCredential(testFileCmd.Subject, nil) get, err := fileClient.DownloadStream(context.Background(), nil) if err != nil { fmt.Println("unable to get localFile properties ", err.Error()) os.Exit(1) } // reading all the bytes downloaded. retryReader := get.NewRetryReader(context.Background(), &file.RetryReaderOptions{MaxRetries: 3}) defer retryReader.Close() fileBytesDownloaded, err := io.ReadAll(retryReader) if err != nil { fmt.Println("error reading the byes from response and failed with error ", err.Error()) os.Exit(1) } if fileInfo.Size() == 0 { // If the fileSize is 0 and the len of downloaded bytes is not 0 // validation fails if len(fileBytesDownloaded) != 0 { fmt.Printf("validation failed since the actual localFile size %d differs from the downloaded localFile size %d\n", fileInfo.Size(), len(fileBytesDownloaded)) os.Exit(1) } // If both the actual and downloaded localFile size is 0, // validation is successful, no need to match the md5 os.Exit(0) } // memory mapping the resource on local path. mmap, err := NewMMF(localFile, false, 0, fileInfo.Size()) if err != nil { fmt.Println("error mapping the destination localFile: ", localFile, " localFile size: ", fileInfo.Size(), " Error: ", err.Error()) os.Exit(1) } // calculating and verify the md5 of the resource // both locally and on the container. actualMd5 := md5.Sum(mmap) expectedMd5 := md5.Sum(fileBytesDownloaded) if actualMd5 != expectedMd5 { fmt.Println("the uploaded localFile's md5 doesn't matches the actual localFile's md5 for localFile ", testFileCmd.Object) os.Exit(1) } if testFileCmd.CheckContentMD5 && len(get.ContentMD5) == 0 { fmt.Println("ContentMD5 should not be empty") os.Exit(1) } // verify the user given metadata supplied while uploading the localFile against the metadata actually present in the localFile if !validateMetadata(testFileCmd.MetaData, get.Metadata) { fmt.Println("meta data does not match between the actual and uploaded localFile.") os.Exit(1) } // verify the content-type expectedContentType := "" if testFileCmd.NoGuessMimeType { expectedContentType = testFileCmd.ContentType } else { expectedContentType = http.DetectContentType(mmap) } expectedContentType = strings.Split(expectedContentType, ";")[0] if !validateString(expectedContentType, common.IffNotNil(get.ContentType, "")) { str1 := fmt.Sprintf(" %s %s", expectedContentType, common.IffNotNil(get.ContentDisposition, "")) fmt.Println(str1 + "mismatch content type between actual and user given localFile content type") os.Exit(1) } //verify the content-encoding if !validateString(testFileCmd.ContentEncoding, common.IffNotNil(get.ContentEncoding, "")) { fmt.Println("mismatch content encoding between actual and user given localFile content encoding") os.Exit(1) } if !validateString(testFileCmd.ContentDisposition, common.IffNotNil(get.ContentDisposition, "")) { fmt.Println("mismatch content disposition between actual and user given value") os.Exit(1) } if !validateString(testFileCmd.ContentLanguage, common.IffNotNil(get.ContentLanguage, "")) { fmt.Println("mismatch content encoding between actual and user given value") os.Exit(1) } if !validateString(testFileCmd.CacheControl, common.IffNotNil(get.CacheControl, "")) { fmt.Println("mismatch cache control between actual and user given value") os.Exit(1) } mmap.Unmap() localFile.Close() // verify the number of pageranges. // this verifies the page-size and azcopy pagefile implementation. if testFileCmd.VerifyBlockOrPageSize { numberOfPages := int(testFileCmd.NumberOfBlocksOrPages) resp, err := fileClient.GetRangeList(context.Background(), nil) if err != nil { fmt.Println("error getting the range list ", err.Error()) os.Exit(1) } if numberOfPages != (len(resp.Ranges)) { fmt.Println("number of ranges uploaded is different from the number of expected to be uploaded") os.Exit(1) } } }