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 }