github.com/0chain/gosdk@v1.17.11/zboxcore/sdk/sync.go (about) 1 package sdk 2 3 import ( 4 "crypto/md5" 5 "encoding/hex" 6 "encoding/json" 7 "io" 8 "io/ioutil" 9 "log" 10 "os" 11 "path/filepath" 12 "sort" 13 "strings" 14 15 "github.com/0chain/errors" 16 "github.com/0chain/gosdk/core/common" 17 "github.com/0chain/gosdk/core/sys" 18 "github.com/0chain/gosdk/zboxcore/fileref" 19 l "github.com/0chain/gosdk/zboxcore/logger" 20 ) 21 22 // For sync app 23 const ( 24 // Upload - Upload file to remote 25 Upload = "Upload" 26 27 // Download - Download file from remote 28 Download = "Download" 29 30 // Update - Update file in remote 31 Update = "Update" 32 33 // Delete - Delete file from remote 34 Delete = "Delete" 35 36 // Conflict - Conflict in file 37 Conflict = "Conflict" 38 39 // LocalDelete - Delete file from local 40 LocalDelete = "LocalDelete" 41 ) 42 43 // FileInfo file information representation for sync 44 type FileInfo struct { 45 Size int64 `json:"size"` 46 MimeType string `json:"mimetype"` 47 ActualSize int64 `json:"actual_size"` 48 Hash string `json:"hash"` 49 Type string `json:"type"` 50 EncryptedKey string `json:"encrypted_key"` 51 LookupHash string `json:"lookup_hash"` 52 CreatedAt common.Timestamp `json:"created_at"` 53 UpdatedAt common.Timestamp `json:"updated_at"` 54 } 55 56 // FileDiff file difference representation for sync 57 type FileDiff struct { 58 Op string `json:"operation"` 59 Path string `json:"path"` 60 Type string `json:"type"` 61 } 62 63 func (a *Allocation) getRemoteFilesAndDirs(dirList []string, fMap map[string]FileInfo, exclMap map[string]int, remotePath string) ([]string, error) { 64 childDirList := make([]string, 0) 65 remotePath = strings.TrimRight(remotePath, "/") 66 for _, dir := range dirList { 67 ref, err := a.ListDir(dir) 68 if err != nil { 69 return []string{}, err 70 } 71 for _, child := range ref.Children { 72 if _, ok := exclMap[child.Path]; ok { 73 continue 74 } 75 relativePathFromRemotePath := strings.TrimPrefix(child.Path, remotePath) 76 fMap[relativePathFromRemotePath] = FileInfo{ 77 Size: child.Size, 78 ActualSize: child.ActualSize, 79 Hash: child.Hash, 80 MimeType: child.MimeType, 81 Type: child.Type, 82 EncryptedKey: child.EncryptionKey, 83 LookupHash: child.LookupHash, 84 CreatedAt: child.CreatedAt, 85 UpdatedAt: child.UpdatedAt, 86 } 87 if child.Type == fileref.DIRECTORY { 88 childDirList = append(childDirList, child.Path) 89 } 90 } 91 } 92 return childDirList, nil 93 } 94 95 // GetRemoteFileMap retrieve the remote file map 96 // - exclMap is the exclude map, a map of paths to exclude 97 // - remotepath is the remote path to get the file map 98 func (a *Allocation) GetRemoteFileMap(exclMap map[string]int, remotepath string) (map[string]FileInfo, error) { 99 // 1. Iteratively get dir and files separately till no more dirs left 100 remoteList := make(map[string]FileInfo) 101 dirs := []string{remotepath} 102 var err error 103 for { 104 dirs, err = a.getRemoteFilesAndDirs(dirs, remoteList, exclMap, remotepath) 105 if err != nil { 106 l.Logger.Error(err.Error()) 107 break 108 } 109 if len(dirs) == 0 { 110 break 111 } 112 } 113 l.Logger.Debug("Remote List: ", remoteList) 114 return remoteList, err 115 } 116 117 func calcFileHash(filePath string) string { 118 fp, err := os.Open(filePath) 119 if err != nil { 120 log.Fatal(err) 121 } 122 defer fp.Close() 123 124 h := md5.New() 125 if _, err := io.Copy(h, fp); err != nil { 126 log.Fatal(err) 127 } 128 return hex.EncodeToString(h.Sum(nil)) 129 } 130 131 func getRemoteExcludeMap(exclPath []string) map[string]int { 132 exclMap := make(map[string]int) 133 for idx, path := range exclPath { 134 exclMap[strings.TrimRight(path, "/")] = idx 135 } 136 return exclMap 137 } 138 139 func addLocalFileList(root string, fMap map[string]FileInfo, dirList *[]string, filter map[string]bool, exclMap map[string]int) filepath.WalkFunc { 140 return func(path string, info os.FileInfo, err error) error { 141 if err != nil { 142 l.Logger.Error("Local file list error for path", path, err.Error()) 143 return nil 144 } 145 // Filter out 146 if _, ok := filter[info.Name()]; ok { 147 return nil 148 } 149 lPath, err := filepath.Rel(root, path) 150 if err != nil { 151 l.Logger.Error("getting relative path failed", err) 152 } 153 // Allocation paths are like unix, so we modify all the backslashes 154 // to forward slashes. File path in windows contain backslashes. 155 lPath = "/" + strings.ReplaceAll(lPath, "\\", "/") 156 // Exclude 157 if _, ok := exclMap[lPath]; ok { 158 if info.IsDir() { 159 return filepath.SkipDir 160 } else { 161 return nil 162 } 163 } 164 // Add to list 165 if info.IsDir() { 166 *dirList = append(*dirList, lPath) 167 } else { 168 fMap[lPath] = FileInfo{Size: info.Size(), Hash: calcFileHash(path), Type: fileref.FILE} 169 } 170 return nil 171 } 172 } 173 174 func getLocalFileMap(rootPath string, filters []string, exclMap map[string]int) (map[string]FileInfo, error) { 175 localMap := make(map[string]FileInfo) 176 var dirList []string 177 filterMap := make(map[string]bool) 178 for _, f := range filters { 179 filterMap[f] = true 180 } 181 err := filepath.Walk(rootPath, addLocalFileList(rootPath, localMap, &dirList, filterMap, exclMap)) 182 // Add the dirs at the end of the list for dir deletiion after all file deletion 183 for _, d := range dirList { 184 localMap[d] = FileInfo{Type: fileref.DIRECTORY} 185 } 186 l.Logger.Debug("Local List: ", localMap) 187 return localMap, err 188 } 189 190 func isParentFolderExists(lFDiff []FileDiff, path string) bool { 191 subdirs := strings.Split(path, "/") 192 p := "/" 193 for _, dir := range subdirs { 194 p = filepath.Join(p, dir) 195 for _, f := range lFDiff { 196 if f.Path == p { 197 return true 198 } 199 } 200 } 201 return false 202 } 203 204 func findDelta(rMap map[string]FileInfo, lMap map[string]FileInfo, prevMap map[string]FileInfo, localRootPath string) []FileDiff { 205 var lFDiff []FileDiff 206 207 // Create a remote hash map and find modifications 208 rMod := make(map[string]FileInfo) 209 for rFile, rInfo := range rMap { 210 if pm, ok := prevMap[rFile]; ok { 211 // Remote file existed in previous sync also 212 if pm.Hash != rInfo.Hash { 213 // File modified in remote 214 rMod[rFile] = rInfo 215 } 216 } 217 } 218 219 // Create a local hash map and find modification 220 lMod := make(map[string]FileInfo) 221 for lFile, lInfo := range lMap { 222 if pm, ok := rMap[lFile]; ok { 223 // Local file existed in previous sync also 224 if pm.Hash != lInfo.Hash { 225 // File modified in local 226 lMod[lFile] = lInfo 227 } 228 } 229 } 230 231 // Iterate remote list and get diff 232 rDelMap := make(map[string]string) 233 for rPath := range rMap { 234 op := Download 235 bRemoteModified := false 236 bLocalModified := false 237 if _, ok := rMod[rPath]; ok { 238 bRemoteModified = true 239 } 240 if _, ok := lMod[rPath]; ok { 241 bLocalModified = true 242 delete(lMap, rPath) 243 } 244 if bRemoteModified && bLocalModified { 245 op = Conflict 246 } else if bLocalModified { 247 op = Update 248 } else if _, ok := lMap[rPath]; ok { 249 // No conflicts and file exists locally 250 delete(lMap, rPath) 251 continue 252 } else if _, ok := prevMap[rPath]; ok { 253 op = Delete 254 // Remote allows delete directory skip individual file deletion 255 rDelMap[rPath] = "d" 256 rDir, _ := filepath.Split(rPath) 257 rDir = strings.TrimRight(rDir, "/") 258 if _, ok := rDelMap[rDir]; ok { 259 continue 260 } 261 } 262 lFDiff = append(lFDiff, FileDiff{Path: rPath, Op: op, Type: rMap[rPath].Type}) 263 } 264 265 // Upload all local files 266 for lPath := range lMap { 267 op := Upload 268 if _, ok := lMod[lPath]; ok { 269 op = Update 270 } else if _, ok := prevMap[lPath]; ok { 271 op = LocalDelete 272 } 273 if op != LocalDelete { 274 // Skip if it is a directory 275 lAbsPath := filepath.Join(localRootPath, lPath) 276 fInfo, err := sys.Files.Stat(lAbsPath) 277 if err != nil { 278 continue 279 } 280 if fInfo.IsDir() { 281 continue 282 } 283 } 284 lFDiff = append(lFDiff, FileDiff{Path: lPath, Op: op, Type: lMap[lPath].Type}) 285 } 286 287 // If there are differences, remove childs if the parent folder is deleted 288 if len(lFDiff) > 0 { 289 sort.SliceStable(lFDiff, func(i, j int) bool { return lFDiff[i].Path < lFDiff[j].Path }) 290 l.Logger.Debug("Sorted diff: ", lFDiff) 291 var newlFDiff []FileDiff 292 for _, f := range lFDiff { 293 if f.Op == LocalDelete || f.Op == Delete { 294 if !isParentFolderExists(newlFDiff, f.Path) { 295 newlFDiff = append(newlFDiff, f) 296 } 297 } else { 298 // Add only files for other Op 299 if f.Type == fileref.FILE { 300 newlFDiff = append(newlFDiff, f) 301 } 302 } 303 } 304 return newlFDiff 305 } 306 return lFDiff 307 } 308 309 // GetAllocationDiff retrieves the difference between the remote and local filesystem representation of the allocation 310 // - lastSyncCachePath is the path to the last sync cache file, which carries exact state of the remote filesystem 311 // - localRootPath is the local root path of the allocation 312 // - localFileFilters is the list of local file filters 313 // - remoteExcludePath is the list of remote exclude paths 314 // - remotePath is the remote path of the allocation 315 func (a *Allocation) GetAllocationDiff(lastSyncCachePath string, localRootPath string, localFileFilters []string, remoteExcludePath []string, remotePath string) ([]FileDiff, error) { 316 var lFdiff []FileDiff 317 prevRemoteFileMap := make(map[string]FileInfo) 318 // 1. Validate localSycnCachePath 319 if len(lastSyncCachePath) > 0 { 320 // Validate cache path 321 fileInfo, err := sys.Files.Stat(lastSyncCachePath) 322 if err == nil { 323 if fileInfo.IsDir() { 324 return lFdiff, errors.Wrap(err, "invalid file cache.") 325 } 326 content, err := ioutil.ReadFile(lastSyncCachePath) 327 if err != nil { 328 return lFdiff, errors.New("", "can't read cache file.") 329 } 330 err = json.Unmarshal(content, &prevRemoteFileMap) 331 if err != nil { 332 return lFdiff, errors.New("", "invalid cache content.") 333 } 334 } 335 } 336 337 // 2. Build a map for exclude path 338 exclMap := getRemoteExcludeMap(remoteExcludePath) 339 340 // 3. Get flat file list from remote 341 remoteFileMap, err := a.GetRemoteFileMap(exclMap, remotePath) 342 if err != nil { 343 return lFdiff, errors.Wrap(err, "error getting list dir from remote.") 344 } 345 346 // 4. Get flat file list on the local filesystem 347 localRootPath = strings.TrimRight(localRootPath, "/") 348 localFileList, err := getLocalFileMap(localRootPath, localFileFilters, exclMap) 349 if err != nil { 350 return lFdiff, errors.Wrap(err, "error getting list dir from local.") 351 } 352 353 // 5. Get the file diff with operation 354 lFdiff = findDelta(remoteFileMap, localFileList, prevRemoteFileMap, localRootPath) 355 l.Logger.Debug("Diff: ", lFdiff) 356 return lFdiff, nil 357 } 358 359 // SaveRemoteSnapshot saves the remote current information to the given file. 360 // This file can be passed to GetAllocationDiff to exactly find the previous sync state to current. 361 // - pathToSave is the path to save the remote snapshot 362 // - remoteExcludePath is the list of paths to exclude 363 func (a *Allocation) SaveRemoteSnapshot(pathToSave string, remoteExcludePath []string) error { 364 bIsFileExists := false 365 // Validate path 366 fileInfo, err := sys.Files.Stat(pathToSave) 367 if err == nil { 368 if fileInfo.IsDir() { 369 return errors.Wrap(err, "invalid file path to save.") 370 } 371 bIsFileExists = true 372 } 373 374 // Get flat file list from remote 375 exclMap := getRemoteExcludeMap(remoteExcludePath) 376 remoteFileList, err := a.GetRemoteFileMap(exclMap, "/") 377 if err != nil { 378 return errors.Wrap(err, "error getting list dir from remote.") 379 } 380 381 // Now we got the list from remote, delete the file if exists 382 if bIsFileExists { 383 err = os.Remove(pathToSave) 384 if err != nil { 385 return errors.Wrap(err, "error deleting previous cache.") 386 } 387 } 388 by, err := json.Marshal(remoteFileList) 389 if err != nil { 390 return errors.Wrap(err, "failed to convert JSON.") 391 } 392 err = ioutil.WriteFile(pathToSave, by, 0644) 393 if err != nil { 394 return errors.Wrap(err, "error saving file.") 395 } 396 // Successfully saved 397 return nil 398 }