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  }