1 package util
2
3 import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "time"
10
11 dfutil "github.com/devfile/library/v2/pkg/util"
12
13 "github.com/redhat-developer/odo/pkg/testingutil/filesystem"
14
15 gitignore "github.com/sabhiram/go-gitignore"
16 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17 "k8s.io/klog"
18 )
19
20 const DotOdoDirectory = ".odo"
21 const fileIndexName = "odo-file-index.json"
22 const DotGitIgnoreFile = ".gitignore"
23
24
25 type FileIndex struct {
26 metav1.TypeMeta
27 Files map[string]FileData
28 }
29
30
31 func NewFileIndex() *FileIndex {
32
33 return &FileIndex{
34 TypeMeta: metav1.TypeMeta{
35 Kind: "FileIndex",
36 APIVersion: "v1",
37 },
38 Files: make(map[string]FileData),
39 }
40 }
41
42 type FileData struct {
43 Size int64
44 LastModifiedDate time.Time
45 RemoteAttribute string `json:"RemoteAttribute,omitempty"`
46 }
47
48
49
50 func ReadFileIndex(filePath string) (*FileIndex, error) {
51
52 var fi FileIndex
53 if _, err := os.Stat(filePath); os.IsNotExist(err) {
54 return NewFileIndex(), nil
55 }
56
57 byteValue, err := os.ReadFile(filePath)
58 if err != nil {
59 return nil, err
60 }
61
62 err = json.Unmarshal(byteValue, &fi)
63 if err != nil {
64
65
66
67
68
69 return NewFileIndex(), nil
70 }
71 return &fi, nil
72 }
73
74
75 func ResolveIndexFilePath(directory string) (string, error) {
76 directoryFi, err := os.Stat(filepath.Join(directory))
77 if err != nil {
78 return "", err
79 }
80
81 switch mode := directoryFi.Mode(); {
82 case mode.IsDir():
83
84 return filepath.Join(directory, DotOdoDirectory, fileIndexName), nil
85 case mode.IsRegular():
86
87
88 return filepath.Join(filepath.Dir(directory), DotOdoDirectory, fileIndexName), nil
89 }
90
91 return directory, nil
92 }
93
94
95 func GetIndexFileRelativeToContext() string {
96 return filepath.Join(DotOdoDirectory, fileIndexName)
97 }
98
99
100 func AddOdoDirectory(gitIgnoreFile string) error {
101 return addOdoDirectory(gitIgnoreFile, filesystem.DefaultFs{})
102 }
103
104 func addOdoDirectory(gitIgnoreFile string, fs filesystem.Filesystem) error {
105 return addFileToIgnoreFile(gitIgnoreFile, DotOdoDirectory, fs)
106 }
107
108
109
110
111 func TouchGitIgnoreFile(directory string) (gitIgnoreFile string, isNewFile bool, err error) {
112 return touchGitIgnoreFile(directory, filesystem.DefaultFs{})
113 }
114
115 func touchGitIgnoreFile(directory string, fs filesystem.Filesystem) (gitIgnoreFile string, isNewFile bool, err error) {
116 _, err = fs.Stat(directory)
117 if err != nil {
118 return "", false, err
119 }
120
121 gitIgnoreFile = filepath.Join(directory, DotGitIgnoreFile)
122
123
124 if _, err = fs.Stat(gitIgnoreFile); os.IsNotExist(err) {
125 var f filesystem.File
126 f, err = fs.OpenFile(gitIgnoreFile, os.O_WRONLY|os.O_CREATE, 0600)
127 if err != nil {
128 return gitIgnoreFile, false, fmt.Errorf("failed to create .gitignore file: %w", err)
129 }
130 defer f.Close()
131 isNewFile = true
132 }
133
134 return gitIgnoreFile, isNewFile, nil
135 }
136
137
138 func DeleteIndexFile(directory string) error {
139 indexFile, err := ResolveIndexFilePath(directory)
140 if os.IsNotExist(err) {
141 return nil
142 } else if err != nil {
143 return err
144 }
145 return dfutil.DeletePath(indexFile)
146 }
147
148
149 type IndexerRet struct {
150 FilesChanged []string
151 FilesDeleted []string
152 RemoteDeleted []string
153 NewFileMap map[string]FileData
154 ResolvedPath string
155 }
156
157
158
159 func CalculateFileDataKeyFromPath(absolutePath string, rootDirectory string) (string, error) {
160
161 rootDirectory = filepath.FromSlash(rootDirectory)
162
163 relativeFilename, err := filepath.Rel(rootDirectory, absolutePath)
164 if err != nil {
165 return "", err
166 }
167
168 return relativeFilename, nil
169 }
170
171
172 func GenerateNewFileDataEntry(absolutePath string, rootDirectory string) (string, *FileData, error) {
173
174 relativeFilename, err := CalculateFileDataKeyFromPath(absolutePath, rootDirectory)
175 if err != nil {
176 return "", nil, err
177 }
178
179 fi, err := os.Stat(absolutePath)
180
181 if err != nil {
182 return "", nil, err
183 }
184 return relativeFilename, &FileData{
185 Size: fi.Size(),
186 LastModifiedDate: fi.ModTime(),
187 }, nil
188 }
189
190
191
192 func write(filePath string, fi *FileIndex) error {
193 jsonData, err := json.Marshal(fi)
194 if err != nil {
195 return err
196 }
197
198
199 return os.WriteFile(filePath, jsonData, 0600)
200 }
201
202
203
204 func WriteFile(newFileMap map[string]FileData, resolvedPath string) error {
205 newfi := NewFileIndex()
206 newfi.Files = newFileMap
207 err := write(resolvedPath, newfi)
208
209 return err
210 }
211
212
213
214
215 func RunIndexerWithRemote(directory string, originalIgnoreRules []string, remoteDirectories map[string]string) (ret IndexerRet, err error) {
216 directory = filepath.FromSlash(directory)
217 ret.ResolvedPath, err = ResolveIndexFilePath(directory)
218 if err != nil {
219 return ret, err
220 }
221
222
223 existingFileIndex, err := ReadFileIndex(ret.ResolvedPath)
224 if err != nil {
225 return ret, err
226 }
227
228 returnedIndex, err := runIndexerWithExistingFileIndex(directory, originalIgnoreRules, remoteDirectories, existingFileIndex)
229 if err != nil {
230 return IndexerRet{}, err
231 }
232 returnedIndex.ResolvedPath = ret.ResolvedPath
233 return returnedIndex, nil
234 }
235
236
237
238 func runIndexerWithExistingFileIndex(directory string, ignoreRules []string, remoteDirectories map[string]string, existingFileIndex *FileIndex) (ret IndexerRet, err error) {
239 destPath := ""
240 srcPath := directory
241
242 ret.NewFileMap = make(map[string]FileData)
243
244 fileChanged := make(map[string]bool)
245 filesDeleted := make(map[string]bool)
246 fileRemoteChanged := make(map[string]bool)
247
248 if len(remoteDirectories) == 0 {
249
250 pathOptions := recursiveCheckerPathOptions{directory, filepath.Dir(srcPath), filepath.Base(srcPath), filepath.Dir(destPath), filepath.Base(destPath)}
251 innerRet, err := recursiveChecker(pathOptions, ignoreRules, remoteDirectories, *existingFileIndex)
252
253 if err != nil {
254 return IndexerRet{}, err
255 }
256
257 for k, v := range innerRet.NewFileMap {
258 ret.NewFileMap[k] = v
259 }
260
261 for _, remote := range innerRet.FilesChanged {
262 fileChanged[remote] = true
263 }
264
265 for _, remote := range innerRet.RemoteDeleted {
266 fileRemoteChanged[remote] = true
267 }
268
269 for _, remote := range innerRet.FilesDeleted {
270 filesDeleted[remote] = true
271 }
272 }
273
274 for remoteAttribute := range remoteDirectories {
275 path := globEscape(filepath.Join(directory, remoteAttribute))
276 matches, err := filepath.Glob(path)
277 if err != nil {
278 return IndexerRet{}, err
279 }
280 if len(matches) == 0 {
281 return IndexerRet{}, fmt.Errorf("path %q doesn't exist", remoteAttribute)
282 }
283 for _, fileName := range matches {
284 if checkFileExist(fileName) {
285
286
287
288
289 fileAbsolutePath, err := dfutil.GetAbsPath(fileName)
290 if err != nil {
291 return IndexerRet{}, err
292 }
293 klog.V(4).Infof("Got abs path: %s", fileAbsolutePath)
294 klog.V(4).Infof("Making %s relative to %s", srcPath, fileAbsolutePath)
295
296
297
298 destFile, err := filepath.Rel(filepath.FromSlash(srcPath), filepath.FromSlash(fileAbsolutePath))
299 if err != nil {
300 return IndexerRet{}, err
301 }
302
303
304 srcFile := filepath.Join(filepath.Base(srcPath), destFile)
305
306 if value, ok := remoteDirectories[filepath.ToSlash(destFile)]; ok {
307 destFile = value
308 }
309
310 klog.V(4).Infof("makeTar srcFile: %s", srcFile)
311 klog.V(4).Infof("makeTar destFile: %s", destFile)
312
313
314 pathOptions := recursiveCheckerPathOptions{directory, filepath.Dir(srcPath), srcFile, filepath.Dir(destPath), destFile}
315 innerRet, err := recursiveChecker(pathOptions, ignoreRules, remoteDirectories, *existingFileIndex)
316 if err != nil {
317 return IndexerRet{}, err
318 }
319
320 for k, v := range innerRet.NewFileMap {
321 ret.NewFileMap[k] = v
322 }
323
324 for _, remote := range innerRet.FilesChanged {
325 fileChanged[remote] = true
326 }
327
328 for _, remote := range innerRet.RemoteDeleted {
329 fileRemoteChanged[remote] = true
330 }
331
332 for _, remote := range innerRet.FilesDeleted {
333 filesDeleted[remote] = true
334 }
335 } else {
336 return IndexerRet{}, fmt.Errorf("path %q doesn't exist", fileName)
337 }
338 }
339 }
340
341
342 for fileName, value := range existingFileIndex.Files {
343 if _, ok := ret.NewFileMap[fileName]; !ok {
344 klog.V(4).Infof("Deleting file: %s", fileName)
345
346 if value.RemoteAttribute != "" {
347 currentRemote := value.RemoteAttribute
348 for _, remote := range findRemoteFolderForDeletion(currentRemote, remoteDirectories) {
349 fileRemoteChanged[remote] = true
350 }
351 } else {
352 ignoreMatcher := gitignore.CompileIgnoreLines(ignoreRules...)
353 matched := ignoreMatcher.MatchesPath(fileName)
354 if matched {
355 continue
356 }
357 filesDeleted[fileName] = true
358 }
359 }
360 }
361
362 if len(fileRemoteChanged) > 0 {
363 ret.RemoteDeleted = []string{}
364 }
365 if len(fileChanged) > 0 {
366 ret.FilesChanged = []string{}
367 }
368 if len(filesDeleted) > 0 {
369 ret.FilesDeleted = []string{}
370 }
371 for remote := range fileRemoteChanged {
372 ret.RemoteDeleted = append(ret.RemoteDeleted, remote)
373 }
374 for remote := range fileChanged {
375 ret.FilesChanged = append(ret.FilesChanged, remote)
376 }
377 for remote := range filesDeleted {
378 ret.FilesDeleted = append(ret.FilesDeleted, remote)
379 }
380
381 return ret, nil
382 }
383
384
385 type recursiveCheckerPathOptions struct {
386
387
388
389
390
391 directory, srcBase, srcFile, destBase, destFile string
392 }
393
394
395
396
397
398
399 func recursiveChecker(pathOptions recursiveCheckerPathOptions, ignoreRules []string, remoteDirectories map[string]string, existingFileIndex FileIndex) (IndexerRet, error) {
400 klog.V(4).Infof("recursiveTar arguments: srcBase: %s, srcFile: %s, destBase: %s, destFile: %s", pathOptions.srcBase, pathOptions.srcFile, pathOptions.destBase, pathOptions.destFile)
401
402
403
404 pathOptions.destBase = filepath.ToSlash(pathOptions.destBase)
405 pathOptions.destFile = filepath.ToSlash(pathOptions.destFile)
406 klog.V(4).Infof("Corrected destinations: base: %s file: %s", pathOptions.destBase, pathOptions.destFile)
407
408 joinedPath := filepath.Join(pathOptions.srcBase, pathOptions.srcFile)
409 matchedPathsDir, err := filepath.Glob(globEscape(joinedPath))
410 if err != nil {
411 return IndexerRet{}, err
412 }
413
414 if len(matchedPathsDir) == 0 {
415 return IndexerRet{}, fmt.Errorf("path %q doesn't exist", joinedPath)
416 }
417
418 joinedRelPath, err := filepath.Rel(pathOptions.directory, joinedPath)
419 if err != nil {
420 return IndexerRet{}, err
421 }
422
423 var ret IndexerRet
424 ret.NewFileMap = make(map[string]FileData)
425
426 fileChanged := make(map[string]bool)
427 fileRemoteChanged := make(map[string]bool)
428
429 ignoreMatcher := gitignore.CompileIgnoreLines(ignoreRules...)
430
431 for _, matchedPath := range matchedPathsDir {
432 stat, err := os.Stat(matchedPath)
433 if err != nil {
434 return IndexerRet{}, err
435 }
436
437
438 rel, err := filepath.Rel(pathOptions.directory, matchedPath)
439 if err != nil {
440 return IndexerRet{}, err
441 }
442 match := ignoreMatcher.MatchesPath(rel)
443
444 if match {
445 return IndexerRet{}, nil
446 }
447
448 if joinedRelPath != "." {
449
450
451 if _, ok := existingFileIndex.Files[joinedRelPath]; !ok {
452 fileChanged[matchedPath] = true
453 klog.V(4).Infof("file added: %s", matchedPath)
454 } else if !stat.ModTime().Equal(existingFileIndex.Files[joinedRelPath].LastModifiedDate) {
455 fileChanged[matchedPath] = true
456 klog.V(4).Infof("last modified date changed: %s", matchedPath)
457 } else if stat.Size() != existingFileIndex.Files[joinedRelPath].Size {
458 fileChanged[matchedPath] = true
459 klog.V(4).Infof("size changed: %s", matchedPath)
460 }
461 }
462
463 if stat.IsDir() {
464
465 if stat.Name() == DotOdoDirectory {
466 return IndexerRet{}, nil
467 }
468
469 if joinedRelPath != "." {
470 folderData, folderChangedData, folderRemoteChangedData := handleRemoteDataFolder(pathOptions.destFile, matchedPath, joinedRelPath, remoteDirectories, existingFileIndex)
471 folderData.Size = stat.Size()
472 folderData.LastModifiedDate = stat.ModTime()
473 ret.NewFileMap[joinedRelPath] = folderData
474
475 for data, value := range folderChangedData {
476 fileChanged[data] = value
477 }
478
479 for data, value := range folderRemoteChangedData {
480 fileRemoteChanged[data] = value
481 }
482 }
483
484
485 entries, err := os.ReadDir(matchedPath)
486 if err != nil {
487 return IndexerRet{}, err
488 }
489 if len(entries) == 0 {
490 continue
491 }
492 for _, entry := range entries {
493 f, err := entry.Info()
494 if err != nil {
495 return IndexerRet{}, err
496 }
497 if _, ok := remoteDirectories[filepath.Join(joinedRelPath, f.Name())]; ok {
498 continue
499 }
500
501 opts := recursiveCheckerPathOptions{pathOptions.directory, pathOptions.srcBase, filepath.Join(pathOptions.srcFile, f.Name()), pathOptions.destBase, filepath.Join(pathOptions.destFile, f.Name())}
502 innerRet, err := recursiveChecker(opts, ignoreRules, remoteDirectories, existingFileIndex)
503 if err != nil {
504 return IndexerRet{}, err
505 }
506
507 for k, v := range innerRet.NewFileMap {
508 ret.NewFileMap[k] = v
509 }
510
511 for _, remote := range innerRet.FilesChanged {
512 fileChanged[remote] = true
513 }
514 for _, remote := range innerRet.RemoteDeleted {
515 fileRemoteChanged[remote] = true
516 }
517 }
518 } else {
519 fileData, fileChangedData, fileRemoteChangedData := handleRemoteDataFile(pathOptions.destFile, matchedPath, joinedRelPath, remoteDirectories, existingFileIndex)
520 fileData.Size = stat.Size()
521 fileData.LastModifiedDate = stat.ModTime()
522 ret.NewFileMap[joinedRelPath] = fileData
523
524 for data, value := range fileChangedData {
525 fileChanged[data] = value
526 }
527
528 for data, value := range fileRemoteChangedData {
529 fileRemoteChanged[data] = value
530 }
531 }
532 }
533
534
535 if len(fileRemoteChanged) > 0 {
536 ret.RemoteDeleted = []string{}
537 }
538 if len(fileChanged) > 0 {
539 ret.FilesChanged = []string{}
540 }
541 for remote := range fileRemoteChanged {
542 ret.RemoteDeleted = append(ret.RemoteDeleted, remote)
543 }
544 for file := range fileChanged {
545 ret.FilesChanged = append(ret.FilesChanged, file)
546 }
547
548 return ret, nil
549 }
550
551
552 func handleRemoteDataFile(destFile, path, relPath string, remoteDirectories map[string]string, existingFileIndex FileIndex) (FileData, map[string]bool, map[string]bool) {
553 destFile = filepath.ToSlash(destFile)
554 fileChanged := make(map[string]bool)
555 fileRemoteChanged := make(map[string]bool)
556
557 remoteDeletionRequired := false
558
559 remoteAttribute := destFile
560 if len(remoteDirectories) == 0 {
561
562 remoteAttribute = ""
563 if existingFileIndex.Files[relPath].RemoteAttribute != "" && existingFileIndex.Files[relPath].RemoteAttribute != destFile {
564
565
566
567
568 fileChanged[path] = true
569 if existingFileIndex.Files[relPath].RemoteAttribute != "" {
570 remoteDeletionRequired = true
571 }
572 }
573 } else {
574 if value, ok := remoteDirectories[relPath]; ok {
575 remoteAttribute = value
576 }
577
578 if existingFileData, ok := existingFileIndex.Files[relPath]; !ok {
579
580 fileChanged[path] = true
581 } else {
582
583
584
585 if existingFileData.RemoteAttribute != remoteAttribute && (remoteAttribute != relPath || existingFileData.RemoteAttribute != "") {
586 fileChanged[path] = true
587 remoteDeletionRequired = true
588 }
589 }
590 }
591
592 if remoteDeletionRequired {
593
594
595 currentRemote := existingFileIndex.Files[relPath].RemoteAttribute
596 if currentRemote == "" {
597 currentRemote = relPath
598 }
599 fileRemoteChanged[currentRemote] = true
600 for _, remote := range findRemoteFolderForDeletion(currentRemote, remoteDirectories) {
601 fileRemoteChanged[remote] = true
602 }
603 }
604
605 return FileData{
606 RemoteAttribute: filepath.ToSlash(remoteAttribute),
607 }, fileChanged, fileRemoteChanged
608 }
609
610
611 func handleRemoteDataFolder(destFile, path, relPath string, remoteDirectories map[string]string, existingFileIndex FileIndex) (FileData, map[string]bool, map[string]bool) {
612 destFile = filepath.ToSlash(destFile)
613 remoteAttribute := destFile
614
615 fileChanged := make(map[string]bool)
616 fileRemoteChanged := make(map[string]bool)
617
618 remoteChanged := false
619
620 if len(remoteDirectories) == 0 {
621 remoteAttribute = ""
622
623
624
625
626 if existingFileIndex.Files[relPath].RemoteAttribute != "" && existingFileIndex.Files[relPath].RemoteAttribute != destFile {
627 fileChanged[path] = true
628 if existingFileIndex.Files[relPath].RemoteAttribute != "" {
629 remoteChanged = true
630 }
631 }
632 } else {
633 if value, ok := remoteDirectories[relPath]; ok {
634 remoteAttribute = value
635 }
636
637 if existingFileData, ok := existingFileIndex.Files[relPath]; !ok {
638 fileChanged[path] = true
639 } else {
640
641
642
643 if existingFileData.RemoteAttribute != remoteAttribute && (remoteAttribute != relPath || existingFileData.RemoteAttribute != "") {
644 fileChanged[path] = true
645 remoteChanged = true
646 }
647 }
648 }
649
650 if remoteChanged {
651
652
653 currentRemote := existingFileIndex.Files[relPath].RemoteAttribute
654 if currentRemote == "" {
655 currentRemote = relPath
656 }
657 fileRemoteChanged[currentRemote] = true
658 for _, remote := range findRemoteFolderForDeletion(currentRemote, remoteDirectories) {
659 fileRemoteChanged[remote] = true
660 }
661 }
662
663 return FileData{
664 RemoteAttribute: filepath.ToSlash(remoteAttribute),
665 }, fileChanged, fileRemoteChanged
666 }
667
668
669 func checkFileExist(fileName string) bool {
670 _, err := os.Stat(fileName)
671 return !os.IsNotExist(err)
672 }
673
674
675 func findRemoteFolderForDeletion(currentRemote string, remoteDirectories map[string]string) []string {
676 var remoteDelete []string
677 currentRemote = filepath.ToSlash(currentRemote)
678 for currentRemote != "" && currentRemote != "." && currentRemote != "/" {
679
680 found := false
681 for _, remote := range remoteDirectories {
682 if strings.HasPrefix(remote, currentRemote+"/") || remote == currentRemote {
683 found = true
684 break
685 }
686 }
687 if !found {
688 remoteDelete = append(remoteDelete, currentRemote)
689 }
690 currentRemote = filepath.ToSlash(filepath.Clean(filepath.Dir(currentRemote)))
691 }
692 return remoteDelete
693 }
694
695
696
697
698 func globEscape(path string) string {
699 var sects []string
700 for _, ch := range path {
701 strCh := string(ch)
702 switch strCh {
703 case "*", "?", "[":
704 strCh = "[" + strCh + "]"
705 }
706 sects = append(sects, strCh)
707 }
708 return strings.Join(sects, "")
709 }
710
View as plain text