github.com/Microsoft/azure-vhd-utils@v0.0.0-20230613175315-7c30a3748a1b/upload/metadata/metaData.go (about)

     1  package metadata
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"time"
    12  
    13  	"github.com/Azure/azure-sdk-for-go/storage"
    14  	"github.com/Microsoft/azure-vhd-utils/upload/progress"
    15  	"github.com/Microsoft/azure-vhd-utils/vhdcore/diskstream"
    16  )
    17  
    18  // The key of the page blob metadata collection entry holding VHD metadata as json.
    19  //
    20  const metaDataKey = "diskmetadata"
    21  
    22  // MetaData is the type representing metadata associated with an Azure page blob holding the VHD.
    23  // This will be stored as a JSON string in the page blob metadata collection with key 'diskmetadata'.
    24  //
    25  type MetaData struct {
    26  	FileMetaData *FileMetaData `json:"fileMetaData"`
    27  }
    28  
    29  // FileMetaData represents the metadata of a VHD file.
    30  //
    31  type FileMetaData struct {
    32  	FileName         string    `json:"fileName"`
    33  	FileSize         int64     `json:"fileSize"`
    34  	VHDSize          int64     `json:"vhdSize"`
    35  	LastModifiedTime time.Time `json:"lastModifiedTime"`
    36  	MD5Hash          []byte    `json:"md5Hash"` // Marshal will encodes []byte as a base64-encoded string
    37  }
    38  
    39  // ToJSON returns MetaData as a json string.
    40  //
    41  func (m *MetaData) ToJSON() (string, error) {
    42  	b, err := json.Marshal(m)
    43  	if err != nil {
    44  		return "", err
    45  	}
    46  	return string(b), nil
    47  }
    48  
    49  // ToMap returns the map representation of the MetaData which can be stored in the page blob metadata colleciton
    50  //
    51  func (m *MetaData) ToMap() (map[string]string, error) {
    52  	v, err := m.ToJSON()
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  
    57  	return map[string]string{metaDataKey: v}, nil
    58  }
    59  
    60  // NewMetaDataFromLocalVHD creates a MetaData instance that should be associated with the page blob
    61  // holding the VHD. The parameter vhdPath is the path to the local VHD.
    62  //
    63  func NewMetaDataFromLocalVHD(vhdPath string) (*MetaData, error) {
    64  	fileStat, err := getFileStat(vhdPath)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	fileMetaData := &FileMetaData{
    70  		FileName:         fileStat.Name(),
    71  		FileSize:         fileStat.Size(),
    72  		LastModifiedTime: fileStat.ModTime(),
    73  	}
    74  
    75  	diskStream, err := diskstream.CreateNewDiskStream(vhdPath)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  	defer diskStream.Close()
    80  	fileMetaData.VHDSize = diskStream.GetSize()
    81  	fileMetaData.MD5Hash, err = calculateMD5Hash(diskStream)
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  
    86  	return &MetaData{
    87  		FileMetaData: fileMetaData,
    88  	}, nil
    89  }
    90  
    91  // NewMetadataFromBlob returns MetaData instance associated with a Azure page blob, if there is no
    92  // MetaData associated with the blob it returns nil value for MetaData
    93  //
    94  func NewMetadataFromBlob(blobClient storage.BlobStorageClient, containerName, blobName string) (*MetaData, error) {
    95  	allMetadata, err := blobClient.GetBlobMetadata(containerName, blobName)
    96  	if err != nil {
    97  		return nil, fmt.Errorf("NewMetadataFromBlob, failed to fetch blob metadata: %v", err)
    98  	}
    99  	m, ok := allMetadata[metaDataKey]
   100  	if !ok {
   101  		return nil, nil
   102  	}
   103  
   104  	b := []byte(m)
   105  	metadata := MetaData{}
   106  	if err := json.Unmarshal(b, &metadata); err != nil {
   107  		return nil, fmt.Errorf("NewMetadataFromBlob, failed to deserialize blob metadata with key %s: %v", metaDataKey, err)
   108  	}
   109  	return &metadata, nil
   110  }
   111  
   112  // CompareMetaData compares the MetaData associated with the remote page blob and local VHD file. If both metadata
   113  // are same this method returns an empty error slice else a non-empty error slice with each error describing
   114  // the metadata entry that mismatched.
   115  //
   116  func CompareMetaData(remote, local *MetaData) []error {
   117  	var metadataErrors = make([]error, 0)
   118  	if !bytes.Equal(remote.FileMetaData.MD5Hash, local.FileMetaData.MD5Hash) {
   119  		metadataErrors = append(metadataErrors,
   120  			fmt.Errorf("MD5 hash of VHD file in Azure blob storage (%v) and local VHD file (%v) does not match",
   121  				base64.StdEncoding.EncodeToString(remote.FileMetaData.MD5Hash),
   122  				base64.StdEncoding.EncodeToString(local.FileMetaData.MD5Hash)))
   123  	}
   124  
   125  	if remote.FileMetaData.VHDSize != local.FileMetaData.VHDSize {
   126  		metadataErrors = append(metadataErrors,
   127  			fmt.Errorf("Logical size of the VHD file in Azure blob storage (%d) and local VHD file (%d) does not match",
   128  				remote.FileMetaData.VHDSize, local.FileMetaData.VHDSize))
   129  	}
   130  
   131  	if remote.FileMetaData.FileSize != local.FileMetaData.FileSize {
   132  		metadataErrors = append(metadataErrors,
   133  			fmt.Errorf("Size of the VHD file in Azure blob storage (%d) and local VHD file (%d) does not match",
   134  				remote.FileMetaData.FileSize, local.FileMetaData.FileSize))
   135  	}
   136  
   137  	if remote.FileMetaData.LastModifiedTime != local.FileMetaData.LastModifiedTime {
   138  		metadataErrors = append(metadataErrors,
   139  			fmt.Errorf("Last modified time of the VHD file in Azure blob storage (%v) and local VHD file (%v) does not match",
   140  				remote.FileMetaData.LastModifiedTime, local.FileMetaData.LastModifiedTime))
   141  	}
   142  
   143  	if remote.FileMetaData.FileName != local.FileMetaData.FileName {
   144  		metadataErrors = append(metadataErrors,
   145  			fmt.Errorf("Full name of the VHD file in Azure blob storage (%s) and local VHD file (%s) does not match",
   146  				remote.FileMetaData.FileName, local.FileMetaData.FileName))
   147  	}
   148  
   149  	return metadataErrors
   150  }
   151  
   152  // getFileStat returns os.FileInfo of a file.
   153  //
   154  func getFileStat(filePath string) (os.FileInfo, error) {
   155  	fd, err := os.Open(filePath)
   156  	if err != nil {
   157  		return nil, fmt.Errorf("fileMetaData.getFileStat: %v", err)
   158  	}
   159  	defer fd.Close()
   160  	return fd.Stat()
   161  }
   162  
   163  // calculateMD5Hash compute the MD5 checksum of a disk stream, it writes the compute progress in stdout
   164  // If there is an error in reading file, then the MD5 compute will stop and it return error.
   165  //
   166  func calculateMD5Hash(diskStream *diskstream.DiskStream) ([]byte, error) {
   167  	progressStream := progress.NewReaderWithProgress(diskStream, diskStream.GetSize(), 1*time.Second)
   168  	defer progressStream.Close()
   169  
   170  	go func() {
   171  		s := time.Time{}
   172  		fmt.Println("Computing MD5 Checksum..")
   173  		for progressRecord := range progressStream.ProgressChan {
   174  			t := s.Add(progressRecord.RemainingDuration)
   175  			fmt.Printf("\r Completed: %3d%% RemainingTime: %02dh:%02dm:%02ds Throughput: %d MB/sec",
   176  				int(progressRecord.PercentComplete),
   177  				t.Hour(), t.Minute(), t.Second(),
   178  				int(progressRecord.AverageThroughputMbPerSecond),
   179  			)
   180  		}
   181  	}()
   182  
   183  	h := md5.New()
   184  	buf := make([]byte, 2097152) // 2 MB staging buffer
   185  	_, err := io.CopyBuffer(h, progressStream, buf)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	return h.Sum(nil), nil
   190  }