// Copyright (c) 2022 Proton AG // // This file is part of Proton Mail Bridge. // // Proton Mail Bridge is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Proton Mail Bridge is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . package updater import ( "crypto/sha256" "errors" "io" "os" "path/filepath" "github.com/sirupsen/logrus" ) func syncFolders(localPath, updatePath string) (err error) { backupDir := filepath.Join(filepath.Dir(updatePath), "backup") if err = createBackup(localPath, backupDir); err != nil { return } if err = removeMissing(localPath, updatePath); err != nil { restoreFromBackup(backupDir, localPath) return } if err = copyRecursively(updatePath, localPath); err != nil { restoreFromBackup(backupDir, localPath) return } return nil } func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) { logrus.WithField("from", folderToCleanPath).Debug("Remove missing") // Create list of files. existingRelPaths := map[string]bool{} err = filepath.Walk(itemsToKeepPath, func(keepThis string, _ os.FileInfo, walkErr error) error { if walkErr != nil { return walkErr } relPath, walkErr := filepath.Rel(itemsToKeepPath, keepThis) if walkErr != nil { return walkErr } logrus.WithField("path", relPath).Trace("Keep the path") existingRelPaths[relPath] = true return nil }) if err != nil { return } delList := []string{} err = filepath.Walk(folderToCleanPath, func(removeThis string, _ os.FileInfo, walkErr error) error { if walkErr != nil { return walkErr } relPath, walkErr := filepath.Rel(folderToCleanPath, removeThis) if walkErr != nil { return walkErr } logrus.Debug("check path ", relPath) if !existingRelPaths[relPath] { logrus.Debug("path not in list, removing ", removeThis) delList = append(delList, removeThis) } return nil }) if err != nil { return } for _, removeThis := range delList { if err = os.RemoveAll(removeThis); err != nil && !os.IsNotExist(err) { logrus.Error("remove error ", err) return } } return nil } func restoreFromBackup(backupDir, localPath string) { logrus.WithField("from", backupDir). WithField("to", localPath). Error("recovering") if err := copyRecursively(backupDir, localPath); err != nil { logrus.WithField("from", backupDir). WithField("to", localPath). Error("Not able to recover.") } } func createBackup(srcFile, dstDir string) (err error) { logrus.WithField("from", srcFile).WithField("to", dstDir).Debug("Create backup") if err = mkdirAllClear(dstDir); err != nil { return } return copyRecursively(srcFile, dstDir) } func mkdirAllClear(path string) error { if err := os.RemoveAll(path); err != nil { return err } return os.MkdirAll(path, 0o750) } // checksum assumes the file is a regular file and that it exists. func checksum(path string) (hash string) { file, err := os.Open(filepath.Clean(path)) if err != nil { return } defer file.Close() //nolint:errcheck,gosec hasher := sha256.New() if _, err := io.Copy(hasher, file); err != nil { return } return string(hasher.Sum(nil)) } // srcDir including app folder. // dstDir including app folder. func copyRecursively(srcDir, dstDir string) error { //nolint:funlen return filepath.Walk(srcDir, func(srcPath string, srcInfo os.FileInfo, err error) error { if err != nil { return err } srcIsLink := srcInfo.Mode()&os.ModeSymlink == os.ModeSymlink srcIsDir := srcInfo.IsDir() // Non regular source (e.g. named pipes, sockets, devices...). if !srcIsLink && !srcIsDir && !srcInfo.Mode().IsRegular() { logrus.Error("File ", srcPath, " with mode ", srcInfo.Mode()) return errors.New("irregular source file. Copy not implemented") } // Destination path. srcRelPath, err := filepath.Rel(srcDir, srcPath) if err != nil { return err } dstPath := filepath.Join(dstDir, srcRelPath) logrus.Debug("src: ", srcPath, " dst: ", dstPath) // Destination exists. dstInfo, err := os.Lstat(dstPath) if err == nil { dstIsLink := dstInfo.Mode()&os.ModeSymlink == os.ModeSymlink dstIsDir := dstInfo.IsDir() // Non regular destination (e.g. named pipes, sockets, devices...). if !dstIsLink && !dstIsDir && !dstInfo.Mode().IsRegular() { logrus.Error("File ", dstPath, " with mode ", dstInfo.Mode()) return errors.New("irregular target file. Copy not implemented") } if dstIsLink { if err = os.Remove(dstPath); err != nil { return err } } if !dstIsLink && dstIsDir && !srcIsDir { if err = os.RemoveAll(dstPath); err != nil { return err } } // NOTE: Do not return if !dstIsLink && dstIsDir && srcIsDir: the permissions might change. if dstInfo.Mode().IsRegular() && !srcInfo.Mode().IsRegular() { if err = os.Remove(dstPath); err != nil { return err } } } else if !os.IsNotExist(err) { return err } // Create symbolic link and return. if srcIsLink { logrus.Debug("It is a symlink") linkPath, err := os.Readlink(srcPath) if err != nil { return err } logrus.Debug("link to ", linkPath) return os.Symlink(linkPath, dstPath) } // Create dir and return. if srcIsDir { logrus.Debug("It is a dir") return os.MkdirAll(dstPath, srcInfo.Mode()) } // Regular files only. // If files are same return. if os.SameFile(srcInfo, dstInfo) || checksum(srcPath) == checksum(dstPath) { logrus.Debug("Same files, skip copy") return nil } // Create/overwrite regular file. srcReader, err := os.Open(filepath.Clean(srcPath)) if err != nil { return err } defer srcReader.Close() //nolint:errcheck,gosec return copyToTmpFileRename(srcReader, dstPath, srcInfo.Mode()) }) } func copyToTmpFileRename(srcReader io.Reader, dstPath string, dstMode os.FileMode) error { logrus.Debug("Tmp and rename ", dstPath) tmpPath := dstPath + ".tmp" if err := copyToFileTruncate(srcReader, tmpPath, dstMode); err != nil { return err } return os.Rename(tmpPath, dstPath) } func copyToFileTruncate(srcReader io.Reader, dstPath string, dstMode os.FileMode) error { logrus.Debug("Copy and truncate ", dstPath) dstWriter, err := os.OpenFile(filepath.Clean(dstPath), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, dstMode) //nolint:gosec // Cannot guess the safe part of path if err != nil { return err } defer dstWriter.Close() //nolint:errcheck,gosec _, err = io.Copy(dstWriter, srcReader) return err }