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)
}
}
}