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